[ Team LiB ] |
10.8 Adding Hostname Checking to Certificate Verification10.8.1 ProblemYou 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 SolutionA 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 DiscussionThe 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.
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 |
[ Team LiB ] |