C# RSA 导入公钥

Alv*_*nus 0 c# encryption public-key-encryption

我需要验证签名的数据。我不知道如何使用公钥。

    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);
    }
Run Code Online (Sandbox Code Playgroud)

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

-----BEGIN PUBLIC KEY-----
XXXXXXXXXXXXXXXXXXXXXXXXXX
-----END PUBLIC KEY-----
Run Code Online (Sandbox Code Playgroud)

我怎样才能验证它?


笔记

这个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
Run Code Online (Sandbox Code Playgroud)

错误

提供者版本错误。


更新

所以我根据@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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

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

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
Run Code Online (Sandbox Code Playgroud)

更新

所以即使我更改了数据而不转换为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;
    }
Run Code Online (Sandbox Code Playgroud)

公钥是:

-----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-----
Run Code Online (Sandbox Code Playgroud)

这是消息数据:

{“msg_id”:1220037023,“message”:“你好”,“thumbnail”:“https://accounts.tokopedia.com/image/v1/u/25088898/user_thumbnail/desktop”,“full_name”:“阿尔文” ,"shop_id":858157,"user_id":25088898,"payload":{"attachment_type":0,"image":{"image_thumbnail":"","image_url":""},"product":{" image_url":"","名称":"","价格":"","product_id":0,"product_url":""}}}

签名是:

nWjyoCpDZfYbhDcVyVMlJfu/3A1gMOgUyPosuLrtycQWSkvLNPhFGtFHW7kI3ByMXLzMiZRIyc6mg5p4AVrozey8+XIV2A3layynfGy10LiJXULDttP/ldTPmg4VdXOIONNEm283dCzVEsiiGhWcx uwx0XD0fD0CLIwwLN4nwOJiRroY6zyWzRCavv8q5zRHWNJnNRN6t6g6SpqZ4HQPOqRlUgMDH2mqLoiZbngnoOvkG1HgvJ1oySL+45rI/ZBLYUE/rZ4N5abI4oTxJ7K8REya1WxX6YVo0B9Gll2 +xI+Z1G9QZCvZQRVYsYf8f0FmbmqDWQebbSm+ULSC6T69yBXvDIA17+TK/fZhlGCjuHClyZbJlpYYUJSIv1Sac8zTGj9rlStiSFR4a96p33SjqPlkbYXT9akDTMH4ao1SIUKNjVRSW8lN7pBZoLNyQwSR 6yYqSixAu6vbiS/DyLsfFDfheK3s8MkzdM7t0U4eqkbHsHbnJFEhXIAPwjgxd3a3uEfD47A0YpJMWQ1ve9WpPJWWSxApRMP80HzQIute86XNGNedLOhxBF9OeO4o82PCxJ9JGS4nRK+AGPAxQzgZq0 8jp5C2TdFXwwW3uAYViNE3u2Pdi17MDDhZ8fDAvhGWn1l8tbiZM/FN9HMR1mXO/jV/PhqDeJ80E6/R1O2POHM=

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

#!/bin/bash

body=$1
publickey=$2
signature=$3
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*
Run Code Online (Sandbox Code Playgroud)

Top*_*aco 5

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

PEM 密钥使用实例 WLOG 从字符串加载PemReader(或者可以从文件系统加载)。使用DotNetUtilitiesfrom BouncyCastleRSAParameters创建一个实例,可以直接从RSACngwith导入该实例ImportParameters()RSACng还封装了签名/验证的方法。请注意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
Run Code Online (Sandbox Code Playgroud)

使用 .NET Framework 4.8 进行测试,

附带说明:BouncyCastle 本身支持使用SignerUtilities类进行签名/验证,因此RSACng不必一定使用。


编辑:

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

正如已经提到的,隐式VerifyData()执行散列,即消息不得散列并且不需要该方法。第一个参数应该是:ConvertToSHA256()VerifyData()

Encoding.UTF8.GetBytes(data)
Run Code Online (Sandbox Code Playgroud)

data消息在哪里。

请尝试这个。

如果您仍然有问题,请编辑您的问题并发布可重现的示例,即消息、签名和公钥的具体数据,就像我在示例中所做的那样。


编辑:

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

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

如果在发布的 OpenSSL 中显式指定长度digest(或显式使用 32 字节),则验证失败,即签名时摘要输出长度不会应用为盐长度。但是,如果使用 指定长度max,则验证成功,即签名时使用了最大可能的盐长度。
相比之下,BouncyCastle/C# 将摘要输出长度用作默认值,对于 SHA256,该长度为 32 字节。
因此,验证失败的原因是盐长度不同:签名时的最大盐长度和使用 C# 验证时的摘要输出长度。
注意:由于发布的 OpenSSL 语句中未指定盐长度,因此使用 OpenSSL 默认值。不幸的是,OpenSSL 文档中没有提供这一点,但就成功验证而言,它只能是maxor auto(验证auto当然也是成功的,因为盐长度是直接从签名确定的)。

最大盐长度究竟是多少?盐不能有任何长度。根据 PSS 规范,针对 4096 位密钥/签名和 SHA256(此处)导出了以下最大可能盐长度(以字节为单位):

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
Run Code Online (Sandbox Code Playgroud)

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

准确地说,478 只是这一签名的盐长度。为了得到明确的答案,必须知道创建签名的实现方式。这可能会使用不同的盐长度,这意外地对应于所发布签名的最大盐长度。这意味着不同的签名可以使用不同的盐长度。但由于这种逻辑不太可能,因此可以假设用于签名的实现应用了最大盐长度。

问题仍然是如何在 C# 中指定与默认值不同的盐长度。据我所知,这对于 C# 板载方法(即RSACng. 但同样,答案是 BouncyCastle,它提供了该类PssSigner

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;

...
Run Code Online (Sandbox Code Playgroud)

至此验证成功。