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.
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.
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.