使用 rsa 在 c# 中验证来自节点的签名数据

Posted

技术标签:

【中文标题】使用 rsa 在 c# 中验证来自节点的签名数据【英文标题】:Verifying signed data from node in c# using rsa 【发布时间】:2021-12-09 14:47:23 【问题描述】:

我有以下代码在 .js 脚本中对一些数据进行签名:

const  RSA_PKCS1_PSS_PADDING  = require('constants');
const crypto = require('crypto');

const  publicKey, privateKey  = crypto.generateKeyPairSync('rsa', 
  modulusLength: 2048,
  publicKeyEncoding: 
    type: 'spki',
    format: 'pem',
  ,
  privateKeyEncoding: 
    type: 'pkcs8',
    format: 'pem',
  ,
);

const fs = require('fs');

const keys = fs.createWriteStream('keys.txt');
keys.write(`$publicKey\n`);
keys.write(`$privateKey\n`);

function signature(verifyData) 
  return crypto.createSign('sha256').sign(
    keyLike: Buffer.from(verifyData),
    key: privateKey,
    padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
  ).toString('base64');

脚本会用我的公钥和私钥创建一个txt文件,如下:

-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----


-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----

我尝试了几种方法来为相同的输入生成与 .js 脚本相同的哈希值,但均未成功。它也无法验证 .js 脚本创建的任何哈希值。以下是我的实现:

private RsaKeyParameters readPrivateKey(string privateKeyFileName)

    RsaKeyParameters keyPair;
    using (var reader = File.OpenText(privateKeyFileName))
        keyPair = (RsaKeyParameters)new PemReader(reader).ReadObject();
    return keyPair;


bool VerifyDataBouncyCastle(string bodyData, string signature)

    var data = bodyData;
    var signatureBytes = Convert.FromBase64String(signature);
    var signer = SignerUtilities.GetSigner("SHA256WITHRSA");
    signer.Init(false, readPrivateKey($"DiretorioBase\\public.txt"));
    signer.BlockUpdate(Encoding.UTF8.GetBytes(data), 0, data.Length);
    var success = signer.VerifySignature(signatureBytes);
    return success;


string SignDataBouncyCastle(string data)
 
    // Verify using the public key
    var signer = SignerUtilities.GetSigner("SHA256WITHRSA");
    signer.Init(true, readPrivateKey($"DiretorioBase\\private.txt"));
    signer.BlockUpdate(Encoding.UTF8.GetBytes(data), 0, data.Length);
    return Convert.ToBase64String(signer.GenerateSignature());



public byte[] SignDataNetCore(byte[] data)

    // privateKey does not have the ---BEGIN and ---END headers.
    var privateKey = File.ReadAllText($"DiretorioBase\\private.txt");
    var rsaPrivateKey = RSA.Create();
    rsaPrivateKey.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out _);
    return rsaPrivateKey.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);


public bool VerifyDataNetCore(byte[] data, byte[] signature)

    var publicKey = File.ReadAllText($"DiretorioBase\\public.txt");
    var rsaPublicKey = RSA.Create();
    rsaPublicKey.ImportFromPem(publicKey);
    return rsaPublicKey.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

以上方法都不会使用 .js 脚本生成的相同输入和相同密钥生成相同的签名。 我错过了什么?

--编辑--

我改变了 .js 签名方法是这样的:

function signature(verifyData) 
  var cSign = crypto.createSign('sha256');

  cSign.update(verifyData);

  return cSign.sign(
    key: privateKey,
    padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
  );

C# 验证代码如下:

        bool isVerified()
        
            string x509Pem = @"-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----";

            byte[] message = Encoding.UTF8.GetBytes(validar);
            byte[] signature = Convert.FromBase64String(hash64);
            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);
            return verified;
        

它仍然返回 false。

【问题讨论】:

您正在使用不同的填充:在 NodeJS 代码中使用概率性更现代的 PSS,而在 C# 代码中使用确定性较旧的 PKCS#1 v1.5。概率填充每次都会为相同的数据生成不同的签名。对于这样的填充,测试不是比较而是验证签名。 S.例如here 用于 C# 中的 PSS。 您还应该检查您的 NodeJS 代码。签名时,消息实际上是用update() 传递的。我在文档中找不到 keyLike 选项。可能它被忽略了,所以你最终签署了一个 empty 字符串。 感谢@Topaco 的回答,但它不起作用。我编辑了第一篇文章,所以你可以看到我做了什么。 已经很接近了。有关详细信息,请参阅我的答案。 【参考方案1】:

PSS 有许多参数,包括盐长度。 RFC8017, A.2.3. RSASSA-PSS 定义了与摘要的输出长度相对应的默认盐长度,即 SHA256 为 32 个字节。

您最近的 C# 代码应用了使用此默认盐长度的 C# 内置类。不能指定不同的盐长度!

另一方面,NodeJS 代码默认为可能的最大盐长度 (crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN),由以下公式给出:<keysize> - <digest output length> - 2 = 256 - 32 - 2 = 222

因此,这两个代码不兼容!


与 C# 内置类不同,BouncyCastle 允许配置盐长度:

string x509Pem = @"-----BEGIN PUBLIC KEY-----
                   ...
                   -----END PUBLIC KEY-----";
string validar = "...";
string hash64 = "...";

byte[] message = Encoding.UTF8.GetBytes(validar);
byte[] signature = Convert.FromBase64String(hash64);
PemReader pr = new PemReader(new StringReader(x509Pem));
AsymmetricKeyParameter publicKey = (AsymmetricKeyParameter)pr.ReadObject();

PssSigner pssSigner = new PssSigner(new RsaEngine(), new Sha256Digest(), 256 - 32 - 2);
pssSigner.Init(false, publicKey);
pssSigner.BlockUpdate(message, 0, message.Length);
bool valid = pssSigner.VerifySignature(signature); // succeeds when the maximum possible salt length is used

至此,验证成功。


请注意,在 NodeJS 代码中,您可以显式将盐长度更改为摘要的输出长度 (crypto.constants.RSA_PSS_SALTLEN_DIGEST)。然后验证也可以使用内置的 C# 类。

【讨论】:

以上是关于使用 rsa 在 c# 中验证来自节点的签名数据的主要内容,如果未能解决你的问题,请参考以下文章

使用 RSA C# 签名和验证签名

RSA公私钥和签名、验签过程

C#中RSA加密解密和签名与验证的实现

使用 mbedtls 生成的 RSA 签名,无法使用 C# (bouncycastle) 应用程序进行验证

iOS小技能:RSA签名验签加密解密的原理

iOS小技能:RSA签名验签加密解密的原理