如何通过 .NET 使用 RSA 和 SHA256 对文件进行签名?

Posted

技术标签:

【中文标题】如何通过 .NET 使用 RSA 和 SHA256 对文件进行签名?【英文标题】:How can I sign a file using RSA and SHA256 with .NET? 【发布时间】:2011-11-18 15:56:36 【问题描述】:

我的应用程序将获取一组文件并对其进行签名。 (我不是要签署程序集。)我可以从一个 .p12 文件中获取私钥。

这是我尝试使用的代码,但我得到了 System.Security.Cryptography.CryptographicException "Invalid algorithm specified."

X509Certificate pXCert = new X509Certificate2(@"keyStore.p12", "password");
RSACryptoServiceProvider csp = (RSACryptoServiceProvider)pXCert.PrivateKey;
string id = CryptoConfig.MapNameToOID("SHA256");
return csp.SignData(File.ReadAllBytes(filePath), id);

根据answer 无法完成(RSACryptoServiceProvider 不支持 SHA-256),但我希望可以使用不同的库,例如 Bouncy Castle。

我是新手,我发现 Bouncy Castle 非常令人困惑。我正在将 Java 应用程序移植到 C#,并且我必须使用相同类型的加密来签署文件,所以我坚持使用 RSA + SHA256。

如何使用 Bouncy Castle、OpenSSL.NET、Security.Cryptography 或其他我没听说过的第 3 方库来做到这一点?我假设,如果它可以在 Java 中完成,那么它可以在 C# 中完成。

更新:

这是我从 poupou 的 anwser 中的链接得到的

X509Certificate2 cert = new X509Certificate2(KeyStoreFile, password");
RSACryptoServiceProvider rsacsp = (RSACryptoServiceProvider)cert.PrivateKey;
CspParameters cspParam = new CspParameters();
cspParam.KeyContainerName = rsacsp.CspKeyContainerInfo.KeyContainerName;
cspParam.KeyNumber = rsacsp.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2;
RSACryptoServiceProvider aescsp = new RSACryptoServiceProvider(cspParam);
aescsp.PersistKeyInCsp = false;
byte[] signed = aescsp.SignData(File.ReadAllBytes(file), "SHA256");
bool isValid = aescsp.VerifyData(File.ReadAllBytes(file), "SHA256", signed);

       

问题是我得到的结果与使用原始工具不同。据我通过阅读代码可以看出,进行实际签名的 CryptoServiceProvider 没有使用密钥存储文件中的 PrivateKey。对吗?

【问题讨论】:

另见Signing and verifying signatures with RSA C#、how to sign bytes using my own rsa private key using rs256 algorithm?、Signing data with private key in c#、How can I sign a file using RSA and SHA256 with .NET?、Signing a string with RSA private key on .NET?等 【参考方案1】:

RSA + SHA256 可以并且将会工作......

您后面的示例可能并非一直有效,它应该使用哈希算法的 OID,而不是它的名称。根据您的第一个示例,这是通过调用 CryptoConfig.MapNameToOID(AlgorithmName) 获得的,其中 AlgorithmName 是您提供的(即“SHA256”)。

首先您需要的是带有私钥的证书。我通常使用公钥文件 (.cer) 从 LocalMachine 或 CurrentUser 存储中读取我的数据来识别私钥,然后枚举证书并匹配哈希...

X509Certificate2 publicCert = new X509Certificate2(@"C:\mycertificate.cer");

//Fetch private key from the local machine store
X509Certificate2 privateCert = null;
X509Store store = new X509Store(StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
foreach( X509Certificate2 cert in store.Certificates)

    if (cert.GetCertHashString() == publicCert.GetCertHashString())
        privateCert = cert;

无论您如何到达那里,一旦您获得带有私钥的证书,我们就需要重建它。由于证书创建它的私钥的方式,这可能是必需的,但我不确定为什么。无论如何,我们首先导出密钥,然后使用您喜欢的任何中间格式重新导入它,最简单的是 xml:

//Round-trip the key to XML and back, there might be a better way but this works
RSACryptoServiceProvider key = new RSACryptoServiceProvider();
key.FromXmlString(privateCert.PrivateKey.ToXmlString(true));

完成后,我们现在可以对一条数据进行如下签名:

//Create some data to sign
byte[] data = new byte[1024];

//Sign the data
byte[] sig = key.SignData(data, CryptoConfig.MapNameToOID("SHA256"));

最后,验证可以直接用证书的公钥完成,不需要像我们用私钥那样重建:

key = (RSACryptoServiceProvider)publicCert.PublicKey.Key;
if (!key.VerifyData(data, CryptoConfig.MapNameToOID("SHA256"), sig))
    throw new CryptographicException();

【讨论】:

私钥的导出/导入可能会从不支持 SHA256(与证书关联的那个)的 CSP 切换到支持它的另一个 CSP(例如“AES”CSP) 我在导出/导入之前和之后序列化了 key.CspKeyContainerInfo 以比较它们,它确实有不同的提供程序类型,从 Microsoft Base Cryptographic Provider v1.0Microsoft Enhanced RSA and AES Cryptographic Provider 我还没有看到具有可导出私钥的生产环境。这不是一个好的答案。 这对我不起作用,我得到以下异常,“在 mscorlib.dll 中发生'System.Security.Cryptography.CryptographicException' 类型的未处理异常附加信息:密钥无效用于指定状态”从行:key.FromXmlString(privateCert.PrivateKey.ToXmlString(true)); @Visar 那么更好的答案是什么?没有私钥怎么签名?【参考方案2】:

在安全环境中无法使用 privateKey.toXMLString(true) 或 privateKey.exportParameters(true),因为它们要求您的私钥是可导出的,这不是一个好的做法。

更好的解决方案是像这样显式加载“增强”加密提供程序:

// Find my openssl-generated cert from the registry
var store = new X509Store(StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindBySubjectName, "myapp.com", true);
var certificate = certificates[0];
store.Close();
// Note that this will return a Basic crypto provider, with only SHA-1 support
var privKey = (RSACryptoServiceProvider)certificate.PrivateKey;
// Force use of the Enhanced RSA and AES Cryptographic Provider with openssl-generated SHA256 keys
var enhCsp = new RSACryptoServiceProvider().CspKeyContainerInfo;
var cspparams = new CspParameters(enhCsp.ProviderType, enhCsp.ProviderName, privKey.CspKeyContainerInfo.KeyContainerName);
privKey = new RSACryptoServiceProvider(cspparams);

【讨论】:

谢谢。很干净。因此,您只需创建一个空的(正确的)RSACryptoServiceProvider,获取它的 ProviderType 和 ProviderName,并将它们与私钥的 KeyContainerName 结合起来形成一个工作私钥。 不幸的是,在这样做之后,您无法将公钥与使用此密钥签名的 verifydata 一起使用 我同意安全环境的担忧,但是,当使用这种方法时,生成的结果无法使用证书中的公钥进行验证。 这似乎产生了完全不正确的哈希值。它似乎没有访问原始私钥,因为我没有收到安全弹出窗口。 这种方法不起作用,我不知道它是如何获得这么多赞成票的。它使用的密钥不是证书中的密钥,因此所有签名都不正确。【参考方案3】:

我就是这样处理这个问题的:

 X509Certificate2 privateCert = new X509Certificate2("certificate.pfx", password, X509KeyStorageFlags.Exportable);

 // This instance can not sign and verify with SHA256:
 RSACryptoServiceProvider privateKey = (RSACryptoServiceProvider)privateCert.PrivateKey;

 // This one can:
 RSACryptoServiceProvider privateKey1 = new RSACryptoServiceProvider();
 privateKey1.ImportParameters(privateKey.ExportParameters(true));

 byte[] data = Encoding.UTF8.GetBytes("Data to be signed"); 

 byte[] signature = privateKey1.SignData(data, "SHA256");

 bool isValid = privateKey1.VerifyData(data, "SHA256", signature);

【讨论】:

这看起来就是 jose-jwt 的做法。 github.com/dvsekhvalnov/jose-jwt/blob/master/UnitTests/… 这个对我来说很好用。关键元素(请原谅双关语)是使证书可导出,然后使用私钥创建一个新的 RSACSP。唯一的细微差别是我使用“csp.FromXmlString(cert.PrivateKey.ToXmlString(true))”创建了第二个 CSP。【参考方案4】:

我决定更改密钥文件以指定适当的加密服务提供商,完全避免了 .NET 中的问题。

因此,当我使用 PEM 私钥和 CRT 公共证书创建 PFX 文件时,我会按如下方式进行:

openssl pkcs12 -export -aes256 -CSP "Microsoft Enhanced RSA and AES Cryptographic Provider" -inkey priv.pem -in pub.crt -out priv.pfx

关键部分是-CSP“Microsoft Enhanced RSA and AES Cryptographic Provider”

-inkey 指定私钥文件,-in 指定要合并的公共证书。)

您可能需要针对手头的文件格式进行调整。此页面上的命令行示例可以帮助解决这个问题: https://www.sslshopper.com/ssl-converter.html

我在这里找到了这个解决方案: http://hintdesk.com/c-how-to-fix-invalid-algorithm-specified-when-signing-with-sha256/

【讨论】:

我采用了您的解决方案,将使用“makecert”的自签名证书替换为由 openSSL 创建的证书。使用 makecert windows 命令,即使我指定 -sy 24 表示 CSP 为 24,它仍在创建 CSP = 1 的证书,下面是一个 C# 代码,用于检查是否有人有同样的问题。 var x509Certificate = new X509Certificate2(@"cert.pfx", "Password", X509KeyStorageFlags.Exportable); Console.WriteLine(x509Certificate.ToString(true)); Console.ReadLine(); 在您拥有 RSASHA2 证书但仍然抱怨算法错误的情况下 - 这是唯一有效的答案。 谢谢。使用正确的供应商会带来很大的不同。 Powershell: $cert = New-SelfSignedCertificate -Subject "OiosAML test" -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" -CertStoreLocation Cert:\LocalMachine\My -NotAfter (Get-Date).AddYears(3) -HashAlgorithm SHA256跨度> 【参考方案5】:

当您使用证书获取您的 RSACryptoServiceProvider 时,底层 CryptoAPI 提供程序是什么真的很重要。默认情况下,当您使用“makecert”创建证书时,它是“RSA-FULL”,它仅支持 SHA1 哈希作为签名。您需要支持 SHA2 的新“RSA-AES”。

因此,您可以使用附加选项创建证书:-sp "Microsoft Enhanced RSA and AES Cryptographic Provider"(或等效的 -sy 24),然后您的代码将在没有密钥杂耍的情况下工作。

【讨论】:

【参考方案6】:

使用可以在更新的框架上使用它。

    public byte[] GetSignature(byte[] inputData)
    
        using (var rsa = this.signingCertificate.GetRSAPrivateKey())
        
            return rsa.SignData(inputData, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        
    

    public bool ValidateSignature(byte[] inputData, byte[] signature)
    
        using (var rsa = this.signingCertificate.GetRSAPublicKey())
        
            return rsa.VerifyData(inputData, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        
    

上面的signingCertificate 是一个带有私钥的X509Certificate2。此方法不需要您导入任何现有密钥,并且可以在安全的环境中使用。

【讨论】:

【参考方案7】:

这是我在无需修改证书的情况下签署字符串的方法(向 Microsoft 增强型 RSA 和 AES 加密提供商)。

        byte[] certificate = File.ReadAllBytes(@"C:\Users\AwesomeUser\Desktop\Test\ServerCertificate.pfx");
        X509Certificate2 cert2 = new X509Certificate2(certificate, string.Empty, X509KeyStorageFlags.Exportable);
        string stringToBeSigned = "This is a string to be signed";
        SHA256Managed shHash = new SHA256Managed();
        byte[] computedHash = shHash.ComputeHash(Encoding.Default.GetBytes(stringToBeSigned));


        var certifiedRSACryptoServiceProvider = cert2.PrivateKey as RSACryptoServiceProvider;
        RSACryptoServiceProvider defaultRSACryptoServiceProvider = new RSACryptoServiceProvider();
        defaultRSACryptoServiceProvider.ImportParameters(certifiedRSACryptoServiceProvider.ExportParameters(true));
        byte[] signedHashValue = defaultRSACryptoServiceProvider.SignData(computedHash, "SHA256");
        string signature = Convert.ToBase64String(signedHashValue);
        Console.WriteLine("Signature : 0", signature);

        RSACryptoServiceProvider publicCertifiedRSACryptoServiceProvider = cert2.PublicKey.Key as RSACryptoServiceProvider;
        bool verify = publicCertifiedRSACryptoServiceProvider.VerifyData(computedHash, "SHA256", signedHashValue);
        Console.WriteLine("Verification result : 0", verify);

【讨论】:

仅当用户通过将证书 pfx 标记为可导出来导入证书 pfx 时才有效...【参考方案8】:

根据blog,它应该与 FX 3.5 一起使用(请参阅下面的注释)。然而,重要的是要记住,大多数 .NET 加密都是基于 CryptoAPI(即使 CNG 在最近的 FX 版本中越来越多地出现)。

关键 点是 CryptoAPI 算法支持取决于所使用的加密服务提供程序 (CSP),并且在 Windows 之间会有所不同版本(即适用于 Windows 7 的功能可能不适用于 Windows 2000)。

阅读 cmets(来自博客条目)以了解一种可能 解决方法,您可以在创建 RSACCryptoServiceProvider 实例时指定 AES CSP(而不是默认的)。这似乎对某些人有用,YMMV。

注意:这让很多人感到困惑,因为所有已发布的 .NET 框架都包含一个 托管 SHA256 实现,CryptoAPI 无法使用该实现。 FWIW Mono 不会遇到此类问题 ;-)

【讨论】:

这种解决方法看起来可能会奏效。我必须做更多的测试才能确定。 所以我在 cmets 中尝试了解决方法,但它看起来不像使用私钥。对吗? 那篇博文是我需要解释我们现有代码在做什么的澄清。【参考方案9】:

我知道这是一个旧线程,但对于那些仍然停留在过去并寻找答案的人来说,以下基于@BKibler 的答案对我有用。 cmets 表示它没有使用正确的密钥,这是因为解决方案缺少几个密钥设置。

// Find my openssl-generated cert from the registry
var store = new X509Store(StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindBySubjectName, "myapp.com", true);
var certificate = certificates[0];
store.Close();

// Note that this will return a Basic crypto provider, with only SHA-1 support
var privKey = (RSACryptoServiceProvider)certificate.PrivateKey;

// Force use of the Enhanced RSA and AES Cryptographic Provider with openssl-generated SHA256 keys
var enhCsp = new RSACryptoServiceProvider().CspKeyContainerInfo;

if (!Enum.TryParse<KeyNumber>(privKey.CspKeyContainerInfo.KeyNumber.ToString(), out var keyNumber))
     throw new Exception($"Unknown key number privKey.CspKeyContainerInfo.KeyNumber");

var cspparams = new CspParameters(enhCsp.ProviderType, enhCsp.ProviderName, privKey.CspKeyContainerInfo.KeyContainerName)

     KeyNumber = (int)keyNumber,
     Flags = CspProviderFlags.UseExistingKey
;

privKey = new RSACryptoServiceProvider(cspparams);

您需要同时设置“KeyNumber”和“Flags”,以便使用现有(不可导出)密钥,并且您可以使用证书中的公钥进行验证。

【讨论】:

【参考方案10】:

当我正在使用的证书不在用户/计算机证书存储中时,我注意到 .NET 中的类似问题是使用了错误的私钥(或者是完全错误?我不记得了)。将它安装到存储解决了我的场景的问题并且事情开始按预期工作 - 也许你可以尝试一下。

【讨论】:

我正在从文件加载证书,我没有使用任何 Windows 安装的证书。

以上是关于如何通过 .NET 使用 RSA 和 SHA256 对文件进行签名?的主要内容,如果未能解决你的问题,请参考以下文章

使用 WCF 生成 SOAP 请求 (RSA-SHA256 PKCS #1 v1.5)

启用TLSv1.2和TLS_RSA_WITH_AES_256_CBC_SHA256密码套件

使用SHA-256和RSA 2048进行加密和签名

如何在 Crypto++ 中使用 RSA OAEP SHA-256 加密/解密数据

为啥我使用 OpenSSL 和 Java 生成的 RSA-SHA256 签名不同?

如何在 python 中使用 RSA SHA-256 签署令牌?