9.6 Using Kerberos Encryption

9.6.1 Problem

You need to use encryption in code that already uses Kerberos for authentication.

9.6.2 Solution

Kerberos is primarily an authentication service employed for network services. As a side effect of the requirements to perform authentication, Kerberos also provides an API for encryption and decryption, although the number of supported ciphers is considerably fewer than those provided by other cryptographic protocols. Authentication yields a cryptographically strong session key that can be used as a key for encryption.

This recipe works on Unix and Windows with the Heimdal and MIT Kerberos implementations. The code presented here will not work on Windows systems that are Kerberos-enabled with the built-in Windows support, because Windows does not expose the Kerberos API in such a way that the code could be made to work. In particular, the encryption and decryption functions used in this recipe are not present on Windows unless you are using either Heimdal or MIT Kerberos. Instead, you should use CryptoAPI on Windows (see Recipe 5.25).

9.6.3 Discussion

Kerberos provides authentication between clients and servers, communicating over an established data connection. The Kerberos API provides no support for establishing, terminating, or passing arbitrary data over a data connection, whether pipes, sockets, or otherwise. Once its job has been successfully performed, a cryptographically strong session key that can be used as a key for encryption is "left behind."

We present a discussion of how to authenticate using Kerberos in Recipe 8.13. In this recipe, we pick up at the point where Kerberos authentication has completed successfully. At this point, you'll be left with at least a krb5_context object and a krb5_auth_context object. Using these two objects, you can obtain a krb5_keyblock object that contains the session key by calling krb5_auth_con_getremotesubkey( ). The prototype for this function is as follows:

krb5_error_code krb5_auth_con_getremotesubkey(krb5_context context,
                                              krb5_auth_context auth_context,
                                              krb5_keyblock **key_block);

Once you have the session key, you can use it for encryption and decryption.

Kerberos supports only a limited number of symmetric ciphers, which may vary depending on the version of Kerberos that you are using. For maximum portability, you are limited primarily to DES and 3-key Triple-DES in CBC mode. The key returned from krb_auth_con_getremotesubkey( ) will have an algorithm already associated with it, so you don't even have to choose. As part of the authentication process, the client and server will negotiate the strongest cipher that both are capable of supporting, which will (we hope) be Triple-DES (or something stronger) instead of DES, which is actually rather weak. In fact, if DES is negotiated, you may want to consider refusing to proceed.

Many different implementations of Kerberos exist today. The most prominent among the free implementations is the MIT implementation, which is distributed with Darwin and many Linux distributions. Another popular implementation is the Heimdal implementation, which is distributed with FreeBSD and OpenBSD. Unfortunately, while the two implementations share much of the same API, there are differences. In particular, the API for encryption services that we will be using in this recipe differs between the two. To determine which implementation is being used, we test for the existence of the KRB5_GENERAL_ _ preprocessor macro, which will be defined by the MIT implementation but not the Heimdal implementation.

Given a krb5_keyblock object, you can determine whether DES was negotiated using the following function:

#include <krb5.h>
   
int spc_krb5_isdes(krb5_keyblock *key) {
#ifdef KRB5_GENERAL_ _
  if (key->enctype =  = ENCTYPE_DES_CBC_CRC || key->enctype =  = ENCTYPE_DES_CBC_MD4 ||
      key->enctype =  = ENCTYPE_DES_CBC_MD5 || key->enctype =  = ENCTYPE_DES_CBC_RAW)
    return 1;
#else
  if (key->keytype =  = ETYPE_DES_CBC_CRC || key->keytype =  = ETYPE_DES_CBC_MD4 ||
      key->keytype =  = ETYPE_DES_CBC_MD5 || key->keytype =  = ETYPE_DES_CBC_NONE ||
      key->keytype =  = ETYPE_DES_CFB64_NONE || key->keytype =  = ETYPE_DES_PCBC_NONE)
    return 1;
#endif
  return 0;
}

The krb5_context object and the krb5_keyblock object can then be used together as arguments to spc_krb5_encrypt( ), which we implement below. The function also requires a buffer that holds the data to be encrypted along with the size of the buffer, as well as a pointer to receive a dynamically allocated buffer that will hold the encrypted data on return, and a pointer to receive the size of the encrypted data buffer.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <krb5.h>
   
int spc_krb5_encrypt(krb5_context ctx, krb5_keyblock *key, void *inbuf,
                     size_t inlen, void **outbuf, size_t *outlen) {
#ifdef KRB5_GENERAL_ _
  size_t        blksz, newlen;
  krb5_data     in_data;
  krb5_enc_data out_data;
   
  if (krb5_c_block_size(ctx, key->enctype, &blksz)) return 0;
  if (!(inlen % blksz)) newlen = inlen + blksz;
  else newlen = ((inlen + blksz - 1) / blksz) * blksz;
  
  in_data.magic  = KV5M_DATA;
  in_data.length = newlen;
  in_data.data   = malloc(newlen);
  if (!in_data.data) return 0;
   
  memcpy(in_data.data, inbuf, inlen);
  spc_add_padding((unsigned char *)in_data.data + inlen, inlen, blksz);
   
  if (krb5_c_encrypt_length(ctx, key->enctype, in_data.length, outlen)) {
    free(in_data.data);
    return 0;
  }
   
  out_data.magic   = KV5M_ENC_DATA;
  out_data.enctype = key->enctype;
  out_data.kvno    = 0;
  out_data.ciphertext.magic  = KV5M_ENCRYPT_BLOCK;
  out_data.ciphertext.length = *outlen;
  out_data.ciphertext.data   = malloc(*outlen);
  if (!out_data.ciphertext.data) {
    free(in_data.data);
    return 0; 
  }
   
  if (krb5_c_encrypt(ctx, key, 0, 0, &in_data, &out_data)) {
    free(in_data.data);
    return 0;
  }
   
  *outbuf = out_data.ciphertext.data;
  free(in_data.data);
  return 1;
#else
  int           result;
  void          *tmp;
  size_t        blksz, newlen;
  krb5_data     edata;
  krb5_crypto   crypto;
   
  if (krb5_crypto_init(ctx, key, 0, &crypto) != 0) return 0;
   
  if (krb5_crypto_getblocksize(ctx, crypto, &blksz)) {
    krb5_crypto_destroy(ctx, crypto);
    return 0;
  }
  if (!(inlen % blksz)) newlen = inlen + blksz;
  else newlen = ((inlen + blksz - 1) / blksz) * blksz;
  if (!(tmp = malloc(newlen))) {
    krb5_crypto_destroy(ctx, crypto);
    return 0;
  }
  memcpy(tmp, inbuf, inlen);
  spc_add_padding((unsigned char *)tmp + inlen, inlen, blksz);
   
  if (!krb5_encrypt(ctx, crypto, 0, tmp, inlen, &edata)) {
    if ((*outbuf = malloc(edata.length)) != 0) {
      result = 1;
      memcpy(*outbuf, edata.data, edata.length);
      *outlen = edata.length;
    }
    krb5_data_free(&edata);
  }
   
  free(tmp);
  krb5_crypto_destroy(ctx, crypto);
  return result;
#endif
}

The decryption function works identically to the encryption function. Remember that DES and Triple-DES are block mode ciphers, so padding may be necessary if the data you're encrypting is not an exact multiple of the block size. While the Kerberos library will do any necessary padding for you, it does so by padding with zero bytes, which is a poor way to pad out the block. Therefore, we do our own padding using the code from Recipe 5.11 to perform PKCS block padding.

#include <stdlib.h>
#include <string.h>
#include <krb5.h>
   
int spc_krb5_decrypt(krb5_context ctx, krb5_keyblock *key, void *inbuf,
                     size_t inlen, void **outbuf, size_t *outlen) {
#ifdef KRB5_GENERAL_ _
  int           padding;
  krb5_data     out_data;
  krb5_enc_data in_data;
   
  in_data.magic   = KV5M_ENC_DATA;
  in_data.enctype = key->enctype;
  in_data.kvno    = 0;
  in_data.ciphertext.magic  = KV5M_ENCRYPT_BLOCK;
  in_data.ciphertext.length = inlen;
  in_data.ciphertext.data   = inbuf;
   
  out_data.magic  = KV5M_DATA;
  out_data.length = inlen;
  out_data.data   = malloc(inlen);
  if (!out_data.data) return 0;   
  
  if (krb5_c_block_size(ctx, key->enctype, &blksz)) {
    free(out_data.data);
    return 0;
  }
  if (krb5_c_decrypt(ctx, key, 0, 0, &in_data, &out_data)) {
    free(out_data.data);
    return 0;
  }
   
  if ((padding = spc_remove_padding((unsigned char *)out_data.data +
                               out_data.length - blksz, blksz)) =  = -1) {
    free(out_data.data);
    return 0;
  }
   
  *outlen = out_data.length - (blksz - padding);
  if (!(*outbuf = realloc(out_data.data, *outlen))) *outbuf = out_data.data;
  return 1;
#else
  int         padding, result;
  void        *tmp;
  size_t      blksz;
  krb5_data   edata;
  krb5_crypto crypto;
  
  if (krb5_crypto_init(ctx, key, 0, &crypto) != 0) return 0;
  if (krb5_crypto_getblocksize(ctx, crypto, &blksz) != 0) {
    krb5_crypto_destroy(ctx, crypto);
    return 0;
  }
  if (!(tmp = malloc(inlen))) {
    krb5_crypto_destroy(ctx, crypto);
    return 0;
  }
  memcpy(tmp, inbuf, inlen);
  if (!krb5_decrypt(ctx, crypto, 0, tmp, inlen, &edata)) {
    if ((padding = spc_remove_padding((unsigned char *)edata.data + edata.length -
                                 blksz, blksz)) != -1) {
      *outlen = edata.length - (blksz - padding);
      if ((*outbuf = malloc(*outlen)) != 0) {
        result = 1;
        memcpy(*outbuf, edata.data, *outlen);
      }
    }
    krb5_data_free(&edata);
  }
  
  free(tmp);
  krb5_crypto_destroy(ctx, crypto);
  return result;
#endif
}

9.6.4 See Also

Recipe 5.11, Recipe 5.25, Recipe 8.13