将 Openssl 签名转换为 .NET6

Converting Openssl signing to .NET6

应用程序调用 openssl 进行签名使用

openssl rsautl -sign -in rasi.bin -inkey riktest.key -out allkiri.bin

如何将其转换为 .NET 6 以便不需要调用 openssl?

riktest.key 是包含

的文本文件
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAfddAQEArYjDH7msMFifeYc1AG/TkKpcz2LITI73sC0eqnlgmWi3F7PD
Bo8lWrCw32h3v/FFMrK8KuktlnBtsSLaCCz1DWuXORzHaW7EqG8O8QNzFSmhIoqp
...

这是 ASP.NET6 MVC 应用程序。 .NET 6 System.Security.Cryptography 命名空间是否包含此 OpenSsl 功能?

为什么通常无法使用本机 .NET 方法

对于 RSASignaturePadding.Pkcs1,本机 .NET 实现 SignData() and SignHash() follow the RSASSA-PKCS1-v1_5 signature scheme described in RFC8017, which applies EMSA-PKCS1-v1_5 作为编码操作:对消息进行哈希处理并签署以下值(即使用私钥加密):

EM = 0x00 || 0x01 || PS || 0x00 || T

这里PS由这么多0xff值组成,EM的大小等于密钥模数的大小。 T 是 DigestInfo 值的 DER 编码,它包含摘要 OID 和散列,例如对于 SHA256:

3031300d060960864801650304020105000420 || H 

其中 H 是要签名的消息 M 的 32 字节 SHA56 哈希值。

相比之下,openssl rsautl直接使用RSA算法,如NOTES部分所述,即对以下数据进行签名:

EM' = 0x00 || 0x01 || PS || 0x00 || M

这通常无法通过本机 .NET 方法实现(特殊用例除外,见下文):SignData() 散列因此失败,SignHash() 不散列但在内部(像 SignData()) 生成 DigestInfo 值的 DER 编码。

另一种选择是 BouncyCastle,它使用 NoneWithRSA 算法签名,就像 openssl rsautl.

该算法的一个缺点是,由于缺少散列,只能对短消息进行签名,因为较长的消息无法满足长度标准(根据该标准,EM' 的大小必须对应于密钥的模数)。

密钥导入

发布的密钥是 PKCS#1 格式的 PEM 编码私钥。

.NET 支持使用 ImportFromPem() since .NET 5, but the import of DER encoded keys has been supported since .NET Core 3.0. A private DER encoded key in PKCS#1 format can be imported with ImportRSAPrivateKey() 导入 PEM 编码密钥(private/public,PKCS#8/PKCS#1 格式)(PEM 和 DER 编码之间的转换很简单,包括删除页眉、页脚和换行符以及剩余正文的 Base64 解码)。

BouncyCastle 支持使用 PemReader class.

导入 PEM 编码密钥

已发布的 OpenSSL 功能的可能实现与 BouncyCastle

rasi.bin保存来自dataToSign[=110的数据时,以下代码生成与OpenSSL语句相同的签名=] 持有来自 privatePkcs1Pem:

的密钥
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System;
using System.IO;

...

// For testing purposes a 512 bits key is used.
// In practice, keys >= 2048 bits must be used for security reasons!
string privatePkcs1Pem = @"-----BEGIN RSA PRIVATE KEY-----
                            MIIBOwIBAAJBANoHbFSEZoOSB9Kxt7t8PoBwmauaODjECHqJgtTU3h4MW5K3857+
                            04Flc6x6a9xxyvCKS5RtOP2gaOlOVtrph0ECAwEAAQJBALu8LpRr2RWrdV7/tfQT
                            HIJd8oQnbAe9DIvuwh/fF08IwApOE/iGL+Ded49eoHHu1OXycZhpHavN/sQMnssP
                            FNECIQDyDIW7V5UUu16ZAeupeQ7zdV6ykVngd0bb3FEn99EchQIhAOaYe3ll211q
                            SIXVjKHudMn3xe6Vvguc9O7cwCB+gyqNAiEAsr3kk6/de23SMZNlf8TR8Z8eyybj
                            BAuQ3BMaKzWpyjECIFMR0UFNYTYIyLF12aCoH2h2mtY1GW5jj5TQ72GFUcktAiAf
                            WWXnts7m8kZWuKjfD0MQiW+w4iAph+51j+wiL3EMAQ==
                            -----END RSA PRIVATE KEY-----";
byte[] dataToSign = Convert.FromHexString("3031300d060960864801650304020105000420d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592");

// Import private PKCS#1 key, PEM encoded
PemReader pemReader = new PemReader(new StringReader(privatePkcs1Pem));
AsymmetricKeyParameter privateKeyParameter = ((AsymmetricCipherKeyPair)pemReader.ReadObject()).Private;

// Sign raw data
ISigner signer = SignerUtilities.GetSigner("NoneWithRSA");
signer.Init(true, privateKeyParameter);
signer.BlockUpdate(dataToSign, 0, dataToSign.Length);
byte[] signature = signer.GenerateSignature();

Console.WriteLine(Convert.ToHexString(signature)); // 8C83CAD897EDA249FEC9EBA231061D585DAFC99177267E3E71BB8A3FCE07CC6663BF4DF7AF2E1C1945D2A6BB42EB25F042228B591FC18CDA82D92CAAE844670C

特殊用例 - 当可以使用本机 C# 方法时

如果rasi.bin包含DigestInfo值的DER编码,则不需要BouncyCastle。
以下示例假定 rasi.bin 包含消息 DigestInfo 值的 DER 编码 The quick brown fox跳过以 SHA256 为摘要的懒狗。 IE。最后 32 个字节对应于 SHA256 哈希。
那么使用本机 .NET 方法的可能实现是:

using System;
using System.Security.Cryptography;

...

// For testing purposes a 512 bits key is used.
// In practice, keys >= 2048 bits must be used for security reasons!
string privatePkcs1Pem = @"-----BEGIN RSA PRIVATE KEY-----
                            MIIBOwIBAAJBANoHbFSEZoOSB9Kxt7t8PoBwmauaODjECHqJgtTU3h4MW5K3857+
                            04Flc6x6a9xxyvCKS5RtOP2gaOlOVtrph0ECAwEAAQJBALu8LpRr2RWrdV7/tfQT
                            HIJd8oQnbAe9DIvuwh/fF08IwApOE/iGL+Ded49eoHHu1OXycZhpHavN/sQMnssP
                            FNECIQDyDIW7V5UUu16ZAeupeQ7zdV6ykVngd0bb3FEn99EchQIhAOaYe3ll211q
                            SIXVjKHudMn3xe6Vvguc9O7cwCB+gyqNAiEAsr3kk6/de23SMZNlf8TR8Z8eyybj
                            BAuQ3BMaKzWpyjECIFMR0UFNYTYIyLF12aCoH2h2mtY1GW5jj5TQ72GFUcktAiAf
                            WWXnts7m8kZWuKjfD0MQiW+w4iAph+51j+wiL3EMAQ==
                            -----END RSA PRIVATE KEY-----";
byte[] sha256DigestInfoDer = Convert.FromHexString("3031300d060960864801650304020105000420d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592");
byte[] sha256HashToSign = new byte[32];
Buffer.BlockCopy(sha256DigestInfoDer, sha256DigestInfoDer.Length - sha256HashToSign.Length, sha256HashToSign, 0, sha256HashToSign.Length);

using (RSA rsa = RSA.Create())
{ 
    rsa.ImportFromPem(privatePkcs1Pem);
    byte[] signature = rsa.SignHash(sha256HashToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // pass the SHA256 hash, internally the DER encoding of the DigestInfo is generated (which is why the digest must be specified)
    Console.WriteLine(Convert.ToHexString(signature)); // 8C83CAD897EDA249FEC9EBA231061D585DAFC99177267E3E71BB8A3FCE07CC6663BF4DF7AF2E1C1945D2A6BB42EB25F042228B591FC18CDA82D92CAAE844670C
}

给出了 same 签名,因为 rasi.bin 在这两种情况下是相同的。

但是,请记住最后一种方法只有在 rasi.bin 包含 DigestInfo 值的 DER 编码时才有效,而第一个解决方案适用于 rasi.bin 中的 arbitrary 数据(只要满足长度标准)。