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

使用 XML 数字签名进行应用程序许可

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (163投票s)

2003 年 9 月 8 日

Ms-PL

17分钟阅读

viewsIcon

1053027

downloadIcon

22765

使用基于请求和签名的许可机制,通过 XML 数字签名来许可您的应用程序。

引言

在大多数开发人员的职业生涯中,应用程序许可都会成为一个问题。存在许多解决方案,但它们通常价格昂贵且难以实现。很少有方案能与现有代码库无缝集成。在这种情况下,可能需要编写一个内部许可机制。然而,这样的任务面临许多问题。

任何许可机制中最大的挑战之一是创建一个无法被用户破解的方案。一些方案依赖于 Internet 来提供身份验证系统。显然,这种方案需要 Internet 连接,这可能会给用户带来负担。然而,将信息存储在用户的计算机上,也给了用户破解许可方案的机会。需要的是一个可以驻留在用户计算机上但又无法被更改的解决方案。

这样的解决方案确实存在:XML 数字签名。实际上,公钥密码学一般都可以解决这个问题,但 XML[1] 和 XML 数字签名[2] 的易用性和灵活性使得使用 XML 数字签名成为一个有吸引力的解决方案。

本文将解释公钥密码学基本原理及其与 XML 数字签名的关系,详细介绍密钥对的创建,提供签名和验证 XML 文档的示例,并讨论提供可靠应用程序许可的不同实现。

注意:这不是一个可以直接添加到您应用程序中的完整解决方案,而是一个关于如何使用 XML 数字签名对 XML 进行签名的教程。我仅提供一个简单的实现作为示例。有许多第三方库使用类似的技术,它们已准备好集成到您的产品中并可供部署。

密码学

简而言之,密码学只是隐藏文本的手段。明文经过不同复杂度的算法处理,生成密文,即不可读的文本[4]。将明文转换为密文称为加密,将密文转换回明文称为解密。虽然许多开发人员可能被细节所困扰,但这并不一定是一个复杂的过程。密码学自公元前 1900 年就已存在。最初,密码学不过是“非标准”的符号。即使是相对现代的密码学算法并不总是复杂的。ROT13 曾经(或许仍然)在 USENET 中使用,通过将字母字符向右移动 13 位来隐藏明文,使其不被索引器读取。例如,“A”变成“N”,“B”变成“O”,以此类推。

诚然,许多现代密码学算法都很复杂,但您通常只需要了解基本原理就可以在应用程序中实现和使用密码学。最简单的密码学形式使用对称算法。这意味着两个或多个方共享相同的密钥(或密码)来加密和解密信息。前面提到的 ROT13 就是对称密钥的众多示例之一,其中密钥只是字符应向右移动 13 位的规则。大多数现代密码学算法使用非对称算法,每个方都同时拥有私钥和公钥。这也称为公钥密码学。最流行的算法是 RSA[5],在 SSL、PGP、S/MIME 和许多其他安全标准中都可以找到示例。

在使用非对称算法时,用户通过私有数据或随机数据生成一对私钥和公钥(称为密钥对)。私钥被保密和安全地存储,而公钥则自由分发。发送方使用接收方的公钥加密信息,而接收方使用其私钥解密信息。信息也可以以类似的方式签名,只是发送方使用其私钥加密明文或密文的哈希值,接收方使用发送方的公钥解密并验证哈希值。如果信息以任何方式被更改,哈希值将无效,签名验证将失败。将此技术与 XML 结合使用,就产生了我们将用于许可应用程序的 XML 数字签名。

XML 数字签名

XML 数字签名利用前面文章中讨论的公钥密码学,解决了验证信息是否来自特定来源以及信息是否未被更改的问题。此标准是 Microsoft WS-Security 规范[6] 中包含的众多标准之一,可用于验证 Web 服务响应的来源,或验证任何 XML 数据自签名以来是否未被更改。

签名是通过将对 XML 文档内容(全部或部分)的多个转换引用链接在一起创建的。这些转换之一是哈希——一种仅对源内容唯一的单向校验和;即使更改单个字符也会产生完全不同的哈希。最流行的哈希算法是 MD5[7] 和 SHA1[8]。由于 XML 文档可能具有不同的缩进和空白字符量,因此通常会应用规范化方法。规范化方法会删除空白字符和其他格式,从而将 XML 数据简化为其最简单的形式。这一点很重要,因为对具有相同数据但格式不同的 XML 文档进行哈希处理会产生两个不同的哈希值。最后,应用签名转换来加密哈希值。

XML 数字签名——前面提到的各项内容的组合,并且通常包含签名它的公钥——相对于被签名的 XML 文档可以采取不同的形式。包容型签名包含在被签名 XML 文档的文档元素下,而包含型签名则将 XML 文档包含在签名内。签名也可以是分离型的。由于包容型模式保留了原始 XML 模式[9],仅在“http://www.w3.org/2000/09/xmldsig#”命名空间中引用了合格的Signature元素

<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="license"
  targetNamespace="https://codeproject.org.cn/dotnet/xmldsiglic.asp"
  elementFormDefault="qualified"
  xmlns="https://codeproject.org.cn/dotnet/xmldsiglic.asp"
  xmlns:mstns="https://codeproject.org.cn/dotnet/xmldsiglic.asp"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"
  version="1.0">
  <xs:import id="schema" namespace="http://www.w3.org/2000/09/xmldsig#"
  schemaLocation="xmldsig-core-schema.xsd" />
  <xs:element name="license">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="computerName" type="xs:string"
          minoccurs="1" maxoccurs="1" nillable="false" />
        <xs:element name="expires" type="xs:dateTime"
          minOccurs="1" maxOccurs="1" nillable="false" />
        <xs:element ref="dsig:Signature"
          minOccurs="0" maxOccurs="1" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

由于 dsig:Signature 元素是可选的,因此此模式适用于已签名和未签名的许可证。它还允许应用程序在不验证签名或从其他不同模式(例如包含型模式)中提取内容的情况下解析许可证文档。我更喜欢这种类型的签名,并在本文后面的示例和示例源代码中使用它。

代码

以下代码将演示创建密钥对,以及签名和验证 XML 内容。所呈现的原理可应用于许多需要——或将受益于——XML 数字签名的解决方案。虽然示例源代码确实对许可请求文件进行了签名,并根据特定公钥验证了已签名的许可,但有关此解决方案如何应用于许可的细节将在后面介绍。

密钥创建

密钥创建是创建一个公钥和私钥,或者一个密钥对的直接过程。虽然存在许多创建密钥对的实用工具,但将使用 Microsoft .NET Framework SDK 提供的 sn.exe[10] 实用工具。此实用工具允许您执行许多与强命名程序集相关的任务,包括生成密钥对并将其保存到二进制文件中。

首先,在命令行中创建密钥对

sn.exe -k KeyFile.snk

在示例源代码中,相同的密钥对用于许可请求签名和强命名程序集的签名。在生产环境中,您可能希望使用单独的密钥,以防止使用破解的密钥来签名修改后的程序集或签名伪造的许可请求。

由于公钥不应包含在签名输出中——稍后将讨论这一点——因此应从验证代码中获取它。.NET 类可以轻松地从表示公钥的 XML 片段创建加密密钥,因此公钥将作为资源嵌入到示例 Verify.exe 实用工具中。为了便于进行此步骤并简化签名,公钥将导入到您计算机上的密钥容器中。

sn.exe -m y
sn.exe -i KeyFile.snk CodeProject

第一个命令只是告诉后面的容器命令作用于计算机而不是用户的容器。指定 'n' 以在用户容器上执行命令。第二个命令将密钥对安装到名为 'CodeProject' 的密钥容器中,该容器提供了从各种加密实用工具轻松访问密钥容器的途径。示例 Sign.exe 签名实用工具可以轻松访问此密钥容器——比读取 XML 文档和导入文档元素的内部 XML 要容易得多。虽然此方法以及许多其他方法都是此类解决方案的可行选项,但在本文和示例源代码中,除了签名验证之外,都将使用密钥容器。

最后,必须从密钥对中提取公钥并将其导出到 XML 文档。由于密钥对现在位于密钥容器中,因此此代码变得简单。

CspParameters parms = new CspParameters(1);
parms.Flags = CspProviderFlags.UseMachineKeyStore;
parms.KeyContainerName = "CodeProject";
parms.KeyNumber = 2;
RSACryptoServiceProvider csp = new RSACryptoServiceProvider(parms);
Console.WriteLine(csp.ToXmlString(false));

感谢 Pol Degryse 指出我必须明确设置要使用的存储区,尽管 sn.exe 设置了默认存储区,以及 Security MVP Michel Gallant 在 microsoft.public.dotnet.security 上指出每个容器使用两个密钥对,并且我必须指定 AT_SIGNATURE (2) 而不是默认的 AT_KEYEXCHANGE (1),这导致密钥对在不同计算机上不唯一。

由于此代码将 XML 片段写入标准输出,因此只需将标准输出重定向到一个文件,然后将其嵌入到 Verify.exe 项目中。

ExtractPubKey.exe > PubKey.xml

将文件 PubKey.xml 作为嵌入式资源添加到 Verify 项目中以供将来使用。

签名

要使用包容型 XML 数字签名对 XML 文档进行签名,我们需要考虑各种因素。由于要签名的内容包含签名,而签名又包含内容的哈希值,因此我们显然不能对整个内容进行签名,因为直到计算哈希值之后才能计算签名。我们还必须确保在计算签名时忽略空白字符,否则即使由于各种传输问题导致的空白字符变化也会导致签名文档无效。

转换算法[11] 通过将特定算法应用于原始内容或来自其他转换的输出,来解决这些以及其他许多问题。一种特殊的转换是在签名计算中消除空白字符和可选注释:规范化[12]。规范化使用字符集来确定哪些字符不应包含在签名计算中。根据所使用的规范化方法,可以选择移除计算签名中的注释。

另一个值得关注的转换算法是包容型签名转换[13]。此转换使用 XPath 表达式“not(ancestor-or-self::dsig:Signature)”在计算过程中过滤签名。最终结果是仅对内容进行签名,或根据哈希值进行验证。签名——在哈希值更改时会发生更改——不包含在哈希中。

一旦选择了必要的转换,就可以将它们应用于要签名​​的内容。

// Load the license request file.
XmlDocument xmldoc = new XmlDocument();
xmldoc.Load(args[0]);

// Get the key pair from the key store.
CspParameters parms = new CspParameters(1);
parms.Flags = CspProviderFlags.UseMachineKeyStore;
parms.KeyContainerName = "CodeProject";
parms.KeyNumber = 2;
RSACryptoServiceProvider csp = new RSACryptoServiceProvider(parms);

// Creating the XML signing object.
SignedXml sxml = new SignedXml(xmldoc);
sxml.SigningKey = csp;

// Set the canonicalization method for the document.
sxml.SignedInfo.CanonicalizationMethod = 
  SignedXml.XmlDsigCanonicalizationUrl; // No comments.

// Create an empty reference (not enveloped) for the XPath
// transformation.
Reference r = new Reference("");

// Create the XPath transform and add it to the reference list.
r.AddTransform(new XmlDsigEnvelopedSignatureTransform(false));

// Add the reference to the SignedXml object.
sxml.AddReference(r);

// Compute the signature.
sxml.ComputeSignature();

// Get the signature XML and add it to the document element.
XmlElement sig = sxml.GetXml();
xmldoc.DocumentElement.AppendChild(sig);

// Write-out formatted signed XML to console (allow for redirection).
XmlTextWriter writer = new XmlTextWriter(Console.Out);
writer.Formatting = Formatting.Indented;

try
{
  xmldoc.WriteTo(writer);
}
finally
{
  writer.Flush();
  writer.Close();
}

在代码中,出于将在许可主题中更详细讨论的原因,签名密钥对的公钥未包含在输出中。

验证

要验证已签名的 XML 文档,我们只需加载已签名的 XML 文档,从嵌入式 XML 公钥资源中重构加密服务提供程序 (CSP),并根据 CSP 中的公钥验证已签名 XML 文档中的 Signature 节点。我们用于签名 XML 的 SignedXml 对象将自动确定应用了哪些标准转换,并根据文档签名时使用的转换来计算和验证哈希值。

// Get the XML content from the embedded XML public key.
Stream s = null;
string xmlkey = string.Empty;
try
{
  s = typeof(Verify).Assembly.GetManifestResourceStream(
    "CodeProject.XmlDSigLic.PubKey.xml");

  // Read-in the XML content.
  StreamReader reader = new StreamReader(s);
  xmlkey = reader.ReadToEnd();
  reader.Close();
}
catch (Exception e)
{
  Console.Error.WriteLine("Error: could not import public key: {0}",
    e.Message);
  return 1;
}

// Create an RSA crypto service provider from the embedded
// XML document resource (the public key).
RSACryptoServiceProvider csp = new RSACryptoServiceProvider();
csp.FromXmlString(xmlkey);

// Load the signed XML license file.
XmlDocument xmldoc = new XmlDocument();
xmldoc.Load(args[0]);

// Create the signed XML object.
SignedXml sxml = new SignedXml(xmldoc);

try
{
  // Get the XML Signature node and load it into the signed XML object.
  XmlNode dsig = xmldoc.GetElementsByTagName("Signature",
    SignedXml.XmlDsigNamespaceUrl)[0];
  sxml.LoadXml((XmlElement)dsig);
}
catch
{
  Console.Error.WriteLine("Error: no signature found.");
  return 1;
}

// Verify the signature.
if (sxml.CheckSignature(csp))
  Console.WriteLine("SUCCESS: Signature valid.");
else
  Console.WriteLine("FAILED: Signature invalid.");

许可

使用所描述的概念进行应用程序许可,可以解决保护您的许可代码的技术方面问题,同时为用户提供一个简单的许可机制,该机制可以基于 Web 或电子邮件,仅用于获取许可。然而,仅凭这些概念不足以提供有效的许可工具集。在设计应用程序许可模型时,您必须考虑许多事项。此处讨论的一些概念已实现并已简化,以提供一个简单的示例。

您必须考虑的一个问题是,许可证文件可以从一台计算机传输到另一台计算机。如果一个人购买了许可证,而该许可证没有与该个人的机器或个人身份绑定的唯一标识符,那么他或她可以轻松地发布或转售该许可证。

为了解决这个问题,您应该在许可证文件中包含一些唯一标识符。如果您想将许可证限制为单台计算机,可以包含 MAC 地址或计算机 SID 在许可证中,并在验证签名后,将存储的唯一标识符与计算机的标识符进行比较。尽管签名的 XML 文档可能是有效的,但两个不同的标识符将使许可证无效。有许多标识符可用于识别计算机,但您应确保标识符在不同计算机之间是唯一的。

您还可以——取决于您的许可证模型——使用唯一的个人标识符,但它必须是自动获取的。如果您向用户询问或查询应用程序中的信息,而这些信息可以在不影响用户环境的情况下进行更改(例如,未经身份验证从 Outlook 获取电子邮件地址),那么用户可以轻易地撒谎。一个例子是使用用户的 Microsoft Passport PID。由于用户在获取此信息时必须进行身份验证,因此可以合理地信任该用户身份,具体取决于您的策略。

在此示例中,我仅从 Environment 类获取计算机名称。在实践中,我编写了一个托管 C++ 程序集,该程序集可以获取计算机 SID,该 SID 对于给定域中的每台计算机都是唯一的。

您还应该考虑实现过期日期,或者签名日期与之后的一定天数相结合,在签名失效之前。这种做法在密码学系统中很常见,可以为停止向许可违规者或已不存在的客户提供产品和服务,他们可能还会整体出售其设备。您甚至可以提供一个吊销列表(强制过期的许可证列表)——这可以在 SSL[14] 实现中找到——但这将强制依赖 Internet 来定期检查吊销列表。

在此主题的最后——但绝不是最终的考虑——是客户端代码不应包含任何私钥信息,也不应允许身份或密钥欺骗。

在前面的代码中,XML 公钥文件已嵌入到验证应用程序中。它同样可以被直接编码到源代码中。由于代码包含此公钥,因此它可以根据该密钥验证签名,而不是使用 XML Signature 元素中的公钥。如果验证代码信任已签名文档中的公钥,那么任何人都可以用他们的私钥签名一个新许可证,并且他们的公钥将被包含并用于验证文档。

嵌入私钥(或整个密钥对)可能会更糟,因为他们不仅可以用您的私钥签署新许可证,还可以提取密钥对并用它来签名看起来像您或您的组织的程序集。他们可以轻松地反编译、更改、重新编译并签名您的程序集,同时保持其有效性,有效地用潜在的恶意代码替换您的代码并将其分发给客户。毕竟,程序集是用您的密钥对签名的。

即使保护好您的私钥——这是支持密钥容器而不是密钥对文件的另一个有力论据——这种机制仍然可以被规避。由于 .NET 程序集即使使用 Microsoft .NET Framework SDK 中的 ildasm.exe[15] 等工具进行反编译、更改并使用新的签名密钥重新编译,因此您应该非常注意混淆您的代码,将验证程序集深埋在一系列自动根据其强名称验证依赖程序集的程序集中,同时 CLR 验证每个程序集的哈希值,并保持对代码的控制。没有任何密码系统是完美的,但您可以采取措施使其“防傻瓜”,即免受不知情“傻瓜”的侵害。

这些信息是如何获得的?

可以通过请求流程获取这些信息。这在某些密码学系统中很常见,例如 SSL,其中个人请求使用其分配的信息来签署许可证。在我们的示例中,一个单独的客户端应用程序——例如小型 Windows 或命令行实用程序,甚至是一个网站上的 ActiveX 控件或 Java 小程序——或者您应用程序的试用版本,将收集关于用户机器或身份的唯一信息。然后,用户或小程序会将该请求发送给您或您的组织进行签名,然后返回给用户。通过这种方式,您可以获得验证客户端机器或个人身份所需的信息,同时控制私有信息,从而控制签名请求的能力。如果您实现了过期日期或用户更改了相关信息,他们将必须使用更新后的数据重新提交请求给您签名——如果您或您的组织希望续订许可证。这使您对许可的应用程序拥有最终控制权,并且可以根据您的设计变得灵活。

为了继续我们的示例,让我们采用我们简单的许可证模式,并查看在客户端机器上生成的示例请求文件。

<license>
  <computerName>MYCOMPUTER</computerName>
  <expires>2003-09-31T00:00:00</expires>
</license>

请求文件包含计算机名称(请记住,这只是一个示例,并且不能保证计算机名称是唯一的,并且很容易被更改)和过期日期。用户随后会将许可证请求文件或片段发送给您或您的组织。在审核并可能生成采购订单后,您将在请求文件上运行签名应用程序,并通过自动方式或提供安装文件的说明将其发送回用户。

如果将上述请求传递给示例 Sign.exe 应用程序,将生成以下输出。

<license>
  <computerName>MYCOMPUTER</computerName>
  <expires>2003-09-31T00:00:00</expires>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod
        Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
      <SignatureMethod
        Algorithm="http://www.w3.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.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>eU3Par59M28X1c1DNORnhmW0Z2Y=</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>epyuHLJbmyscoVMg2pZZAtZJbBHsZFUCwE4Udv+u3TfiAms2HpLgN3cL
      NtRlxyQpvWt1FKAB/SCk1jr0IaeE7oEjCp2mDOOHhTUTyiv2vMJgCRecC1PLcrmR9ABhqk
      itsjzrCt7V3eF5SpObdUFqcj+n9gjuFnPQtlQeWcvKEcg=</SignatureValue>
  </Signature>
</license>

注意:SignatureValue 的值已为了方便您而进行了换行,但它应该在一行上。

如果您将签名文档通过 Verify.exe 应用程序,您将看到签名是有效的,并且文档未被更改。如果您更改文档中的任何内容——例如过期日期或计算机名称——并再次运行验证应用程序,签名将无效,因为加密的哈希值与更改内容的计算哈希值不同。再加上对计算机名称、过期日期,甚至文件是否存在进行检查,您将获得有效的许可证验证。

摘要

XML 数字签名使用行业标准的加密来对 XML 文档进行签名。使用 XML 文档作为许可证请求文件,可以让您控制客户可以对您的应用程序做什么以及可以在多长时间内使用,并允许您提供一种独立(disconnected)的许可机制,该机制既安全又唯一地绑定到特定机器或用户。提供此功能级别的 API 已经存在于 Microsoft .NET Framework 基本类库中,易于使用,并提供与您其他 .NET 应用程序相同的面向对象编程模型。

许可您的应用程序不必很困难。通过 XML 数字签名和精心设计的验证,您可以拥有一个简单而有效的许可机制,该机制既可扩展又使用行业标准。

参考文献

下面是本文档编写过程中使用或您可能有助于更好地理解相关主题的参考列表。多年来为本文档讨论的主题领域进行的一般研究中使用的其他参考资料未包含在内。

  1. 可扩展标记语言 (XML): http://www.w3.org/XML/
  2. XML 签名语法和处理: http://www.w3.org/TR/xmldsig-core/
  3. RSA 实验室 | 密码学 FAQ: http://www.rsasecurity.com/rsalabs/faq/index.html
  4. Counterpane 实验室:应用密码学 (Bruce Schneier): http://www.schneier.com/book-applied.html
  5. RSA Security: http://www.rsasecurity.com/
  6. WS-Security 规范索引页: http://msdn.microsoft.com/library/en-us/dnglobspec/html/wssecurspecindex.asp
  7. MD5: http://www.ietf.org/rfc/rfc1321.txt
  8. SHA1: http://csrc.nist.gov/publications/fips/fips180-1/fip180-1.txt
  9. XML Schema: http://www.w3.org/XML/Schema
  10. 强命名工具 (sn.exe): http://msdn.microsoft.com/library/en-us/cptools/html/cpgrfstrongnameutilitysnexe.asp
  11. 转换算法: http://www.w3.org/TR/xmldsig-core/#sec-TransformAlg
  12. 规范化算法: http://www.w3.org/TR/xmldsig-core/#sec-c14nAlg
  13. 包容型签名转换: http://www.w3.org/2000/09/xmldsig#enveloped-signature
  14. SSL 3.0 规范: http://wp.netscape.com/eng/ssl3/
  15. MSIL 反汇编器 (ildasm.exe): http://msdn.microsoft.com/library/en-us/cptools/html/cpconmsildisassemblerildasmexe.asp
© . All rights reserved.