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

在共享 Web 托管环境中使用 RSA 公钥加密

2007 年 10 月 20 日

CPOL

10分钟阅读

viewsIcon

297527

downloadIcon

5332

本文介绍了一种在由共享托管公司托管的 Web 服务器上运行的脚本中使用 RSA 公钥加密的方法。它还演示了如何在 .NET 中使用 RSA 来解决“实际世界”问题,即对许可证代码进行签名,使其无法伪造。

Screenshot - rsa.gif

引言

如果您打算在共享托管公司维护的 Web 服务器上使用 RSA 加密(许多小型网站都是如此),您可能会感到运气不佳。 .NET 提供的 RSA 加密类(RSACryptoServiceProvider)在此类环境中不可用,因为它会危及服务器自身私钥的安全性。本文提供了一种替代解决方案,无需使用 RSACryptoServiceProvider,因此可以随处使用。

什么是公钥加密(15 字以内)?

公钥加密提供了一种安全、防窃听的方式,可以在无需事先约定任何秘密密钥信息的情况下,将少量数据从一方传输到另一方。要实现这一点,您需要一个**公私钥对**。公钥可公开获取,而私钥则由(通常是首次生成它的人)保密。用公钥加密的数据只能由持有私钥副本的人解密,而用私钥加密的数据则可以确定来自该密钥的持有者。这里有更多阅读材料:这里

我们的公司 AlpineSoft 使用 RSA 公钥加密来生成无法伪造的软件许可证代码。当用户购买我们的共享软件应用程序时,他们会收到一封包含使用我们私钥签名的许可证代码的电子邮件。在接收端,软件使用我们的公钥验证此代码,只有在签名有效的情况下,许可证代码才会被接受。此过程是自动化的,但我们在公共 Web 服务器上的签名过程中遇到了问题,该服务器运行在共享托管服务上。本文的主题就是这个问题以及我们如何克服它。

什么是数字签名?

数字签名是一种证明信息(在我们的例子中是许可证代码)来自特定来源的方法。实际上签名的是数据本身,而是数据的**哈希值**(也称为消息摘要)。哈希值是通过“单向”哈希算法计算得出的固定长度的字节字符串(MD5 为 16 字节,SHA1 为 20 字节)。其原理是,每个消息几乎都会生成一个唯一的哈希值,并且人为地构造另一个生成相同哈希值的不同消息几乎是不可能的。如果您想了解更多关于哈希的信息,请阅读此文

要对数据进行签名,我们这样做:

  1. 计算要签名的数据的哈希值
  2. 使用我们的私钥加密哈希值
  3. 将数据和加密的哈希值发送给接收者

换句话说,数字签名只是对要签名的数据进行加密的哈希值。在接收端,签名数据的完整性按以下方式验证:

  1. 使用我们的公钥解密数字签名
  2. 计算收到的数据的哈希值
  3. 比较从数据计算出的哈希值与解密签名中包含的哈希值

如果值不同,则签名无效。此过程的关键在于,只有发送者才能生成有效的加密哈希值,因为只有发送者知道私钥。

在 .NET 中使用数字签名

.NET 中的 RSA 加密服务由 RSACryptoServiceProvider 类提供。还有用于执行哈希的类,即 MD5CryptoServiceProviderSHA1CryptoServiceProvider。一旦您理解了它们的工作原理,这些类都不难使用。

密钥生成

您需要做的第一件事是生成您自己的私钥-公钥对。您还可以将其导出为 XML 格式,以便以后引用。您只需要执行一次此操作,并且可以在任何计算机上执行,不一定是在您将用于签名内容的计算机上。其执行机制很简单:

RSACryptoServiceProvider rsacp = new RSACryptoServiceProvider (512);
string my_key_pair_formatted_as_an_xml_string = rsacp.ToXmlString (true);

这里的 512 是您所需的密钥长度(以位为单位)。微软并没有明确说明,但 RSACryptoServiceProvider 构造函数会为您生成指定长度的随机密钥对。生成的 XML 字符串封装了构成您密钥的魔术数字。它看起来像这样(格式化后):

<RSAKeyValue>
    <Modulus>zjFmn/hT8J3wZqW5IhU4aQggHtqZmL+OpWO1HCgo4x38HAbRXrrzXH2d3FA0AOSipfluDh1vSq/
        FMC/Kvm//xw==</Modulus>
    <Exponent>AQAB</Exponent>
    <P>+yOEiADmhrquMF4vms1fo4jItF/PlziDNleyxEZLqFk=</P>
    <Q>0i8pKzRseS6ODbgFTw0pZEf9Z+SXtsyDWqk09CVH5R8=</Q>
    <DP>0hE0k5rFOV8/wv+VrFQrspwA3jfiaeiAgN08kEcIlAk=</DP>
    <DQvLrE5ZRa1TkpwVk3eKAyVeGihu9XFel9+gsJ6OA6cJSM=</DQ>
    <InverseQ>Uj3vYWWtNW346N6Tn25RmDWfNrlysdKl1qkANGx4JEI=</InverseQ>
    <D>zXQgBAoW6c0WO9Gp1TI70TxNdTDwl2lYI6hkUDgb9aChZfOswClaRV/
        ApM+QZQQmFYZbWphmMtlUrEAuMe6C4Q==</D>
</RSAKeyValue>

如果看起来令人生畏,请不要担心——它们只是一堆非常大的数字,以 base64 字符串的形式编码,以便可以将其存储为文本(“base64”是一种方便的表示二进制数据的方式,可以转换为可打印文本)。您看到的是一个**私钥**,即它包含签名或解密消息所需的所有信息。要验证或加密消息,您只需要公钥,它只包含前两个字段。

<RSAKeyValue>
    <Modulus>zjFmn/hT8J3wZqW5IhU4aQggHtqZmL+OpWO1HCgo4x38HAbRXrrzXH2d3FA0AOSipfluDh1vSq/
        FMC/Kvm//xw==</Modulus>
    <Exponent>AQAB</Exponent>
</RSAKeyValue>

签名数据

要在您的签名脚本中签名您的许可证代码字符串(或其他内容),您必须首先使用您之前制作的私钥加载一个 RSACryptoServiceProvider 实例,方法如下:

RSACryptoServiceProvider rsacp = new RSACryptoServiceProvider (512);
string my_private_key = "<our PRIVATE key in XML form, as generated above>";
rsacp.FromXmlString (my_private_key);

现在您已准备好开始签名内容。要签名我们的许可证代码,我们执行类似的操作:

string license_code = "Licensed to J.T. Ripper, London";
ASCIIEncoding ByteConverter = new ASCIIEncoding ();
byte [] sign_this = ByteConverter.GetBytes (license_code);
byte [] signature = rsacp.SignData (sign_this, new SHA1CryptoServiceProvider ());
string base64_string = Convert.ToBase64String (signature);

这实际上做了几件事:

  1. 将许可证代码字符串转换为字节数组,使其适合哈希
  2. 计算这些字节的 20 字节 SHA1 哈希值
  3. 将哈希值打包到 PKCS#1 包装器中
  4. 通过用我们的私钥加密来签名哈希值
  5. 将数字签名(二进制)编码为 base64 字符串,以便通过电子邮件或其他方式传输

步骤 2、3 和 4 都在调用 SignData 时完成。

验证签名数据

在接收端验证许可证代码,本质上是上述过程的镜像。

RSACryptoServiceProvider rsacp = new RSACryptoServiceProvider (512);
string my_public_key = "<our PUBLIC key in XML form, as generated above>";
rsacp.FromXmlString (my_public_key);

string license_code = "<license code received by email>";
ASCIIEncoding ByteConverter = new ASCIIEncoding ();
byte [] verify_this = ByteConverter.GetBytes (license_code);
string base64_encoded_signature = "<base64-encoded signature generated above>";
byte [] signature = Convert.FromBase64String (base64_encoded_signature);
bool ok = rsacp.VerifyData (verify_this, new SHA1CryptoServiceProvider (), signature);
if (!ok)
     barf ();

这实际上做了以下事情:

  1. 使用我们的公钥加载一个 RSACryptoServiceProvider 实例
  2. 将通过电子邮件收到的许可证代码字符串转换为字节数组,使其适合哈希
  3. 计算这些字节的 20 字节 SHA1 哈希值
  4. 将为传输而编码为 base64 的数字签名转换回字节数组
  5. 解密数字签名中包含的哈希值并移除 PKCS#1 包装器
  6. 将解密的哈希值与我们刚刚计算出的哈希值进行比较

步骤 3、5 和 6 都在调用 VerifyData 时完成。

就这样,告别软件盗版。或者至少应该是这样,但是……

但是有一个问题

在弄清楚如何使我们的许可证代码安全后,我们以为可以坐下来休息了,但当我们尝试在我们的公共 Web 服务器(运行在共享托管环境中)上运行签名脚本时,我们从 RSACryptoServiceProvider.FromXmlString() 收到了以下安全异常:

System.Security.SecurityException: Request for the permission of type
'System.Security.Permissions.KeyContainerPermission, mscorlib, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.

救命!我们该怎么办??花了一点时间谷歌搜索,并向我们(出色的) Web 托管提供商 Liquid Six 发送了一封快速电子邮件,我们了解到原因深埋在 Windows 加密 API 中,RSACryptoServiceProvider 基于此。本质上,允许脚本加载自己的私钥会危及 Windows 密钥存储的安全性,因此所有有信誉的 Web 托管提供商都会禁用它,以免恶意脚本窃取/覆盖托管提供商自己的私钥。这在我看来是 Windows 加密 API 的一个重大失误,但这就是现状。我想我们只能这样了。

进一步的谷歌搜索发现了两个重要的资源:Chew Keong TAN 最出色的BigInteger 类和一些 LGPL 的“C”代码,用于执行所需的计算和 PKCS#1 封装,来自XySSL(最初由 Christopher Devine 编写)。这些资源对我特别有用,因为(a)能够处理数百位数字是专业领域,并且(b)我讨厌 ASN.1(PKCS#1 格式基于此)。计算本身非常简单。

几天之后,东拼西凑,**EZRSA** 就诞生了。EZRSA 几乎可以完成 RSACryptoServiceProvider 的所有功能,但完全在托管代码中运行,并且**不**使用 Windows 加密 API。因此,**它可以在任何地方运行,无论您的 Web 托管提供商对您施加何种信任级别**(这正是我们需要的)。

Using the Code

使用 EZRSA 比理解它在做什么要简单得多。它与 RSACryptoServiceProvider 几乎是即插即用的兼容,因此如果您已经使用后者编写了代码,您应该可以轻松地将其转换为使用 EZRSA。EZRSA 在 .NET 1.1 或更高版本下运行(但如果您想在 .NET 1.1 下运行,则需要手动编译或在 Visual Studio .NET 2003 下构建;真讨厌!)。

我包含了一个 Visual Studio 2005 项目来构建 EZRSA 作为 DLL(*AlpineSoft.EZRSA.dll*),然后您可以将其直接放入 Web 服务器的 bin 目录中。我还包含了一个 Chew Keong TAN 的BigInteger 类的副本,因为他页面上的那个有一个错误,在我提供的版本中已修复。

如果您正在寻找预编译的 DLL(为 .NET 2.0 构建),您会在演示程序中找到它(见文章顶部的链接)。此程序演示了基本原理,并展示了如何使用 RSA 来加密和解密数据,如果您对这些感兴趣。但请注意,不要(以任何形式)使用 RSA 来加密大型消息。它不是为此设计的,而且非常慢。相反,使用您的 RSA 公钥加密 DES、Blowfish 或任何对称密钥,并先将其发送给接收者。然后,您可以使用此对称密钥通过您选择的加密方法安全地加密消息正文,因为只有接收者才能解密对称密钥。我不知道在共享托管环境中执行此操作是否存在任何“信任级别”问题。

关注点

在完成这个项目时,最让我印象深刻的是 C# 是一个多么好的编程语言。当我完成后,我从 C 语言转换过来的代码变得更短、更易读。这为添加一些原始代码显然不具备的附加功能(例如 RSACryptoServiceProvider 兼容性)留出了时间。我还发现,RSA 部分基于两位狡猾的家伙 Pierre Fermat(1601-1665)和 Leonhard Euler(1707-1783)的工作,他们都在数学,尤其是数论方面取得了突破性进展,这很有趣。

最后,关于如何安全地保管您的私钥。上述方法的主要问题是您的私钥会出现在您的服务器端脚本中(尽管它永远不会发送到浏览器)。正是这个限制促使微软在加密 API 中将私钥锁定在密钥容器中。不幸的是,他们将其设为唯一的使用方式,我认为这是一个错误。无论如何,从 .NET 的角度来看,使事情更安全的一种显而易见的方法是将您的私钥编译到一个 DLL 中,或许将其包装在一个简单的 API 中,以便(例如)签名一个字符串。这样,如果有人入侵了您的 Web 服务器,至少他们不会得到您的私钥的明文副本。

哦,还有一件事。现在显然有可能在不到一年的时间里破解一个 640 位 RSA 密钥,只要您下定决心。这说明计算机发展得有多快。当 RSA 在 1977 年设计出来时,估计需要超过 10 亿年。

历史

  • 2007 年 10 月:初始版本
  • 2009 年 3 月:更新以正确处理使用 SignedXml 类签名 XML 文档。请注意,使用 EZRSA 时,SignedXml::CheckSignature 总是会失败,因为 SignedXml::CheckSignature 在内部检查传入的 RSA 对象是否派生自 RSACryptoServiceProvider,而 EZRSA 不(也不能)这样做。抱歉,这是微软的错误,不是我的,我对此无能为力。请使用 RSACryptoServiceProvider 来检查 XML 签名(如果可以的话),或者您可以参考 Microsoft .NET 源代码(现已公开),并根据您找到的内容编写自己的 CheckSignature 函数。
    感谢 Wout de Zeeuw 的协助(实际上大部分工作是他完成的)。
  • 2009 年 6 月:修复了 Encrypt() 中的两个重要错误,均由 xxyyz(又名 KurtA)报告。非常感谢他提供了错误报告和解决方案!有关详细信息,请参见他下面的帖子。
  • 2011 年 3 月:增加了对 SHA-256 哈希的支持。感谢 Pierre Arnaud 的贡献。

http://www.alpinesoft.co.uk

© . All rights reserved.