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地址也正常了。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335