Secure Programming Cookbook for C and C++: Recipes for Cryptography, Authentication, Input Validation & More
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:
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:
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:
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:
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:
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:
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:
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:
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
|