作者:Mike Downen、Shawn Farkas 相关技术:XML、.NET Framework、C#、安全性
[摘要]XML签名和XML加密标准目前被广泛地用作构建快(building-block)技术。本文解释了XML签名和XML加密标准,并且说明了如何通过.NET使用它们。
注:本文的某些部分基于 .NET Framework 2.0 的预发布版本。与这些部分有关的所有信息都有可能更改。
XML签名和XML加密标准目前被广泛地用作积木(building-block)技术。Microsoft Office InfoPath使用XML签名对部分或整个表单进行签名。Web服务使用XML签名对SOAP消息进行签名,并且使用XML加密技术对它们进行加密。基于ClickOnce的应用程序的XML清单(Visual Studio 2005中的新增功能)也使用XML签名。.NET Framework 1.x包含XML签名标准的对象模型,而.NET Framework 2.0则添加了其他支持,同时还添加了XML加密的对象模型。本文解释了XML签名和XML加密标准,并且说明了如何通过.NET使用它们。有关实际的XML签名规范,请参阅位于http://www.w3.org/TR/xmldsig-core的W3C标准。
在深入探讨XML签名标准之前,让我们回顾一下数字签名的基础知识。因为防止恶意用户在传输期间改变消息很重要,所以数字签名保护数据的完整性,并且可以检测数据在到达接收地的途中受到的任何更改。因为能够标识发送方也很重要,所以消息通常使用发送方的私有(秘密)密钥进行签名,并且用相应的公钥进行验证,从而使接收者在知道发送方的公钥时可以确认发送方的标识。这可以防止恶意用户通过尝试作为已知的发送方发送消息,或者通过截获来自已知发送方的消息并将其替换为他们自己的消息(一种中间人形式的攻击),冒充已知的发送方。
要创建数字签名,首先需要使用加密哈希函数来对需签名的消息进行哈希运算。对于任何长度的输入,加密哈希函数都会返回固定长度的位组,称为哈希值。该哈希值无法容易地重新转换为原来的输入。即使输入中只有一个位发生更改,哈希值也会以不可预知的方式更改,因此无法仅仅通过查找类似的哈希值来找到与原始输入类似的输入。一个常用的哈希函数是SHA-1,它可以产生160位的哈希值。下一个步骤是使用签名算法和您的私钥对该哈希值进行签名,以产生签名值。您用您的私钥创建该签名,以便具有您的公钥的其他人可以对其进行验证(本文稍后将对此进行详细讨论)。RSA是一种流行的用于签名的加密算法。在您将消息和该签名发送给接收者之后,验证过程开始。收到的消息被在签名时使用的相同哈希函数用来进行哈希运算;然后,通过将签名值以及公钥和计算得到的哈希一起传递给签名算法,对签名值进行验证。如果计算得到的哈希与签名哈希相匹配,则签名有效。如果这两个哈希不匹配,则表明数据或签名已经更改,因此不能确保数据的完整性。还可以使用密钥哈希算法签名和验证数据,但是这超出了本文讨论的范围。.NET Framework已经为所有种类的哈希、加密/解密和签名/验证算法包含了一组丰富的类。有关这些类的详细信息,请参阅.NET Framework SDK文档以及由Brian LaMacchia等人编写的《NET Framework Security 》(Addison-Wesley, 2002)。
您可以使用XML签名对任何种类的数据进行签名,这些数据包括XML文档的某个部分、其他XML文档或任何格式的其他数据。但是,实际上,XML签名最常用于对以XML表示的其他数据进行签名。XML签名标准还非常灵活,它允许您在签名之前对数据进行筛选和转换,并且使您可以精确地选择要签名的内容以及签名方式。
一个简单的文档:
<docRoot>
<a>Hello</a>
<b>World</b>
</docRoot>
经过签名的文档:
<docRoot>
<a>Hello</a>
<b>World</b>
<Signature xmlns="http://www.w3c.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod
Algorithm="http://www.w3c.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod
Algorithm="http://www.w3c.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="">
<Transforms>
<Transform
Algorithm=
"http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</Transforms>
<DigestMethod
Algorithm="http://www.w3c.org/2000/09/xmldsig#sha1"/>
<DigestValue>cbPT0951Ghb2G3UjpVjWw+7q0Bc=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>IoEwS(3 lines of Base64 text)XSo=</SignatureValue>
</Signature>
</docRoot>
图1 签名前后的简单XML文档
让我们考察一个简单的XML签名方案:对整个文档进行签名并且包含签名(参见图1)。请注意已经被添加到文档中的Signature元素,该元素包含XML签名。让我们看一下每个元素所包含的内容:
SignedInfo——该元素的子元素包含有关所签名的内容以及签名方式的所有信息。签名算法实际上应用于该元素及其所有子元素以生成签名。
CanonicalizationMethod——该元素指定了用于SignedInfo元素以便将XML规范化的规范化(C14N)算法。我们将在下文中讨论C14N。
SignatureMethod——该元素指定了该签名的签名算法。在该示例中,签名算法是带有RSA(用于对产生的哈希值进行签名)的SHA-1(用于哈希运算)。
Reference——这些元素指定了将要签名的数据以及在哈希运算之前应当如何对该数据进行处理。URI属性(它表示统一资源标识符)标识要签名的数据,而Transforms元素(稍后描述)指定在进行哈希运算之前如何处理数据。在该示例中,我们将使用特殊的URI——空字符串,它指定包含签名的文档是要包含在签名中的数据。XML签名标准对Reference数据使用间接签名机制。该标准不是对Reference中的所有数据进行哈希运算然后加密哈希值,而是使用由Reference的DigestMethod元素所指定的算法对每个Reference的数据进行哈希运算,然后将哈希值存储到Reference的DigestValue元素中。接下来,对SignedInfo元素和它的所有子元素(包括Reference元素)进行哈希运算;哈希值被加密以生成签名。因此,您实际上是对Reference元素中所引用数据的哈希的哈希进行签名,但是该方案仍然可以保护数据的完整性。图2和图3在匹配的XML旁边显示了签名和验证过程。
Transforms——每个Reference元素都可以具有零个或更多个为它指定的转换。这些转换按照它们在XML中列出的顺序应用于该Reference的数据。转换使您可以在对Reference的数据进行哈希运算之前对该数据进行筛选或修改。在该示例中,我们将使用包封式签名转换,该转换选择了包含文档中除Signature元素以外的所有XML。我们必须从将被签名的数据中移除Signature元素,否则,当我们存储签名值时,可能会修改我们尝试签名的数据。我们将在下文中详细讨论转换。
SignatureValue——该元素包含通过签名SignedInfo元素及其所有子元素而计算得到的签名值。
图2 签名过程
现在,让我们讨论用于创建签名的处理模型(参见图2)。首先,对于签名中的每个Reference元素:
下一个步骤是使用在签名的CanonicalizationMethod元素中指定的算法规范化SignedInfo元素及其子元素。然后,使用在签名的SignatureMethod元素中指定的算法对SignedInfo元素及其子元素进行签名。签名值被存储在SignatureValue元素中。
图3 验证过程
签名验证是刚刚描述的过程的逆过程(参见图3)。首先,必须使用CanonicalizationMethod元素中指定的C14N算法规范化SignedInfo元素及其子元素。然后,必须针对SignedInfo元素及其子元素验证SignatureValue元素中存储的签名值。
最后,对于签名中的每个Reference元素:
如果签名验证成功,并且每个Reference的哈希值与签名中存储的哈希值相等,则XML签名有效。否则,或者由Reference元素之一引用的数据已经更改,或者Signature元素已经更改。
嵌入到由其签名的文档中的签名称为信封签名。用于创建这种签名的代码如图4所示,用于验证该签名的代码如图5所示。
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
// Also, add a reference to System.Security.dll
// Assume the data to sign is in the data.xml file, load it, and
// set up the signature object.
XmlDocument doc = new XmlDocument();
doc.Load("data.xml");
SignedXml sig = new SignedXml(doc);
// Make a random RSA key, and set it on the signature for signing.
RSA key = new RSACryptoServiceProvider();
sig.SigningKey = key;
// Create a Reference to the containing document, add the enveloped
// transform, and then add the Reference to the signature
Reference refr = new Reference("");
refr.AddTransform(new XmlDsigEnvelopedSignatureTransform());
sig.AddReference(refr);
// Compute the signature, add it to the XML document, and save
sig.ComputeSignature();
doc.DocumentElement.AppendChild(sig.GetXml());
doc.Save("data-signed.xml");
图4 创建一个信封签名
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
// Also, add a reference to System.Security.dll
// Load the signed data
XmlDocument doc = new XmlDocument();
doc.PreserveWhitespace = true;
doc.Load("data-signed.xml");
// Find the Signature element in the document
XmlNamespaceManager nsm = new XmlNamespaceManager(new NameTable());
nsm.AddNamespace("dsig", SignedXml.XmlDsigNamespaceUrl);
XmlElement sigElt = (XmlElement)doc.SelectSingleNode(
"//dsig:Signature", nsm);
// Load the signature for verification
SignedXml sig = new SignedXml(doc);
sig.LoadXml(sigElt);
// Verify the signature, assume the public key part of the
// signing key is in the key variable
if (sig.CheckSignature(key))
Console.WriteLine("Signature verified");
else
Console.WriteLine("Signature not valid");
图5 验证一个信封签名
您已经了解了如何创建和验证包封式签名,它们很常用并且在对整个XML文档进行签名时很方便,而且XML签名标准还使您可以通过在Reference元素中指定不同的URI对其他数据进行签名。因此,让我们接下来考察一下不同类型的引用。
除了包封式引用(其URI属性为空字符串的Reference元素)以外,在XML签名标准中还定义了其他两个宽泛类型的引用:对分离数据的引用以及通过ID对XML数据进行的引用。分离数据位于包含签名的XML文档的外部,这些引用可以指向另外一个XML文档或任何其他类型的资源。当您在一个签名中对多个资源(例如,一个XML文档以及由该文档引用的其他一些文件)进行签名时,通常会使用该类型的引用。下面的XML代码片段显示了分离引用的一个示例:
<Reference URI="http://www.example.com/foo.jpg">
<DigestMethod
Algorithm="http://www.w3c.org/2000/09/xmldsig#sha1" />
<DigestValue>cbPT0951Ghb2G3UjpVjWw+7q0Bc=</DigestValue>
</Reference>
为了使用Framework中的类来创建分离引用,只需在创建引用对象时将该引用的URI设置为资源的URI,然后 将该引用添加到签名中,如下所示:
// Create a Reference to detached data, assume a SignedXml object in sig
Reference refr = new Reference("http://www.example.com/foo.jpg");
sig.AddReference(refr);
通过ID对XML数据进行的引用指向包含签名的文档内部或签名本身内部的XML。对于这些引用,签名引擎寻找其ID属性与引用中的URI匹配(不包括#)的元素。以下为一个基于ID的引用的示例:
<Reference URI="#myData">
<DigestMethod
Algorithm="http://www.w3c.org/2000/09/xmldsig#sha1" />
<DigestValue>cbPT0951Ghb2G3UjpVjWw+7q0Bc=</DigestValue>
</Reference>
如果ID为“myData”的元素位于包含签名的文档中,则该引用是完整的,并且签名引擎将在处理该签名时找到它。您通常使用该类型的引用将签名的作用范围限制到示例文档的特定部分。例如,在文档处理应用程序中,审阅者通常只对他审阅的XML文档部分(而不是整个文档)进行签名。该标准还允许您向Object元素中的签名添加任意数据。Object元素的XML看起来类似于以下代码行:
<Object Id="myData">Your XML goes here</Object>
要创建基于ID的引用(该引用指向包含签名的文档中已经存在的元素),请添加如下所示的代码:
// Create a Reference to XML data in the containing document,
// assume a SignedXml object in sig
Reference refr = new Reference("#myData");
sig.AddReference(refr);
如果基于ID的引用指向了签名中的一个Object元素,则除了添加该引用以外,还必须向签名中添加一个DataObject,如刚才显示的代码中所示。数据对象可以包含您传入的任何XML。
// Adds a DataObject with an Id of "#myData" to the signature, assume a
// SignedXml object in sig, and xml data of type XmlNodeList in data
DataObject dobj = new DataObject();
dobj.Id = "myData"; // Note: no #
dobj.Data = data; // XML Data of the Object
sig.AddObject(dobj);
您通常使用指向Object元素的引用来对有关签名的元数据进行签名,例如,签名者的唯一标识符或有关该签名的其他一些信息。
转换通过允许您在生成引用的数据的哈希值之前修改该数据,使您可以对已签名的内容进行更多的控制。例如,信封式签名转换在对XML文档进行签名之前会移除Signature节点。引用可以指定任何数量的转换,这些转换按照在Transforms元素中指定的顺序而做用。.NET Framework中的类除了支持我们前面提到的信封式签名转换以外,还支持下列转换:
XPath表达式被指定为Transform元素下的XPath元素的文本内容。需要注意的是,XPath转换充当筛选器,而不是充当在作为输入传递的XML中选择节点的手段。该转换针对作为输入传递给该转换的每个节点计算XPath表达式,结果被转换为布尔值。输入节点将被考虑以便传递计算,并且如果计算的结果为true,则输入节点将被包含在转换的输出中。考虑转换的以下XML输入:
<a>
<b>Some data</b>
<c>More data</c>
</a>
<d>
<b>Even more data</b>
</d>
假设我们只希望选择“b”节点进行签名。带有XPath表达式“ancestor-or-self::b”的XPath转换将返回以下节点集(它正是我们所需要的):
<b>Some data</b>
<b>Even more data</b>
该XPath表达式的Transform元素看起来类似于下面的代码片段:
<Transform
Algorithm="http://www.w3c.org/TR/1999/REC-xpath-19991116"
>
<XPath>ancestor-or-self::b</XPath>
</Transform>
要在签名时以编程方式创建转换,请创建某个转换对象的实例,适当设置它的属性,并且将其添加到它所应用于的引用。以下示例将上一个示例中使用的XPath转换添加到某个引用中,以便创建转换:
// Add an XPath transform to a reference.
// Assume a Reference object in refr
XmlDocument doc = new XmlDocument();
doc.LoadXml("ancestor-or-self::b");
XmlDsigXPathTransform xptrans = new XmlDsigXPathTransform();
xptrans.LoadInnerXml(doc.ChildNodes);
refr.AddTransform(xptrans);
规范化的目的是为两个逻辑上相同但可能不是由相同的文本表示的XML片段产生相同的XML数据。例如,请观察下面两个代码片段。它们在逻辑上是相同的;它们的不同之处仅仅在于文本表示。但是如果您要对它们按原样进行哈希运算,则得到的哈希值将是不同的:
<root>
<a>Some text</a>
<b attr1="yes" attr2="no"></b>
<c Id="foo">More text</c>
</root>
<root>
<a>Some text</a>
<b attr2="no" attr1="yes" />
<c Id="foo">More text</c>
</root>
要避免该问题,该标准中指定的默认规范化算法将执行很多任务,包括消除开始和结束标记中的空白以及将空元素转换为开始/结束标记对。但是,它不会更改元素内容中的任何空白。所执行操作的完整列表可以在Canonical XML(位于http://www.w3.org/TR/xml-c14n)中得到。
签名引擎在必要时自动规范化数据,以便符合W3C标准。特别地,每当签名引擎需要将XML数据转换为二进制数据以便进行哈希运算时,都会规范化该数据。例如,当它准备对SignedInfo元素及其子元素进行签名时,就会发生这种情况。当它准备引用或转换的输出以便进行签名时,也可能发生这种情况。例如,如果您使用基于ID的引用(指向包含签名的文档中的其他XML数据),并且该引用没有与其相关联的转换,则签名引擎在对该引用的XML数据进行哈希运算之前将规范化该数据。
XML签名标准提供了KeyInfo元素,帮助进行密钥管理。该元素可以存储密钥名称、密钥值、密钥检索方法或证书信息,以帮助接收方验证签名。该标准没有指定应当如何信任以及是否应当信任KeyInfo元素中的任何信息。如果发送方和接收方共享一个受信任密钥列表,或者如果您发现了其他某种用于将密钥名称映射到密钥的方法,则KeyInfo元素可能很有用。.NET Framework 1.x具有一些对密钥名称、值和检索方法的支持,.NET Framework 2.0还包含对X.509证书的支持。让我们假设发送方和接收方共享一个密钥列表,接收方对于他期望从其接收消息的每个发送方都具有一个公钥。签名应用程序可以添加以下代码,以便向签名中添加KeyInfo元素:
// Adds an KeyInfo element with RSA public key information to the
// signature.
// Assumes a SignedXml object in sig, and an RSA object in key.
KeyInfo ki = new KeyInfo();
ki.AddClause(new RSAKeyValue(key));
sig.KeyInfo = ki;
该代码应当在调用ComputeSignature之前添加。它将在签名中产生一个KeyInfo元素,该元素看起来类似于以下代码:
<KeyInfo>
<KeyValue>
<RSAKeyValue>
<Modulus>4LfG(2 lines of Base64 text)2Fr=</Modulus>
<Exponent>AQAB</Exponent>
</RSAKeyValue>
</KeyValue>
</KeyInfo>
这表示用来对XML文档进行签名的RSA公钥。接收方应用程序应当将该密钥与受信任密钥列表进行比较,如果该公钥不在列表中,则不应当信任文档。否则,攻击者就可以在传输过程中替换已经签名的文档,并且用另外一个密钥对其进行签名。如果签名包含与此类似的RSAKeyValue,则验证代码可以调用SignedXml类的CheckSignature方法(它不采用任何参数),并且.NET Framework将根据RSAKeyValue元素计算出该密钥。下面是一个示例:
// Verify a signature that includes RSAKeyInfo or DSAKeyInfo.
// Assume a SignedXml object in sig.
bool verified = sig.CheckSignature();
伴随XML签名的灵活性而来的是一定数量的风险。因为转换是如此灵活,所以可能很难精确计算出签名涵盖了哪些数据,这可能导致意外的或不安全的结果。这些签名配置文件可以通过指定应用程序所支持的签名形式,在该方面提供帮助。尽管没有相应于签名配置文件的标准,但签名配置文件起码应当指定应用程序期望签名具有的引用和转换,以便您可以确保所期望签名的数据确实进行了签名。签名配置文件还可以包含其他数据,例如,期望签名数据具有的签名算法或密钥大小。应用程序应当检查并强制它所创建和验证的那些签名符合该应用程序所支持的签名配置文件。要更好地理解配置文件为什么如此重要,请考虑图1。假设您要编写接受XML签名数据的应用程序,但是您的应用程序只期望使用信封式签名转换而非任何其他转换的签名。现在,有人向您发送了带有额外XPath转换的签名文档,如图6所示。
<docRoot>
<a>Hello</a>
<b>World</b>
<Signature xmlns="http://www.w3c.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod
Algorithm="http://www.w3c.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod
Algorithm="http://www.w3c.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="">
<Transforms>
<Transform
Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-
signature"/>
<Transform
Algorithm="http://www.w3c.org/TR/1999/REC-xpath-19991116"
>
<XPath>ancestor-or-self::a</XPath>
</Transform>
</Transforms>
<DigestMethod
Algorithm="http://www.w3c.org/2000/09/xmldsig#sha1"/>
<DigestValue>mN4R0653F4ethOiTBeAu+7q0Be=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>X4Ie(3 lines of Base64 text)nP3=</SignatureValue>
</Signature>
</docRoot>
图6 使用额外的转换对文档进行签名
在图6的示例中,签名只涵盖示例文档中的“a”元素。如果您刚刚加载了该文档并且调用了SignedXml类的CheckSignature方法,则即使“b”元素未被该签名涵盖,该签名仍然可能验证,这是因为签名引擎将应用在该签名中指定的转换。如果应用程序依赖于该签名涵盖了“b”元素这一前提,则数据的完整性已经遭到损害。应用程序应当检验只有一个引用具有作为URI的空字符串并且该引用具有一个转换——信封式签名,从而验证它所期望的签名配置文件。它会在验证签名时拒绝任何其他签名配置文件。一些用于检验该签名配置文件的示例代码显示在图7中。
// This method checks the signature profile for the signature
// in the supplied document. It ensures there is only one
// Signature element and only one enveloped reference with only
// one enveloped signature transform
public bool CheckSignatureProfile(XmlDocument doc)
{
// Make sure there is only one Signature element
XmlNamespaceManager nsm = new XmlNamespaceManager(new NameTable());
nsm.AddNamespace("dsig", SignedXml.XmlDsigNamespaceUrl);
XmlNodeList sigList = doc.SelectNodes("//dsig:Signature", nsm);
if (sigList.Count > 1)
return false; //Wrong number of Signature elements
//Make sure the Signature element has only one Reference
XmlElement sigElt = (XmlElement)sigList[0];
XmlNodeList refList = sigElt.SelectNodes(
"dsig:SignedInfo/dsig:Reference", nsm);
if (refList.Count > 1)
return false; //Wrong number of Reference elements
// Make sure the Reference URI is ""
XmlElement refElt = (XmlElement)refList[0];
XmlAttributeCollection refAttrs = refElt.Attributes;
XmlNode uriAttr = refAttrs.GetNamedItem("URI");
if ((uriAttr == null) || (uriAttr.Value != ""))
return false; // Wrong type of reference
// Make sure the only tranform is the enveloped signature transform
XmlNodeList transList = refElt.SelectNodes(
"dsig:Transforms/dsig:Transform", nsm);
if (transList.Count != 1)
return false; //Wrong number of Transform elements
XmlElement transElt = (XmlElement)transList[0];
string transAlg = transElt.GetAttribute("Algorithm");
if (transAlg != SignedXml.XmlDsigEnvelopedSignatureTransformUrl)
return false; //Wrong type of transform
return true;
}
图7 检验一个签名配置文件
迄今为止,我们已经考察了XML签名标准的一些不同方面以及.NET Framework中对它的支持。让我们将这些功能中的某些功能一起放到一个更为完整的示例中。假设您要编写一个应用程序以便交换XML形式的消息,并且您希望对该消息的全部内容进行签名。您还希望将一些有关签名者的XML数据作为对象添加到Signature元素中,以便只对该数据的signerID元素进行签名。您的应用程序可以访问一个众所周知的密钥列表,因此您还将在签名中存储公钥信息,并且检验以确保该密钥在验证期间映射到一个众所周知的密钥。用于签名和验证消息的代码包含在本文的完整代码下载中。用该代码对消息进行签名将产生如图8所示的XML签名。
<root>
<myData1>Some Data</myData1>
<myData2>More data</myData2>
<Signature xmlns="http://www.w3c.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod
Algorithm="http://www.w3c.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod
Algorithm="http://www.w3c.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="">
<Transforms>
<Transform
Algorithm=
"http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</Transforms>
<DigestMethod
Algorithm="http://www.w3c.org/2000/09/xmldsig#sha1"/>
<DigestValue>cbPT0951Ghb2G3UjpVjWw+7q0Bc=</DigestValue>
</Reference>
<Reference URI="#signer">
<Transforms>
<Transform
Algorithm=
"http://www.w3c.org/TR/1999/REC-xpath-19991116"
>
<XPath xmlns:my="http://example">
ancestor-or-self::my:SignerID
</XPath>
</Transform>
</Transforms>
<DigestMethod
Algorithm="http://www.w3c.org/2000/09/xmldsig#sha1"/>
<DigestValue>mN4R0653F4ethOiTBeAu+7q0Be</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>IoEwS...</SignatureValue>
<KeyInfo>
<KeyValue>
<RSAKeyValue>
<Modulus>4LfG(2 lines of Base64 text)2Fr=</Modulus>
<Exponent>AQAB</Exponent>
</RSAKeyValue>
</KeyValue>
</KeyInfo>
<Object Id="signer">
<my:SignerData xmlns:my="http://example">
<my:SignerName>Mike</my:SignerName>
<my:SignerID>4815</my:SignerID>
</my:SignerData>
</Object>
</Signature>
</root>
图8 一个稍微复杂一些的签名文档
迄今为止讨论的所有XML签名特性在.NET Framework 1.x和2.0中都可用。XML加密和X.509证书集成对于版本2.0而言是新增特性。通过X.509证书集成,可以更容易地将X.509证书用于XML签名。通过新增的X509CertificateEx类和相关的类,可以更容易地操纵和使用证书,并且XML签名对象模型在适当的时候使用这些类。在本文的结尾,我们将对X.509集成进行更详细的讨论。XML加密是另外一个W3C标准。正如XML签名指定了有关创建XML形式的数字签名的格式和处理模型一样,XML加密对如何加密XML形式的数据进行了标准化。XML数字签名是通过SignedXml类驱动的,而XML加密是使用新的EncryptedXml类执行的。尽管XML加密可以用来加密任意数据,但它最常用于加密其他XML。当以这种方式使用时,您将在文档的加密方式上拥有很多的灵活性。例如,可以用不同的密钥加密XML文档的不同节点,同时将某些节点保留为明文。而且,由于用EncryptedXml类加密某些内容会产生XML,因此您甚至可以加密已经加密的结果,该过程称为“超级加密”。让我们考察一下一些已经加密的XML(参见图9)。一件可以立刻注意到的有趣的事情是,XML加密标准对某些元素(包括KeyInfo元素)使用XML签名命名空间。
一个简单的文档:
<docRoot>
<a>Hello</a>
<b>World</b>
</docRoot>
经过加密的文档:
<docRoot>
<EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element"
xmlns="http://www.w3.org/2001/04/xmlenc#"
>
<EncryptionMethod
Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc"/>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod
Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyName>recipient_public_key</KeyName>
</KeyInfo>
<CipherData>
<CipherValue>PrI6(3 lines of Base64 text)Dwy4=</CipherValue>
</CipherData>
</EncryptedKey>
</KeyInfo>
<CipherData>
<CipherValue>awcH(3 lines of Base64 text)NNqQ=</CipherValue>
</CipherData>
</EncryptedData>
</docRoot>
图9 加密前后的XML文档
EncryptedData是通过加密XML生成的根元素,并且它包含有关数据的所有信息。EncryptedData包含三个主要的子元素:EncryptionMethod指定用来加密数据的算法;KeyInfo元素提供有关使用哪个密钥来解密数据的信息;CipherData或CipherReference元素则包含实际的加密信息。EncryptionMethod元素指定用来加密和解密密码数据的算法,这些算法由URI指定,就像XML签名标准一样。EncryptionMethod同时用于加密数据和加密密钥,但并非每个算法都可以同时用于这两个场合。图10显示了每个算法可以用在哪个场合。请注意,高级加密标准(Advanced Encryption Standard,AES)还可用于192位和128位密码;您只需在URI中更改密钥大小。
EncryptedXml类的URI属性 | 加密数据 | 加密密钥 | |
---|---|---|---|
AES | XmlEncAES256Url | √ | |
XmlEncAES256KeyWrapUrl | √ | ||
DES | XmlEncDESUrl | √ | |
TripleDES | XmlEncTripleDESUrl | √ | |
XmlEncTripleDESKeyWrapUrl | √ | ||
RSA | XmlEncRSA1_5Url | √ |
图10 使用加密算法的场合
在加密或解密任何数据之前,加密引擎需要知道应当使用哪个密钥来加密和解密。可以用两种方式标识密钥,最容易的方式是为该密钥分配一个名称,并且在KeyInfo元素内部放置一个KeyName元素。解密文档的应用程序可以获得KeyName标记,并提供与给定的名称相匹配的密钥。该应用程序不仅提供密钥的名称,还可以将密钥作为EncryptedKey直接嵌入到KeyInfo元素中。EncryptedKey与EncryptedData包含相同的元素:加密方法、用来解密该密钥的密钥以及构成加密密钥的密码数据。加密密钥通常与命名密钥结合使用,以作为随机会话密钥。首先,生成一个随机会话密钥并使用它来加密XML;然后,用需要解密文档的众所周知的命名密钥加密会话密钥本身;最后,将该命名密钥插入到加密会话密钥的KeyInfo元素中,并且将该加密会话密钥附加到加密数据中。图9中的示例说明了这一点。XML数据的加密方法是256位的AES,而AES算法的密钥也已经被加密。EncryptedKey元素包含有关如何加密AES算法的密钥的信息,在该示例中,它是使用名为recipients_public_key的RSA密钥加密的。使用XML加密的应用程序必须将该名称映射到实际的密钥。
可以将实际的加密数据嵌入到EncryptedData元素中,或者将其放到单独的位置,然后从EncryptedData中引用它。如果要将密码数据直接放到EncryptedData中,则会将其作为Base64编码的二进制文件放到CipherData元素中。图9中的示例使用了一个CipherData元素。另一个方案是将加密数据放到EncryptedData元素外部。可以将密码文本放在从该文档中的另一个元素到远程Web站点的任何位置。在这两种情况下,都将使用CipherReference元素而不是CipherData元素(参见图11)。
CipherReference的位置 | URI格式 | 密码文本格式 |
---|---|---|
相同文档中 | #order | Base64字符串 |
远程Web站点 | http://www.example.com/order.bin | 二进制 |
图11 CipherReferences
对远程Web站点的CipherReferences提出了一个有趣的安全方案。在解密XML的过程中,解密引擎必须转到任意Web站点并下载密码文本。由于要解密的文档可能不会受到与完成解密的代码同等程度的信任,因此应当在沙箱中完成该操作。这是通过向EncryptedXml对象提供它将在解析任何CipherReferences时使用的证据来完成的。通过沙箱可以更安全地执行代码,因为解密应用程序可能不具有与提供加密数据的站点相同的权限。例如,如果应用程序试图解密不受信任的站点,并且该不受信任的站点不能够访问位于安全的、受信任站点上的某些受信任的数据,则它可以通过包含密码引用,让解密应用程序为它访问该文件。由于.NET安全策略是以证据为中心的,因此所提供的证据通常应当至少包含Site、Zone和Url对象,如下所示:
// Create evidence based on the referring document
Evidence evidence = new Evidence();
evidence.AddHost(new Zone(SecurityZone.Internet));
evidence.AddHost(new Site("untrustedsite"));
evidence.AddHost(new Url("untrustedsite/encrypted.xml"));
EncryptedXml exml = new EncryptedXml(untrustedDoc, evidence);
现在,让我们考察一下如何使用.NET Framework 2.0中的类。该示例说明了一个销售CD的Web站点,每次购买活动都被归档到一个XML文档中,其中包含有关订购了哪些商品、发货信息和信用卡号的详细信息。图12显示了订单的一些示例XML。
<order>
<purchase>
<item quantity="1">Def Leppard: Pyromania</item>
<item quantity="1">Ozzy Osbourne: Goodbye to Romance</item>
</purchase>
<shipping>
<to>Shawn Farkas</to>
<street>One Microsoft Way</street>
<zip>98052</zip>
</shipping>
<payment>
<card type="visa">0000-0000-0000-0000</card>
</payment>
</order>
图12 一个CD订单的XML
在该实例中,公司内部的任何人查看订单中的项目是一件可以接受的事情,但是您可能希望对某些更为敏感的数据(例如,发货地址和信用卡信息)进行保密。实际上,您甚至可能希望对它们分别进行加密,以便只有计帐部门能够访问信用卡信息,并且只有发货部门能够访问发货地址。要做到这一点,需要对付款元素下的XML部分进行加密,以便只有计帐部门能够访问它,只有发货部门可以获得的单独密钥将用来加密发货元素。最后,整个订单将用公司中任何人都可以获得的密钥加密。
第一步是创建一个EncryptedXml对象。这是通过传入具有要加密或解密的数据的文档完成的,如下所示:
// Assumes the order is in the order.xml file.
XmlDocument doc = new XmlDocument();
doc.Load("order.xml");
EncryptedXml exml = new EncryptedXml(doc);
在加密XML之前,必须将要使用的密钥映射到它们的相应名称,这些名称将出现在KeyName元素中。可以使用EncryptedXml的AddKeyMapping方法完成该任务:
// Set up the key mapping. Assumes a method called GetBillingKey
// that returns the RSA key for the billing department.
RSA billingKey = GetBillingKey();
exml.AddKeyNameMapping("billing", billingKey);
在设置了密钥-名称映射以后,加密XML就很容易了。第一步是调用Encrypt方法,它完成实际的加密,并且返回一个EncryptedData对象以表示文档的加密部分。之后,您需要调用一个工具方法,将原始XML文档的未加密部分换为新的加密数据:
// Find the element to encrypt.
XmlElement paymentElement =
doc.SelectSingleNode("//order/payment") as XmlElement;
// Encrypt the payment element, passing in the key name.
EncryptedData encryptedPayment =
exml.Encrypt(paymentElement, "billing");
// Swap the encrypted element for the unencrypted element.
EncryptedXml.ReplaceElement(paymentElement, encryptedPayment, true);
这将产生图13中所示的加密XML。
<order>
<purchase>
<item quantity="1">Def Leppard: Pyromania</item>
<item quantity="1">Ozzy Osbourne: Goodbye to Romance</item>
</purchase>
<shipping>
<to>Shawn Farkas</to>
<street>One Microsoft Way</street>
<zip>98052</zip>
</shipping>
<payment>
<EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element"
xmlns="http://www.w3.org/2001/04/xmlenc#"
>
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/
xmlenc#aes256-cbc" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/
xmlenc#kw-aes256" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyName>billing</KeyName>
</KeyInfo>
<CipherData>
<CipherValue>
Sce6lLD+u2f8HzPFyuGxTF32z4mb2ugql3JuJIPAqIP98iYs+Muhqg==
</CipherValue>
</CipherData>
</EncryptedKey>
</KeyInfo>
<CipherData>
<CipherValue>FXKC(3 lines of Base64 text)ApqQt</CipherValue>
</CipherData>
</EncryptedData>
</payment>
</order>
图13 加密了支付信息的CD订单
请观察一下加密数据,您可以看到我们已经描述的各个部分。首先,EncryptionMethod元素显示了AES-256的URI,这意味着该文档用带有256位密钥的AES算法(由RijndaelManaged类实现)加密。Encrypt方法为您生成一个随机的会话密钥;该密钥在KeyInfo元素中加密。通过观察EncryptedKey元素,您可以看到它是用RSA算法并借助于一个名为“billing”的密钥加密的。CipherData元素存放了该密钥的加密值。在KeyInfo的后面是包含加密内容(原来的付款元素)的CipherData。解密刚才说明的文档很容易,这要归功于EncryptedXml的DecryptDocument方法。首先,加载含有加密内容的文档:
// Assumes the encrypted order is in encrypted.xml
XmlDocument doc = new XmlDocument("encrypted.xml");
EncryptedXml exml = new EncryptedXml(doc, documentEvidence);
接下来,设置密钥-名称映射,如下所示:
// Set up the key mapping. Assumes a method called GetBillingKey
// that returns the RSA key for the billing department.
RSA billingKey = GetBillingKey();
exml.AddKeyNameMapping("billing", billingKey);
最后,调用DecryptDocument。该方法将负责解密密码文本,并且将加密的XML替换为它的解密内容。
// Decrypt the encrypted XML in the document
exml.DecryptDocument();
在幕后,加密引擎将在调用DecryptDocument时寻找任何EncryptedData元素,在这种情况下,它只找到付款元素下面的那个元素。在找到该元素以后,它将查看KeyInfo子句并且看到它持有加密的256位AES密钥。加密密钥的KeyInfo将显示它是用名为billing的RSA密钥加密的。然后,它检查密钥映射表以寻找名为billing的密钥。我们为一个名为billing的密钥添加了一个密钥映射,因此引擎随后将解密加密密钥。既然引擎具有密钥,那么它将解密CipherData。然后,用解密的CipherData的结果来替换EncryptedData元素。当DecryptDocument被调用时,引擎将对它在该文档中找到的每个EncryptedData元素执行这一过程。
安全方面的难题之一是信任。在网络中安全地分发和存储受信任的公钥并不容易,但是,Windows提供了大量基础结构来解决该问题。加密API(Cryptographic API,CAPI)提供了对分发、操纵和存储X.509证书的支持。CAPI提供的支持也称为公钥基础结构(Public Key Infrastructure,PKI)。
X.509证书提供有关密钥的其他信息。证书指定了密钥的颁发者、密钥的接收者、证书何时有效以及实际的密钥信息。证书是由证书颁发机构(Certificate Authorities,CA)颁发的,CA是您给予信任以担保证书持有者身份的实体(例如,某个外部公司或您所在公司的IT部门)。如果您具有一个有效证书,它包含由您信任的CA颁发的属于您的计帐部门的公钥,则您可以确保该公钥确实是计帐部门的。
通过Windows PKI支持,您可以安全地在计算机上的证书存储器中存储证书,在网络计算机上的证书存储器中添加和移除证书,在网络计算机上添加和移除受信任的CA,获得和验证有关单个证书的信息等等。例如,这使您可以将证书(包含您所在公司计帐部门的公钥)分发到公司网络中的所有计算机。在.NET Framework 1.x中,必须调用非托管API才能利用该支持的大部分功能。在.NET Framework 2.0中,可以在托管代码中通过X509CertificateEx类和相关的类使用这些API中的大多数API。XML签名类直接支持X509CertificateEx类。要用证书创建XML签名,只需从该证书的PrivateKey属性中获得私钥,然后将其用作SignedXml对象的签名密钥。
// Use the private key from the certificate. Assumes a SignedXml
// object in sig and an X509CertificateEx object in cert.
sig.SigningKey = cert.PrivateKey;
当然,该证书必须具有相关联的私钥,否则PrivateKey属性返回null。您还可以通过使用KeyInfoX509Data类将X.509证书信息添加到已签名的KeyInfo元素中,如以下代码所示:
// Add X.509 certificate info to the KeyInfo element. Assumes a
// SignedXml object in sig and an X509CertificateEx in cert.
KeyInfoX509Data keyInfoX509 =
new KeyInfoX509Data(cert, X509IncludeOption.EndCertOnly);
sig.KeyInfo.AddClause(keyInfoX509);
要验证用证书的私钥签名的XML签名,可以调用SignedXml类的CheckSignature方法的新重载,该新重载采用一个X509CertificateEx对象和一个布尔值。如果该布尔值被设置为true,则该方法将针对证书中的公钥验证签名,并且通过检查密钥使用率以及生成到受信任的根颁发者的链条来验证该证书。
// Check the signature against the cert and verify the cert. Assumes a
// SignedXml object in sig and an X509CertificateEx object in cert.
bool verified = sig.CheckSignature(cert, true);
还可以使用不带任何参数的CheckSignature方法来验证签名,但是它不会同时验证X.509证书(如果使用了一个这样的证书对XML进行签名)。作为一个单独的步骤,您必须根据KeyInfo重新创建X509CertificateEx对象并对其进行验证。有关出于测试目地创建X.509证书的更多信息,请参阅.NET Framework SDK文档中的证书创建工具(Makecert.exe)主题。
.NET Framework 2.0向签名引擎中添加了几个新的转换和规范化算法。这些算法包括Exclusive C14N规范化算法、XML解密转换和LTA转换。图14包含.NET Framework 2.0签名引擎中提供的转换和规范化算法的完整表格。
Class | Description |
---|---|
XmlDecryptionTransform | Decrypts encrypted XML |
XmlDsigBase64Transform | Decodes base64 encoded data |
XmlDsigC14NTransform | Performs C14N canonicalization (see http://www.w3.org/TR/xml-c14n for more information) |
XmlDsigEnvelopedSignatureTransform | Removes an enveloped signature from a document |
XmlDsigExcC14NTransform | Performs exclusive C14N canonicalization (see http://www.w3.org/TR/2002/REC-xml-exc-c14n-20020718 for more information) |
XmlDsigXPathTransform | Applies an XPath filter to the input XML |
XmlDsigXsltTransform | Applies an XSLT transform to the input XML |
XmlLicenseTransform | Implements the LTA transform |
XmlDsigC14NWithCommentsTransform | Performs C14N canonicalization, but leaves comments in the canonicalized XML |
XmlDsigExcC14NWithCommentsTransform | Performs exclusive C14N canonicalization, but leaves comments in the canonicalized XML |
图14 转换和规范化算法库
我们在这里讨论了XML签名标准的基础知识以及它在.NET Framework 1.x中是如何实现的。我们还讨论了.NET Framework 2.0中的一些新功能,包括对XML加密标准的支持以及对XML签名的X.509证书集成。通过这些积木技术,可以使用标准与其他应用程序互操作,并且将标准支持内置到您自己的应用程序中。
Mike Downen 是 Microsoft 的 CLR 安全小组的项目经理,并从事代码访问安全性、加密术和新的 ClickOnce 安全模型方面的工作。您可以通过 mdownen@microsoft.com 与 Mike 联系。
Shawn Farkas 是 Microsoft 的 CLR 安全小组的见习软件设计工程师。他从事 ClickOnce、加密技术和 IL 验证方面的工作。请在 http://blogs.msdn.com/shawnfa 查看他的网络日记。 http://blogs.msdn.com/shawnfa/archive/2004/07/21/190073.aspx