级别: 中级
孙 瑛霖, 软件工程师, IBM 中国软件开发实验室 SOA设计中心
2007 年 4 月 16 日
XML 数字签名技术用于对 XML 格式的数据进行数字签名,以保证报文的完整性,不可否认性,以及提供身份认证信息。JSR 105 提供了 XML 数字签名的 Java 接口,而最近发布的 Java SE 6 则包括了 JSR 105的 实现,从而为基于 Java 的应用程序提供了标准的 XML 数字签名接口。本文首先简要介绍技术背景,以实例来讲解 XML 数字签名的语法和处理规则,之后用具体的程序例子解释如何使用 Java SE 6 生成各种格式的 XML 数字签名并进行验证。
数字签名是非对称密钥技术的一种应用模式,用于保证报文的完整性,不可否认性,以及提供身份认证信息。数字签名的原理如图 1 所示。
图 1:数字签名的原理
发送者在发送报文之前,先选用某种摘要算法为报文生成一个摘要值,并使用自己的私钥对摘要值加密,然后将加密后的摘要附在报文后面,一同发送给报文的接收者。接收者收到报文后,从中分离出原始报文和加密后的报文摘要,使用与发送者相同的摘要算法计算原始报文的摘要值 D
,并使用发送者的公共密钥将加密后的报文摘要解密得到摘要值 D’
,检查 D
与 D’
是否匹配。
如果匹配,那么由于密钥对的唯一性,所以可以确定报文发送者的身份,而且由于数据摘要算法的特点,还可以确定原始报文在传输过程中没有被篡改。
XML 发展至今,已经逐渐成为标准的数据描述技术,在分布式应用中广泛地用于数据的交换。由于 XML 数据本身的特殊性和使用 XML 进行数据传输的分布式应用的特点,在对 XML 文档的特定部分进行签名,多方签名,以及签名后保持 XML 文档原有的良构特性等诸多方面,传统的数字签名技术都无法很好地实现。
基于这样的问题,W3C 组织制订了 XML 数字签名规范,规定了标准的 XML 数字签名语法和处理规则。同传统意义的数字签名相比,XML 数字签名能够对 XML 文档进行细粒度地分析,支持多种方式的文档数据转换,只对文档的特定部分进行签名和验证,并且能够保持 XML 文档的良构特性。此外,XML 数字签名提供的密钥信息表示方法清晰易读,更加便于签名的自动验证处理。
本节用一个简单的例子来介绍 XML 数字签名的语法和处理规则。
表 1签名前的 XML 文档
|
表 1 中的 XML 文档描述了 Peter 的信用卡支付记录。在按照 XML 数字签名规范对整个文档签名之后,生成的 XML 文档如表 2 所示:
表 2 签名后的 XML 文档
|
所有与 XML 数字签名相关的信息都存放在 <Signature>
元素中。<Signature>
元素包含有几个主要的子元素:
-
<Reference>
元素至少包含一个<Reference>
元素,每个<Reference>
元素用于对待签名数据进行引用,包含有引用方式、转换方法、摘要算法和摘要值等信息。<Reference>
还包含有 XML 数据的规则化方法,并指定了数字签名所使用的算法。 -
<SignatureValue>
元素包含对<Reference>
元素规范化后的内容进行签名生成的数字签名的值。 -
<KeyInfo>
元素用于指定验证签名所需的公共密钥相关信息。
XML 数字签名的过程大致为:
- 1. 根据每个
<Reference>
元素中指定的资源引用方式,摘要算法,数据转换方法等信息,对引用资源进行转换,然后对转换后的结果计算出摘要值。 - 2. 根据
<SignedInfo>
元素中指定的 XML 数据的规范化方法对<SignedInfo>
规则化,对规范化之后的数据生成摘要值,并使用私钥对摘要值进行加密,将生成的加密摘要值存放在<SignatureValue>
元素中。
XML 数字签名的验证主要包括两个步骤,首先需要对 <SignedInfo>
元素中包含的数据引用部分进行验证,然后对整个 <SignedInfo>
元素的签名值进行验证。其间任何一步验证失败则代表整个 XML 数字签名验证失败。
1. 对数据引用的验证
对 <SignedInfo>
中每一个 <Reference>
执行如下验证步骤:
- 1) 应用指定的数据转换方法取得引用的数据对象
- 2) 使用指定的摘要生成算法生成摘要值
- 3) 将生成的摘要值同
<Reference>
中<DigestValue>
元素包含的摘要值相比较,如果不匹配,则验证失败。
2. 对 <SignedInfo> 签名值的验证
- 1) 从
<KeyInfo>
元素中的<KeyValue>
元素或者根据<KeyInfo>
元素中指定的信息从外部获取用于验证数字签名的数据发送方公共密钥。 - 2) 使用验证密钥将
<SignatureValue>
元素中的加密签名值解密,得到值D
- 3) 使用
<SignatureMethod>
元素指定的签名算法对规则化之后的<SignedInfo>
元素计算摘要值,得到值D’
- 4) 判断
D
和D’
是否匹配,如果不匹配,则验证失败。
在 W3C 推出 XML 数字签名规范之后不久,很多组织和厂商就已经开始提供实现产品。目前,除了各大厂商推出的实现产品之外,应用比较广泛的开源产品是 Apache XML Security 项目。该项目实现了 W3C 的 XML 数字签名规范和 XML 加密规范,并且提供 Java 和 C++ 两个版本供用户选用。
JSR 105 (Java XML Digital Signature API Specification) 规定了 XML 数字签名规范的标准 Java 实现接口,于 2005 年 6 月 24 日最终发布。随后,于 2006 年秋季发布的 Java SE 6 (产品代号 Mustang) 将 JSR105 纳入 Java 标准库中,为基于 Java 的上层应用提供标准的 XML 数字签名支持。从此,需要使用 XML 安全特性的 Java 项目有了来自 Java 核心平台的基础支持,再也不需要为选择合适的第三方产品而烦恼。
本节使用具体的程序例子介绍如何使用 Java SE 6中的标准 Java 接口生成各种格式的 XML 数字签名并进行验证。所有的程序例子都使用下表中的 XML 文档,其中主要包含有 Simon 和 Peter 二人的信用卡支付记录。
表 3 程序中使用的 XML 文档
|
本节的程序基于 Java SE 6,请读者自行下载安装并配置开发环境。
3.1 生成并验证 Enveloped 格式的 XML 数字签名
Enveloped 格式的签名指签名元素包含于被签名数据中,如表 4 所示:
表 4 Enveloped格式的 XML 数字签名
|
3.1.1 生成签名
1. 创建 XMLSignatureFactory
实例
XMLSignatureFactory
是与签名相关的 XML 元素对象的创建工厂。本文在这里创建以DOM 处理机制实现的 XMLSignatureFactory
实例:
2. 创建对整个 XML 文档的引用
这一步创建 <Reference>
元素,引用整个 XML 文档:
1. Transform envelopedTransform = |
创建 Reference 的时候将 URI 参数指定为 ""
表示对整个 XML 文档进行引用;摘要算法指定为 SHA1;这里将转换方式指定为 ENVELOPED
,这样在对整个文档进行引用并生成摘要值的时候,<Signature>
元素不会被计算在内。
3. 创建 <SignedInfo>
元素
<Reference>
元素创建好之后,下一步是创建 <SignedInfo>
元素:
1. CanonicalizationMethod c14nWithCommentMethod = |
因为最终的数字签名是针对 <SignedInfo>
元素而生成的,所以需要指定该 XML 元素的规范化方法,以确定最终被处理的数据。这里指定为 INCLUSIVE_WITH_COMMENTS
, 表示在规范化 XML 内容的时候会将 XML 注释也包含在内。
至此,待签名的内容(<SignedInfo>
元素)已指定好,再只需要签名所使用的密钥就可以创建数字签名了。
4. 创建密钥对
XML 数字签名规范规定了多种在 <KeyInfo>
中指定验证密钥的方式,比如 <KeyName>
,<KeyValue>
,<X509Data>
,<PGPData>
等等。这里使用 XML 数字签名规范规定必须实现的 <DSAKeyValue>
来指定验证签名所需的公共密钥。在程序中使用 java.security
包生成 DSA 密钥对。
首先创建密钥对:
1. KeyPairGenerator kpGen = KeyPairGenerator.getInstance("DSA"); |
然后以公钥为参数创建 <KeyValue>
元素:
1. KeyInfoFactory keyInfoFac = fac.getKeyInfoFactory(); |
根据创建好的 <KeyValue>
元素创建 <KeyInfo>
元素:
|
这里创建的密钥对,其中的公钥已经用于创建 <KeyInfo>
元素并存放在其中,供签名验证使用,而其中的私钥则会在下一步被用于生成签名。
5. 创建 <Signature>
元素
前面已经创建好 <SignedInfo>
和 <KeyInfo>
元素,为了生成最终的数字签名,需要根据这两个元素先创建 <Signature>
元素,然后进行签名,创建出 <SignatureValue>
元素。
1. XMLSignature signature = fac.newXMLSignature(signedInfo, keyInfo); |
XMLSignature
类中的 sign
方法用于对文档进行签名,在调用 sign
方法之前,还需要创建 DOMSignContext
对象,为方法调用提供上下文信息,包括签名所使用的私钥和最后生成的 <Signature>
元素所在的目标父元素:
1. DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); |
这里首先使用 JAXP 的 DOM 接口将待签名文档解析,然后根据前面创建的私钥和待签名文档的根元素创建 DOMSignContext
对象。
请注意,到这里为止都只是创建各种生成签名所需数据的 XML 元素表示,并没有开始真正地生成数字签名。
6. 最后一步,生成签名
1. signature.sign(domSignCtx); |
sign
方法会生成签名值,并作为元素值创建 <SignatureValue>
元素,然后将整个 <Signature>
元素加入为待签名文档根元素的直接子元素。
7. 输出签名后的文档
数字签名生成之后,使用 JAX P的 XML 转换接口将签名后的 XML 文档输出,查看签名结果:
1. TransformerFactory tf = TransformerFactory.newInstance(); |
3.1.2 验证签名
本节介绍如何使用 Java SE 6提供的 XML 数字签名 API 对上一节生成的数字签名进行验证。验证代码如表5:
表 5 验证 XML 数字签名
|
1. 解析签名后生成的 XML 文档(行2 - 行6)
对于 Enveloped 格式的 XML 签名而言,生成的 <Signature>
元素位于被签名的 XML 中,所以这里首先使用 JAXP 将签名后生成的 XML 文档解析。
2. 查找签名元素(行8 - 行14)
所有与 XML 数字签名相关的信息都存放在 <Signature>
元素中,所以需要先取到 <Signature>
元素。由于前面在生成签名的时候将 <Signature>
元素存放为文档根元素的直接子元素,所以这里根据元素名 ”Signature” 进行搜索,搜索到的第一个元素即为 <Signature>
元素。
3. 构造 <Signature>
元素(行16 - 行19)
使用 XMLSignatureFactory
类的 unmarshalXMLSignature
方法从 DOM 节点构造出 XMLSignature
对象,为下一步验证签名作准备。
4. 获取验证签名所需的公共密钥(行20 - 行22)
在本例中,验证密钥以 <DSAKeyValue>
的格式存放于 <KeyInfo>
元素中,这里使用 XMLSignature
对象的接口取出公钥。
5. 创建DOMValidateContext
(行24 - 行25)
在验证签名的过程中,需要创建 DOMValidateContext
对象来指定上下文信息,参数为前面获取到的验证公钥和 <Signature>
元素。
6. 验证签名(行27 - 行28)
验证签名所需的所有信息都已就绪,开始使用XMLSignature
对象提供的接口进行验证。
7. 检查验证结果(行30 - 行46)
如果签名验证失败,则分别对 <Reference>
元素的签名值和其中的每一个引用进行验证,进一步确定导致验证失败的原因。如果签名值验证成功,而某个引用验证失败,则说明是该引用新生成的摘要值与原文档中的摘要值不匹配导致验证失败。
以签名后生成的 XML 文档作为输入,执行验证程序,从程序的输出信息可以判断出验证成功:
Signature passed core validation |
接下来将包含有 <Signature>
元素的待验证文档作一点改动,把 Peter 的支付信息中的金额改为 60000 美元,再次执行程序进行验证,则验证失败:
Core validation failed |
输出信息提示签名值验证成功,而对整个文档的引用验证失败。签名值验证成功是因为 <SignedInfo>
没有改动,对文档的引用验证失败是因为前面修改了文档中的数据。
Enveloping 格式的签名指 <Signature>
元素包含着被签名的数据内容,如表 6 所示:
表 6 Enveloping 格式的数字签名
|
在 Enveloping 格式的数字签名中,待签名的 XML 内容需要通过 URI 或者 Transform 进行引用。
3.2.1 生成签名
1. // Create XMLObject refering to Simon's payment info |
为了引用待签名的内容,首先查找到 Simon 的支付记录对应的 DOM 元素,使用 XMLStructure
对其进行包装,然后生成 XMLObject
,并为其指定 id
为 "SimonPayment"
。随后在创建 Referenc
的时候,同样指定引用 id
为 "SimonPayment"
。在创建 XMLSignature
对象的时候将待签名的 XMLObject
作为参数,这些 XMLObject
包含的 XML 内容将会成为 <Signature>
元素的子元素,从而创建出 Enveloping 格式的签名。程序的其他部分与生成 Enveloped 格式的数字签名相同。
签名之后生成的 <Signature>
元素如下:
1. <Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> |
表 5 中的验证程序同样可以用来验证上面生成的 Enveloping 格式的 XML 签名。
Detached 格式的签名指 <Signature>
元素与被签名的数据内容之间是彼此分离的,既不是包含关系也不是被包含关系,如下表所示:
1. .............. |
Detached 格式多用于对外部独立的数据对象进行签名,使用 URI 来引用外部数据对象,也可以引用同一 XML 文档中的其他元素,对之进行数字签名。
表3中待签名的 XML 文档包含有两次信用卡交易的支付信息,第一个是 Simon 的信用卡交易记录,第二个是 Peter 的。本节将对 Peter的支付记录进行数字签名,所使用的签名格式是 Detached 格式。为了让 <Signature>
元素能够引用到包含有 Peter 信用卡交易信息的 XML 元素,这里用 id
属性对 Peter 的 <PaymentInfo>
元素加以引用。
3.3.1 生成签名
本节的大部分代码都与生成 Enveloped 格式签名的代码相同,只是在创建 Reference
的时候有些不同:
1. XMLSignatureFactory fac = |
"#PeterPayment"
用于引用位于同一文档中的 Peter 的 <PaymentInfo>
元素。
3.3.2 验证签名
使用表 5 中的 validate
方法对生成的数字签名进行验证,输出信息表示验证成功。然后在生成的 XML 文档中,将 Simon 的支付金额改为 30000,重新验证,结果依然为验证成功。这是因为只对 Peter 的支付记录进行签名,所以 Simon 的支付信息改变,不会影响数字签名的验证结果。如果再将 Peter 的支付金额改为 50000,重新验证,则输出信息显示验证失败。
同传统意义的数字签名技术相比,XML 数字签名技术有很多不可替代的优点。它能够在保持 XML 文档良构性的前提下,对文档内容进行细粒度的签名和验证;通过资源引用和转换的机制,扩大了签名的作用范围,更能够满足分布式应用系统中的安全需求。
W3C 组织制订的 XML 数字签名规范规定了标准的 XML 签名语法和处理规则,而Java SE 6则为 XML 数字签名提供了标准的 Java 接口。目前这些 XML 数字签名规范和程序标准还在进一步完善中,相信随着技术的发展,XML 数字签名技术必将得到越来越广泛的应用。
- Donald E. Eastlake, Joseph M. Reagle, David Solo, XML-Signature Syntax and Processing (http://www.w3.org/TR/xmldsig-core/), W3C Recommendation, Feb 12, 2002
- JSR105 - XML Digital Signature APIs(http://jcp.org/en/jsr/detail?id=105)
- JSR106 - XML Digital Encryption APIs(http://jcp.org/en/jsr/detail?id=106)
- Apache XML Security(http://xml.apache.org/security/)
- Sun Java SE 6(http://java.sun.com/javase/6/)
- developerWorks 中国网站 XML 技术专区
- developerWorks 中国网站 Java 技术专区
孙瑛霖,软件工程师,现就职于IBM中国SOA设计中心,对网络与分布式系统、安全和SOA等技术有着浓厚的兴趣。 |