Windows下驗證https證書
最近在寫一個Windows桌面程序需要給https請求加上證書驗證,使用的http庫是libcurl+openssl,使用openssl自帶的證書驗證功能,只能內嵌CA證書,但是我的程序不方便更新,所以最好的方式是使用Windows的證書存儲做驗證,這裡有兩種方式。
遍歷Windows信任證書,將這些證書加入到證書存儲區
使用Windows介面驗證證書鏈
遍歷Windows信任證書,將這些證書加入到證書存儲區
這種方式的缺點是如果Windows沒有安裝服務端使用的CA證書,驗證會失敗。
void addCertificatesForStore(X509_STORE *certStore,const char *subSystemName)
{
HCERTSTORE storeHandle = NULL;
PCCERT_CONTEXT windowsCertificate = nullptr;
do
{
HCERTSTORE storeHandle = CertOpenSystemStoreA(NULL, subSystemName);
if (!storeHandle) {
break;
}
while (windowsCertificate=CertEnumCertificatesInStore(storeHandle, windowsCertificate)) {
X509 *opensslCertificate = d2i_X509(nullptr, const_cast<unsigned char const **>(&windowsCertificate->pbCertEncoded),
windowsCertificate->cbCertEncoded);
if (opensslCertificate) {
X509_STORE_add_cert(certStore, opensslCertificate);
X509_free(opensslCertificate);
}
}
} while (false);
if (storeHandle) {
CertCloseStore(storeHandle, 0);
}
}
int sslContextFunction(void* curl, void* sslctx, void* userdata)
{
auto certStore = SSL_CTX_get_cert_store(reinterpret_cast<SSL_CTX *>(sslctx));
if (certStore) {
addCertificatesForStore(certStore, "CA");
addCertificatesForStore(certStore, "AuthRoot");
addCertificatesForStore(certStore, "ROOT");
}
return CURLE_OK;
}
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1);
curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, sslContextFunction);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
使用Windows介面驗證證書鏈
這種方式的好處是即使Windows證書不全,也能自動更新,缺點是驗證時間可能會很長。
將libcurl的openssl替換為winssl即可,替換後在實體機上運行正常,但是在虛擬機內新安裝的Win7卻出現了錯誤提示:由於吊銷伺服器已離線,吊銷功能無法檢查吊銷。
打開今日頭條,查看更多圖片原來Windows在驗證證書的時候會默認通過網路獲取CA的CRL(證書吊銷列表),檢查該證書是否已被吊銷。我們可以通過libcurl設置不檢查CRL(這樣做會不安全)。
curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NO_REVOKE);
1
禁用證書吊銷檢查後依然出錯了,顯示15秒超時。
為了知道是哪一步出問題了,我自己寫了驗證證書代碼(使用Windows介面)來代替libcurl的默認實現,代碼可以參考libcurl,php,chromium,使用CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY可以阻止從網路獲取CRL。
static int sslContextFunction(void* curl, void* sslctx, void* userdata)
{
SSL_CTX *sslContext = reinterpret_cast<SSL_CTX *>(sslctx);
SSL_CTX_set_verify(sslContext, SSL_VERIFY_PEER, NULL);
SSL_CTX_set_cert_verify_callback(sslContext, sslVerifyCallback, userdata);
return CURLE_OK;
}
static int sslVerifyCallback(X509_STORE_CTX *x509_store_ctx, void *arg)
{
BOOL ret = FALSE;
PCCERT_CONTEXT certCtx = nullptr;
PCCERT_CHAIN_CONTEXT certChainCtx = nullptr;
unsigned char *derBuf = nullptr;
unsigned char *certNameUtf8 = nullptr;
int derLen;
#if OPENSSL_VERSION_NUMBER < 0x10100000L
X509 *cert = x509_store_ctx->cert;
#else
X509 *cert = X509_STORE_CTX_get0_cert(x509_store_ctx);
#endif
do
{
/* First convert the x509 struct back to a DER encoded buffer and let Windows decode it into a form it can work with */
derLen = i2d_X509(cert, &derBuf);
if (derLen < 0) {
LOG_ERROR("encoding X509 certificate failed");
break;
}
certCtx = CertCreateCertificateContext(X509_ASN_ENCODING, derBuf, derLen);
if (certCtx == NULL) {
LOG_ERROR("creating certificate context failed");
break;
}
/* Next fetch the relevant cert chain from the store */
CERT_ENHKEY_USAGE enhkeyUsage = { 0 };
CERT_USAGE_MATCH certUsage = { 0 };
CERT_CHAIN_PARA chainParams = { sizeof(CERT_CHAIN_PARA) };
LPSTR usages[] = { szOID_PKIX_KP_SERVER_AUTH, szOID_SERVER_GATED_CRYPTO, szOID_SGC_NETSCAPE };
enhkeyUsage.cUsageIdentifier = 3;
enhkeyUsage.rgpszUsageIdentifier = usages;
certUsage.dwType = USAGE_MATCH_TYPE_OR;
certUsage.Usage = enhkeyUsage;
chainParams.RequestedUsage = certUsage;
DWORD chainFlags = CERT_CHAIN_CACHE_END_CERT|CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT;
if (!CertGetCertificateChain(NULL, certCtx, NULL, certCtx->hCertStore, &chainParams, chainFlags, NULL, &certChainCtx)) {
LOG_ERROR("getting certificate chain failed");
break;
}
if (certChainCtx) {
LOG_INFO("cert chain context error status:%08x,info status:%08x", certChainCtx->TrustStatus.dwErrorStatus,
certChainCtx->TrustStatus.dwInfoStatus);
}
/* Then verify it against a policy */
auto certName = X509_get_subject_name(cert);
auto index = X509_NAME_get_index_by_NID(certName, NID_commonName, -1);
if (index < 0) {
LOG_ERROR("unable to locate certificate CN");
break;
}
ASN1_STRING_to_UTF8(&certNameUtf8, X509_NAME_ENTRY_get_data(X509_NAME_get_entry(certName, index)));
std::wstring serverName;
if (!StrUtils::utf8ToUnicode(serverName, (char*)(certNameUtf8))) {
LOG_ERROR("unable to convert cert name to wide character string");
break;
}
SSL_EXTRA_CERT_CHAIN_POLICY_PARA sslPolicyParams = { sizeof(SSL_EXTRA_CERT_CHAIN_POLICY_PARA) };
CERT_CHAIN_POLICY_PARA chainPolicyParams = { sizeof(CERT_CHAIN_POLICY_PARA) };
CERT_CHAIN_POLICY_STATUS chainPolicyStatus = { sizeof(CERT_CHAIN_POLICY_STATUS) };
sslPolicyParams.dwAuthType = AUTHTYPE_SERVER;
sslPolicyParams.pwszServerName =const_cast<wchar_t*>(serverName.c_str());
sslPolicyParams.fdwChecks =0x00001000; // SECURITY_FLAG_IGNORE_CERT_CN_INVALID
chainPolicyParams.pvExtraPolicyPara = &sslPolicyParams;
chainPolicyParams.dwFlags = CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS;
auto verifyResult = CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, certChainCtx, &chainPolicyParams, &chainPolicyStatus);
if (verifyResult && chainPolicyStatus.dwError == ERROR_SUCCESS) {
ret = TRUE;
} else {
if (verifyResult) {
LOG_ERROR("check cert chain policy failed with errorcode:%08x", chainPolicyStatus.dwError);
} else {
LOG_ERROR("unable check cert chain policy");
}
}
} while (false);
if (derBuf) {
OPENSSL_free(derBuf);
}
if (certNameUtf8) {
OPENSSL_free(certNameUtf8);
}
if (certCtx) {
CertFreeCertificateContext(certCtx);
}
if (certChainCtx) {
CertFreeCertificateChain(certChainCtx);
}
return ret;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
重新運行發現,CertGetCertificateChain會阻塞30秒然後返回錯誤碼0x01010040,即CERT_TRUST_REVOCATION_STATUS_UNKNOWN|CERT_TRUST_IS_OFFLINE_REVOCATION|CERT_TRUST_IS_PARTIAL_CHAIN,前面兩個flag是證書吊銷信息可以不管,最後一個flag的意思是證書鏈未完成;啟用CAPI2日誌重新運行,發現很多錯誤,原來是從網路獲取CTL(證書信任列表)以及根證書時超時了。
為什麼要從網路獲取CTL以及根證書呢,運行certmgr.msc可以發現新裝的Win7根證書很少,當遇到系統沒有的根證書時就需要查詢證書信任列表以及下載根證書。但是手動在瀏覽器上下載CTL和根證書發現一點也不慢,很正常。
調用CertGetCertificateChain時抓取程序dump,發現程序阻塞在了WinHttpGetProxyForUrl,原來crypt介面使用winhttp來獲取CTL和根證書,在發起請求前會使用WinHttpGetProxyForUrl獲取代理信息。
WinHttpGetProxyForUrl為什麼會一直阻塞呢,使用IDA分析發現,WinHttpGetProxyForUrl內部會使用RPC調用WPAD服務查詢代理信息,然後調用WaitForMultipleObjects等待返回,所以歸根到底是WPAD服務阻塞了。
Win7默認開啟了WPAD(WinHTTP Web Proxy Auto-Discovery Service),該服務可以讓程序自動發現代理伺服器,WPAD 可以藉助 DNS 伺服器或 DHCP 伺服器來查詢代理自動配置(PAC)文件的位置。關閉掉Internet選項-連接-區域網設置-自動檢測設置或者禁用WPAD服務後重新運行,發現正常了。
為什麼WPAD服務會阻塞呢,調用CertGetCertificateChain時抓取WPAD服務dump,發現WPAD會調用gethostname獲取本地主機名,然後調用getaddrinfo解析,最終阻塞在了Nbt_ResolveName。
查閱資料發現解析本地主機名超時和netbios以及dhcp有關係,虛擬機下的Windows本地連接會生成一個「連接特定的DNS後綴」localdomain,生成手動填寫本地連接ip地址也正常了。
---------------------
作者:土豆吞噬者
原文:https://blog.csdn.net/xiongya8888/article/details/86419588
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
※如何修改request參數值並應用到springMvc中
※Crunch團隊分享SpringCloud微服務的使用經驗
TAG:程序員小新人學習 |