适用于:Web 服务规范(WS-Security 规范以及其他规范)
Microsoft .NET Framework
摘要:本文探讨 XML 数字签名规范,介绍其处理模型和某些功能。此外,还可以通过本文更加详细、深入地了解有关 WS-Security 规范如何实现其消息安全功能。(本文包含一些指向英文站点的链接。)
目录
简介
不涉及复杂数学的数字签名加密
签名格式
小结
简介
数字签名十分重要,因为它可以提供端到端的消息完整性保证,还可以提供有关消息发件人的验证信息。为了达到较好的效果,签名必须是应用程序数据的一部分,这样可以在创建消息时生成签名,并可以在最终使用和处理消息时对签名进行验证。
除消息保密性功能以外,SSL/TLS 还提供消息完整性保障,但这项功能仅在传送过程中有效。服务器(或更常见的对等接收方)接收消息后,必须“解除”SSL 保护才能处理消息。
更为有趣的是,SSL 只在通信端点之间起作用。如果在开发新的 Web 服务时使用传统的 HTTP 服务器(例如 IIS 或 Apache)作为网关,或者与具有 SSL 加速器的大型企业进行通信时,消息完整性可以一直得到保障,直到 SSL 连接终止。
可以拿传统信函进行类比说明。当将支票邮寄到电话公司时,通常会在支票(即“消息”)上签名,并将支票放入信封中以达到保密和邮寄的目的。收到消息后,电话公司会拆开信封,将信封扔在一边,然后处理支票。当然,也可以让消息成为信封的一部分,例如将付款贴在明信片上邮寄过去,但这样做显然很愚蠢。
XML 签名可以定义一系列 XML 元素,这些元素可以内嵌或以其他方式附加在任何 XML 文档中。这样,收件人可以验证收到的消息与发件人原本发送的消息是否相同。
XML 签名的语法和处理规范(本文中缩写为 XML DSIG)是由 W3C 和 IETF 联合制定的。自 2002 年 2 月以来,它一直是正式的 W3C 推荐规范,并得到了广泛的应用:.NET Framework 中的 System.Security.Cryptography.Xml 就应用了此规范。在 WS-Security 具有的验证、内容完整性和内容保密性三种功能当中,XML DSIG 可以提供完整性,并可用于进行发件人验证。
不涉及复杂数学的数字签名加密
在深入了解 XML DSIG 之前,需要了解一些基本加密算法。本节将探讨这些概念,但是不要害怕:并不会涉及复杂数学。
数字签名对某些内容提供完整性检查。如果原始内容的某个字节已经被修改(例如在价格上多加了个零,“2”改成了“4”,或者“否”改成了“是”等),那么签名验证将失败。下面是它的工作原理。
首先需要“散列”消息。加密散列使用的是任意字节流,并将其转换为某个固定长度的值,这个值称为“摘要”。摘要是一个单向过程:从计算角度来说,无法通过散列重新创建消息,也不可能找到可以产生相同摘要值的两封不同消息。
最常用的散列机制是 SHA1,即“安全散列算法”。此算法由美国政府创建,并在 1995 年作为标准发布。访问 http://www.itl.nist.gov/fipspubs/fip180-1 可以获得完整的规范。SHA1 可以处理 2**64 字节以内的任何消息,并生成一个 20 字节的结果。(这意味着将有 2**160 个可能的摘要值;比较起来,目前估计宇宙中的质子数目大约是 2**250。)
所以,如果我生成消息 M 并创建摘要(用 H(M) 代表“M 的散列”),您将收到 M 和 H(M),您可以创建自己的摘要 H'(M),如果两个摘要值匹配,表明您收到了我发送的消息。要保护 M 使其不被修改,我只需要保护 H(M),使它不被修改。
那该怎么做呢? 有两种常用的方法:第一种是将共享密钥混合在摘要中,即创建 H(S+M)。您收到消息后,可以使用自己的 S 副本来创建 H'(S+M)。新的摘要称为 HMAC,即“散列后的消息验证代码”(Hashed Messsage Authentication Code)。
在使用 HMAC 时,完整性保护的有效性取决于攻击者计算出 S 的能力。因此,S 应该不容易被猜出,并应该经常更改 S。符合以上要求的最好方法之一是使用 Kerberos。在 Kerberos 中,中央机构将在两个实体希望通信时分配包含临时会话密钥的“票”。此会话密钥将用作共享密钥。在要将签名发送给您时,我将得到一张票,以和你通信。我打开票中的属于我的那一部分来获得 S,然后将消息、消息的 HMAC 和票中您的那一部分发送给您。您打开票(使用原来通过 Kerberos 注册的密钥),获取 S 以及有关我身份的信息。您现在可以获得消息 M,生成自己的 H'(S+M),并查看它们是否匹配。如果匹配,表明您原封不动地收到了我的消息,并且 Kerberos 将通知您我是谁。
另外一个保护摘要的方法是使用公钥加密算法,例如 RSA。公钥加密算法中有两个密钥:一个是只有持有者知道的私钥,另一个是要与密钥持有者通信的任何人都知道的公钥。在公钥加密算法中,使用私钥加密的任何内容都可以使用公钥解密,反之亦然。
下面通过一个简单的示例来说明公钥加密算法的工作原理。在此示例中,我们将消息内容限制为从 a 到 z 的字母,并给这些字母指派 0 到 26 的值。要对消息进行加密,我们需要添加私钥的值;在本示例中,此值为 +4:
字母 h e l l o
数值 8 5 12 12 15
私钥 4 4 4 4 4
加密后的值 12 9 16 16 19
要对消息进行解密,我们需要添加公钥,值为 +22;如果结果超出了数字范围,就加上或减去 26,直到值有效。(换句话说,要对消息进行解密,我们需要添加公钥,并用结果对 26 取模。)
加密后的值 12 9 16 16 19
公钥 22 22 22 22 22
原始加密后的值 34 31 38 38 41
规范化的值 8 5 12 12 15
纯文本 h e l l o
RSA 的工作原理与此相同,只是使用求幂的方法而不是加法,此外,数字的位数比较多。
使用 RSA 生成一个摘要:H(M),并使用我的私钥进行加密,则 {H(M)}private-key 就是我的签名。您收到消息 M 后,可以生成摘要 H'(M),并使用我的公钥来对签名进行解密,就得到我生成的 H(M)。如果 H(M) 和 H'(M) 相同,表明两封消息 M 是相同的。而且,您会发现拥有私钥的人,也就是“我”,是消息的发件人。
签名的格式
XML-DSIG 使用一个单独的命名空间,我们假设示例中存在以下声明:
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
顶层的 <ds:Signature> 元素是相当简单的。此元素具有的信息包括要签名的内容、签名、用于创建签名的密钥以及存储任意信息的空间。
<element name="Signature" type="ds:SignatureType"/>
<complexType name="SignatureType">
<sequence>
<element ref="ds:SignedInfo"/>
<element ref="ds:SignatureValue"/>
<element ref="ds:KeyInfo" minOccurs="0"/>
<element ref="ds:Object" minOccurs="0" maxOccurs="unbounded"/>
</sequence>
<attribute name="Id" type="ID" use="optional"/>
</complexType>
我们将看到它们的复杂性是依次递增的。
Id
ds:SignatureValue
ds:Object
ds:SignedInfo
ds:KeyInfo
ds:Signature/@Id 属性
全局 Id 属性允许文档包含多个签名,并提供了标识特殊实例的方法。业务策略中经常使用多个签名,例如某个旅行申请必须同时得到经理和旅行办公室的批准。
ds:SignatureValue 元素
此元素包含实际签名。由于签名通常是二进制数据,XML DSIG 指定签名值通常是具有 Base64 编码内容的简单元素:
<element name="SignatureValue" type="ds:SignatureValueType"/>
<complexType name="SignatureValueType">
<simpleContent>
<extension base="base64Binary">
<attribute name="Id" type="ID" use="optional"/>
</extension>
</simpleContent>
</complexType>
为了解释 SignatureValue,需要了解 SignedInfo 元素中的内容,也就是我们下面将要讨论的内容。迄今为止,SignatureValue 仅是多个字节的不透明字符串:
<SignatureValue>
WvZUJAJ/3QNqzQvwne2vvy7U5Pck8ZZ5UTa6pIwR7GE+PoGi6A1kyw==
</SignatureValue>
ds:Signature/ds:Object 元素
正如下面我们将要看到的,XML DSIG 可以包含多个项。项通常可以独立存在,例如一个 Web 页面或 XML 业务文档,但有时可以将项视为要进行签名的“真实”内容的元数据。例如,数据可能是签名的某个“属性”,例如生成签名时使用的时间戳。
ds:Object 元素可以用于在签名中存放以下数据:
<element name="Object" type="ds:ObjectType"/>
<complexType name="ObjectType" mixed="true">
<sequence minOccurs="0" maxOccurs="unbounded">
<any namespace="##any" processContents="lax"/>
</sequence>
<attribute name="Id" type="ID" use="optional"/>
<attribute name="MimeType" type="string" use="optional"/>
<attribute name="Encoding" type="anyURI" use="optional"/>
</complexType>
Id 属性允许签名具有多个可以被独立寻址的对象。MimeType 用于标识数据,这样其他处理器也可以使用此数据,但不适用于 DSIG 处理器。
Encoding 指定如何预处理内容;目前只定义了 Base64 编码。
下面是两个(具有相同内容的)对象,可以用作提示文档签名时间的简单指示器。对于联机投标、拍卖或其他具有提交期限的活动,签名中提供这一功能的服务可能是非常有用的:
<ds:Object Id="ts-bin" Encoding="http://www.w3.org/2000/09/xmldsig#base64">
V2VkIEp1biAgNCAxMjoxMTowMyBFRFQgMjAwMwo
</ds:Object>
<ds:Object Id="ts-text">
Wed Jun 4 12:11:06 EDT
</ds:Object>
ds:SignedInfo 元素
您听说过这样一句格言吗:“计算机科学中的任何问题都可以用间接的方法来解决”?正如我们就要看到的那样,XML DSIG 是验证这句格言的最好例子。
ds:SignedInfo 的内容可以分为两个部分:关于 SignatureValue 的信息和关于应用程序内容的信息,如以下 XML 架构片断所示:
<element name="SignedInfo" type="ds:SignedInfoType"/>
<complexType name="SignedInfoType">
<sequence>
<element ref="ds:CanonicalizationMethod"/>
<element ref="ds:SignatureMethod"/>
<element ref="ds:Reference" maxOccurs="unbounded"/>
</sequence>
<attribute name="Id" type="ID" use="optional"/>
</complexType>
XML 的语法要求不是很严格。例如,属性的顺序和引用值的方式都不是很严格。对于 XML 处理软件,以下两个示例完全等效:
<a foo='yes' boo="no"/>
<a boo="no" foo="yes" ></a>
(细心的读者可以尝试找到我添加的其他两个不同之处。) 但签名需要消息摘要,此类不同之处会产生重大影响。
为了按照此原则进行工作,内容必须是“标准的”。标准化或 C14N 是在所有可能的输出选项中选取一个路径的过程,这样,无论可能涉及到哪种中间 XML 软件,发件人和收件人可以生成完全相同的字节值。C14N 是一个比较深奥的主题,请参阅有关它的文章。
ds:SignedInfo/ds:CanonicalizationMethod 元素指定如何重新构造准确字节流。ds:SignedInfo/ds:SignatureMethod 元素指定用于创建签名的签名类型,例如 Kerberos 或 RSA。综上所述,这两个元素告诉我们如何创建和保护摘要,使其不被修改。
下面是一个示例:
<ds:SignedInfo>
<ds:CanonicalizationMethod
Algorithm="http://www.w3.org/2001/10/xml-exc-c14n"/>
<ds:SignatureMethod
Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
...
ds:Reference 元素
ds:SignatureValue 元素包含仅包含 ds:SignedInfo 元素的签名:签名摘要中只包含 ds:SignedInfo 的内容。那么如何对其他内容进行签名呢?奥妙在于 ds:Reference 元素。
正如前面的 ds:SignedInfoType 的架构定义所示,签名可以具有多个引用。这将允许单个 XML DSIG 包含多个对象:MIME 消息中的所有部分、XML 文件和可以将 XML 文件转换为 HTML 的 XSLT 脚本等。
ds:Reference 元素引用其他内容。此元素包含内容摘要、如何生成摘要的指示(例如 SHA1)以及在生成摘要之前转换内容时所用的规范。转换使 XML DSIG 变得非常灵活。下面是架构片段:
<element name="Reference" type="ds:ReferenceType"/>
<complexType name="ReferenceType">
<sequence>
<element ref="ds:Transforms" minOccurs="0"/>
<element ref="ds:DigestMethod"/>
<element ref="ds:DigestValue"/>
</sequence>
<attribute name="Id" type="ID" use="optional"/>
<attribute name="URI" type="anyURI" use="optional"/>
<attribute name="Type" type="anyURI" use="optional"/>
</complexType>
Type 属性可以提供处理提示,但通常没什么用处。
URI 指向被引用的实际内容。由于是 URI,因此可以获得 Web 的所有功能。例如,可以对 MSDN 主页上的内容进行签名:
<ds:Reference URI="http://msdn.microsoft.com">
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<ds:DigestValue>HB7i8RaV7ZvuUlaTzZVx0S3POpU=</ds:DigestValue>
</ds:Reference>
还可以引用 XML 文档中的内容,例如前面所示的时间戳:
<ds:Reference URI="#ts-text">
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<ds:DigestValue>pN3j2OeC0+/kCatpvy1dYfG1g68=</ds:DigestValue>
</ds:Reference>
当然,也可以在同一个签名内同时采用这两个引用。
最常见的用法是 URI 片段与 WS-Security 一起使用,以对 SOAP 消息进行签名:
<SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP:Header>
<wsse:Security>
xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/07/secext">
...
<ds:Signature>
...
<ds:SignedInfo>
<ds:Reference URI='#Body'>
...
</ds:Reference>
...
<ds:SignedInfo>
...
</ds:Signature>
...
</wsse:Security>
</SOAP:Header>
<SOAP:Body Id='Body'>
...
</SOAP:Body>
</SOAP:Envelope>
正如您可能预期的那样,ds:DigestMethod 指定散列算法,而 ds:DigestValue 则是内容散列的 Base64 值。
ds:Reference 元素中作用最大的部分是可能出现的转换集。ds:Transforms 就是 ds:Transform 元素的列表,每个元素指定一个处理步骤。该架构定义了一个转换数组,其中包含的 ds:XPath 具有定义好的结构:
<element name="Transforms" type="ds:TransformsType"/>
<complexType name="TransformsType">
<sequence>
<element ref="ds:Transform" maxOccurs="unbounded"/>
</sequence>
</complexType>
<element name="Transform" type="ds:TransformType"/>
<complexType name="TransformType" mixed="true">
<choice minOccurs="0" maxOccurs="unbounded">
<any namespace="##other" processContents="lax"/>
<element name="XPath" type="string"/>
</choice>
<attribute name="Algorithm" type="anyURI" use="required"/>
</complexType>
转换的内容将取决于 Algorithm 属性。例如,如果要对简单的 XML 进行签名,那么最可能需要进行指定 C14N 算法的某种转换:
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n"/>
</ds:Transforms>
XML DSIG 定义了几种转换,其中包括 XPath 转换,使用此转换可以很容易地对文档的某一部分进行签名,例如忽略所有文本,只对标记进行签名:
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath>not(self::text())</XPath>
</ds:Transform>
<ds:Transform
Algorithm="http://www.w3.org/TR/2001/10/xml-exc-c14n/>
</ds:Transforms>
定义的其他转换包括内嵌 XSLT 样式表、对加密数据进行解密等。
ds:KeyInfo 元素
至此,我们知道如何引用内容、如何对内容进行转换和散列以及如何创建包含(保护)内容的签名。使用间接方法撤回受保护的内容:ds:SignatureValue 包含 ds:SignedInfo,后者包含的 ds:References 中又包含应用程序数据的摘要值。如果更改了这些元素中的任何元素,数学计算将被终止,而且无法验证签名。
现在要做的最后一件事是标识签名者,或者至少标识生成签名的密钥(或者为了获得更好的加密性,标识保护摘要不被修改的密钥)。这是 ds:KeyInfo 元素要发挥的作用:
<element name="KeyInfo" type="ds:KeyInfoType"/>
<complexType name="KeyInfoType" mixed="true">
<choice maxOccurs="unbounded">
<element ref="ds:KeyName"/>
<element ref="ds:KeyValue"/>
<element ref="ds:RetrievalMethod"/>
<element ref="ds:X509Data"/>
<element ref="ds:PGPData"/>
<element ref="ds:SPKIData"/>
<element ref="ds:MgmtData"/>
<any processContents="lax" namespace="##other"/>
<!-- (1,1) elements from (0,unbounded) namespaces -->
</choice>
<attribute name="Id" type="ID" use="optional"/>
</complexType>
正如我们所看到的那样,XML DSIG 支持多种不同密钥类型和密钥基础结构,而 WS-Security 则在此基础上更进一步。我们将只看到两样东西:一个简单的名称和一个 X.509 证书。在为封闭环境生成自定义应用程序时,可以使用 ds:KeyName:
<element name="KeyName" type="string"/>
这取决于验证签名的过程,验证的过程中将把名称映射到内部存储中,并获取对应的密钥。ds:KeyName 的值通常包括电子消息地址或目录项。
可以通过 ds:X509Data 元素支持 X.509 证书。签名者可以在这个元素中嵌入证书(采用 Base64 编码)或者嵌入可以标识证书的任何其他形式:主题名称、颁发者名称和序列号、密钥标识符或其他格式。签名者还可以包括当前的证书吊销列表 (CRL),以显示在对文档进行签名时签名者的身份是有效。下面的架构片段显示标识 X.509 证书的其他方法:
<element name="X509Data" type="ds:X509DataType"/>
<complexType name="X509DataType">
<sequence maxOccurs="unbounded">
<choice>
<element name="X509IssuerSerial" type="ds:X509IssuerSerialType"/>
<element name="X509SKI" type="base64Binary"/>
<element name="X509SubjectName" type="string"/>
<element name="X509Certificate" type="base64Binary"/>
<element name="X509CRL" type="base64Binary"/>
<any namespace="##other" processContents="lax"/>
</choice>
</sequence>
</complexType>
<complexType name="X509IssuerSerialType">
<sequence>
<element name="X509IssuerName" type="string"/>
<element name="+X509SerialNumber" type="integer"/>
</sequence>
</complexType>
由于不同的应用程序使用不同的架构来存储和检索证书,XML 数字签名经常会通过在同一个 ds:KeyInfo 元素中内嵌同一个密钥的多个名称来包括这些名称。在本示例中,我们既提供了用户容易理解的名称(用于 GUI 应用程序来实现弹出窗口),还在颁发者表单中提供了唯一标识符和序列号(用于目录搜索):
<ds:KeyInfo>
<ds:KeyName>
[email protected]
</ds:KeyName>
<ds:X509Data>
<ds:X509SubjectName>
cn=Rich Salz, o=DataPower, c=US
</ds:X509SubjectName>
<ds:X509IssuerSerial>
<ds:IssuerName>
ou=Development, o=DataPower, c=US
</ds:IssuerName>
<ds:SerialNumber>32</ds:SerialNumber>
</ds:X509IssuerSerial>
</ds:X509Data>
</ds:KeyInfo>
小结
本文对 XML DSIG 规范进行了深入探讨,通过使用架构定义来描述可用功能,并介绍了生成和验证 XML DSIG 文档的处理过程。本文从基本签名元素 (ds:SignedInfo) 入手,探讨了它如何通过引用应用程序内容来保护该内容。在结尾部分,我们探讨了 ds:KeyInfo 元素,了解应用程序如何验证签名以及确认签名者身份的有效性。这三个方面提供了保护 XML(和其他)内容完整性的最基本、最低级别的组件。不用感到奇怪,它们的灵活性意味着直接使用它们可能是相当复杂的。
XML DSIG 的最常用策略之一当然是和 WS-Security 规范一起使用。这样就为数据保护和用户验证提供了更加面向应用程序的视图。要学习如何在 WS-Security 内部使用 XML DSIG,请参阅了解
WS-Security。