WinHTTP 中的 SSL

Microsoft Windows HTTP Services (WinHTTP) 支持安全套接字层 (SSL) 事务,包括客户端证书。 本主题介绍 SSL 事务所涉及的概念,以及如何使用 WinHTTP 处理它们。

安全套接字层 (Secure Sockets Layer)

SSL 是确保安全 HTTP 事务的既定标准。 SSL 提供一种机制,用于对客户端和服务器之间的所有事务执行高达 128 位加密。 它使客户端能够使用服务器证书来验证服务器是否属于受信任的实体。 它还使服务器能够使用客户端证书确认客户端的标识。

在 SSL 握手中协商加密、服务器标识和客户端标识时,客户端首先从安全超文本传输协议 (HTTPS) 服务器请求资源时,会协商这些问题中的每一个问题。 实质上,客户端和服务器都提供了必需和首选设置的列表。 如果可以同意并满足一组常见的要求,则会建立 SSL 连接。

WinHTTP 提供用于使用 SSL 的高级接口。 虽然 SSL 握手和事务的详细信息在内部处理,但 WinHTTP 使你能够检索加密级别,指定安全协议,并与服务器和客户端证书交互。 以下部分提供有关创建基于 WinHTTP 的应用程序的详细信息,这些应用程序选择 SSL 协议版本、检查服务器证书,以及选择要发送到 HTTPS 服务器的客户端证书。

服务器证书

服务器证书从服务器发送到客户端,以便客户端可以获取服务器的公钥,并确保服务器已由证书颁发机构验证。 证书可以包含不同类型的数据。 例如,X.509 证书包括证书的格式、证书序列号、用于对证书签名的算法、颁发证书的证书颁发机构的名称 (CA) 颁发证书、请求证书的实体的名称和公钥以及 CA 的签名。

使用 WinHTTP 应用程序编程接口 (API) 时,可以通过调用 WinHttpQueryOption 并指定 WINHTTP_OPTION_SECURITY_CERTIFICATE_STRUCT 标志来检索服务器证书。 服务器证书在 WINHTTP_CERTIFICATE_INFO 结构中返回。 如果希望检索证书上下文,请改为指定 WINHTTP_OPTION_SERVER_CERT_CONTEXT 标志。

如果服务器证书包含错误,则可以在状态回调函数中获取有关该错误的详细信息。 WINHTTP_CALLBACK_STATUS_SECURE_FAILURE通知指示服务器证书出错。 lpvStatusInformation 参数包含一个或多个详细的错误标志。 有关详细信息 ,请参阅WINHTTP_STATUS_CALLBACK

客户端证书

在 SSL 握手期间,服务器可能需要身份验证。 客户端通过向服务器提供有效的客户端证书进行身份验证。 使用 WinHTTP,可以从本地 证书存储选择和发送证书。 以下部分介绍使用 WinHTTP API 或 WinHttpRequest 对象时提供客户端证书的过程。

WinHTTP API

WinHttpSendRequestWinHttpReceiveResponse 都无法指示请求失败,因为 HTTPS 服务器需要身份验证。 在这些情况下,调用 GetLastError 以返回ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED。 收到此错误后,请使用相应的 CryptoAPI 函数查找相应的证书。 指示此证书应通过调用具有WINHTTP_OPTION_CLIENT_CERT_CONTEXT标志的 WinHttpSetOption 来发送下一个请求。

下面的代码示例演示如何在返回ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED错误后打开 证书存储 并基于使用者名称找到证书。

  if( !WinHttpReceiveResponse( hRequest, NULL ) )
  {
    if( GetLastError( ) == ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED )
    {
      //MY is the store the certificate is in.
      hMyStore = CertOpenSystemStore( 0, TEXT("MY") );
      if( hMyStore )
      {
        pCertContext = CertFindCertificateInStore( hMyStore,
             X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
             0,
             CERT_FIND_SUBJECT_STR,
             (LPVOID) szCertName, //Subject string in the certificate.
             NULL );
        if( pCertContext )
        {
          WinHttpSetOption( hRequest, 
                            WINHTTP_OPTION_CLIENT_CERT_CONTEXT,
                            (LPVOID) pCertContext, 
                            sizeof(CERT_CONTEXT) );
          CertFreeCertificateContext( pCertContext );
        }
        CertCloseStore( hMyStore, 0 );

        // NOTE: Application should now resend the request.
      }
    }
  }

在重新发送包含客户端证书的请求之前,可以确定应用程序是否可接受的加密级别。 调用 WinHttpQueryOption 并指定 WINHTTP_OPTION_SECURITY_FLAGS 标志以确定所使用的加密级别。

SSL 客户端身份验证的颁发者列表检索

当 WinHttp 客户端应用程序向需要 SSL 客户端身份验证的安全 HTTP 服务器发送请求时,如果应用程序未提供客户端证书,WinHttp 将返回 ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED 。 对于在 Windows Server 2008 和 Windows Vista 上运行的计算机,WinHttp 使应用程序能够检索身份验证质询中服务器提供的证书颁发者列表。 颁发者列表指定由服务器授权颁发客户端证书的证书颁发机构列表 (CA) 。 应用程序筛选颁发者列表以获取所需的证书。

WinHttpSendRequestWinHttpReceiveResponse 返回 ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED时,WinHttp 客户端应用程序将检索颁发者列表。 返回此错误后,应用程序使用 WINHTTP_OPTION_CLIENT_CERT_ISSUER_LIST 选项调用 WinHttpQueryOptionlpBuffer 参数必须足够大,才能包含指向SecPkgContext_IssuerListInfoEx结构的指针。 下面的代码示例演示如何检索颁发者列表。

#include <windows.h>
#include <winhttp.h>
#include <schannel.h>

//...

void GetIssuerList(HINTERNET hRequest)
{
  SecPkgContext_IssuerListInfoEx* pIssuerList = NULL;
  DWORD dwBufferSize = sizeof(SecPkgContext_IssuerListInfoEx*);

  if (WinHttpQueryOption(hRequest,
           WINHTTP_OPTION_CLIENT_CERT_ISSUER_LIST,
           &pIssuerList,
           &dwBufferSize) == TRUE)
  {
    // Use the pIssuerList for cert store filtering.
    GlobalFree(pIssuerList); // Free the issuer list when done.
  }
}

SecPkgContext_IssuerListInfoEx结构、cIssuersaIssuers 中的信息可用于搜索证书,如下面的代码示例所示。 有关详细信息,请参阅 CertFindChainInStore

PCERT_CONTEXT pClientCert = NULL;
PCCERT_CHAIN_CONTEXT pClientCertChain = NULL;

CERT_CHAIN_FIND_BY_ISSUER_PARA SrchCriteria;
::ZeroMemory(&SrchCriteria, sizeof(CERT_CHAIN_FIND_BY_ISSUER_PARA));
SrchCriteria.cbSize = sizeof(CERT_CHAIN_FIND_BY_ISSUER_PARA);

SrchCriteria.cIssuer = pIssuerList->cIssuers;
SrchCriteria.rgIssuer = pIssuerList->aIssuers;

pClientCertChain = CertFindChainInStore(
            hClientCertStore,
            X509_ASN_ENCODING,
            CERT_CHAIN_FIND_BY_ISSUER_CACHE_ONLY_URL_FLAG |
            // Do not perform wire download when building chains.
            CERT_CHAIN_FIND_BY_ISSUER_CACHE_ONLY_FLAG,
            // Do not search pCacheEntry->_ClientCertStore 
            // for issuer certs.
            CERT_CHAIN_FIND_BY_ISSUER,
            &SrchCriteria,
            NULL);

if (pClientCertChain)
{
    pClientCert = (PCERT_CONTEXT) pClientCertChain->rgpChain[0]->rgpElement[0]->pCertContext;

    CertDuplicateCertificateContext(pClientCert);

    CertFreeCertificateChain(pClientCertChain);

    pClientCertChain = NULL;
}

可选客户端 SSL 证书

从 Windows Server 2008 和 Windows Vista 开始,WinHttp API 支持可选的客户端证书。 当服务器请求客户端证书时, WinHttpSendRequestWinHttpRecieveResponse 将返回 ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED 错误。 如果服务器请求证书,但不要求证书,则应用程序可以指定此选项来指示它没有证书。 服务器可以选择其他身份验证方案,或允许匿名访问服务器。 应用程序在 WinHttpSetOptionlpBuffer 参数中指定WINHTTP_NO_CLIENT_CERT_CONTEXT宏,如以下代码示例所示。

BOOL fRet = WinHttpSetOption ( hRequest,
                               WINHTTP_OPTION_CLIENT_CERT_CONTEXT,
                               WINHTTP_NO_CLIENT_CERT_CONTEXT,
                               0);

如果设置了 WINHTTP_NO_CLIENT_CERT_CONTEXT ,并且服务器仍需要客户端证书,则可能会发送 403 HTTP 状态代码。 有关详细信息,请参阅 WINHTTP_OPTION_CLIENT_CERT_ISSUER_LIST 选项。

WinHttpRequest 对象

使用 WinHttpRequest 对象的 SetClientCertificate 方法选择要通过请求发送到服务器的客户端证书。 通过使用 SetClientCertificate 方法指定证书选择字符串来选择证书。 证书选择字符串由由反斜杠分隔的证书位置、 证书存储和使用者名称组成。 下表列出了此选择字符串的组件。

组件 说明 可能值
位置 确定在其中存储证书的注册表项。 可能的值是“LOCAL_MACHINE”,指示 证书存储 位于 HKEY_LOCAL_MACHINE
和“CURRENT_USER”,指示 证书存储 位于非模拟HKEY_CURRENT_USER下。
此组件区分大小写。
证书存储 指示包含相关证书的 证书存储 的名称。 典型的 证书存储 是“MY”、“Root”和“TrustedPeople”。 此组件区分大小写。
使用者名称 标识指定 证书存储中的证书。 选择包含为此组件指定的字符串的第一个证书。 使用者名称可以是任何字符串。 空字符串指示应使用 证书存储 中的第一个证书。 此组件不区分大小写。

证书存储名称和位置是可选的组件。 但是,如果指定 证书存储,还必须指定该 证书存储的位置。 默认位置CURRENT_USER,默认 证书存储 为“MY”。

下面的代码示例演示如何指定使用者为“我的Middle-Tier证书”的证书应从HKEY_LOCAL_MACHINE下的注册表中的“个人”证书存储中选择。

HttpReq.SetClientCertificate("LOCAL_MACHINE\Personal\My Middle-Tier Certificate")

注意

在某些语言中,反斜杠是转义字符。 请记得修改证书选择字符串来考虑这一点。 例如,在 Microsoft JScript中,使用两个相邻反斜杠,而不是一个。

如果未指定证书,并且 HTTPS 服务器需要客户端证书,WinHTTP 将在默认 证书存储中选择第一个证书。 如果不存在证书,则会引发错误。 如果不接受证书,服务器将返回 403 状态代码,以指示无法满足请求。 然后,可以使用 SetClientCertificate 选择更合适的证书并重新发送请求。