8.15 Performing Password-Based Authentication and Key Exchange

8.15.1 Problem

You want to establish a secure channel without using public key cryptography at all. You want to avoid tunneling a traditional authentication protocol over a protocol like SSL, instead preferring to build your own secure channel with a good protocol.

8.15.2 Solution

SAX (Symmetric Authenticated eXchange) is a protocol for creating a secure channel that does not use public key cryptography.

PAX (Public key Authenticated eXchange) is similar to SAX, but it uses public key cryptography to prevent against client spoofing if the attacker manages to get the server-side authentication database. The public key cryptography also makes PAX a bit slower.

8.15.3 Discussion

The SAX and PAX protocols both perform authentication and key exchange. The protocols are generic, so they work in any environment. However, in this recipe we'll show you how to use SAX and PAX in the context of the Authenticated eXchange (AX) library, available from http://www.zork.org/ax/. This library implements SAX and PAX over TCP/IP using a single API.

Let's take a look at how these protocols are supposed to work from the user's point of view. The server needs to have authentication information associated with the user. The account setup must be done over a preexisting secure channel. Perhaps the user sits down at a console, or the system administrator might do the setup on behalf of the user while they are talking over the phone.

Account setup requires the user's password for that server. The password is used to compute some secret information stored on the server; then the actual password is thrown away.

At account creation time, the server picks a salt value that is used to thwart a number of attacks. The server can choose to do one of two things with this salt:

  • Tell it to the user, and have the user type it in the first time she logs in from any new machine (the machine can then cache the salt value for subsequent connections). This solution prevents attackers from learning anything significant by guessing a password, because the attacker has to guess the salt as well. The salt effectively becomes part of the password.

  • Let the salt be public, in which case the attacker can try out passwords by attempting to authenticate with the server.

8.15.3.1 The server

The first thing the server needs to be able to do is create accounts for users. User credential information is stored in objects of type AX_CRED. To compute credentials, use the following function:

void AX_compute_credentials(char *user, size_t ulen, char *pass, size_t plen, 
                            size_t ic, size_t pksz, size_t minkl, size_t maxkl, 
                            size_t public_salt, size_t saltlen, AX_CRED *out);

This function has the following arguments:

user

Arbitrary binary string representing the unique login ID of the user.

ulen

Length of the username.

pass

The password, an arbitrary binary string.

plen

Length of the password in bytes.

ic

Iteration count to be used in the internal secret derivation function. See Recipe 4.10 for recommendations on setting this value (AX uses the derivation function from that recipe).

pksz

Determines whether PAX credentials or SAX credentials should be computed. If you are using PAX, the value specifies the length of the modulus of the public key in bits, which must be 1,024, 2,048, 4,096, or 8,192. If you are using SAX, set this value to 0.

minkl

Minimum key length we will allow the client to request when doing an exchange, in bytes. We recommend 16 bytes (128 bits).

maxkl

Maximum key length we will allow the client to request when doing an exchange, in bytes. Often, the protocol you use will only want a single fixed-size key (and not give the client the option to choose), in which case, this should be the same value as minkl.

public_salt

If this is nonzero, the server will give out the user's salt value when requested. Otherwise, the server should print out the salt at account creation time and have the user enter it on first login from a new client machine.

salt_len

Length of the salt that will be used. The salt value is not actually entirely random. Three bytes of the salt are used to encode the iteration count and the public key size. The rest of it is random. We recommend that, if the salt is public, you use 16-byte salts. If the salt is kept private, you will not want to make them too large, because you will have to convert them into a printable format that the user has to carry around and enter. The minimum size AX allows is 11 bytes, which base64-encodes to 15 characters.

out

Pointer to a container into which credentials will be placed. You are expected to allocate this object.

AX provides an API for serializing and deserializing credential objects:

char    *AX_CRED_serialize(AX_CRED *c, size_t *outlen);
AX_CRED *AX_CRED_deserialize(char *buf, size_t buflen);

These two functions each allocate their result with malloc( ) and return 0 on error.

In addition, if the salt value is to stay private, you will need to retrieve it so that you can encode it and show it to the user. AX provides the following function for doing that:

char *AX_get_salt(AX_CRED *creds, size_t *saltlen);

The result is allocated by malloc( ). The size of the salt is placed into the memory pointed to by the second argument.

Now that we can set up account information and store credentials in a database, we can look at how to actually set up a server to handle connections. The high-level AX API does most of the work for you. There's an actual server abstraction, which is of type AX_SRV.

You do need to define at least one callback, two if you want to log errors. In the first callback, you must return a credential object for the associated user. The callback should be a pointer to a function with the following signature:

AX_CRED *AX_get_credentials_callback(AX_SRV *s, char *user, size_t ulen, 
                                     char *extra, size_t elen);

This function has the following arguments:

s

Pointer to the server object. If you have multiple servers in a single program, you can use this pointer to determine which server produced the request.

user

Username given to the server.

ulen

Length of the username.

extra

Additional application-specific information the client passed to the server. You can use this for whatever purpose you want. For example, you could use this field to encode the server name the client thinks it's connecting to, in order to implement virtual servers.

elen

Length of the application-specific data.

If the user does not exist, you must return 0 from this callback.

The other callback allows you to log errors when a key exchange fails. You do not have to define this callback. If you do define it, the signature is the same as in the previous callback, except that it takes an extra parameter of type size_t that encodes the error, and it does not return anything. As of this writing, there are only two error conditions that might get reported:

AX_SOCK_ERR

Indicates that a generic socket error occurred. You can use your platform's standard API to retrieve more specific information.

AX_CAUTH_ERR

Indicates that the server was unable to authenticate the client.

The first error can represent a large number of failures. In most cases, the connection will close unexpectedly, which can indicate many things, including loss of connectivity or even the client's failing to authenticate the server.

To initialize a server, we use the following function:

AX_SRV *AX_srv_listen(char *if, unsigned short port, size_t protocol,
                      AX_get_creds_cb cf,  AX_exchange_status_cb sf);

This function has the following arguments:

if

String indicating the interface on which to bind. If you want to bind on all interfaces a machine has, use "0.0.0.0".

port

Port on which to bind.

protocol

Indication of which protocol you're using. As of this writing, the only valid values are SAX_PROTOCOL_v1 and PAX_PROTOCOL_v1.

cf

callback for retrieving credentials discussed above.

sf

Callback for error reporting discussed above. Set this to NULL if you don't need it.

This function returns a pointer to an object of type AX_SRV. If there's an error, an exception is thrown using the XXL exception-handling API (discussed in Recipe 13.1). All possible exceptions are standard POSIX error codes that would indicate some sort of failure when calling the underlying socket API.

To close down the server and deallocate associated memory, pass the object to AX_srv_close( ).

Once we have a server object, we need to wait for a connection to come in. Once a connection comes in, we can tell the server to perform a key exchange with that connection. To wait for a connection to come in, use the following function (which will always block):

AX_CLIENT *AX_srv_accept(AX_SRV *s);

This function returns a pointer to an AX_CLIENT object when there is a connection. Again, if there's an error, an exception gets thrown, indicating an error caught by the underlying socket API.

At this point, you should launch a new thread or process to deal with the connection, to prevent an attacker from launching a denial of service by stalling the key exchange.

Once we have received a client object, we can perform a key exchange with the following function:

int AX_srv_exchange(AX_CLIENT *c, char *key, size_t  *kl, char *uname, size_t *ul, 
                   char *x, size_t *xl);

This function has the following arguments:

c

Pointer to the client object returned by AX_srv_accept( ). This object will be deallocated automatically during the call.

key

Agreed-upon key.

kl

Pointer into which the length of the agreed-upon key in bytes is placed.

uname

Pointer to memory allocated by malloc( ) that stores the username of the entity on the other side. You are responsible for freeing this memory with free( ).

ul

Pointer into which the length of the username in bytes is placed.

x

Pointer to dynamically allocated memory representing application-specific data. The memory is allocated with malloc( ), and you are responsible for deallocating this memory as well.

xl

Pointer into which the length of the application-specific data is placed.

On success, AX_srv_exchange( ) will return a connected socket descriptor in blocking mode that you can then use to talk to the client. On failure, an XXL exception will be raised. The value of the exception will be either AX_CAUTH_ERR if we believe the client refused our credentials or AX_SAUTH_ERR if we refused the client's credentials. In both cases, it is possible that an attacker's tampering with the data stream caused the error. On the other hand, it could be that the two parties could not agree on the protocol version or key size.

With a valid socket descriptor in hand, you can now use the exchanged key to set up a secure channel, as discussed in Recipe 9.12. When you are finished communicating, you may simply close the socket descriptor.

Note that whether or not the exchange with the client succeeds, AX_srv_exchange( ) will free the AC_CLIENT object passed into it. If the exchange fails, the socket descriptor will be closed, and the client will have to reconnect in order to attempt another exchange.

8.15.3.2 The client

The client side is a bit less work. We first connect to the server with the following function:

AX *AX_connect(char *addr, unsigned short port, char *uname, size_t ulen, 
               char *extra, size_t elen, size_t protocol);

This function has the following arguments:

addr

IP address (or DNS name) of the server as a NULL-terminated string.

port

Port to which we should connect on the remote machine.

uname

Username.

ulen

Length of the username in bytes.

extra

Application-specific data discussed above.

elen

Length of the application-specific data in bytes.

protocol

Indication of the protocol you're using to connect. As of this writing, the only valid values are SAX_PROTOCOL_v1 and PAX_PROTOCOL_v1.

This call will throw an XXL exception if there's a socket error. Otherwise, it will return an object dynamically allocated with malloc( ) that contains the key exchange state.

If the user is expected to know the salt (i.e., if the server will not send it over the network), you must enter it at this time, with the following function:

void AX_set_salt(AX *p, char *salt, size_t saltlen);

AX_set_salt( ) expects the binary encoding that the server-side API produced. It is your responsibility to make sure the user can enter this value. Note that this function copies a reference to the salt and does not copy the actual value, so do not modify the memory associated with your salt until the AX context is deallocated (which happens as a side effect of the key exchange process; see the following discussion).

Note that, the first time you make the user type in the salt on a particular client machine, you should save the salt to disk. We strongly recommend encrypting the salt with the user's supplied password, using an authenticated encryption mode and the key derivation function from Recipe 4.10.

Once the client knows the salt, it can initiate key exchange using the following function:

int AX_exchange(AX *p, char *pw, size_t pwlen, size_t keylen, char *key);

This function has the following arguments:

p

Pointer to the context object that represents the connection to the server.

pw

Password, treated as a binary string (i.e., not NULL-terminated).

pwlen

Length of the associated password in bytes.

keylen

Key length the client desires in the exchange. The server must be prepared to serve up keys of this length; otherwise, the exchange will fail.

key

Buffer into which the key will be placed if authentication and exchange are successful.

On success, AX_exchange( ) will return a connected socket descriptor in blocking mode that you can then use to talk to the server. On failure, an XXL exception will be raised. The value of the exception will be either AX_CAUTH_ERR if we believe the server refused our credentials or AX_SAUTH_ERR if we refused the server's credentials. In both cases, it is possible that an attacker's tampering with the data stream caused the error. On the other hand, it could be that the two parties could not agree on the protocol version or key size.

With a valid socket descriptor in hand, you can now use the exchanged key to set up a secure channel, as discussed in Recipe 9.12. When you are finished communicating, you may simply close the socket descriptor.

Whether or not the connection succeeds, AX_exchange( ) automatically deallocates the AX object passed into it. If the exchange does fail, the connection to the server will need to be reestablished by calling AX_connect( ) a second time.

8.15.4 See Also

  • AX home page: http://www.zork.org/ax/

  • Recipe 4.10, Recipe 9.12, Recipe 13.1