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);

使用Windows接口验证证书链

这种方式的好处是即使Windows证书不全,也能自动更新,缺点是验证时间可能会很长。

将libcurl的openssl替换为winssl即可,替换后在实体机上运行正常,但是在虚拟机内新安装的Win7却出现了错误提示:由于吊销服务器已脱机,吊销功能无法检查吊销。


原来Windows在验证证书的时候会默认通过网络获取CA的CRL(证书吊销列表),检查该证书是否已被吊销。我们可以通过libcurl设置不检查CRL(这样做会不安全)。

curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NO_REVOKE);

禁用证书吊销检查后依然出错了,显示15秒超时。


为了知道是哪一步出问题了,我自己写了验证证书代码(使用Windows接口)来代替libcurl的默认实现,代码可以参考libcurlphpchromium,使用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;
}

重新运行发现,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地址也正常了。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。