[ Team LiB ] Previous Section Next Section

10.9 Using a Whitelist to Verify Certificates

10.9.1 Problem

You have a certificate that you want to compare against a list of known good certificates.

10.9.2 Solution

The average certificate is generally small, often under 2 KB in size. Because a certificate is both reasonably small and cannot be undetectably modified once it has been signed by a CA, it might seem reasonable to do a byte-for-byte comparison of the certificate with a list of certificates. One problem with this approach is that if you are comparing a certificate against a sizable list, performing the comparisons can become a time-consuming operation. The other problem is that of storing all the certificates in the list against which the certificate to verify will be compared. A better way is to compute the fingerprint of each certificate and store the fingerprint instead of the entire certificate. Fingerprints are generally only 16 or 20 bytes in size, depending on the message digest algorithm used to compute them.

10.9.3 Discussion

In OpenSSL, computing the fingerprint of a certificate is as simple as a single call to X509_digest( ). Comparing fingerprints is done with a byte-for-byte comparison. The only work you really need to do is to decide on which message digest algorithm to use. MD5 is still the most popular algorithm, but we recommend using something stronger, such as SHA1. MD5 only has a 16-byte output, and there are known attacks against it, whereas SHA1 has a 20-byte output, and there are no known attacks against it.

#include <string.h>
#include <openssl/evp.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
   
int spc_fingerprint_cert(X509 *cert, EVP_MD *digest, unsigned char *fingerprint,
                        int *fingerprint_length) {
   
  if (*fingerprint_length < EVP_MD_size(digest))
    return 0;
  if (!X509_digest(cert, digest, fingerprint, fingerprint_length))
    return 0;
  return *fingerprint_length;
}
   
int spc_fingerprint_equal(unsigned char *fp1, int fp1len, unsigned char *fp2,
                         int fp2len) {
  return (fp1len =  = fp2len && !memcmp(fp1, fp2, fp1len));
}

Using CryptoAPI on Windows, computing the fingerprint of a certificate is also very simple. A single call to CryptHashCertificate( ) with the certificate's CERT_CONTEXT object is all that's necessary. The following implementation of SpcFingerPrintCert( ) makes two calls so that it can verify that the buffer is big enough to hold the hash.

#include <windows.h>
#include <wincrypt.h>
   
DWORD SpcFingerPrintCert(PCCERT_CONTEXT pCertContext, ALG_ID Algid,
                         BYTE *pbFingerPrint, DWORD *pcbFingerPrint) {
  DWORD cbComputedHash;
   
  if (!CryptHashCertificate(0, Algid, 0, pCertContext->pbCertEncoded,
                            pCertContext->cbCertEncoded, 0, &cbComputedHash)) 
    return 0;
  if (*pcbFingerPrint < cbComputedHash) return 0;
  CryptHashCertificate(0, Algid, 0, pCertContext->pbCertEncoded,
                       pCertContext->cbCertEncoded, pbFingerPrint,
                       pcbFingerPrint);
  return *pcbFingerPrint;
}
   
int SpcFingerPrintEqual(BYTE *pbFingerPrint1, DWORD cbFingerPrint1,
                        BYTE *pbFingerPrint2, DWORD cbFingerPrint2) {
  return (cbFingerPrint1 =  = cbFingerPrint2 && 
          !memcmp(pbFingerPrint1, pbFingerPrint2, cbFingerPrint1));
}

You can use a whitelist in place of normal certificate verification routines. Whitelists are most often useful in servers that want to authenticate clients, rather than the other way around, but they can be used either way. In server mode, you can use the SSL_VERIFY_PEER flag to request a certificate from the client, but remember that the client does not have to supply a certificate in response to a request. If you want to require that the client respond, you also need to use the SSL_VERIFY_FAIL_IF_NO_PEER_CERT flag so that the connection is terminated if the client does not send a certificate.

The downside to using these flags is that OpenSSL will attempt to verify the certificate on its own. With a little trickery, we can short-circuit OpenSSL's certificate verification routines and do a little post-connection verification of our own. We will do this by setting up a verify callback function that always returns success. The verify callback is called for each certificate in the chain when verifying a certificate. It is called with the X509_STORE_CTX containing everything relevant, as well as a boolean indicator of whether OpenSSL has determined the certificate to be valid or not. Typically, the callback will return the same verification status, but it is not required. The callback can reverse the decision that OpenSSL has made.

int spc_whitelist_callback(int ok, X509_STORE_CTX *store) {
  return 1;
}

Once the connection has been established, we can get a copy of the peer's certificate, compute its fingerprint, and compare it against the fingerprints we have in our list. The list can be stored in memory, in a disk file, on a flash memory card, or on some other medium. How the list is stored is irrelevant; what is important is the comparison of fingerprints. The functions shown in the previous code are flexible in that they allow you to choose any message digest algorithm you like. Note, though, that if you are always using the same ones, the functions can be simplified, and you need not keep track of the fingerprint length because you know that a message digest is a fixed size (MD5 is 16 bytes; SHA1 is 20 bytes). The following snippet of code roughly demonstrates the work that needs to be done to employ whitelist-based certificate verification:

int             fingerprint_length;
SSL             *ssl;
EVP_MD          *digest;
SSL_CTX         *ctx;
unsigned char   fingerprint[EVP_MAX_MD_SIZE];
spc_x509store_t spc_store;
   
spc_init_x509store(&spc_store);
spc_x509store_setcallback(&spc_store, spc_whitelist_callback);
spc_x509store_setflags(&spc_store, SPC_X509STORE_SSL_VERIFY_PEER |
                       SPC_X509STORE_SSL_VERIFY_FAIL_IF_NO_PEER_CERT);
ctx = spc_create_sslctx(&spc_store);
/* use the ctx to establish a connection.  This will yield an SSL object */
cert = SSL_get_peer_certificate(ssl);
digest = EVP_sha1(  );
fingerprint_length = sizeof(fingerprint);
spc_fingerprint_cert(cert, digest, fingerprint, &fingerprint_length);
/* use the fingerprint to compare against the list of known cert fingerprints */
    [ Team LiB ] Previous Section Next Section