From owner-freebsd-current Sun Dec 15 12:24:10 1996 Return-Path: Received: (from root@localhost) by freefall.freebsd.org (8.8.4/8.8.4) id MAA26056 for current-outgoing; Sun, 15 Dec 1996 12:24:10 -0800 (PST) Received: from skynet.ctr.columbia.edu (skynet.ctr.columbia.edu [128.59.64.70]) by freefall.freebsd.org (8.8.4/8.8.4) with SMTP id MAA25975 for ; Sun, 15 Dec 1996 12:23:46 -0800 (PST) Received: (from wpaul@localhost) by skynet.ctr.columbia.edu (8.6.12/8.6.9) id PAA05216 for current@freebsd.org; Sun, 15 Dec 1996 15:22:40 -0500 From: Bill Paul Message-Id: <199612152022.PAA05216@skynet.ctr.columbia.edu> Subject: Plan for integrating Secure RPC -- comments wanted To: current@freebsd.org Date: Sun, 15 Dec 1996 15:22:39 -0500 (EST) X-Mailer: ELM [version 2.4 PL24] Content-Type: text Sender: owner-current@freebsd.org X-Loop: FreeBSD.org Precedence: bulk Okay, I've decided on a final plan for integrating Secure RPC into the main source tree. There were a couple of problems that had to be dealt with which didn't have easy solutions, so I'm going to outline them here to give people a chance to tell me they suck, call me a looney, or present alternate solutions. A couple things first. In the process of merging in the Secure RPC support to our existing library, I added a few extra useful bits from later versions of RPC released by Sun. The publickey and netname lookup functions are taken from TIRPCSRC 1.0, which was the first release of TI-RPC and was based on SunOS. Our library is based on RPCSRC 4.0, which is older; while it does have publickey and netname lookup code, it uses only YP whereas the newer versions can use either YP or local files (using the '+' hack). Also, in later versions of TI-RPC, there were some new clnt_control() commands added to the various transports. Some of these only work if you have TLI, hence they are no-ops, but the others work as expected. Also, I have also added support for a third transport called "unix" to go with the existing "tcp" and "udp" transports. This transport uses AF_UNIX SOCK_STREAM sockets and works just like the "tcp" transport, except it can only be used by local processes. This transport is useful mainly to support the keyserv(8) program: keyserv(8) should only communicate with local processes, but there's no 100% bulletproof way to guarantee this with the "tcp" and "udp" transports (thanks to IP spoofing). In TI-RPC, keyserv(8) uses the loopback transport for this; BSD has no loopback transport, so I opted to use AF_UNIX sockets instead. (Once this change goes in, I also plan to modify rpc.yppasswdd to use this transport instead of the hack it has now.) Lastly, I decided to add support for version 2 of the keyserv protocol. The keyserv(8) in TI-RPC 2.x uses this protocol, and it's necessary to make NIS+ work. I have modified the keyserv code from TI-RPC 2.3 and used that as well as the key_call.c module and the key_prot.x protocol definition from the same release. Anyway, those are relatively small issues. Here are the larger ones: Adding new authentication flavors --------------------------------- Authentication in RPC works by having the client create an AUTH structure containing credential and verifier data and attaching it to its CLIENT handle. The AUTH data is transmitted to the server whenever an RPC is made. Included in this data is an integer value that denotes the authentication 'flavor' being used. The flavors our library currently supports are AUTH_NONE (0), AUTH_UNIX (1) and AUTH_SHORT (2). (For all these flavors, there are _no_ verifiers, hence they're all insecure.) When an RPC server receives a request, it calls a wrapper function named _authenticate() to choose an appropriate authenticator function based on the flavor value. In RPCSRC 4.0, this is done using an array of functions called svcauthsw[]. The flavor value is used as an index into this array, much like a device major value is used as an index into the devsw[] array. If the flavor value is too high, _authenticate() rejects the credentials. Otherwise, the corresponding authenticator function is invoked and tries to decode the credentials and, if needed, the verifiers. The problem with this is that the svcauthsw[] array is hardcoded into the RPC library (and, consequently, into libc). While an RPC client can set up its AUTH structure independent of the RPC library, the only way to add new authentication flavors to an RPC server is to modify the array and recompile library. In TI-RPC, Sun fixed this by changing the array into a linked list and adding a new function called svc_auth_reg(), which RPC servers can use to register new flavors with the library. Now, when an RPC is received, the _authenticate() wrapper traverses the list looking for an entry with a flavor that matches the value sent by the client. What I want to do is use the svc_auth.c module from TI-RPC so that our library has this capability. The AUTH_DES support can then be imported as src/lib/libdesrpc, seperate from libc. Technically it doesn't have to be this way, but doing it like this provides a good example as to how new authentication modules should look. The desrpc library will contain the client side auth_des.c module, the server side svc_auth_des.c module, the xcrypt.c module (which usually goes in librpcsvc but which fits better here) and all the necessary glue code, including the interface to the DES encryption system (note that _no_ actual DES crypto code will actually be present here -- more on this in a minute). My other motivation for this is to allow the AUTH_GSSAPI authentication code in the Kerberos V distribution to be added without having to modify libc. The AUTH_GSSAPI implementation is provided as a completely seperate version of the RPCSRC 4.0 library. If the code is imported into the system as is, we will end up with two seperate RPC libraries in the tree. This is pretty wasteful, and unnecessary if the RPC code in libc allows new flavors to be added without modifying any code. Splitting out the DES code --------------------------- We obviously can't provide the DES support with the core OS since that would violate U.S. crypto export laws. (The Linux distributors don't seem to care and are happily exporting it anyway, but I think it's safe to say we don't want to follow their example. :) Consequently, the DES code has to be shipped seperately as part of the DES or secure distributions. The trick is to isolate the DES support in such a way that it can be provided seperately with a minimum of hassle to both us and the end users. In terms of the source code, everything ultimately boils down to a single function called _des_crypt(). The Secure RPC source distribution from Sun includes everything needed to implement Secure RPC _except_ this function. We need to maintain this separation. This is not as simple as it seems. For the moment, it is enough to supply a dummy libdes.so shared library with the core OS, but this is only because presently there are no programs in the core OS that need to use Secure RPC (the Secure RPC utilities such as keylogin, keylogout, keyserv, rpc.ypupdated, newkey and chkey will need it, but they're all going to be linked dynamically). However this will change once NIS+ support is integrated. Right now, there are several programs in /bin and /sbin which use NIS code as a side effect of calling libc functions that have NIS support in them. All the executables in these directories are linked static, hence each has a copy of the NIS support in them. This includes things like /bin/csh, /sbin/dump, /sbin/mountd, /sbin/mount_*, and many more. Right now, we deal with this problem by providing replacement executables in the DES distribution (/sbin/init and /bin/ed). But in this case there will be a _lot_ more programs affected, and replacing all of them just doesn't seem like a good solution to me. The question then is how to seperate out the DES code without actually changing any executables. There are actually three ways, all of which suck. It's a matter of choosing the least suckiest. The very suckiest is the solution that Sun uses in Solaris, namely the name service switch. The name service switch uses shared objects to contain the various kinds of lookup routines: files, DNS, NIS and NIS+. You can even create your own. The top-level lookup routines (gethostbyname(), getpwent(), etc...) call a stub which reads the /etc/nsswitch.conf file and dlopen()s the proper lookup modules depending on what it finds there. Using this trick, one can supply an NIS+ lookup module with no DES support in the core OS and a replacement module with real DES support in the encryption kit. The problem with this solution is that in the end, you really don't have any static executables anymore: all programs that use the name service switch must be linked with libdl.so. (The exception to this rule in Solaris is /sbin/sh, which is truly static.) (Note that it appears that Sun really doesn't take advantage of the name service switch to separate out DES support. In fact, I'm not really sure what they do.) The sucky part about all this is that I would have to implement my own name service switch and make all kinds of modifications to the system to support it. Also, if I understand things correctly, static executables can't call dlopen() and friends in 2.2 and 3.0 since those functions are in crt0.o, but not in scrt0.o. The second solution is to put the DES support in the kernel. It's possible to create a syscall LKM that implements the _des_crypt() function and have libdesrpc call this system call instead of a library function. A dummy LKM could be supplied with the core OS that does no DES at all (or uses some other cipher that is exportable), while a real DES LKM could be shipped with the secure dist. In this case, no executables would have to be changed at all. The sucky part about this is that it bloats the kernel. It also has 'kludge' written all over it in big red letters. The third solution, which is the one I've settled on, is to create a 'DES daemon,' i.e. a server program that does the encryption. Since Secure RPC needs keyserv(8) to be running anyway, I decided to (ab)use it as the encryption server. (This is another case where the "unix" transport comes in handy: only local processes can use the service.) Basically I turned the _des_crypt() function into a remote procedure call: the data block to be crypted/decrypted, the key, and other arguments are sent to keyserv, which does the work, then sends back the results. This way, the only program that needs to call _des_crypt() as a local function is keyserv. Also, to make the addition of DES support as easy as possible, I fixed keyserv so that it tries to dlopen() libdes.so in order to obtain DES support rather than linking it to libdes.so at compile time. At startup, keyserv, tries to dlopen() /usr/lib/libdes.so.3.x and looks for the '__des_crypt()' symbol. If it finds it, it uses that for its encryption and keyserv then operates in DES mode. If it fails, it falls back to RC4 encryption (with a 40 bit key, just like nutscrape). Since libdes.so is not supplied with the base OS, keyserv will always operate in RC4 mode until you install the secure dist. (The user can specify an alternate path for the shared lib with a command line switch as well.) The end result: you don't need to replace keyserv at all. To enable DES support, you just copy libdes.so.3.x to /usr/lib, then restart keyserv. Presto! You have real AUTH_DES support. No muss, no fuss, no recompiling, no switching keyserv executables. The sucky part is that calling _des_crypt() as an RPC is slower than calling it as a local function. Using an AF_UNIX socket helps a little since the kernel doesn't have to do any protocol processing, but it's still slower. (This also makes Secure RPC programs dependent on keyserv, but in fact they're already dependent on keyserv anyway because of how Secure RPC works.) Authenticating local connections -------------------------------- The last sticky issue has to do with the way keyserv(8) works. Keyserv's purpose is to store users' unencrypted secret keys. It keeps them in memory, and they're all lost when keyserv exits. (The exception is root's secret key, which can be stored in /etc/.rootkey. This is needed in case a system crashes and reboots while the system operator is away and can't reload root's secret key with keylogin(1). In some cases, system daemons (like rpc.ypupdated and rpc.nisd) need root's secret key to work correctly and will break if it's not available.) Utilities that call keyserv usually supply their UID as one of the arguments in the RPC call. They also supply their UID in their AUTH_UNIX credentials (keyserv requires clients to use AUTH_UNIX creds). The problem is that there's no way for keyserv to be sure that the client isn't lying, and it _needs_ to be sure, otherwise a user can fool it into revealing other users' secret keys. In RPCSRC 4.0, this was handled with the ugly and bletcherous keyenvoy(8) program. Keyserv was set up so that it would only accept requests from 127.0.0.1 on reserved ports, meaning that only root on the local host could talk to it. The keyenvoy(8) program was installed suid-root, and you had to use it as a middleman when communicating with keyserv. The RPC library would actually fork/exec keyenvoy and use it (via a pipe) to do its transcations with keyserv. This meant that all programs that used Secure RPC would do the same thing. This is disgusting for a number of reasons, the least of which is that it wrecks performance of programs that need to make many calls to keyserv (like rpc.nisd). Also, since keyserv was forced to use only the "tcp" and "udp" transports, there was a chance it could be fooled into handling remote requests using IP spoofing. This latter problem goes away if you use the "unix" transport, but the performance issue remains. In TI-RPC, Sun did away with keyenvoy and made keyserv use the loopback transport. They also took advantage of certain features in SysV networking that allow keyserv to learn the UID of the process on the other side of the transport endpoint. (I don't quite understand how it works, but it uses t_getinfo().) This credential information is set by the kernel, hence it's not possible for the client to falsify it (unless it has root privileges, but if this is the case then system security has already been compromised and you have bigger problems). Unfortunately, BSD doesn't have any equivalent mechanism for learning the UID of a process on the other side of an AF_UNIX socket, and we don't have STREAMS/TLI networking. This presents a serious problem since it is vital for keyserv to have such a mechanism in order for its security model to work. A workaround is to use SysV message queues. When a message queue is created, the kernel notes the UID and GID of the creator in its ipc_perms structure. Again, the client is not able to falsify this information. But while FreeBSD does have message queue support, it is optional and not part of the GENERIC kernel. It would therefore be a mistake to depend on it. The only way I've found to deal with this problem effectively is to create a special syscall LKM which can verify the identity of a process on the other side of an AF_UNIX socket. Keyserv calls this new system call with its socket descriptor and the PID of the remote process as arguments. (The remote process supplies its PID in the verifier in its AUTH_UNIX credentials. It's okay to let the client tell keyserv its PID since keyserv will know if it's lying, and it saves some work in the syscall.) The syscall uses the descriptor to find the socket structure for keyserv's side of the connection, then, using the protocol control block, it finds the socket on the other side. It then checks the descriptor table of the specified PID to see if it has a pointer to the same socket structure. If it does, then the syscall returns the UID of the other process, otherwise it returns an error. It's possible to add this code to the kernel directly, but I consider it a hack and not worthy of being added to the kernel source tree. Making it a loadable kernel module works just as well, and it can be easily loaded at the same time keyserv is launched at system startup. (It can be part of the same sysconfig knob that turns on keyserv.) So, sum up, these are the three main issues and how I solved them: - Adding new RPC authentication flavors: use svc_auth_reg() and supply the code in a seperate library. - Segregating the DES crypto code: move it all into keyserv(8) and access it via remote procedure calls. (Also support runtime dynamic loading of the libdes library and use RC4 as a replacement if DES is unavailable.) - Allowing keyserv to authenticate local clients: use a special system call supplied as a loadable kernel module. If nobody complains strenuously about any of this, I'm going to go ahead and start importing things using this plan in a few weeks. (I still need to finish rpc.ypupdated.) Comments, criticisms, screams of terror, or large bags of cash are welcome and encouraged. -Bill -- ============================================================================= -Bill Paul (212) 854-6020 | System Manager, Master of Unix-Fu Work: wpaul@ctr.columbia.edu | Center for Telecommunications Research Home: wpaul@skynet.ctr.columbia.edu | Columbia University, New York City ============================================================================= "It is not I who am crazy; it is I who am mad!" - Ren Hoek, "Space Madness" =============================================================================