ASP.NET Web应用程序中符合FIPS的数字签名






4.78/5 (5投票s)
在本文中,我将解释我如何创建符合FIPS的数字签名解决方案,以及我选择的技术的原因。
引言
以下是关于为Web应用程序实现数字签名的一些近期经验的叙述。强制.NET Framework使用符合联邦信息处理标准(FIPS)的算法被证明不是直观的。以下提供了一个解决方案,并详细记录了使用的技术及其选择的原因。
客户的初衷是在智能卡上使用私钥,但如果没有安装额外的软件,则无法访问私钥(CAPICOM ActiveX控件允许这样做,但它已不再包含在更新的Windows操作系统中,而且无论如何都不是一个完全可移植的解决方案)。另一种技术是在文档通过工作流向前移动时,在服务器上创建公钥/私钥对,并使用新生成的私钥创建数字签名。
RSA加密和数字签名
RSA是一种公钥密码系统,由Ron Rivest、Adi Shamir和Leonard Adleman于1977年在MIT创建,其姓氏是RSA名称的由来。有趣的是,一位名叫Clifford Cocks的英国数学家在1973年开发了一个类似的密码系统,但它一直被英国情报部门保密到1997年。RSA加密的主要概念是使用非常大的素数来创建数学上相关的用于加密和解密数据的值。这些值称为公钥和私钥,公钥通常发布在某种目录中,例如证书颁发机构。私钥与哈希算法一起,可用于创建数字签名。
我决定将数字签名存储在我的Web应用程序中作为序列化对象,因此我创建了一个名为DigitalSignature
的struct
。基本定义如下
private struct DigitalSignature
{
public byte[] PublicKey;
public byte[] Signature;
public byte[] UserId;
public byte[] UserName;
}
如您所见,此struct
有四个部分。两个成员存储当前用户的姓名和ID。ID可以是登录名、电子邮件或任何其他标识用户的标识符,姓名是显示在页面上的全名。Signature
是正在签名的对象的加密哈希(详细信息稍后)。PublicKey
是一组数学值,用于解密哈希(RSA公钥/私钥对的“public
”部分,或更技术地说,是模数和指数)。我将它们放入一个struct
中,以便我可以将它们序列化并存储在单个数据库列中。存储在数据库中时,签名看起来会像这样
0x9F0100003C5253414B657956616C75653E3C4D6F64756C75733E7536413274792F7045383172464D796F71305371485430587A2F56307831692B554B3259557A636957426B4531656662756764526C444A54732B2F4B596538653447776735705557654D694F645A556B66326F7A46564256617143744B33526746722B6663385462792B7459364F31752F3857744C546450754D68336F30463749745930615971554132615130354843304B6D3838784D4B5967685543723830525941504E6E6B314D6941664B41317766666B2B41374262756541434B6C6963457A78496C3959676B4C717159634F374D6B34586F444551766F65744D4C7736724B6E3364613556562B676D52664C616B444B306B536C6E444B2F6B457034397030654857554577446B6F5156366B48635964724749543173493436415237513231572B4779627A4C646878456647755A2F3466434E516E575546766F66564A77526F6C383279743264726E39413663354F57364C513D3D3C2F4D6F64756C75733E3C4578706F6E656E743E415141423C2F4578706F6E656E743E3C2F5253414B657956616C75653E0001000024419DE220AFC0F77D54EF98D3D729F4F444D6AEA57E8F17ABA74EB03F67E1AA3E265AB21673F044AC5CD5DD32E25A970A1FD89B5EB662F908EDFD76F157A8542BC227C75F315236C7D42BBDD8E168327FA0B549BF7D73CC64F08FE8D2722FFFFDAB27DBAA7DA83F88EACA1BAD1F4690CB87AE91BDFCFA026BB034B6C45E240C1413105C3F87AA0408A8E04F17E8CFD454C2E5FBFB2174E0B3234C2C0CE29BDF4CEAFD5B378750E116577E4070410FD19169A1437473EA4971F4ABC75C806986210AAE812D4117FCB3944251DA1AD9B3457F8A8317B413E7B00DA51A38D61D25162CE942A2BF61911FB02E9531F1A91E603FE0525B5464EEE9D179A41E7C90ED00010000756610345159126C31BCE512AA3DBBAE585CAA06E3A21EC94AFDF836B626A6C6A10E1D13C19619ADFE6A2BAAD89F2E4BA158EC836C2813A41B4D423F5D5298C6B01C2F4151CB823AD59FB20D444DB2F9E217D15E395E7AB6A19EBC4F2827174D7238DFB373DA451961891758A585E8E3F4E132E70377234D1298147865F35E68D522909A076DE5A7DF1661809F9A07513A8291938E7B8CC2FE6F75E7244CD299D79D25AA0727EC0AD081BA8A9D8273D01DAB7FAFD72B7E1276817926F13EFC853152A6A2AA34D9FFD27025F886639F9DDFDF63626A2CC092BF2BCF6EF92D3515E87C854E6B40A0C510894E0FB35BB355B9E8559CE73C43F84FFFE27B590E159A00010000B8D3ECFA1B64E8A451E45C33CBE9CDB7A7F402C0541D40FC1FF2CC8305C4CC450C6191417307BF1D4A1A3B79F8237C829487E78D78DF0931ECF715ACFF7977272A05EC4A7359B79B208BF3BA63061F59C39F03EDB5E9D8787AF45C564BCD26A8F2548AE5F7EF1D4946387DCEE9198F9CEB2B74D67AF24483EBDE4F61F7CFB7D4E72A787F30CE223E9B4B1866E55479377A25ECF96C4E6C15D78296B9B5931EFABFED012339EA21861C41C0B961C7157C9323E5F5C9EBBDB2AA4E945085FE07CF0AC73E442612467340ACFAC45A661C6F4950E3325131EC60597FD2DD463A6448E63F62528BF4CB8AFDED6BD275A8CF2586B2936381CBADDB81F02B5D160088B5
长度会略有不同,但由于使用了2048位密钥和SHA-512,通常会接近1200字节。生成的公钥信息约为415字节,每个哈希正好是256字节,其余是少量的开销。
我还定义了一些常量来指定哈希算法和RSA密钥大小,如下所示
/// <summary>
/// The hash algorithm to use when generating digital signatures.
/// </summary>
public const string HashAlgorithm = "System.Security.Cryptography.SHA512CryptoServiceProvider";
/// <summary>
/// The number of bits to use for the key size when creating public/private
/// key pairs to use when digitally signing data.
/// </summary>
public const int RsaKeySize = 2048;
要创建新的公钥/私钥对,我使用以下代码块实例化了RSACryptoServiceProvider
类
RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider(Constants.RsaKeySize);
在2048位密钥大小下,这行代码在我的工作站上运行需要两到三秒钟。将密钥大小增加到4096会导致执行时间延长20秒。这是因为生成公钥/私钥对时需要进行数学运算。提供程序将基于非常大的素数生成一些非常大的数字,但会随机选择数字。确保所选数字是素数需要一些时间。例如,这里有一组以384位密钥大小选择的数字
e=65537
d=9773246456229309607437744074133646299943718950328390643269336611042842984820786028726742014932282000188069915373873
n=25316571264897243626191598157569042591281087306232084489652070244388241177057425104381474781040592424701059944739309
模数n是指定密钥大小的数字。数字n和d在数学上是相关的,并且如果知道用于创建n的两个素数因子(p和q),就可以确定这种关系。由于这些素数因子没有公开,并且计算大素数因子极其耗时,因此计算d在实践中是不可能的。对于更大的密钥大小,逆向工程的难度呈指数级增长——为2048位密钥创建的数字是难以想象的。
学习RSA加密
我创建了一个工具来实验和学习RSA加密。使用此工具时,可以看到创建公钥/私钥对时生成的所有数字以及一些加密公式的实际应用。这是截图
用户可以在右上角的下拉菜单中选择密钥强度,然后单击“生成密钥对”按钮,查看使用选定密钥强度创建的极其庞大的数字(所有数字都已转换为十进制显示)。请注意,使用4096位或更高的密钥强度生成密钥可能会导致显著的延迟。
您可以通过本文开头的链接下载该工具及其源代码。
为什么指数e总是等于65537?RFC 4871(第3.3.1节)在使用RSA-SHA1签名算法时建议使用此数字,作为性能和安全性之间的折衷。它是一个素数,足够大,使得逆向工程几乎不可能,又足够小,可以保持计算速度。如果需要,可以在传递给RSACryptoServiceProvider
构造函数的CspParameters
集合中指定一个不同的指数。
数字e和n一起是公钥,而d和n一起是私钥。创建数字签名包括对要签名的数据进行哈希处理,用私钥加密哈希,并将结果与公钥一起存储,以便以后可以解密和验证。
创建数字签名
RSACryptoServiceProvider
类提供了一个SignData()
方法,该方法接受要签名的数据和一个表示哈希算法名称的string
。此方法非常方便,并且接受要使用的哈希算法作为string
允许开发人员在配置文件中指定算法名称,并轻松地以后更改它。不幸的是,当在一台仅设置为使用符合FIPS的算法的机器上执行时,使用SignData()
方法将始终导致FIPS错误,即使指定了符合FIPS的哈希算法。确定原因需要检查一些.NET Framework代码。
SignData()
方法使用反射调用与指定哈希算法名称关联的类的static Create()
方法。例如,指定哈希算法为"SHA512CryptoServiceProvider"
将导致执行SHA512CryptoServiceProvider.Create()
方法。SHA512CryptoServiceProvider
类从SHA512
类继承其Create()
方法,其中包含以下代码
public static SHA512 Create()
{
return SHA512.Create("System.Security.Cryptography.SHA512");
}
因此,尽管指定了名称"System.Security.Cryptography.SHA512CryptoServiceProvider"
,但会实例化"System.Security.Cryptography.SHA512"
。SHA512
类**不**符合FIPS标准,因此是导致错误的原因。
解决方法是直接在代码中实例化SHA512CryptoServiceProvider
并调用ComputeHash()
方法来创建数据的哈希。然后,不要调用RSACryptoServiceProvider
的SignData()
方法,而是调用RSACryptoServiceProvider
的SignHash()
方法来签名您刚刚生成的哈希。以下是一些执行此操作的代码
/// <summary>
/// Uses RSA to digitally sign supplied data.
/// </summary>
/// <param name="request">The <see cref="Document"/>
/// object containing the data to process.</param>
/// <returns>A byte stream representing a digital signature.</returns>
public static byte[] SignRequest(Document request, Person person)
{
DigitalSignature digitalSignature = new DigitalSignature();
// Create a public/private key pair
using (RSACryptoServiceProvider rsaProvider =
new RSACryptoServiceProvider(Constants.RsaKeySize))
{
// Use a FIPS compliant hash algorithm
using (SHA512CryptoServiceProvider hashProvider = new SHA512CryptoServiceProvider())
{
// Get the public key to be stored with the signature and used later for verification
digitalSignature.PublicKey = Encoding.UTF8.GetBytes(
rsaProvider.ToXmlString(includePrivateParameters: false));
// Create the hash with a specific provider for FIPS compliance (using the rsaProvider
// SignData method will cause hashing to occur with the SHA512Managed class, which is
// not FIPS compliant).
digitalSignature.Signature = rsaProvider.SignHash(
hashProvider.ComputeHash(Encoding.UTF8.GetBytes(
new JavaScriptSerializer().Serialize(request))),
Constants.HashAlgorithm);
// Store the name and ID of the person doing the signing for non-repudiation
digitalSignature.UserId = rsaProvider.SignHash(
hashProvider.ComputeHash(Encoding.UTF8.GetBytes(person.LogOnName)),
Constants.HashAlgorithm);
digitalSignature.UserName = rsaProvider.SignHash(
hashProvider.ComputeHash(Encoding.UTF8.GetBytes(person.FullName)),
Constants.HashAlgorithm);
}
}
return digitalSignature.ToArray();
}
要签名的数据包含在Document
域对象中。序列化此对象会创建一个易于哈希的数据string
。我使用了JavaScriptSerializer
,因为它很方便,但任何序列化器都可以。由于ComputeHash()
方法需要字节数组作为输入,所以我使用UTF8编码对生成的string
进行了编码,确保了最小的字节数组,同时也支持Unicode字符(如果需要)。
由于我想将生成的数字签名与公钥一起存储在单个数据库字段中,所以我需要将整个内容转换为字节数组。因此,需要一个DigitalSignature struct
。我定义的struct
还包含将所有包含的数据转换为字节数组的方法,以及从字节数组创建struct
实例(稍后用于验证)的方法。我尝试了各种序列化器,但使用BinaryWriter
实际上得到了一个更小的字节数组。例如,使用JavaScriptSerializer
会得到一个3K以上的字节数组,而使用BinaryWriter
则将大小保持在1200字节左右。DigitalSignature struct
的完整代码如下
private struct DigitalSignature
{
public byte[] PublicKey;
public byte[] Signature;
public byte[] UserId;
public byte[] UserName;
public static DigitalSignature FromArray(byte[] array)
{
DigitalSignature digitalSignature = new DigitalSignature();
MemoryStream stream = null;
try
{
stream = new MemoryStream(array);
using (BinaryReader reader = new BinaryReader(stream))
{
stream = null; // The BinaryReader now controls the stream and will dispose of it
digitalSignature.PublicKey = reader.ReadBytes(reader.ReadInt32());
digitalSignature.Signature = reader.ReadBytes(reader.ReadInt32());
digitalSignature.UserId = reader.ReadBytes(reader.ReadInt32());
digitalSignature.UserName = reader.ReadBytes(reader.ReadInt32());
}
}
finally // This pattern prevents Code Analysis CA2202 warning
{
if (stream != null)
{
stream.Dispose();
}
}
return digitalSignature;
}
public byte[] ToArray()
{
byte[] array;
MemoryStream stream = null;
try
{
stream = new MemoryStream();
using (BinaryWriter writer = new BinaryWriter(stream))
{
stream = null; // The BinaryWriter now controls the stream and will dispose of it
writer.Write(PublicKey.Length);
writer.Write(PublicKey);
writer.Write(Signature.Length);
writer.Write(Signature);
writer.Write(UserId.Length);
writer.Write(UserId);
writer.Write(UserName.Length);
writer.Write(UserName);
writer.Flush();
array = ((MemoryStream)writer.BaseStream).ToArray();
}
}
finally // This pattern prevents Code Analysis CA2202 warning
{
if (stream != null)
{
stream.Dispose();
}
}
return array;
}
}
验证数字签名
数字签名的目的是确保数据完整性和不可否认性。当文档在审批工作流中向前推进时,可能会被具有适当权限的人员更改。为了解决这个问题,并能够验证文档在特定时间点的数字签名,我设计的Web应用程序的工作方式很像一个源代码控制系统,它存储文档的所有版本。底层数据库的设计允许轻松加载文档在某个特定人员将其向前推进审批过程时包含的数据。然后可以检查此文档快照的数据完整性,确保应用程序中显示的数据仍然是签名时存在的数据的准确反映。
为了验证数据完整性,会加载文档快照,并使用创建数字签名时使用的相同哈希算法重新进行哈希处理。然后,存储在数字签名中的哈希会与相应的公钥一起被解密,并与新的哈希进行比较。如果哈希匹配,则确保了完整性。以下代码执行此功能
/// <summary>
/// Verifies that the supplied data is correctly represented in the supplied digital signature.
/// </summary>
/// <param name="request">The <see cref="Document"/>
/// object containing the data to verify.</param>
/// <param name="signature">
/// An array of bytes representing a digital signature containing previously signed data.
/// </param>
/// <exception cref="ArgumentNullException">
/// Thrown if <paramref name="request"/> is null.
/// </exception>
/// <exception cref="ArgumentNullException">
/// Thrown if <paramref name="signature"/> is null or contains no data.
/// </exception>
/// <returns>True if the supplied data matches the digital signature; otherwise, false.</returns>
public static bool VerifySignatureData(Document request, byte[] signature)
{
DigitalSignature digitalSignature;
if (null == request)
{
throw new ArgumentNullException("request", "Data to be signed must be supplied.");
}
if (null == signature || signature.Length == 0)
{
throw new ArgumentNullException("signature", "Signature data be supplied.");
}
using (RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider())
{
// Use a FIPS compliant hash algorithm
using (SHA512CryptoServiceProvider hashProvider = new SHA512CryptoServiceProvider())
{
digitalSignature = DigitalSignature.FromArray(signature);
rsaProvider.FromXmlString(Encoding.UTF8.GetString(digitalSignature.PublicKey));
return rsaProvider.VerifyHash(
hashProvider.ComputeHash(Encoding.UTF8.GetBytes(
new JavaScriptSerializer().Serialize(request))),
Constants.HashAlgorithm, digitalSignature.Signature);
}
}
}
为了确保不可否认性,用户的姓名和ID也被存储在数字签名中,并与公钥相关联。只要此数字签名不以任何方式被更改,用户就不可能否认签名了文档并将其向前推进审批流程。由于用户信息在存储前也会被哈希和加密,并且只能用相关的公钥解密,因此以任何方式更改数字签名都会导致验证失败。
关注点
本文分享了在ASP.NET Web应用程序中创建数字签名的技术,并记录了一个允许使用符合FIPS的算法的解决方法。
历史
- 2015年11月16日:初稿
- 2015年11月17日:更新格式