C# RSA 导入 Public 密钥

C# RSA Import Public Key

我需要验证签名数据。我不知道如何使用 public 键。

    public bool VerifyData(string data, string signature)
    {
        //decode signature from base 64
        byte[] signatureByte = System.Convert.FromBase64String(signature);

        //hash data to sha256
        string hashedData = ConvertToSHA256(data);
        byte[] hashedDataByte = System.Convert.FromBase64String(hashedData);

        //verify with RSA PSS
        string absPath = System.Web.Hosting.HostingEnvironment.MapPath("~/App_Data/TP/public");
        string publicKeyString = File.ReadAllText(absPath);
        publicKeyString = RemoveRSAHeaderAndFooter(publicKeyString);

        RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
        //This causes error 
        RSA.ImportCspBlob(System.Convert.FromBase64String(publicKeyString));

        RSAParameters rsaParams = RSA.ExportParameters(true);
        RSACng RSACng = new RSACng();
        RSACng.ImportParameters(rsaParams);

        return RSACng.VerifyData(hashedDataByte, signatureByte, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
    }

RSA.ImportCspBlob 导致错误。我的 public 键是字符串类型。它看起来像这样:

-----BEGIN PUBLIC KEY-----
XXXXXXXXXXXXXXXXXXXXXXXXXX
-----END PUBLIC KEY-----

如何验证?


备注

这个shell脚本可以用来验证:

openssl base64 -A -d -in $temp.sign -out $temp.sha256
openssl dgst -sha256 -sigopt rsa_padding_mode:pss -verify $publickey -signature $temp.sha256 $temp.key

错误

Bad Version of provider.


更新

所以我根据@Topaco更新了我的代码:

    private async Task<bool> IsContentValid(string data)
    {
        bool valid = false;
        string signature = Request.Headers.GetValues("tkpd-signature").FirstOrDefault();

        //decode signature from base 64
        byte[] signatureByte = System.Convert.FromBase64String(signature);

        //hash data to sha256
        string hashedData = ConvertToSHA256(data);
        byte[] hashedDataByte = System.Convert.FromBase64String(hashedData);

        //verify with RSA PSS
        string absPath = System.Web.Hosting.HostingEnvironment.MapPath("~/Keys/tppublic");
        string publicKeyString = File.ReadAllText(absPath);

        PemReader pr = new PemReader(new StringReader(publicKeyString));
        AsymmetricKeyParameter publicKey = (AsymmetricKeyParameter)pr.ReadObject();

        RSAParameters rsaParams = DotNetUtilities.ToRSAParameters((RsaKeyParameters)publicKey);
        RSACng rsaCng = new RSACng();
        rsaCng.ImportParameters(rsaParams);

        valid = rsaCng.VerifyData(hashedDataByte, signatureByte, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
        return valid;
    }

    private string ConvertToSHA256(string data)
    {
        using (SHA256 mySHA256 = SHA256.Create())
        {
            var crypt = new System.Security.Cryptography.SHA256Managed();
            var hash = new System.Text.StringBuilder();
            byte[] crypto = crypt.ComputeHash(Encoding.UTF8.GetBytes(data));
            foreach (byte theByte in crypto)
            {
                hash.Append(theByte.ToString("x2"));
            }
            return hash.ToString();
        }
    }

不知道是我之前的步骤有误,还是验证有误

如果我运行脚本,验证成功。这个脚本:

openssl base64 -A -d -in $temp.sign -out $temp.sha256
openssl dgst -sha256 -sigopt rsa_padding_mode:pss -verify $publickey -signature $temp.sha256 $temp.key

更新

所以即使我没有转换为sha256就更改了数据,验证仍然失败。

这是我的代码:

    private async Task<bool> IsContentValid(string data)
    {
        bool valid = false;
        string signature = Request.Headers.GetValues("tkpd-signature").FirstOrDefault();

        //decode signature from base 64
        byte[] signatureByte = System.Convert.FromBase64String(signature);

        //hash data to sha256
        //string hashedData = ConvertToSHA256(data);
        byte[] hashedDataByte = Encoding.UTF8.GetBytes(data);

        //verify with RSA PSS
        string absPath = System.Web.Hosting.HostingEnvironment.MapPath("~/Keys/tppublic");
        string publicKeyString = File.ReadAllText(absPath);

        PemReader pr = new PemReader(new StringReader(publicKeyString));
        AsymmetricKeyParameter publicKey = (AsymmetricKeyParameter)pr.ReadObject();

        RSAParameters rsaParams = DotNetUtilities.ToRSAParameters((RsaKeyParameters)publicKey);
        RSACng rsaCng = new RSACng();
        rsaCng.ImportParameters(rsaParams);

        valid = rsaCng.VerifyData(hashedDataByte, signatureByte, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
        return valid;
    }

public键是:

-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxYVf6wVycEygE2VTu4q6
fb7eqkDWikliprXeSazUygHXPlbBqDkHmggylaq5M3C8RwExPyvPhtKNZZ7CzuSH
BfW/TxF1SH+htNvr5Tk/kPPQ/S575gmF9KzXXwJq255qfcwNDiiZZDb1tt4IKa4n
bNK1z2GHtX3CasAJDjTH1aFLZHhUStH9mSo4RaXzl5ZUtPJNg+wXVqiTKfDHz3eS
xHxUtpbvenQwlg3uimT+cBmhzYP87WvwM48IaXrqWBTB082z0COacg4FSuovaYeN
uw+UNeGHWPucZ9ZencxyljcXHjeVi+k2oNJqMfbxfOhzJNPua35Tq0MDmP3qaGKF
2YOnRKKaNNaS7XMSx1f64v0HBrUZftcKxQYdKdhXBd1IJoC00ygt0pOx2gNrNq2f
olJIeV9o/8V24ddvgPHWNKLGBtUCMwFV6lPyXWuJm7FlKDvRF5tiUbFKpAwcXZ84
QqIUSNFPfnJSVdxLutIk2o4TRtLPhFABSu3n+aYxWGQ/lOF3E4ZJb7qdbj6gbMNH
1cSa3gGOfMHl3kDipUUUAvRdMPOox9GzC/JFXpVDWtYajaQIF0JwEZ03Nbg41WXw
5t9MpiDWVpfQJZb4zUiZsdHLdcOSF8QZZvGyAl/rq7Bhs2Q6zLYTnpD1sbUHe8sl
kZMzVPgTz12NOn/sz1hCQLMCAwEAAQ==
-----END PUBLIC KEY-----

这是消息数据:

{"msg_id":1220037023,"message":"hello","thumbnail":"https://accounts.tokopedia.com/image/v1/u/25088898/user_thumbnail/desktop","full_name":"Alvin","shop_id":858157,"user_id":25088898,"payload":{"attachment_type":0,"image":{"image_thumbnail":"","image_url":""},"product":{"image_url":"","name":"","price":"","product_id":0,"product_url":""}}}

签名是:

nWjyoCpDZfYbhDcVyVMlJfu/3A1gMOgUyPosuLrtycQWSkvLNPhFGtFHW7kI3ByMXLzMiZRIyc6mg5p4AVrozey8+XIv2A3lAyynfGy10LiJXUlDttP/lDTPmg4VdXoIOnNEm283dCzVEsiiGhWcxuwx0XD0fD0CLIwwLN4nwOJiRroY6zyWzRCavv8q5zRHWNJnNRN6t6g6SpqZ4HQPOqRlUgMDH2mqLoiZbngnoOvkG1HgvJ1oySL+45rI/ZBLYUE/rZ4N5abI4oTxJ7K8REya1WxX6YVo0B9Gll2+xI+Z1G9QZCvZQRVYsYf8f0FmbmqDWQebbSm+UlsC6T69yBXvDIA17+TK/fZhlGCjuHClyZbJlpYYUJSIv1Sac8zTGj9rlStiSFR4a96p33SjqPlkbYXT9akDTMH4ao1SIUKNjVRSw8lN7pBZoLNyQwSR6yYqSIxAu6vbiS/DyLsfFDfheK3s8MkzdM7t0U4eqkbHsHbnJFEhXIAPwjgxd3a3uEfD47A0YpJMWQ1ve9WpPJWWSxApRMP80HzQIute86XNGNedLOhxBF9OeO4o82PCxJ9JGS4nRK+AGPAxQzgZq08jp5C2TdFXwwW3uAYViNE3u2Pdi17MDDhZ8fDAvhGWn1l8tbiZM/FN9HMR1mXO/jV/PhqDeJ80E6/R1O2POHM=

这是我用来验证它的完整 shell 脚本:

#!/bin/bash

body=
publickey=
signature=
temp="./tmp"

if [[ $# -lt 3 ]] ; then
  echo "Usage: verify <request_body> <public_key> <signature>"
  exit 1
fi

echo -n $body > $temp.key
echo -n $signature > $temp.sign
openssl base64 -A -d -in $temp.sign -out $temp.sha256
openssl dgst -sha256 -sigopt rsa_padding_mode:pss -verify $publickey -signature $temp.sha256 $temp.key

rm $temp*

以下 BouncyCastle/C# 代码验证签名消息。使用摘要 SHA256 作为填充 PSS (RSASSA-PSS)。 public 密钥采用 PEM 编码的 X.509 格式。

PEM 密钥使用 PemReader 实例 WLOG 从字符串加载(或者它可以从文件系统加载)。使用 BouncyCastle 中的 DotNetUtilities 创建了一个 RSAParameters 实例,可以使用 ImportParameters() 直接从 RSACng 导入。 RSACng 还封装了方法 Signing/Verifying。请注意,SignData/VerifyData 需要 未散列的 消息(不同于 SignHash/VerifyHash):

using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
...
string x509Pem = @"-----BEGIN PUBLIC KEY-----
                MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDEaA9DZlPzykcUY3aaqeT8Cmcx
                2qUNvB7/QqQQvkPjk+sxeFLqkppuvbbinN3FHMspPhJlOGZf+gRjmwiOoMEkZAHv
                nVfX7gtMxLyUAcXBXFx36t2QE5/45TZ4lzI3udvhAPj7uB1sUKDk5trB8EoX1sVA
                kKC9ynrKTPDnyNRDAwIDAQAB
                -----END PUBLIC KEY-----";

byte[] message = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog");
byte[] signature = Convert.FromBase64String(@"rsyqqY1bkGJJkZ4DOfXF+IOpYRdDETvy//PCYGbs70N5Vm8O0P5yqnnxuO5PT9hsOUgJMZeyWxeQITrvXu8buYyx4cah+DfYhOMUzrmyZbzjciTyqWGVYAcZEJNfS0fP8t0XSp5DjKXd1nmaMbB4LuBNwvuEdboFCtN6KRNPzFY=");
PemReader pr = new PemReader(new StringReader(x509Pem));
AsymmetricKeyParameter publicKey = (AsymmetricKeyParameter)pr.ReadObject();

RSAParameters rsaParams = DotNetUtilities.ToRSAParameters((RsaKeyParameters)publicKey);
RSACng rsaCng = new RSACng();
rsaCng.ImportParameters(rsaParams);

bool verified = rsaCng.VerifyData(message, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
Console.WriteLine(verified); // True

已使用 .NET Framework 4.8 进行测试,

附带说明:BouncyCastle 本身支持 signing/verifying 和 SignerUtilities class,因此不一定要使用 RSACng


编辑:

第一个 OpenSSL 语句 Base64 解码签名,第二个 OpenSSL 语句验证首先使用 SHA256 散列然后签名(使用 PSS 作为填充)的消息。

如前所述,VerifyData() 执行散列 隐式 ,即消息必须 散列并且 ConvertToSHA256() 方法不是必需的。 VerifyData()中的第一个参数应该是:

Encoding.UTF8.GetBytes(data)

其中 data 是消息。

请试试这个。

如果您仍有问题,请编辑您的问题和post一个可重现的示例,即消息、签名和public密钥的具体数据,就像我在示例中所做的那样。


编辑:

PSS padding有不同的参数,这些参数通常被赋予一定的默认值。这些参数之一是盐长度。如果盐长度不为零,PSS 会应用随机生成的盐,这会导致每次生成不同的签名(概率)。盐长度的常见默认值是摘要输出长度,对于 SHA256 为 32 字节,(RFC 8017, A.2.3. RSASSA-PSS).

加盐长度可以在 OpenSSL 中设置 -sigopt rsa_pss_saltlen:<length>。除了具体长度的规范外,还有与版本相关的特殊值,例如对于 v1.1.1 digest(salt 长度对应于摘要输出长度,在本例中为 32 字节),auto(salt 长度由签名确定)和 max(salt 长度对应到最大可能值), here.

如果在 posted OpenSSL 中用 digest(或明确用 32 字节)明确指定长度,则验证失败,即当签署。 但是,如果长度指定为max,则验证成功,即在签名时使用了最大可能的盐长度。
相反,BouncyCastle/C#默认应用摘要输出长度,对于 SHA256 为 32 字节。
因此,验证失败的原因是盐长度不同:签名时的最大盐长度和使用 C# 验证时的摘要输出长度。
注意:由于在 posted OpenSSL 语句中未指定盐长度,因此使用 OpenSSL 默认值。不幸的是OpenSSL文档中没有提供,但是关于验证成功只能是maxauto(用auto验证当然也成功了,因为salt然后直接根据签名确定长度)。

最大盐长度到底是多少?盐不能有任何长度。根据 PSS 规范,针对 4096 位 key/signature 和 SHA256 (here) 得出以下最大可能盐长度(以字节为单位):

signature length - digest output length - 2 = 512 - 32 - 2 = 478

可以通过将 -sigopt rsa_pss_saltlen:478 添加到 posted OpenSSL 语句来轻松验证值 478。验证成功,确认了这个值。

准确的说,478只是这个签名的salt长度。要获得明确的答案,必须知道创建签名的实现方式。这可以使用不同的盐长度,这意外地对应于 posted 签名的最大盐长度。这意味着不同的签名可以使用不同的盐长度。但是由于这种逻辑不太可能,可以假设用于签名的实现应用了最大盐长度。

问题仍然是如何在 C# 中指定不同于默认值的加盐长度。据我所知,这对于 C# 板载方式是不可能的,即 RSACng。但同样,答案是 BouncyCastle,它提供 PssSigner class:

using Org.BouncyCastle.Crypto; 
using Org.BouncyCastle.Crypto.Digests; 
using Org.BouncyCastle.Crypto.Engines; 
using Org.BouncyCastle.Crypto.Signers; 
using Org.BouncyCastle.OpenSsl; 

...

PemReader pr = new PemReader(new StringReader(publicKeyString));
AsymmetricKeyParameter publicKey = (AsymmetricKeyParameter)pr.ReadObject();

PssSigner pssSigner = new PssSigner(new RsaEngine(), new Sha256Digest(), 512 - 32 - 2);
pssSigner.Init(false, publicKey);
byte[] dataByte = Encoding.UTF8.GetBytes(data);
pssSigner.BlockUpdate(dataByte, 0, dataByte.Length);
valid = pssSigner.VerifySignature(signatureByte);
return valid;

...

至此验证成功