python(加密)和 .NET5 (System.Security.Cryptography) 之间的 RSA PSS 签名验证失败

RSA PSS signature verification between python (cryptography) and .NET5 (System.Security.Cryptography) fails

我需要获取从 Python 生成的消息的 RSA PSS 签名并在 .NET 中进行验证。但 .NET 中的验证失败。

我已经使用以下命令生成了 RSA 密钥对:

openssl req -x509 -nodes -newkey rsa:4096 -keyout /tmp/certs/private.pem -out /tmp/certs/public.pem -days 365

Python代码:

from cryptography import x509
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from nacl.encoding import HexEncoder

def sign(message: str) -> str:
    with open("/tmp/certs/private.pem", "rb") as pem:
        private_key = serialization.load_pem_private_key(
            pem.read(),
            password=None,
            backend=default_backend(),
        )
    return HexEncoder.encode(
        private_key.sign(
            message.encode("utf-8"),
            padding.PSS(
                mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
            ),
            hashes.SHA256(),
        )
    ).decode("utf-8")

def verify(message: str, signature: str) -> bool:
    with open("/tmp/certs/public.pem", "rb") as pem:
        cert = x509.load_pem_x509_certificate(pem.read(), default_backend())
        public_key = cert.public_key()
    try:
        signature = HexEncoder().decode(signature)
        public_key.verify(
            signature,
            message.encode(),
            padding.PSS(
                mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
            ),
            hashes.SHA256(),
        )
        return True
    except InvalidSignature:
        return False

def main():
    message = "hello"

    signature = sign(message)
    print(f"Signature: {signature}")

    # print(f"isValidated: {verify(message, signature)}")

if __name__ == '__main__':
    main()

针对 .NET 5.0 的 C# 代码:

using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace HelloWorld
{
    internal static class Program
    {
        private static string ByteArrayToString(IReadOnlyCollection<byte> ba)
        {
            var hex = new StringBuilder(ba.Count * 2);
            foreach (var b in ba)
                hex.AppendFormat("{0:x2}", b);
            return hex.ToString();
        }

        private static byte[] StringToByteArray(string hex)
        {
            var numberChars = hex.Length;
            var bytes = new byte[numberChars / 2];
            for (var i = 0; i < numberChars; i += 2)
                bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
            return bytes;
        }

        private static string Sign(string message)
        {
            var rsa = RSA.Create();
            rsa.ImportFromPem(File.ReadAllText("/tmp/certs/private.pem"));
            
            var messageBytes = Encoding.UTF8.GetBytes(message);

            var signature = rsa.SignData(messageBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
            return ByteArrayToString(signature);
        }

        private static bool Verify(string message, string signature)
        {
            var rsa = RSA.Create();
            var cert = X509Certificate.CreateFromCertFile("/tmp/certs/public.pem");
            var publicKey = cert.GetPublicKey();
            rsa.ImportRSAPublicKey(publicKey, out _);
            
            var messageBytes = Encoding.UTF8.GetBytes(message);
            var signatureBytes = StringToByteArray(signature);
            
            return rsa.VerifyData(messageBytes, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
        }
        
        public static void Main()
        {
            const string message = "hello";
            
            // var signature = Sign(message);
            const string signature = <value_from_python>;
            
            // Console.WriteLine($"Signature is: {signature}");
            Console.WriteLine($"isValidated: {Verify(message, signature)}");
        }
    }
}

验证失败,因为两个代码使用不同的盐长度。 Python 代码显式应用 maximum salt length, the C# code defaults to the digest output length. The latter is also defined as PSS default in RFC8017, A.2.3. RSASSA-PSS.

所以要解决这个问题

  • 要么使用 Python 代码中的摘要输出长度,即 32(SHA256 字节),
  • 或在 C# 代码中最大盐长度:signature length - digest output length - 2 = 512 - 32 - 2 = 478(对于 4096 位密钥)。
    据我所知,这对于 .NET 板载方式是不可能的,但对于 BouncyCastle and the class Org.BouncyCastle.Crypto.Signers.PssSigner, which also provides constructors for defining the salt length. You can find an example ,最后一节。