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 */
|