Pubcookie II: Feeding the Monster

This document is intended to highlight some of the current problems facing Pubcookie, a WebISO solution developed by the University of Washington for internal use and now being developed by a number of institutions to become the reference WebISO system.

I've attempted to bold face the recommendations for future directions throughout the text.

WebISO goals

The goals of the WebISO project is outlined in the requirements document. While this is a useful document it suffers from naming requirements at many different levels of the system, from the compatibility with existing client software ("WEB-ISO shall not require of the principal anything beyond a standard browser.") to what appear to be fairly detailed implementation requirements ("WEB-ISO assertions shall timestamp last use to facilitate inactivity timeouts."). At a protocol level, many of the requirements of the WebISO project attempt to mirror the Kerberos security requirements, elegantly expressed in Designing an Authentication System: a Dialog in Four Parts. In general, this document will attempt to improve Pubcookie to the security level of Kerberos where possible.

The significant difference between Pubcookie (and WebISO in general) and Kerberos is requirement 15:

WEB-ISO shall not require of the principal anything beyond a standard browser.

This eliminates any "trusted" client-side software, which Kerberos assumes is possible. This will eliminate implementation of some of the cryptographic assurances that Kerberos makes available, but using assuming the world's current PKI infrastructure (Verisign et al) and using TLS wherever possible we may hope to build a reasonable secure system.

Pubcookie shortcomings

Security

Pubcookie currently suffers from a number of failings in delivering the end security goals. These bullet points have been developed both through examing the source code and (more easily) reading the Pubcookie design document.

Requirements

Pubcookie also falls short of some of the WebISO requirements. Some of the more interesting ones include:

Integration with legacy systems

There are several things that we've discovered in our work at CMU:

Protocol futures

What should we do to improve the protocol? Where do we want to end up? This section is devoted to identifying problems and offering some specific recommendations.

Cookie handling

Some cookie best practices:

How do we accomplish these things?

Key management: Today

What have we learned from Kerberos about key management? Currently lacking in Pubcookie is any sort of real key management. To refresh the reader, the login server has a single master secret S which is 56 bits long. Each application server has a 56 bit key which is generated by an embarrassingly simple operation:

char *libpbc_mod_crypt_key(char *in, unsigned char *addr_bytes)
{
int i;

for( i=0; i<PBC_DES_KEY_BUF; ++i ) {
in[i] ^= addr_bytes[i % 4];
}

return in;

}

Since this is a easily reversible function (it xors the master key with the IP address) there's really no reason for it. It also creates a dependency with IP addresses. Experience with Kerberos shows that this is not always desirable, so we should avoid IP address dependencies whenever possible. Pubcookie also has a public key infrastructure seperate from the TLS keys needed. Each Pubcookie installation has a granting key and a granting certificate. Each participating server also has its own keypair for its personally scoped cookies. The granting certificate as well as the customized master secret need to be distributed to each application server.

We can compare this to the two key management systems that are in widespread use at many institutions interested in Pubcookie.

Key management: Kerberos

The Kerberos key management system utilizes only symmetric keys. No key is shared by more than two participants: the KDC (read "login server") and the service ("application server"). Every key is versioned, and a service may have multiple keys of the same name but different versions during a transition from one key to another. The KDC will only utilize the latest versioned key.

Revoking a key is as easy as changing the key on the KDC: since the service no longer knows the latest key, it can no longer mutually authenticate. Changing a key involves establishing some sort of secure channel between the service and the KDC (this is frequently done using the old key) and synchronizing the new key on both ends.

Attempting to leverage the Kerberos key distribution for Pubcookie is very tempting. Sites that already have a Kerberos infrastructure can continue to run it as is, issuing additional keys to new services for Pubcookie usage. The login server would have to fake the Kerberos key distribution for non-Kerberos sites, probably posing additional burden on sites without Kerberos or at the very least an additional code implementation headache.

Key management: X.509 certs for TLS

Since x.509 certificates are one half of a public/private key pair, managing them is both easier and more complex. In the current world, browsers have a set of locally trusted certificate authorities and no key distribution needs to be done beyond getting the CA to sign each server's certificate. However, since many higher education sites are loath to pay for each certificate, most have their own private CA whose certificate must be transferred to their web clients. The general problem of key revocation is a hard one in the public key world and is mostly unaddressed by current solutions.

Since every service will already have a x.509 keypair (for secure communication with their web clients) it seems natural to want to extend this use to Pubcookie. Instead of having seperate keypairs for Pubcookie uses, we can adapt the ones already in use for TLS. This requires gathering all the applications' public keys onto the login server and distributing the login server's public key to all applictions. Revoking a key can be as simple as delete the application's public key off of the login server. Cleanly rekeying may be trickier without maintaining key versions at the Pubcookie level.

Choose not to decide

Since the cryptographic operations needed by Pubcookie are, on the whole, fairly modest, we should endeavour to abstract out the key management from the rest of the code. In general, we want these operations, where one of either a or b will always be the login server.

For highest performance, we want operations to be done using symmetric ciphers whenever possible but experience at UW seems to show that performance is not a concern. Regardless, it is possible to design a key system leveraging TLS keypairs that use symmetric ciphers in the overwhelming majority of exchanges with no loss of security if that's what is desired.

We almost certainly want to implement only one of these possibilities (it minimizes the amount of code that might have security flaws) but creating this abstraction will allow for easier reasoning about the logic of the login server versus the cryptographic techniques involved.

Proxying

Many web server want to access backend services on the users' behalf. Frequently, this is done by giving the web server complete access to the backend service or at least the ability to impersonate any user at any time. This means that compromising the web server (which listens to public networks with fairly complicated protocols) can compromise or largely compromise the backend service. WebISO in general and Pubcookie in particular can do better.

Again, we turn to Kerberos to guide us. A user can request a delegatable ticket for a proxy server and then forward it to the proxy server. The proxy server then uses this ticket to authenticate to the backend service. In the Pubcookie world we can function similiarly: by defining authentication requests in a generic fashion, we can enable secure proxying. This leads us to question when we should issue proxy assertions: if we do it for every web server that asks, we allow any web server to access any other service on behalf of the user---clearly unacceptable. But if we require users to give their consent each and every time, consent will quickly become automatic and not useful for security.

Supporting more secure authentication

Currently, Pubcookie has no way to prove possession of an authenticator without sending it across the wire. Since exchanges are suppose to go over TLS (thus the server has already authenticated to the client) this hasn't been considered a huge problem. However, it might be nice in the future to allow for applications that wish to prove mutual identity and establish a session key without the use of TLS protection. This would probably require a "certificate" or "ticket" based approach, where there is a public piece that is sent to the remote server and a private piece that is kept locally. The public part is used to create a challenge/response situation between the two participants.

Multiple realms

CMU requires multiple realm support for its Pubcookie deployment. Ideally the password verification (or whatever other realm-dependant verification) would be done by a login server that is controlled by the organization that controls the authentication infrastructure for that realm. However, in the current Pubcookie structure it's not clear how to support this. Kerberos supports it via cross-realm tickets; we can imagine a similiar thing for Pubcookie, but it's hard to envision how the target application decides which login server to redirect an unauthenticated client to. Reconfiguring each application whenever a realm is added is infeasible.

We should probably implement the simple thing first: the login server must be trusted by all cooperating realms. This can be done by just defining another flavor for multirealm use (flavor_multirealm?). The existing verifiers used by the basic flavor already accept a realm parameter though currently they don't do anything with it.

Finally, we must be aware that adding realms complicates authorization decisions. These sort of decisions are already complicated in the legacy Pubcookie code: does an application want UWNetID? SecurID? Both? Do we want authentication asserts of the form "leg@andrew.cmu.edu using KERBEROS" and "leg@andrew.cmu.edu using SecurID"? Perhaps it would be better to have authentication assertions like "leg@andrew.cmu.edu" (meaning a Kerberos checked login) and "leg@securid.andrew.cmu.edu". Naive applications could then just require an authentication in the appropriate realm and more sophisticated applications could do an LDAP lookup or whatever to realize that these two strings represent one and the same person.

The downside of only having authentication assertions of the form "user@REALM" is that a user who is authenticated via Kerberos & SecurID may be unable to access resources from a naive application requiring only Kerberos credentials. Perhaps a single authentication could convey multiple authentication assertions?

Performance

Fixed sizes

Code futures

Modularity: Cookie API

Let's try to make the code in anticipation of wanting to change it to XML, or "key: value", or whatever else, and try to make the code a little clearer at the same time.

Instead of "const char *name" it might be better just have an enum of all possible fields. That would probably work better with binary packing but wouldn't allow as much extensibility. Not addressed below:

struct cookie;

/* all functions return 0 on success, non-zero on failure */

/* allocates a new, blank cookie */
int pc_new(struct cookie **ret);

/* frees the memory associated with 'pc' */
int pc_dispose(struct cookie *pc);

/* decodes the blob 'buf' into a new struct cookie, if possible, verifying the
integrity of the blob. it will refuse to successfully decode the blob if
it can't verify the integrity. */
int pc_decode(const char *buf, int buflen, struct cookie **pc);

/* encodes the blob into a format suitable for sending over the wire to destination
'dest'. returns "not enough space" if the buffer isn't big enough, otherwise
modifies 'buflen' to the length of the output placed in 'buf'. */
int pc_encode(struct cookie *pc, char *buf, int *buflen, struct destination *dest);

/* retrieves the int value named 'name' into 'val', if present */
int pc_getint(struct cookie *pc, const char *name, int *val);

/* sets the int value named 'name' to 'val' */
int pc_setint(struct cookie *pc, const char *name, int val);

/* retrieves the UTF-8 string named 'name' into 'val', if present.
the memory will persist at least until the next pc call on this
struct cookie. */
int pc_getstr(struct cookie *pc, const char *name, const char **val);

/* sets the UTF-8 string value named 'name' to 'val' */
int pc_setstr(struct cookie *pc, const char *name, const char *val);

All access to cookie data would be required to go through this API. The rest of the code would be completely parametric to the choice of the on-the-wire cookie format.

Modularity: Key API

As discussed above, we might want to allow for future flexibility in the choice of the cryptosystems or other key systems, so here's a strawman API.

struct destination;

/* initialize the key API for use by 'service' running on the local host.
return a destination representing the current host which must be free'd with
key_freedest(). this checks for the necessary local keys and fails if they
don't exist. */
int key_init(const char *service, struct destination **me);

/* given a string representation of the resource in the form service/hostname,
return a 'struct destination' in 'ret'. 'ret' must later be free'd with key_freedest().
this operation will fail if the current host has no way of securely communicating
directly with 'dest'. */
int key_getdest(const char *name, struct destination **ret);

/* free a destination 'dest' */
int key_freedest(struct destination **dest);

/* give a string representation of the form service/hostname for 'dest' */
const char *key_getdestname(struct destination *dest);

/* given a destination 'dest', encode 'inbuf' so that 'dest' may verify that it came from
the current service. if applicable, the key system may return a 'public' part which
can be revealed to all and a 'private' part which could be retained for verifying
knowledge. (note that the API currently has no mechanisms for performing such an on-line
verification.

modifies 'outlen' to the length of 'outbuf' written to. modifies 'publen' (if non-NULL)
to the length of the public prefix of 'outbuf'. 'outlen' must be set to the size
of 'outbuf' initially. */
int key_encode(struct destination *dest, const char *inbuf, int inlen,
char *outbuf, int *outlen, int *publen);

/* verify that 'inbuf' actually came from 'dest' and write the plaintext of the message to
'outbuf', modifying 'outlen' appropriately. 'outlen' must be set to the size of 'outbuf'
initially. */
int key_decode(struct destination *dest, const char *inbuf, int inlen,
char *outbuf, int *outlen);

Some things to keep in mind:

Some things with an x.509 implementation:

Some things with a Kerberos implementation:

Other notes:

Configuration

We've made good progress in making Pubcookie more run-time configurable, but there's still work to be done. Some of the more obvious things (to me):

Unsolvable problems

Flexibility leads to security problems

Throughout this document I've argued for flexibility in the implementation. Well, let's face it: the more complicated our software is, the more likely we're going to make a major screwup.

In my defense, I've also been arguing for abstractions. These abstractions can form testing boundaries and may make understanding the code easier. The easier it is to understand what each piece of code is doing and what each piece is responsible for, the easier it will be to find and fix vulnerabilities. On the other hand, we can get into some fairly complex interactions that some of this structure may obscure.

We should try to choose answers for most of the things we're leaving open right now. None of us are particularly well served by having both an XML and a binary encoding of the cookie contents.

Vulnerabilities to Javascript

Cross-site scripting

Application level security

Browser cookie leaks

User expectations