SChannel

安全通道 (Schannel) 安全包(其身份验证服务标识符为 RPC_C_AUTHN_GSS_SCHANNEL)支持以下基于公钥的协议 SSL(安全套接字层)版本 2.0 和 3.0、传输层安全 (TLS) 1.0 和专用通信技术 (PCT) 1.0。 TLS 1.0 是 Internet 工程任务组 (IETF) 于 1999 年 1 月在 RFC 2246 文档中发放的标准化、稍作修改的 SSL 3.0 版本。 TLS 已标准化,因此鼓励开发人员使用 TLS 而不是 SSL。 包含 PCT 只是为了向后兼容,不应用于新开发。 使用 Schannel 安全包时,DCOM 会根据客户端和服务器功能自动协商最佳协议。

以下主题简要介绍了 TLS 协议及其与 DCOM 配合使用的方式。

注意

这些部分中有关 TLS 协议的所有信息也适用于 SSL 和 PCT 协议。

 

何时使用 TLS

当服务器需要向匿名客户端证明其身份时,TLS 是唯一可用的安全选项。 对于想要参与电子商务的网站来说,这尤其重要,因为它有助于保护敏感信息(如信用卡号)的传输。 TLS 确保电子商务客户可以使用服务器的标识证明确定正在与谁开展业务。 它还通过对每个客户进行身份验证确保电子商务服务器有效运行。

TLS 要求所有服务器向客户端证明其身份。 此外,TLS 还提供让客户端向服务器证明其身份的选项。 这种相互身份验证可用于限制大型企业 Intranet 中某些网页的访问。

TLS 支持最强大的身份验证级别,并提供一个开放体系结构,允许加密强度随时间推移而增加,以跟上技术创新。 TLS 是传输中数据所需的最高安全级别环境的最佳选择。

TLS 工作原理简要概述

TLS 基于公钥基础结构 (PKI) 构建,该基础结构使用公钥/私钥对来启用数据加密和建立数据完整性,并使用 X.509 证书进行身份验证。

许多安全协议(如 Kerberos v5 协议)依赖一个密钥来加密和解密数据。 因此,此类协议取决于加密密钥的安全交换;在 Kerberos 协议中,这是通过从密钥分发中心 (KDC) 获取的票证完成的。 这要求使用 Kerberos 协议的每个人都注册到 KDC,这对旨在吸引世界各地数百万客户的电子商务 Web 服务器来说是一个不切实际的限制。 因此,TLS 依赖于 PKI,它使用两个密钥进行数据加密。当密钥对的一个密钥加密数据时,只有密钥对的另一个密钥可以对其进行解密。 此设计的主要优点是,无需安全交换加密密钥即可执行加密。

PKI 使用一种技术,其中一个密钥保持私有,并且仅可供注册的主体使用,而另一个密钥则公开可供任何人访问。 如果有人想要向密钥对的所有者发送私信,则可以使用公钥加密该消息,并且只能使用私钥来解密消息。

密钥对还用于验证要发送数据的完整性。 为此,密钥对的所有者可以在发送数据之前将数字签名附加到数据。 创建数字签名涉及计算数据的哈希,并使用私钥加密哈希。 任何使用公钥解密数字签名的人都确信数字签名必须由拥有私钥的人员进行。 此外,收件人可以使用与发送方相同的算法来计算数据的哈希,如果计算的哈希与数字签名中发送的哈希匹配,则收件人可以确定数据在经过数字签名后未修改。

使用 PKI 进行大容量数据加密的一个缺点是其性能相对较慢。 由于涉及大量数学,使用依赖密钥对的非对称密码加密和解密数据的速度可能比使用仅依赖单个密钥的对称密码慢 1,000 倍。 因此,TLS 仅使用 PKI 生成数字签名,并协商客户端和服务器将用于批量数据加密和解密的特定于会话的单一密钥。 TLS 支持各种单密钥对称密码,将来可能会增加其他密码。

有关 TLS 握手协议的详细信息,请参阅 TLS 握手协议

有关支持 TLS 协议的加密的更多详细信息,请参阅加密概要

X.509 证书

PKI 必须处理的关键问题是能够信任正在使用的公钥的真实性。 当使用向想要与之开展业务的公司发放的公钥时,需要确定密钥实际上属于公司,而不是想要发现信用卡号的小偷。

为了保证持有密钥对的主体的身份,由证书颁发机构 (CA) 向该主体颁发 X.509 证书。 此证书包含标识主体的信息,包含主体的公钥,并由 CA 进行数字签名。 此数字签名表示 CA 认为证书中包含的公钥确实属于证书标识的主体。

如何信任 CA? 因为 CA 本身包含由更高级别的 CA 签名的 X.509 证书。 此证书签名链一直持续到达根 CA,这是对其自己的证书进行签名的 CA。 如果信任证书根 CA 的完整性,则应能够信任证书本身的真实性。 因此,选择愿意信任的根 CA 是系统管理员的重要职责。

客户端证书

首次出现安全传输层协议时,其主要目的是保证客户端连接到真实可信的服务器,并帮助在传输过程中保护数据隐私。 但是,SSL 3.0 和 TLS 1.0 还包括在协议握手期间传输客户端证书的支持。 此可选功能可实现客户端和服务器的相互身份验证。

是否使用客户端证书应在应用程序的上下文中决定。 如果主要要求是对服务器进行身份验证,则不需要客户端证书。 但是,如果客户端身份验证至关重要,则可以使用客户端的证书,而不是依赖于应用程序中的自定义身份验证。 使用为用户提供单一登录方案的客户端证书优于自定义身份验证。

在 TLS 中使用 TLS

TLS 仅支持模拟 (RPC_C_IMP_LEVEL_IMPERSONATE) 级别的模拟。 如果 COM 协商 TLS 作为代理上的身份验证服务,COM 会将模拟级别设置为模拟,而不考虑进程默认值。 要使模拟在 TLS 中正常工作,客户端必须向服务器提供 X.509 证书,并且服务器必须将该证书映射到服务器上的特定用户帐户。 有关详细信息,请参阅将证书映射到用户帐户的分步指南

TLS 不支持掩蔽。 如果在 CoInitializeSecurityIClientSecurity::SetBlanket 调用中指定了掩蔽标志和 TLS,将返回 E_INVALIDARG。

TLS 不适用于设置为“无”的身份验证级别。 客户端和服务器之间的握手将检查每个客户端设置的身份验证级别,并为连接选择更高的安全设置。

可以通过调用 CoInitializeSecurityCoSetProxyBlanket 来设置 TLS 的安全参数。 以下部分介绍这些调用的细微差别。

服务器如何设置安全毯

如果服务器想要使用 TLS,则必须在 CoInitializeSecurityasAuthSvc 参数中将 Schannel (RPC_C_AUTHN_GSS_SCHANNEL) 指定为身份验证服务。 为了防止客户端使用不太安全的身份验证服务连接到服务器,服务器应在调用 CoInitializeSecurity 时仅将 Schannel 指定为身份验证服务。 服务器在调用 CoInitializeSecurity 后无法更改安全毯。

若要使用 TLS,应在服务器调用 CoInitializeSecurity 时指定以下参数:

  • pVoid 应是指向 IAccessControl 对象的指针或指向 SECURITY_DESCRIPTOR 的指针。 不应为 NULL 或指向 AppID 的指针。
  • cAuthSvc 不能为 0 或 -1。 当 cAuthSvc 为 -1 时,COM 服务器永远不会选择 Schannel。
  • asAuthSvc 必须将 Schannel 指定为可能的身份验证服务。 为此,为 SOLE_AUTHENTICATION_LIST 的 Schannel 成员设置以下 SOLE_AUTHENTICATION_SERVICE 参数:
    • dwAuthnSvc 必须是 RPC_C_AUTHN_GSS_SCHANNEL。
    • dwAuthzSvc 应为 RPC_C_AUTHZ_NONE。 目前将被忽略。
    • pPrincipalName 必须是指向 CERT_CONTEXT 的指针,转换为指向 OLECHAR 的指针,表示服务器的 X.509 证书。
  • dwAuthnLevel 指示从客户端接受的最小身份验证级别,以便成功连接。 不能为 RPC_C_AUTHN_LEVEL_NONE。
  • dwCapabilities 不应设置 EOAC_APPID 标志。 如果 pVoid 指向 IAccessControl 对象,则应设置 EOAC_ACCESS_CONTROL 标志;如果 pVoid 指向标准版CURITY_DESCRIPTOR,则不应设置该标志。 有关可能设置的其他标志,请参阅 CoInitializeSecurity

有关使用 CoInitializeSecurity 的详细信息,请参阅使用 CoInitializeSecurity 设置进程范围安全性

客户端如何设置安全毯

如果客户端想要使用 TLS,则必须在 CoInitializeSecuritypAuthList 参数的身份验证服务列表中指定 Schannel (RPC_C_AUTHN_GSS_SCHANNEL)。 如果在调用 CoInitializeSecurity 时未将 Schannel 指定为可能的身份验证服务,则以后尝试将 Schannel 指定为身份验证服务时调用 CoSetProxyBlanket(或 IClientSecurity::SetBlanket)将失败。

客户端调用 CoInitializeSecurity 时,应指定以下参数:

  • dwAuthnLevel 指定客户端要使用的默认身份验证级别。 不能为 RPC_C_AUTHN_LEVEL_NONE。
  • dwImpLevel 必须是 RPC_C_IMP_LEVEL_IMPERSONATE。
  • pAuthList 必须具有以下 SOLE_AUTHENTICATION_INFO 参数作为列表的成员:
    • dwAuthnSvc 必须是 RPC_C_AUTHN_GSS_SCHANNEL。
    • dwAuthzSvc 必须是 RPC_C_AUTHZ_NONE。
    • pAuthInfo 是指向 CERT_CONTEXT 的指针,转换为指向 void 的指针,表示客户端的 X.509 证书。 如果客户端没有证书或不希望将其证书提供给服务器,pAuthInfo 必须为 NULL,并且将尝试与服务器建立匿名连接。
  • dwCapabilities 是一组指示其他客户端功能的标志。 有关应设置哪些标志的信息,请参阅 CoInitializeSecurity

有关使用 CoInitializeSecurity 的详细信息,请参阅使用 CoInitializeSecurity 设置进程范围安全性

客户端如何更改安全毯

如果客户端想要使用 TLS,但在调用 CoInitializeSecurity 后更改安全毯,则必须使用调用 CoInitializeSecurity 时使用的类似参数调用 CoSetProxyBlanketIClientSecurity::SetBlanket,但存在以下差异:

  • pServerPrincName 以 msstd 或 fullsic 格式指示服务器的主体名称。 有关这些格式的信息,请参阅主体名称。 如果客户端具有服务器的 X.509 证书,则可以通过调用 RpcCertGeneratePrincipalName 来查找主体名称。
  • pAuthInfo 是指向 CERT_CONTEXT 的指针,转换为指向 RPC_AUTH_IDENTITY_HANDLE 的指针,表示客户端的 X.509 证书。 如果客户端没有证书或不希望将其证书提供给服务器,pAuthInfo 必须为 NULL,并且将尝试与服务器建立匿名连接。
  • dwCapabilities 包括指示其他客户端功能的标志。 只有四个标志可用于更改安全毯设置:EOAC_DEFAULT、EOAC_MUTUAL_AUTH、EOAC_ANY_AUTHORITY(此标志已弃用)和 EOAC_MAKE_FULLSIC。 有关详细信息,请参阅 CoSetProxyBlanket

有关使用 CoSetProxyBlanket 的详细信息,请参阅在接口代理级别设置安全性

示例:客户端更改安全毯

以下示例演示客户端如何更改安全毯以容纳服务器要求客户端提供其 X.509 证书的请求。 为简洁起见,省略错误处理代码。

void ClientChangesSecurity ()
{
  HCRYPTPROV                   provider           = 0;
  HCERTSTORE                   cert_store         = NULL;
  PCCERT_CONTEXT               client_cert        = NULL;
  PCCERT_CONTEXT               server_cert        = NULL;
  WCHAR                        *server_princ_name = NULL;
  ISecret                      *pSecret           = NULL;
  MULTI_QI                     server_instance;
  COSERVERINFO                 server_machine;
  SOLE_AUTHENTICATION_LIST     auth_list;
  SOLE_AUTHENTICATION_INFO     auth_info[1];



  // Specify all the authentication info. 
  // The client is willing to connect using SChannel,
  //   with no client certificate.
  auth_list.cAuthInfo     = 1;
  auth_list.aAuthInfo     = auth_info;
  auth_info[0].dwAuthnSvc = RPC_C_AUTHN_GSS_SCHANNEL;
  auth_info[0].dwAuthzSvc = RPC_C_AUTHZ_NONE;
  auth_info[0].pAuthInfo  = NULL;  // No certificate

  // Initialize client security with no client certificate.
  CoInitializeSecurity( NULL, -1, NULL, NULL,
                        RPC_C_AUTHN_LEVEL_PKT,
                        RPC_C_IMP_LEVEL_IMPERSONATE, &auth_list,
                        EOAC_NONE, NULL );
  
  // Specify info for the proxy.
  server_instance = {&IID_ISecret, NULL, S_OK};
  server_machine  = {0, L"ServerMachineName", NULL, 0};
  
  // Create a proxy.
  CoCreateInstanceEx( CLSID_Secret, NULL, CLSCTX_REMOTE_SERVER, 
                      &server_machine, 1, &server_instance);
  pSecret = (ISecret *) server_instance.pItf;

  //** The client obtained the server's certificate during the handshake.
  //** The server requests a certificate from the client.

  // Get the default certificate provider.
  CryptAcquireContext( &provider, NULL, NULL, PROV_RSA_SCHANNEL, 0 );

  // Open the certificate store.
  cert_store = CertOpenSystemStore( provider, L"my" );

  // Find the client's certificate.
  client_cert = 
    CertFindCertificateInStore( cert_store,
                                X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
                                0,
                                CERT_FIND_SUBJECT_STR,
                                L"ClientName",  // Use the principal name
                                NULL );

  // Find the fullsic principal name of the server.
  RpcCertGeneratePrincipalName( server_cert, RPC_C_FULL_CERT_CHAIN, 
                                &server_princ_name );

  // Change the client's security: 
  // Increase the authentication level and attach a certificate.
  CoSetProxyBlanket( pSecret, RPC_C_AUTHN_GSS_SCHANNEL,
                     RPC_C_AUTHZ_NONE,
                     server_princ_name, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
                     RPC_C_IMP_LEVEL_IMPERSONATE, 
                     (RPC_AUTH_IDENTITY_HANDLE *) client_cert,
                     EOAC_NONE );

  cleanup:
  if (server_princ_name != NULL)
    RpcStringFree( &server_princ_name );
  if (client_cert != NULL)
    CertFreeCertificateContext(client_cert);
  if (server_cert != NULL)
    CertFreeCertificateContext(server_cert);
  if (cert_store != NULL)
    CertCloseStore( cert_store, CERT_CLOSE_STORE_CHECK_FLAG );
  if (provider != 0 )
    CryptReleaseContext( provider, 0 );
  if (pSecret != NULL)
    pSecret->Release();
  CoUninitialize();
}

COM 和安全包