65.9K
CodeProject 正在变化。 阅读更多。
Home

Web服务安全入门——第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.72/5 (18投票s)

2004 年 6 月 2 日

27分钟阅读

viewsIcon

156335

downloadIcon

2601

本系列的第二部分介绍了签名和加密原则。它解释了使用 WSE 和 X.509 证书对消息进行数字签名和加密的步骤。

目录

引言

本系列第一篇文章解释了如何使用自定义安全令牌,添加用户名识别/密码验证以确保身份验证。现在让我们处理验证消息的下一种可能性:通过添加二进制安全令牌来使用证书。本文扩展了上一篇文章中构建的示例。

在深入了解实现细节之前,让我们先了解一些基本的安全概念。使用证书,从而使用公钥/私钥技术,可以用于不同的目的,但最可能用于签署消息或加密(部分)消息。这些主题,即消息的签名和加密,构成了本文的内容。
接下来的几段将简要介绍加密、签名和证书。

加密

加密技术可用于确保数据**机密性**。在网络中传输数据对于安全问题来说非常敏感。因此,在数据通过网络之前对其进行加密,并在之后进行解密,可以防止窃听者在传输过程中读取数据。

将明文编码以创建密文的过程称为加密,将密文解码以恢复明文的过程称为解密。

加密有两种类型:**对称密钥加密**和**非对称(公钥)加密**,它们甚至可以结合使用。

对称密钥加密

意味着使用相同的密钥来加密和解密消息。该密钥是秘密密钥,因为它在两个相关实体(发送方和接收方)之间作为**共享秘密**保存。不保密会导致加密数据的机密性受到损害。对称密钥加密通常比公钥加密快得多,最高可达 1000 倍,因为公钥加密需要更高的计算量。

非对称(公钥)加密

基于两个不同的密钥进行加密和解密:**私钥**(仅其所有者知道)和**公钥**(网络上其他实体可用和已知)。公钥通常用于加密消息,保证只有拥有相应私钥的人才能解密消息。

另一种方式是,消息可以使用私钥加密,然后用公钥解密。

加密对于 Web 服务安全非常重要,因为 SOAP 消息默认是纯文本,因此任何接收者和窃听者都可以读取。WSE 支持对称加密和非对称加密。使用前者,Web 服务和客户端在 SOAP 消息通信之外共享一个秘密密钥,客户端使用此密钥加密消息,Web 服务使用相同的共享密钥解密消息。使用非对称加密,客户端使用公钥(X.509 证书的)加密其消息,只有私钥(该 X.509 证书的)的所有者才能解密消息。

签名

公钥还可以用于创建和验证数字签名。目标是实现消息**完整性**,即确保接收者收到的数据在传输过程中没有被更改。

其基础是待签名数据与私钥结合,然后通过某种算法进行转换。因此,只有拥有私钥的人才能创建数字签名,但任何拥有相应公钥的人都可以验证数字签名。重要的一点是:对签名数据进行的任何更改都会使整个签名失效。因此,数字签名允许接收者通过密码学方式验证消息自签名以来未被更改。为了简化签名机制的使用,签名本身可以与签名数据一起发送,以便接收者可以轻松验证消息的来源。

WSE 提供了一种机制,可以使用 UsernameToken、X.509 证书或自定义二进制安全令牌对 SOAP 消息进行数字签名。当在 SOAP 消息接收者的计算机上配置 WSE 时,WSE 会自动验证签名。

证书

那么如何传递公钥或对称密钥呢?面对面传递公钥(最常见的方式)是一种可能性,但显然并非在所有情况下都非常实用。通常需要使用一些公共目录获取公钥,这是一种不如面对面安全的机制。因此,对于从此类目录访问密钥的用户来说,重要的是要确定该密钥确实是它声称的密钥。一种标准化的方法是使用证书。证书是数字签名的声明,其中包含有关所有者及其公钥的信息,将这两部分信息绑定在一起。

证书由称为**证书颁发机构**(CA) 的实体或服务颁发。CA 保证证书所有者及其公钥之间绑定的有效性。**信任**CA 意味着信任该 CA 为其所有者创建和颁发的任何证书都标识了证书的所有者。因此,与证书中公钥对应的私钥被认为只有指定的证书所有者才知道。证书可以包含不同类型的数据。表示证书的标准格式是 X.509。下面列出了此证书格式的成员。

字段 描述
版本 证书格式,例如 3
证书序列号 由颁发 CA 分配的唯一序列号
证书算法标识符和参数 颁发 CA 用于数字签名证书的公钥加密和消息摘要算法
颁发者 颁发 CA 的名称
有效期 开始和到期日期
主题 证书所有者个人或实体的名称
主题公钥信息(包括算法标识符和公钥值)

- 公钥和公钥加密算法列表
- 指定公钥和私钥可用于的加密操作的信息

扩展可选字段
认证机构的数字签名 CA 的数字签名

证书存储

所有证书都存储在证书存储区中,其中有几个默认存储区可用

CURRENT_USER\MY\
当前登录用户的个人证书存储,其他登录用户不可见

LOCAL_MACHINE\MY\
所有用户通用的个人证书存储

CURRENT_USER\Root\
受信任的根证书颁发机构,包含用户信任的根 CA 的证书。具有到根 CA 证书的认证路径的证书,当前用户对证书的所有有效用途都信任。

LOCAL_MACHINE\Root\
如上所述,但所有用户都信任

有一些默认受信任的根 CA,例如 Verisign。尽管我们的示例使用了试用版 Verisign 证书,但这些证书是由 Verisign 测试机构颁发的,默认情况下不受信任。

证书管理与创建

是时候开始使用 X.509 证书实际实现我们的示例了。实际上,从上一篇文章的示例到 X.509 的步骤并不是很大——我们只需要添加几行代码来启用 X.509 证书身份验证。但除此之外,还需要为实际的证书管理做一些努力。证书的创建和管理可以使用 Visual Studio 安装中提供的两个工具完成。

这些工具是 _Certmgr.exe_ 和 _Makecert.exe_。

Certmgr

_Certmgr.exe_ 是 ECM 证书管理器,处理证书导入/导出和管理等任务。具体而言,它具有以下功能(参见 MSDN)

  • 向控制台显示证书、CTL 和 CRL。
  • 将证书、CTL 和 CRL 添加到证书存储。
  • 从证书存储中删除证书、CTL 和 CRL。
  • 将 X.509 证书、CTL 或 CRL 从证书存储保存到文件。

MSDN 这样描述 _Certmgr_

"Certmgr.exe 可处理两种类型的证书存储:StoreFile 和系统存储。无需指定证书存储的类型;Certmgr.exe 可以识别存储类型并执行适当的操作。运行 Certmgr.exe 而不指定任何选项将启动一个 GUI,该 GUI 有助于处理命令行中也提供的证书管理任务。GUI 提供了一个导入向导,它将证书、CTL 和 CRL 从您的磁盘复制到证书存储。"

总而言之,它只是一个工具,使我们能够在本地系统上处理证书的安装、删除和属性管理。

Makecert

第二个工具 _Makecert_ 只能用于测试目的生成 X.509 证书。

我们再次引用 MSDN

"[...] 创建用于数字签名的公钥和私钥对,并将其存储在证书文件中。此工具还将密钥对与指定的发布者名称关联,并创建一个 X.509 证书,将用户指定的名称绑定到密钥对的公钥部分。"

有关支持的命令的详细说明,请查阅文档。

为了公开发行,需要从证书颁发机构购买证书。因此,不要将我们在此处创建的测试证书用于实际场景。

对于我们的目的,创建一个简单的测试证书就足够了。

输入命令

**makecert** -sk myNewKey -r -n "CN=Test" -ss root myNew.cer

将创建这样一个测试证书,发布者名称为 "Test",并保存到名为 myNew.cer 的文件中。

语法如下(再次摘自 MSDN - "数字代码签名分步指南")

  • -**sk**_ 主题密钥_
    持有私钥的主题密钥容器的位置。如果密钥容器不存在,则创建一个。如果 -sk 或 -sv 选项均未使用,则默认创建一个名为 JoeSoft 的密钥容器。
  • -r
    创建自签名证书。
  • **-n** _名称_
    发布者证书的名称。此名称必须符合 X.500 标准。最简单的方法是使用“CN=MyName”格式。例如:-n "CN=Test"。
  • **-ss** _主题证书存储名称_
    将存储生成证书的主题证书存储的名称。
  • myNew.cer
    将证书保存到文件并将文件命名为 saveCertificate.cer。

使用默认测试根创建证书。将创建一个名为 myNewKey 的密钥存储,并且文件将同时输出到存储和文件。

将 X.509 证书添加到 SOAP 消息

现在我们已经创建了自己的证书,我们可以开始编码了。我们的应用程序需要一种打开证书存储并访问其中包含的证书的方法。幸运的是,WSE 为我们提供了无障碍地完成此操作的方法。
但在我们开始实践部分之前,让我们简要了解一下 ASP.NET 安全。

我可以…权限

在我们的示例中,我们希望访问当前用户的根存储。为此,要求访问此存储的应用程序必须具有相应的权限。通常,只有所有者和 SYSTEM 帐户才允许访问相应的文件。由于您的应用程序可能在您系统上特定于任何用户的用户下运行,因此您可能需要调整与证书相关文件的访问权限。如果示例应用程序将以 SYSTEM 或文件所有者的身份运行,则可以跳过以下步骤。
但否则,我们还有一些步骤要做。

通常,ASP.NET 运行的进程将使用 ASPNET 帐户,这是一个低特权用户帐户。出于安全原因,它将缺乏访问本地计算机根存储的权限。

现在你有两个选择:要么在 _machine.config_ 文件中,在 **`/configuration/system.web/processModel`** 键处指定 ASP.NET 使用的帐户,更改 `userName` 和 `password` 属性。默认值是 `userName` 为 `machine`,`password` 为 `AutoGenerate`。`machine` 意味着你正在以 ASPNET 身份运行。将这些值更改为具有所需更多权限的用户——但请注意,这可能会为系统的一些安全相关部分打开大门。所以请注意你的操作……

第二个选项是我最喜欢的,因为它也适用于不作为 ASP.NET 进程运行的应用程序。此选项涉及调整应用程序需要访问的文件权限。这些文件可以在 **C:\Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys** 文件夹中找到。因此,执行以下步骤以授予您的应用程序执行用户所需的权限

  • 打开 Windows 资源管理器。
  • 导航到 C:\Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys 文件夹。
  • 选择所需文件,然后右键单击它们以打开菜单,从中选择“属性”。
  • 打开“安全”选项卡,添加 ASPNET 帐户(或您需要的帐户)并选择“完全控制”选项。

现在您应该可以继续了。
有关 ASP.NET 安全的更多信息,请阅读 使用 ASP.NET 和 IIS 构建和部署更安全站点的入门指南

新的 XML 头部元素

X.509 证书如何添加到 SOAP 消息并随其传输?正如我们已经知道的,所有与 WSE 相关的安全信息都添加到了我上一篇文章中介绍的 `` 头部。我们当时添加了一个 `` 元素以启用用户名/密码验证,这次我们添加一个 `` 元素。这个元素将包含证书的公共版本,证书本身作为 base64 编码数据发送。

我们需要使用的 `` 属性是

  • ValueType
    定义包含的值类型,其中定义了三个值
    • X509v3
      一个 X.509 版本 3 证书
    • Kerberosv5ST
      Kerberos 规范第 5.3.1 节定义的票证授予票证
    • Kerberosv5TGT
      Kerberos 规范第 5.3.1 节定义的服务票证

  • 编码类型
    值如何编码
    • wsse:Base64Binary
    • wsse:HexBinary

  • ID
    唯一的标识符

据此,一个传递 X.509 证书的 `` 元素可能看起来像这样

<wsse:BinarySecurityToken
  ValueType="wsse:X509v3"
  EncodingType="wsse:Base64Binary"
  Id="SecurityToken-c7ff1a4e-276d-4af3-8837-1264ab9f69b3"
>
  MIIBtzCCAWGgAwIBAgIQz4ySuWmd/opBywS0LK...
</wsse:BinarySecurityToken>

因此,使用 WSE 添加安全令牌最终会在 WSE 安全头部中添加一个 `` 元素。

发送证书

首先要做的是检索将添加到我们的 SOAP 消息中的证书。

我们首先创建一个新的存储对象,表示我们要打开的存储——在我们的例子中是 CURRENT_USER\Root 存储。通常,客户端访问当前用户存储,而 Web 服务引用本地计算机存储。

X509CertificateStore *store = X509CertificateStore::CurrentUserStore(
  X509CertificateStore::RootStore->ToString());

函数 `CurrentUserStore(String* storeName)` 使用指定的存储名称(此处由 `RootStore->ToString()` 表示)创建一个新的 `X509CertificateStore`。`RootStore` 是一个常量字段,表示预定义的系统证书存储 "Root"。`ToString()` 应该简单地返回 "Root"。我遇到了以这种方式做的一些问题,因为 `RootStore` 包含一个与 "Root" 不同的字符串,从而导致 `CurrentUserStore(…)` 调用失败。因此,如果您遇到任何问题,请尝试简单地输入 "Root"。
现在打开商店进行阅读

store->OpenRead();

并找到您想使用的证书。从以下函数中选择一个最适合您需求的函数
FindCertificateBySubjectString(String* subjectsubstring)
FindCertificateByHash(unsigned char certHash __gc[])
FindCertificateByKeyIdentifier(unsigned char keyIdentifier __gc[])
FindCertificateBySubjectName(String* subjectstring)。

我认为函数名称是自解释的。我选择了第一种方法来查找我的证书。

X509CertificateCollection *col = (X509CertificateCollection*)store
  ->FindCertificateBySubjectString("Hendrik");

所有函数都返回一个 `X509CertificateCollection` 对象,所以我们简单地取集合中的第一个证书进行后续步骤。

X509Certificate *cert = col->get_Item(0);

现在有了一个证书对象,它可以添加到我们的 SOAP 消息中并用于所有安全目的。所以只需创建一个新的 `X509SecurityToken` 对象,代表二进制安全令牌,

X509SecurityToken *x509st = new X509SecurityToken(cert);

并将其添加到请求的 SOAP 安全头部的 `Tokens` 成员中——与我们上一篇文章中添加 `UsernameToken` 的集合相同。

ws->RequestSoapContext->Security->Tokens->Add(x509st);

这是整个方法

void TestX509Certification()
{
  SimpleWebService *ws = new SimpleWebService();


  X509CertificateCollection *col = 0;
  try {
   X509CertificateStore *store = X509CertificateStore::
    LocalMachineStore( X509CertificateStore::RootStore ); 


    bool bRead = store->OpenRead();
    col = (X509CertificateCollection*)store->
      FindCertificateBySubjectString(S"Hendrik");
    // or use FindCertificateByHash or FindCertificateByKeyIdentifier or
    // FindCertificateBySubjectName
  }
  catch( System::InvalidOperationException *e) {
    throw new ApplicationException(S"Could not read certificate.");
  }

  if( col->Count == 0 )
        return;

  X509Certificate* cert = col->get_Item(0);

  // to be usable for signing, the certificate must support
  // digital signatures and a private key must be available
  if( cert->SupportsDigitalSignature && cert->Key != 0 ) {
    X509SecurityToken *x509st = new X509SecurityToken(cert);
    ws->RequestSoapContext->Security->Tokens->Add(x509st);
    ws->RequestSoapContext->Security->Elements->Add(new Signature(x509st));   
  }

  // set time-to-live to try to prevent some 
  // interceptor from replaying the message
  ws->RequestSoapContext->Timestamp->Ttl = 60000;

  Console::WriteLine(ws->HelloWorldX509());
}

现在,添加 X.509 证书到 SOAP 消息所需的所有步骤都已完成。但仅仅这些并不能很好地进行身份验证。我们目前只得到一个包含证书的 SOAP 消息,该消息可以由 Web 服务接收和验证。但在我们实际使用该证书之前,我们先实现示例的 Web 服务部分。

验证收到的证书

服务安全部分的入口点与上一篇文章中完全相同——检索消息的 SOAP 上下文并查找安全令牌,现在是 `X509SecurityToken` 类型而不是 `UsernameToken`。我们保留了示例 Web 服务的主要功能,即返回一个简单的字符串。我只是将其命名为 `HelloWorldX509` 以阐明其目的。

// get access to the SOAP context of the request
SoapContext* sc = HttpSoapContext::RequestContext;
if( sc == 0 )
  throw new ApplicationException(S"Only SOAP-requests allowed!");
  
bool valid = false;
SecurityToken *st = 0;

// look for a X509 security token
Enumerator *ie = sc->Security->Tokens->GetEnumerator();
while( ie->MoveNext() ) {
  st = (SecurityToken *)ie->get_Current();
  
  // verify that the token represents an X.509 certificate
  if( st != 0 && st->GetType()->Equals(__typeof(X509SecurityToken)) )
  {
  // ... do further security processing ...
  }
}

使用 X.509 证书签署消息

现在我们已经具备了所有先决条件,最终可以保护我们的消息,首先使用数字签名。使用 WSE,向 SOAP 消息添加签名相当容易。您已经了解 `SoapContext::Security::Tokens` 元素,现在我们将使用 `SoapContext::Security::Elements` 属性。这是一个要添加到消息中的安全元素集合,如签名和加密密钥。而在这里,添加签名就是我们要做的。

添加签名后,会在 `` 头部添加一个新元素。它被称为 ``,可以出现零次或多次。相关的命名空间是 **http://www.w3.org/2000/09/xmldsig#**。

在服务器端,WSE 会自动验证收到的签名。如果发生以下任何情况,验证将失败

  • 签名条目内容的语法不符合 XML 签名规范
  • 规范的核心验证失败

这两种可能性仅与加密验证相关。因此,验证失败的另一个原因是应用程序在应用其自己的信任策略时拒绝了消息。原因可能是由不受信任的密钥创建的签名。

客户端部分

在客户端,签署消息意味着在我们添加二进制安全令牌的部分之后再添加一行。只需创建一个新的 `Signature` 对象,将我们的安全令牌作为参数传递,然后将此对象添加到 `Elements` 集合中。

...
X509SecurityToken *x509st = new X509SecurityToken(cert);
ws->RequestSoapContext->Security->Tokens->Add(x509st);
ws->RequestSoapContext->Security->Elements->Add(new Signature(x509st));
...

现在签名已经完成。

现在让我们看看上面所示的已签名 SOAP 消息的片段。

<soap:Envelope>
  <soap:Header>
    <wsse:Security>
      <wsse:BinarySecurityToken ValueType="wsse:X509v3" 
        EncodingType="wsse:Base64Binary" 
        xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility" 
        wsu:Id="SecurityToken-f7997210-3be2-4588-9606-de24cba035ee"
      >
        MIIBtzCCAWGgAwIBAgIQz4ySuWmd/opByw...
      </wsse:BinarySecurityToken>

      <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
          <CanonicalizationMethod Algorithm=
             "http://www.w3.org/2001/10/xml-exc-c14n#" />
          <SignatureMethod Algorithm=
               "http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
          <Reference URI="#Id-abf496e3-37e8-4fbb-9e62-b3b456b19d0b">
            <Transforms>
              <Transform Algorithm=
                "http://www.w3.org/2001/10/xml-exc-c14n#" />
            </Transforms>
            <DigestMethod Algorithm=
               "http://www.w3.org/2000/09/xmldsig#sha1" />
            <DigestValue>LIBpNIQph1yxuRuWmGXXNy9lEKA=</DigestValue>
          </Reference>
          <Reference>
            ...
          </Reference>
          ...
        </SignedInfo>
        <SignatureValue>ZqEf473ru9847a2TNbbInx1D...</SignatureValue>
        <KeyInfo>
          <wsse:SecurityTokenReference>
            <wsse:Reference 
         URI="#SecurityToken-f7997210-3be2-4588-9606-de24cba035ee" />
          </wsse:SecurityTokenReference>
        </KeyInfo>
      </Signature>
    </wsse:Security>
  </soap:Header>
  <soap:Body 
    wsu:Id="Id-abf496e3-37e8-4fbb-9e62-b3b456b19d0b" 
    xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility"
  >
    <HelloWorldX509 xmlns="http://tempuri.org/" />
  </soap:Body>
</soap:Envelope>

请注意安全头部中名为 `` 的新元素部分。它包含三个重要的子元素:`SignedInfo`、`SignatureValue` 和 `KeyInfo`。`SignedInfo` 定义了此消息中正在签名的数据、如何规范化以及用于计算签名的算法。在 `SignedInfo` 中,您将看到一个引用元素列表,这些元素实际上定义了消息中被签名的元素。`URI` 属性现在与被签名元素的 `ID` 属性对应。在上面的片段中,引用的 `URI` 实际上与 body 元素的 `ID` 相似,因此 body 包含在签名中。
您在此片段中看不到的是,默认情况下大多数头部元素都已签名。详细信息如下(不包括命名空间)

  • 正文元素
  • WS-Timestamp SOAP 头的 `Created` 和 `Expires` 元素
  • WS-Routing SOAP 头的 `Action`、`To`、`Id` 和 `From` 元素

Web 服务

在服务器端,涉及更多的代码行。请记住,我们离开了 `HelloWorldX509`,它已经找到了一个有效的 X.509 证书。现在我们必须找到一个数字签名并验证其与此证书的一致性。因此,首先访问 `Security` 元素的 `Elements` 属性并开始遍历它。

Object *obj = 0;
IEnumerator *iter = sc->Security->Elements->GetEnumerator();

while( iter->MoveNext() ) {
  obj = iter->get_Current();

对于每个元素,检查它是否为 `Signature` 类型,这当然是我们正在寻找的。

if( obj && obj->GetType()->Equals(__typeof(Signature)) ) {

如果成功,我们只希望在正文已签名的情况下接受消息。要确定消息的哪些部分已签名,请检查 `Signature` 类的 `SignatureOptions` 属性。`SignatureOptions` 枚举可以是以下值的组合

IncludeNone 指定消息的任何部分都没有签名
IncludePath 指定路由头部中的路径元素应签名
IncludePathAction 指定路由头部中路径元素的 action 子元素应签名
IncludePathFrom 指定路由头部中路径元素的 from 子元素应签名
IncludePathId 指定路由头部中路径元素的 id 子元素应签名。
IncludePathTo 指定路由头部中路径元素的 to 子元素应签名
IncludeSoapBody 指定 SOAP 正文应签名
IncludeTimestamp 指定应签名时间戳。
IncludeTimestampCreated 指定应签名时间戳的 Created 元素。
IncludeTimestampExpires 指定应签名时间戳的 Expires 元素。

由于我们希望正文被签名,所以检查 `IncludeSoapBody` 值

Signature *sig = (Signature*)obj;
if( (sig->SignatureOptions & SignatureOptions::IncludeSoapBody) != 0 ) {

最后,确保消息正文是使用 X.509 证书签名的。在这里,您还可以检查其他安全令牌,如 `UsernameTokens`。如果所有条件都满足,我们就知道是谁发送了消息,并且可以确定消息未经更改地到达。

if( sig->SecurityToken->GetType()->Equals(__typeof(X509SecurityToken)) )
  valid = true;

完整的函数体

String __gc* SimpleWebService::HelloWorldX509()
{
  SoapContext* sc = HttpSoapContext::RequestContext;
  if( sc == 0 )
    throw new ApplicationException(S"Only SOAP-requests allowed!");

  // look for a X509 security token
  bool valid = false;
  SecurityToken *st = 0;

  IEnumerator *ie = sc->Security->Tokens->GetEnumerator();
  while( ie->MoveNext() ) {
    st = (SecurityToken *)ie->get_Current();

    // chech for an X509SecurityToken
    if( st != 0 && st->GetType()->Equals(__typeof(X509SecurityToken)) ) {
      // search signature
      Object *obj = 0;

      IEnumerator *iter = sc->Security->Elements->GetEnumerator();
      while( iter->MoveNext() ) {
        obj = iter->get_Current();

        if( obj && obj->GetType()->Equals(__typeof(Signature)) ) {
          Signature *sig = (Signature*)obj;

          if( (sig->SignatureOptions & 
              SignatureOptions::IncludeSoapBody) != 0 ) {

            if( sig->SecurityToken->GetType()->Equals(
                  __typeof(X509SecurityToken)) ) {
              valid = true;
              break;
            }
          }
        }
      }
      if( valid )
        break;
    }
  }

  if( valid == false )
    throw new ApplicationException(S"Invalid or missing security token");
   
  return S"Hello! X.509 certificate received.";
}

现在我们已经了解了 X.509 证书的首次使用,即签署消息。但请注意,您不需要证书来签署消息——甚至可以使用用户名和密码来完成。这意味着我们可以简单地扩展我们之前的文章示例,使用 `UsernameTokens` 进行身份验证,以签署消息。
假设有一个 `UsernameToken` 实例名为 `ut`,只需键入

ws->RequestSoapContext->Security->Elements->Add(new Signature(ut));

而不是

ws->RequestSoapContext->Security->Elements->Add(new Signature(x509st));

瞧,消息已使用用户名签名。在服务器端,验证过程类似于我们的验证过程——唯一的区别是您现在将检查签名的安全令牌是否为 `UsernameToken` 类型,而不是 `X509SecurityToken`。

签署消息的部分内容

在前面的场景中,我们签署了 SOAP 消息的整个正文。但在某些情况下,这可能是不必要的,甚至是不希望的。请记住我一开始提到的:“对签名数据进行的任何更改都会使整个签名失效。”。因此,如果中间人想要修改正文的某些部分,我们的整个签名就会变得无效。解决方案是只签署消息中需要签署的部分。

要只签署消息的一部分,请为您要签署的每个元素指定一个 Id 属性,并像对头部元素自动完成一样,向这些元素添加一个引用。这就是 WSE 为 `SignatureOptions` 枚举提供的各种选项创建部分签名的方式。除此之外,您可以自己创建 `Id` 属性,并手动向签名的 `SignedInfo` 部分添加一个引用。这允许您只签署正文或 SOAP 头部中的特定部分。

Web 服务再次...

为了让我们的示例使用消息的部分签名,我们向 Web 服务添加一个将被签名的自定义 SOAP 头部。头部很简单,代表一种时间戳。下面是实现我们头部的类

public __gc class MyHeader : 
  public System::Web::Services::Protocols::SoapHeader
{
public:
  [System::Xml::Serialization::XmlAttributeAttribute("Id", 
    Namespace="http://schemas.xmlsoap.org/ws/2002/07/utility")]
  String *Id;

  DateTime Created;
  DateTime Expires;
};

如您所见,头部只包含 3 个成员。`Created` 和 `Expires` 是 `DateTime` 值,表示头部创建时间和过期时间。这与 `Timestamp` 头部所做的类似。第三个属性是一个名为 `Id` 的 `String`。它的值是头部元素的 Id,`SignedInfo/Reference` 元素将引用该 Id。`Id` 必须是消息范围内的唯一标识符,它遵循 http://www.w3.org/2001/XMLSchema 定义的 **xsd:Id** 类型的规则。如果未遵循 **xsd:Id** 类型的规则,WSE 将抛出带有“Malformed reference”消息文本的异常。属性的命名空间必须是 **http://schemas.xmlsoap.org/ws/2002/07/utility**,我们将其传递给 `XmlAttributeAttribute` 构造函数。

现在通过插入 `MyHeader` 类型成员将自定义头部添加到 Web 服务中。

MyHeader* myHeader;

要在函数调用期间访问,您必须指定哪些函数正在使用该头。这可以通过在函数声明上方添加以下行轻松完成

...
[SoapHeaderAttribute("myHeader", Direction=SoapHeaderDirection::In)]
String __gc* HelloWorld_UsingMyHeader();
...

`myHeader` 是表示我们头部的属性,`SoapHeaderDirection::In` 意味着头部仅用于传入消息。其他可能的值是 `SoapHeaderDirection::InOut` 和 `SoapHeaderDirection::Out`。现在,我们的新头部将通过 `myHeader` 成员变量在相应函数中访问。

为了确保我们的头部被我们期望的人签名,我们必须通过再次检查安全头部中的所有 `Signature` 元素来验证它。像以前一样找到一个 `Signature` 对象后,我们现在不关心正文是否被签名。相反,我们检索 `Signature` 包含的所有引用,并检查其中一个引用是否指向我们的头部元素。

IEnumerator* refIter = sig->SignedInfo->References->GetEnumerator();
while( refIter->MoveNext() ) {
  ref = (Reference*)refIter->get_Current();

找到我们已签名的头部只是将头部 `Id`(通过 `myHeader->Id` 访问)与引用的 `Uri` 成员进行简单比较,`Uri` 成员实际上指向此 `Id`。

if( ref->Uri->Equals(String::Format("#{0}", myHeader->Id)) ) {
  ...
}

如果此行成功,我们就知道我们的头部已使用发送者的证书签名,并且未经更改。为了使更改对我们的客户端可见,请重新构建项目并将新的 Dll 包含在 IIS 的 Web 服务文件夹中。

客户端更改

客户端部分并不难。要将新头部添加到我们的消息中,我们首先创建一个 `MyHeader` 对象。

MyHeader* header = new MyHeader();

在 `Guid` 类的帮助下创建了一个唯一的 ID 后,我们可以分配头部的 `Id` 成员。

Guid refId = Guid::NewGuid();
header->Id = String::Format(S"Id-{0}", refId.ToString());

然后只需初始化 `Created` 和 `Expires` 值并将头部添加到我们的 Web 服务。如果您遵循我的命名约定,_wsdl.exe_ 将为您的 Web 服务代理创建一个名为 `MyHeaderValue` 的成员,然后该成员将携带我们的头部对象。

header->Created = DateTime::UtcNow;
header->Expires = header->Created.AddMinutes(5);
ws->MyHeaderValue = header; 

最后要做的是手动将一个引用添加到我们使用的签名的头部。

Signature* sig = new Signature(x509st);
Reference* ref = new Reference(String::Format(S"#{0}", header->Id));
sig->AddReference(ref);

如您所见,该引用使用一个字符串初始化,该字符串等于头部的 ID,只是带有一个 # 前缀,表示引用。

现在,任何调用预期 `MyHeader` 元素但缺少该元素的 Web 服务方法都将被 WSE 拒绝。此外,如果头部未签名或签名因任何原因无效,我们也可以拒绝该消息。

使用证书加密消息

为了使示例正常工作,我们至少需要一个测试 X.509 证书。WSE 使用接收方证书的公钥加密消息,并使用接收方私钥解密消息。因此,由于我们的客户端将加密消息,接收方证书的有效公钥需要位于当前用户的**个人**证书存储中。此外,对于服务器端,此证书的私钥必须存在于本地计算机帐户的**个人**密钥存储中。此外,CA 证书链中的一个证书必须存在于服务器的**受信任**存储中,以便 WSE 知道信任传入证书。对于测试证书,这通常是根机构证书。

处理了消息签名机制后,加密是一个小小的进步。加密消息(或部分消息)和解密过程与我们签名消息的步骤非常相似。它再次基于用于实际加密内容的 X.509 证书。要用于加密,证书的**密钥用途**属性必须包含**数据加密**。通过检查 `X509Certificate` 类的 `SupportsDataEncryption` 属性,可以轻松找出证书是否合适。

所以我们先处理客户端。

发送加密消息

以我们签名章节中的方法为例,我们只需添加以下几行

// check whether the certificate can be used for encryption purposes
if( cert->SupportsDataEncryption ) {
  EncryptedData* ed = new EncryptedData(x509st);
  ws->RequestSoapContext->Security->Elements->Add(ed);
} 

很明显,加密消息的过程与签名消息完全相似。只需创建一个 `EncryptedData` 的新实例,传入要使用的安全令牌,并将其添加到安全头部元素中即可。

那么这些行如何改变我们的 SOAP 消息呢?首先,一个 `` 元素将被添加到安全头部。其次,一个 `` 元素将替换正文标签中包含的原始数据。具体来说,每个要加密的内容都必须替换为 `` 元素。`` 反过来应该包含对根据此处提供的信息加密的每个数据的引用。

有了这些新元素,加密的 SOAP 消息的安全头部包含以下部分

<xenc:EncryptedKey 
  Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey" 
  xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"
>
  <xenc:EncryptionMethod Algorithm=
          "http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
  <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
    <wsse:SecurityTokenReference>
      <wsse:KeyIdentifier ValueType="wsse:X509v3">
           xnps3T2iiIgwG7adOhG9liCyxI4=</wsse:KeyIdentifier>
    </wsse:SecurityTokenReference>
  </KeyInfo>
  <xenc:CipherData>
    <xenc:CipherValue>usAVvHo+pm2cL...</xenc:CipherValue>
  </xenc:CipherData>
  <xenc:ReferenceList>
    <xenc:DataReference 
URI="#EncryptedContent-163b4068-5237-47ee-b1cb-a3fb655149e9"  />
  </xenc:ReferenceList>
</xenc:EncryptedKey>

我们识别出新元素 `` 及其子元素 ``、``、`` 和 ``。`` 用于携带密钥信息,允许不同的密钥类型并提供可扩展性。`` 再次包含对使用该密钥加密的元素的引用。

此示例中唯一加密的元素是正文,最初只包含一个元素

<HelloWorldX509_Extended xmlns="http://tempuri.org/" />

请注意,如果您不指定其他内容,则正文是唯一加密的元素。

这就是对 Web 服务方法的调用。现在经过加密后,它被替换为 EncryptedData 元素,如下所示

<xenc:EncryptedData 
  Id="EncryptedContent-163b4068-5237-47ee-b1cb-a3fb655149e9" 
  Type="http://www.w3.org/2001/04/xmlenc#Content" 
  xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"
>
  <xenc:EncryptionMethod Algorithm=
        "http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
  <xenc:CipherData>
    <xenc:CipherValue>EVb5U61VrXSawk...</xenc:CipherValue>
  </xenc:CipherData>
</xenc:EncryptedData> 

接收加密消息

服务器端不需要我们进行任何编码操作。WSE 如果安装和配置正确,会自动接收给定的证书并检查其有效性。如果成功,消息将被解密并处理。否则,它将被拒绝,并将失败代码发送回消息发送者。

解密工作所需的第一件事是 _Web.config_ 文件中的以下条目

<configuration>
  <system.web>
    <webServices>
      <soapExtensionTypes>
        <add type="Microsoft.Web.Services.WebServicesExtension, 
          Microsoft.Web.Services,Version=1.0.0.0, Culture=neutral, 
          PublicKeyToken=31bf3856ad364e35" 
          priority="1" group="0"/>
      </soapExtensionTypes>
    </webServices>
  </system.web>
</configuration>

它使 Web 服务能够使用 WSE。但由于我们已经在上一篇文章中完成了此操作,因此我们这里无需做任何事情。

现在我们只需要告诉 WSE 在何处查找证书的私钥,解密需要该私钥。由于我们将其放置在本地计算机帐户的**个人**存储中,这就是我们要指向的位置。这通过在 _Web.config_ 中 **`/configuration/microsoft.web.services/security`** 下添加一个 `` 元素来完成。关联的值是 `storeLocation`,我们将为其分配 `LocalMachine` 值,用于本地计算机帐户。另一个可能的值是 `CurrentUser`,但这对于接收加密消息的客户端更实用。

要设置的第二个值是 `verifyTrust`,它必须设置为 `false` 以允许信任测试证书。切勿在生产环境中允许此操作,仅用于测试目的。

<configuration>
  <microsoft.web.services>
    <security>
      <x509 storeLocation="LocalMachine" verifyTrust="false" 
        allowTestRoot="true" />
    </security>
  </microsoft.web.services>
</configuration>

指定要加密的部分

只加密 SOAP 消息的一部分类似于消息的部分签名。我们再次需要要加密的元素的 id。如果我们要加密自定义头部,id 必须以我们已经做过的方式创建。然后,我们创建一个 `EncryptedData` 对象,以安全令牌作为第一个参数。因此,与仅加密整个正文的唯一区别是添加第二个参数,即对 `Id` 的引用。

String *ref = String::Format(S"#{0}", header->Id);
EncryptedData* ed = new EncryptedData(x509st, ref);

然后将元素添加到安全元素中

ws->RequestSoapContext->Security->Elements->Add(ed);

关于客户端,确定加密部分与部分签名再次相似。您需要遍历 `Security` 属性的 `Elements` 集合,并确定当前对象是否为 `EncryptedData` 类型。对于 SOAP 消息中每个被加密的元素,`Elements` 集合中都会有一个这样的 `EncryptedData` 元素。

代码片段如下所示。

Object *obj = 0;
IEnumerator *iter = sc->Security->Elements->GetEnumerator();
while( iter->MoveNext() ) {
  obj = iter->get_Current();
  if( obj && obj->GetType()->Equals(__typeof(EncryptedData)) ) { 
    // do security related processing
  }
}

我没有在我的示例中实现这些东西,因为该技术与签名消息部分没有区别。但我相信您在实现这样的场景时不会遇到任何问题。

测试示例

安装示例类似于上一篇文章,因此如有任何疑问,请参阅该文章。

现在客户端包含三个不同的功能

  • TestAuthentication()
    • 使用用户名/密码身份验证,并且只是上一篇文章的功能
    • 调用 Web 服务的 `HelloWorld()` 函数
  • TestX509Certification()
    • 使用 X.509 证书进行身份验证,并且消息使用此证书进行签名和加密
    • 调用服务器端的 `HelloWorldX509()`
  • TestX509Certification_Extended()
    • 使用 X.509 证书加密消息,并额外发送和加密自定义消息头部
    • Web 服务的对应方是 `HelloWorldX509_Extended()`

要进行测试,只需调用上述任一函数。

继续阅读...

... 在

展望

现在对加密和签名有了初步了解,下一篇文章将继续处理使用共享秘密进行加密。此外,我将介绍自定义二进制安全令牌。

© . All rights reserved.