10.8 Adding Hostname Checking to Certificate Verification

10.8.1 Problem

You have a certificate that has passed initial verification checks as described in Recipe 10.4. Now you want to make sure that it was issued to the host that is claiming ownership of it.

10.8.2 Solution

A certificate often contains a commonName field, and many certificates contain a subjectAltName extension, although neither is required. Normally, when a server presents a certificate, the commonly accepted convention is for either the commonName or the subjectAltName to contain the hostname of the server that is presenting it. Often, if both fields are present, they will contain the same information. If both fields are present and they contain different information, it is most likely because the commonName field contains some information other than a hostname. Even if both fields contain hostnames, the subjectAltName field should always take precedence over the commonName field. Certificate extensions were added to the X.509 standard in Version 3, so older certificates use the commonName field, while newer ones use the subjectAltName extension.

10.8.3 Discussion

The basic certificate verification, as described in Recipe 10.4, is the hard part of verifying a certificate. It ensures that the certificate is valid for the dates it was issued (i.e., the current date is within the certificate's start and end dates), it has not been revoked (provided that you have the relevant CRL), and it was signed by a trusted CA. Now you must make sure that the certificate is valid for the site that is claiming ownership of it. If you do not, any site could present you with Microsoft's certificate, claiming it as their own, and it would successfully verify.

When new certificates are issued, use of the subjectAltName extension is preferred over use of the commonName field, so that should be checked first. If no subjectAltName extension is present, the commonName field should be checked instead. When a subjectAltName is present but does not match, verification of the certificate should fail. Likewise, if the commonName field is checked and it does not match, verification of the certificate should fail. In either case, communication with the peer should be terminated if verification of its certificate fails.

What we have described thus far, particularly in regard to the subjectAltName extension, is simplified a great deal. The subjectAltName extension is actually a container that may contain several different fields, each one responsible for different information. For our purposes, and the purposes of verifying the hostname within a certificate, we are only interested in the dnsName field. When we say that a subjectAltName extension is either present or absent, we are actually concerned with the presence or absence of the dnsName field within the subjectAltName field. In other words, if a subjectAltName extension is present but does not contain a dnsName field, we say that the subjectAltName extension is absent.

If you are using OpenSSL, you will normally have a certificate as an X509 object. The following code will check the hostname in that object:

#include <string.h>
#include <openssl/conf.h>
#include <openssl/x509v3.h>
   
int spc_verify_cert_hostname(X509 *cert, char *hostname) {
  int                   extcount, i, j, ok = 0;
  char                  name[256];
  X509_NAME             *subj;
  const char            *extstr;
  CONF_VALUE            *nval;
  unsigned char         *data;
  X509_EXTENSION        *ext;
  X509V3_EXT_METHOD     *meth;
  STACK_OF(CONF_VALUE)  *val;
   
  if ((extcount = X509_get_ext_count(cert)) > 0) {
    for (i = 0;  !ok && i < extcount;  i++) {
      ext = X509_get_ext(cert, i);
      extstr = OBJ_nid2sn(OBJ_obj2nid(X509_EXTENSION_get_object(ext)));
      if (!strcasecmp(extstr, "subjectAltName")) {
        if (!(meth = X509V3_EXT_get(ext))) break;
        data = ext->value->data;
   
        val = meth->i2v(meth, meth->d2i(0, &data, ext->value->length), 0);
        for (j = 0;  j < sk_CONF_VALUE_num(val);  j++) {
          nval = sk_CONF_VALUE_value(val, j);
          if (!strcasecmp(nval->name, "DNS") && !strcasecmp(nval->value, hostname)) {
            ok = 1;
            break;
          }
        }
      }
    }
  }
   
  if (!ok && (subj = X509_get_subject_name(cert)) &&
      X509_NAME_get_text_by_NID(subj, NID_commonName, name, sizeof(name)) > 0) {
    name[sizeof(name) - 1] = '\0';
    if (!strcasecmp(name, hostname)) ok = 1;
  }
   
  return ok;
}

If you are using CryptoAPI on Windows, you will normally have a certificate as a CERT_CONTEXT object. The following code checks the hostname in that object:

#include <windows.h>
#include <wincrypt.h>
   
static LPWSTR fold_wide(LPWSTR str) {
  int     len;
  LPWSTR  wstr;
   
  if (!(len = FoldStringW(MAP_PRECOMPOSED, str, -1, 0, 0))) return 0;
  if (!(wstr = (LPWSTR)LocalAlloc(LMEM_FIXED, len * sizeof(WCHAR))))
    return 0;
  if (!FoldStringW(MAP_PRECOMPOSED, str, -1, wstr, len)) {
    LocalFree(wstr);
    return 0;
  }
   
  return wstr;
}
   
static LPWSTR make_wide(LPCTSTR str) {
#ifndef UNICODE
  int     len;
  LPWSTR  wstr;
   
  if (!(len = MultiByteToWideChar(CP_UTF8, 0, str, -1, 0, 0)))
    return 0;
  if (!(wstr = (LPWSTR)LocalAlloc(LMEM_FIXED, len * sizeof(WCHAR))))
    return 0;
  if (!MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, len)) {
    LocalFree(wstr);
    return 0;
  }
   
  return wstr;
#else
  return fold_wide(str);
#endif
}
   
BOOL SpcVerifyCertHostName(PCCERT_CONTEXT pCertContext, LPCTSTR hostname) {
  BOOL               bResult = FALSE;
  DWORD              cbStructInfo, dwCommonNameLength, i;
  LPSTR              szOID;
  LPVOID             pvStructInfo;
  LPWSTR             lpszCommonName, lpszDNSName, lpszHostName, lpszTemp;
  CERT_EXTENSION     *pExtension;
  CERT_ALT_NAME_INFO *pNameInfo;
   
  if (!(lpszHostName = make_wide(hostname))) return FALSE;
   
  /* Try SUBJECT_ALT_NAME2 first - it supercedes SUBJECT_ALT_NAME */
  szOID = szOID_SUBJECT_ALT_NAME2;
  pExtension = CertFindExtension(szOID, pCertContext->pCertInfo->cExtension,
                                 pCertContext->pCertInfo->rgExtension);
  if (!pExtension) {
    szOID = szOID_SUBJECT_ALT_NAME;
    pExtension = CertFindExtension(szOID, pCertContext->pCertInfo->cExtension,
                                   pCertContext->pCertInfo->rgExtension);
  }
   
  if (pExtension && CryptDecodeObject(X509_ASN_ENCODING, szOID,
      pExtension->Value.pbData, pExtension->Value.cbData, 0, 0, &cbStructInfo)) {
    if ((pvStructInfo = LocalAlloc(LMEM_FIXED, cbStructInfo)) != 0) {
      CryptDecodeObject(X509_ASN_ENCODING, szOID, pExtension->Value.pbData,
                        pExtension->Value.cbData, 0, pvStructInfo, &cbStructInfo);
      pNameInfo = (CERT_ALT_NAME_INFO *)pvStructInfo;
      for (i = 0;  !bResult && i < pNameInfo->cAltEntry;  i++) {
        if (pNameInfo->rgAltEntry[i].dwAltNameChoice =  = CERT_ALT_NAME_DNS_NAME) {
          if (!(lpszDNSName = fold_wide(pNameInfo->rgAltEntry[i].pwszDNSName)))
            break;
          if (CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE, lpszDNSName,
                             -1, lpszHostName, -1) =  = CSTR_EQUAL)
            bResult = TRUE;
          LocalFree(lpszDNSName);
        }
      }
      LocalFree(pvStructInfo);
      LocalFree(lpszHostName);
      return bResult;
    }
  }
   
  /* No subjectAltName extension -- check commonName */
  dwCommonNameLength = CertGetNameStringW(pCertContext, CERT_NAME_ATTR_TYPE, 0,
                                         szOID_COMMON_NAME, 0, 0);
  if (!dwCommonNameLength) {
    LocalFree(lpszHostName);
    return FALSE;
  }
  lpszTemp = (LPWSTR)LocalAlloc(LMEM_FIXED, dwCommonNameLength * sizeof(WCHAR));
  if (lpszTemp) {
    CertGetNameStringW(pCertContext, CERT_NAME_ATTR_TYPE, 0, szOID_COMMON_NAME,
                       lpszTemp, dwCommonNameLength);
    if ((lpszCommonName = fold_wide(lpszTemp)) != 0) {
      if (CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE, lpszCommonName,
                         -1, lpszHostName, -1) =  = CSTR_EQUAL)
        bResult = TRUE;
      LocalFree(lpszCommonName);
    }
    LocalFree(lpszTemp);
  }
   
  LocalFree(lpszHostName);
  return bResult;
}

Unfortunately, if you are using a version of the Microsoft Windows Platform SDK older than the .NET version, you will experience difficulties compiling and linking this code into your program. The older wincrypt.h header file and crypt32.lib import library are missing the definitions required to use CertGetNameStringW( ), even though they are documented to be available in versions prior to .NET. The definitions required for your code are:

#ifndef CERT_NAME_ATTR_TYPE
WINCRYPT32API
DWORD
WINAPI
CertGetNameStringW(
    IN PCCERT_CONTEXT pCertIntext,
    IN DWORD dwType,
    IN DWORD dwFlags,
    IN void *pvTypePara,
    OUT OPTIONAL LPWSTR pszNameString,
    IN DWORD cchNameString
    );
   
#define CERT_NAME_ATTR_TYPE 3
#endif

CertGetNameStringW( ) is exported from all versions of crypt32.dll that are included with Microsoft Internet Explorer 3.02 or later. You may run into problems linking, however, because the import is missing from crypt32.lib. In our testing, we have experienced no problems using the crypt32.lib distributed with the latest Microsoft Windows Platform SDK. Unfortunately, we have been unable to find an alternative method of obtaining the contents of the commonName field in a certificate other than using this function.

10.8.4 See Also

Recipe 10.4