Skip site navigation (1)Skip section navigation (2)
Date:      Sun, 15 Dec 1996 15:22:39 -0500 (EST)
From:      Bill Paul <wpaul@skynet.ctr.columbia.edu>
To:        current@freebsd.org
Subject:   Plan for integrating Secure RPC -- comments wanted
Message-ID:  <199612152022.PAA05216@skynet.ctr.columbia.edu>

next in thread | raw e-mail | index | archive | help

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"
=============================================================================



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?199612152022.PAA05216>