[ Team LiB ] Previous Section Next Section

8.14 Authenticating with HTTP Cookies

8.14.1 Problem

You are developing a CGI application for the Web and need to store data on the client's machine using a cookie, but you want to prevent the client from viewing the data or modifying it without your application being able to detect the change.

8.14.2 Solution

Web cookies are implemented by setting a value in the MIME headers sent to the client in a server response. If the client accepts the cookie, it will present the cookie back to the server every time the specified conditions are met. The cookie is stored on the client's computer, typically in a plaintext file that can be modified with any editor. Many browsers even provide an interface for viewing and editing cookies that have been stored.

A single MIME header is a header name followed by a colon, a space, and the header value. The format of the header value depends on the header name. Here, we're concerned with only two headers: the Set-Cookie header, which can be sent to the client when presenting a web page, and the Cookie header, which the client presents to the server when the user browses to a site which stores a cookie.

To ensure the integrity of the data that we store on the client's computer with our cookie, we should encrypt and MAC the data. The server does encoding when setting a cookie, then decrypts and validates whenever the cookie comes back. The server does not share its keys with any other entity—it alone uses them to ensure that the data has not been read or modified since it originally left the server.

8.14.3 Discussion

When encrypting and MAC'ing the data stored in a cookie, we encounter a problem: we can use only a limited character set in cookie headers, yet the output of our cryptographic algorithms is always binary. To solve this problem, we encode the binary data into the base64 character set. The base64 character set uses the uppercase letters, the lowercase letters, the numbers, and a few pieces of punctuation to represent data. Out of necessity, the length of data grows considerably when base64-encoded. We can use the spc_base64_encode( ) function from Recipe 4.5 for base64 encoding to suit our purposes.

The first thing that the server must do is call spc_cookie_init( ), which will initialize a context object that we'll use for both encoding and decoding cookie data. To simplify the encryption and MAC'ing process, as well as reduce the complexity of sending and processing received cookies, we'll use CWC mode from Recipe 5.10.

Initialization requires a key to use for encrypting and MAC'ing the data in cookies. The implementation of CWC described in Recipe 5.10 can use keys that are 128, 192, or 256 bits in size. Before calling spc_cookie_init( ), you should create a key using spc_rand( ), as defined in Recipe 11.2. If the cookies you are sending to the client are persistent, you should store the key on the server so that the same key is always used, rather than generating a new one every time the server starts up. You can either hardcode the key into your program or store it in a file somewhere that is inaccessible through the web server so that you are sure it cannot be compromised.

#include <stdlib.h>
#include <string.h>
#include <cwc.h>

static cwc_t spc_cookie_cwc;
static unsigned char spc_cookie_nonce[11];
   
int spc_cookie_init(unsigned char *key, size_t keylen) {
  memset(spc_cookie_nonce, 0, sizeof(spc_cookie_nonce));
  return cwc_init(&spc_cookie_cwc, key, keylen * 8);
}

To encrypt and MAC the data to send in a cookie, use the following spc_cookie_encode( ) function, which requires two arguments:

cookie

Data to be encrypted and MAC'd. spc_cookie_encode( ) expects the data to be a C-style string, which means that it should not contain binary data and should be NULL terminated.

nonce

11-byte buffer that contains the nonce to use (see Recipe 4.9 for a discussion of nonces). If you specify this argument as NULL, a default buffer that contains all NULL bytes will be used for the nonce.

The problem with using a nonce with cookies is that the same nonce must be used for decrypting and verifying the integrity of the data received from the client. To be able to do this, you need a second plaintext cookie that allows you to recover the nonce before decrypting and verifying the encrypted cookie data. Typically, this would be the user's name, and the server would maintain a list of nonces that it has encoded for each logged-in user.

If you do not use a nonce, your system will be susceptible to capture replay attacks. It is worth expending the effort to use a nonce.

The return from spc_cookie_encode( ) will be a dynamically allocated buffer that contains the base64-encoded ciphertext and MAC of the data passed into it. You are responsible for freeing the memory by calling free( ).

char *spc_cookie_encode(char *cookie, unsigned char *nonce) {
  size_t        cookielen;
  unsigned char *out;
   
  cookielen = strlen(cookie);
  if (!(out = (unsigned char *)malloc(cookielen + 16))) return 0;
  if (!nonce) nonce = spc_cookie_nonce;
   
  cwc_encrypt_message(&spc_cookie_cwc, 0, 0, cookie, cookielen, nonce, out);
  cookie = spc_base64_encode(out, cookielen + 16, 0);
   
  free(out);
  return cookie;
}

When the cookies are received by the server from the client, you can pass the encrypted and MAC'd data to spc_cookie_decode( ), which will decrypt the data and verify its integrity. If there is any error, spc_cookie_decode( ) will return NULL; otherwise, it will return the decrypted data in a dynamically allocated buffer that you are responsible for freeing with free( ).

char *spc_cookie_decode(char *data, unsigned char *nonce) {
  int           error;
  char          *out;
  size_t        cookielen;
  unsigned char *cookie;
   
  if (!(cookie = spc_base64_decode(data, &cookielen, 1, &error))) return 0;
  if (!(out = (char *)malloc(cookielen - 16 + 1))) {
    free(cookie);
    return 0;
  }
  if (!nonce) nonce = spc_cookie_nonce;
   
  error = !cwc_decrypt_message(&spc_cookie_cwc, 0, 0, cookie, cookielen,
                               nonce, out);
  free(cookie);
  if (error) {
    free(out);
    return 0;
  }
   
  out[cookielen - 16] = 0;
  return out;
}

8.14.4 See Also

Recipe 4.5, Recipe 4.6, Recipe 4.9, Recipe 5.10, Recipe 11.2

    [ Team LiB ] Previous Section Next Section