You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

用C#实现与OpenSSL一致的RSA私钥签名问题

解决C# RSA签名与OpenSSL结果不一致的问题

让我们一步步解决你的问题,让C#的签名结果和OpenSSL完全一致:

问题背景

你有一个可以正常运行的OpenSSL签名命令:

echo -n "my_password" | openssl pkeyutl -sign -inkey /home/ramunas/Downloads/id_rsa | base64 -w 0

输出结果:

Q9dyYKFvGOELAZYN8O18l30/bC/hVBm+7sy0DFJkspSvQa4bY7xGGsRina99ivxbdsu4BaakXDdA/X9wxP/Ll/GpEUS9xteiVNQkOEYOl27OAhe0OsC6zRDKpRPJEFhfLVt0cHlDILRZ0yuJLfv0UiwF0CTe4Xv5AN3RXrv28nIS8XA0o/TmCB5XlG12Zq7UdFoDF+j6oFXMyWAV9eXa1uQdNzIAm8wPn4e3lEz4v8p/M5PhLlAO2rBTEw79PiqFtKqlJ1OAZan8As02ejBjpe3t55dKuQke5+S1/7Zt7bZozDeswSMYbb643ZbLW+QN0AoO7pMy4w8qugzQZ/gnfw==

所用PKCS8格式私钥:

MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCfHlwo+7jE6TKGGquuLDcrj+gPiWpl7QKpUAa8rqCs+A0iGbIM843U+uRx5mAhoBSJhBL9XPZss+IEKGy29K6bdy8sCXBvU+rjjPC/q91Sg5pw84063h8sOvV0UIFZVFZBt4Ax53qhOWFMAGtNB55LPvih71uz+iI5KneeS6OgAeBy1I+fJG1SiFDyOZsugma4f50ACXtI4hVtYSmM3/hY3XpWGQdUJPKByTfIxHg1v6EdMZ7mlOjYSFZxCVQRSXUAVf9ctPQQ4iA4gn69uveYH/fZQTc+7M3FGGsYVY2q31N+3jRbppOi6NqqISPckfXKnlTNkTkjXM0ZbZSvJrk3AgMBAAECggEAZjItpvTlmzLbjF4c3eTwGj53IWN0vroW93+6k/zknbNDXB98x+y1r5DkoHMCUxZpmlkFiUznyLFP/LOp/2fvrVJDLdlyKU6Qkk23YqOTpqd3zgvfLWv0QYgsleQO77zHGbYt09+EV4BvCU1LnVHDTTjIn9kH4PRKfsTTxwt1sBH//kXwZaRlvKxP0C9EGXPCymgdFO9RAhaVURDx3x7prKGRJbPDSMIP09elhChQ2fHvTDlqvrnr71hIR1ge8uGMkRta0gv3VyXpbAOrVJqxzmiccUIQoocWVLNCPXFZTe/7BFx1esLH4nGUwC57yiw9KudVhl5oeMIcHjKyrnoeIQKBgQDeuGqEqvGzlX/ZTt9S9LBj04OMA+0T2Lg/rg8pbn3MsqCLPikaf55Xpx5Le8fgc9cQzZYaLb22bEMAkl38EEttTK0ChWFnViGVYrxTDuQZA2eKljLrmuiynrjOdJq1Sro7bRsSOPYO32BwdGZ2/KJCJkocgRP2Mi9nALlrBfcRswKBgQC25QZIeynG2DHSR6rUTNhurVqbVHE5CWfmABdUHyg90y0DSjeKLad3z42Zln5mKfcapjWqL4443HAJ8ggdkyyYuif0RO4L2NBBKL/GxSjpeRgEZfmhOMgJLEy9dAzWSaiH1B5/MY1ZWxXWJBinOgu7ME6rrK5pEZDq/ySApkMQbQKBgBPx9fANkvmfIMToME2whf9amMQ4Mcn+NXnWb5spPvDO6lut5GZwGVEGMUtXOW4bUZ46mv+bKwskJNfvQ6VoHCkcnb3FDhT//J/xKKZThg/LY0Fg9AxvYfllB23NuXEU0RqzSaJXIYnxbSSE+Awd0bYU0bw9vvjkQ6R4xyITda3LAoGBALEcB4GZA1OzeEuRazQ7VjuXU+6nTx1UpnBsFis4INf979EHxdDhdRLows7AlZaJfOkpmz463xbhIP4AiytYog8j9hztwzdltgXjWBM8SeoNgdyAg8REIDIB3C56exPoMHOYThAOBDtLoVHFkrod085pcUV960eD4zot6UNLvhXhAoGBAITXSidOCGfkUVj8/oItR/IV+gwqFZFMoXDD8vJ/0G99Rjib4rWcX6B3NxamnNA6EfiGaMTdWZaxFbJOPh2K7OcOlDCWNpMvEQQyI0nhGKkKCujiTfBkSWkKDHdEAC/p0k2rAh4feYabdvX+ePmWAs6/X/syQ69jBk2Y6PhUsJH3

你尝试用C#的RSACryptoServiceProvider实现,但结果不一致,还需要手动跳过26字节,模数首字节也有差异。

核心问题分析

你的代码存在两个关键问题:

  1. 手动解析PKCS8私钥不标准:PKCS8私钥有严格的ASN.1结构,手动跳过26字节是依赖特定格式的hack操作,很容易导致RSA参数(比如模数)导入错误,这就是你看到模数首字节不同的原因。
  2. 签名行为与OpenSSL不匹配:OpenSSL的pkeyutl -sign在不指定-digest时,会直接对原始数据做PKCS1-v1_5类型1填充,然后用私钥加密(这就是RSA签名的本质)。而C#的SignData方法默认会先对数据哈希(比如SHA1),再签名哈希值,两者的逻辑完全不同。

解决方案

步骤1:标准导入PKCS8私钥

我们用.NET内置的ASN.1解析逻辑导入PKCS8私钥,避免手动解析的错误。

步骤2:实现PKCS1-v1_5类型1填充

PKCS1-v1_5类型1填充的规则是:

EM = 0x00 || 0x01 || [全0xFF的填充字节] || 0x00 || 原始数据

填充字节的长度要满足:模数长度 - 原始数据长度 - 3(0x00、0x01、0x00占3字节)。

步骤3:执行签名并生成Base64结果

用私钥加密填充后的字节数组(RSA签名=私钥加密填充消息),然后将结果Base64编码(不换行,对应OpenSSL的base64 -w 0)。

完整代码实现

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Linq;

public class RsaSignatureHelper
{
    // .NET Core/.NET 5+ 直接导入PKCS8私钥(推荐)
    public static RSA CreateRsaFromPkcs8PrivateKey(string base64Pkcs8)
    {
        byte[] pkcs8Bytes = Convert.FromBase64String(base64Pkcs8);
        RSA rsa = RSA.Create();
        rsa.ImportPkcs8PrivateKey(pkcs8Bytes, out _);
        return rsa;
    }

    // .NET Framework 兼容的PKCS8私钥解析
    public static RSACryptoServiceProvider CreateRsaCspFromPkcs8PrivateKey(string base64Pkcs8)
    {
        byte[] pkcs8Bytes = Convert.FromBase64String(base64Pkcs8);
        RSAParameters rsaParams = DecodePkcs8PrivateKey(pkcs8Bytes);
        RSACryptoServiceProvider rsaCsp = new RSACryptoServiceProvider();
        rsaCsp.ImportParameters(rsaParams);
        return rsaCsp;
    }

    private static RSAParameters DecodePkcs8PrivateKey(byte[] pkcs8Bytes)
    {
        using (MemoryStream ms = new MemoryStream(pkcs8Bytes))
        using (BinaryReader reader = new BinaryReader(ms))
        {
            // 读取PKCS8 SEQUENCE头部
            ReadAsn1Sequence(reader);
            // 读取版本号
            reader.ReadInt32();
            // 读取RSA算法标识符
            ReadAsn1Sequence(reader);
            reader.ReadBytes(16); // 跳过RSA OID和空参数
            // 读取私钥数据(OCTET STRING)
            byte[] privateKeyData = ReadAsn1OctetString(reader);
            
            // 解析RSA私钥结构
            using (MemoryStream msKey = new MemoryStream(privateKeyData))
            using (BinaryReader readerKey = new BinaryReader(msKey))
            {
                ReadAsn1Sequence(readerKey);
                readerKey.ReadInt32(); // RSA版本号
                RSAParameters rsaParams = new RSAParameters
                {
                    Modulus = ReadAsn1Integer(readerKey),
                    Exponent = ReadAsn1Integer(readerKey),
                    D = ReadAsn1Integer(readerKey),
                    P = ReadAsn1Integer(readerKey),
                    Q = ReadAsn1Integer(readerKey),
                    DP = ReadAsn1Integer(readerKey),
                    DQ = ReadAsn1Integer(readerKey),
                    InverseQ = ReadAsn1Integer(readerKey)
                };
                return rsaParams;
            }
        }
    }

    // 辅助方法:读取ASN.1 SEQUENCE标签
    private static void ReadAsn1Sequence(BinaryReader reader)
    {
        byte tag = reader.ReadByte();
        if (tag != 0x30)
            throw new InvalidDataException("Expected ASN.1 SEQUENCE tag");
        ReadAsn1Length(reader);
    }

    // 辅助方法:读取ASN.1 OCTET STRING
    private static byte[] ReadAsn1OctetString(BinaryReader reader)
    {
        byte tag = reader.ReadByte();
        if (tag != 0x04)
            throw new InvalidDataException("Expected ASN.1 OCTET STRING tag");
        int length = ReadAsn1Length(reader);
        return reader.ReadBytes(length);
    }

    // 辅助方法:读取ASN.1 INTEGER并移除前导零
    private static byte[] ReadAsn1Integer(BinaryReader reader)
    {
        byte tag = reader.ReadByte();
        if (tag != 0x02)
            throw new InvalidDataException("Expected ASN.1 INTEGER tag");
        int length = ReadAsn1Length(reader);
        byte[] value = reader.ReadBytes(length);
        // 移除前导零(RSA参数不需要前导零)
        if (value.Length > 0 && value[0] == 0)
        {
            Array.Resize(ref value, value.Length - 1);
        }
        return value;
    }

    // 辅助方法:读取ASN.1长度字段
    private static int ReadAsn1Length(BinaryReader reader)
    {
        byte lengthByte = reader.ReadByte();
        if ((lengthByte & 0x80) == 0)
        {
            return lengthByte;
        }
        int lengthLength = lengthByte & 0x7F;
        byte[] lengthBytes = reader.ReadBytes(lengthLength);
        Array.Reverse(lengthBytes);
        return BitConverter.ToInt32(lengthBytes.Concat(new byte[4 - lengthLength]).ToArray(), 0);
    }

    // 实现PKCS1-v1_5类型1填充
    private static byte[] ApplyPkcs1Type1Padding(byte[] data, int modulusLength)
    {
        int dataLength = data.Length;
        int paddingLength = modulusLength - dataLength - 3;
        if (paddingLength < 8)
            throw new ArgumentException("Data is too long for the RSA modulus size");

        byte[] paddedData = new byte[modulusLength];
        paddedData[0] = 0x00;
        paddedData[1] = 0x01;
        // 填充0xFF
        Array.Fill(paddedData, (byte)0xFF, 2, paddingLength);
        paddedData[2 + paddingLength] = 0x00;
        // 复制原始数据
        Buffer.BlockCopy(data, 0, paddedData, 2 + paddingLength + 1, dataLength);
        return paddedData;
    }

    // .NET Core/.NET 5+ 生成与OpenSSL一致的签名
    public static string SignRawData(string privateKeyPkcs8Base64, string data)
    {
        byte[] dataBytes = Encoding.UTF8.GetBytes(data);
        using (RSA rsa = CreateRsaFromPkcs8PrivateKey(privateKeyPkcs8Base64))
        {
            int modulusLength = rsa.KeySize / 8;
            byte[] paddedData = ApplyPkcs1Type1Padding(dataBytes, modulusLength);
            // 用私钥加密填充后的数据(即签名)
            byte[] signatureBytes = rsa.Encrypt(paddedData, RSAEncryptionPadding.Pkcs1);
            // 生成无换行的Base64
            return Convert.ToBase64String(signatureBytes).Replace("\r", "").Replace("\n", "");
        }
    }

    // .NET Framework 生成与OpenSSL一致的签名
    public static string SignRawDataCsp(string privateKeyPkcs8Base64, string data)
    {
        byte[] dataBytes = Encoding.UTF8.GetBytes(data);
        using (RSACryptoServiceProvider rsaCsp = CreateRsaCspFromPkcs8PrivateKey(privateKeyPkcs8Base64))
        {
            int modulusLength = rsaCsp.KeySize / 8;
            byte[] paddedData = ApplyPkcs1Type1Padding(dataBytes, modulusLength);
            // RSACryptoServiceProvider.Encrypt默认使用PKCS1填充
            byte[] signatureBytes = rsaCsp.Encrypt(paddedData, false);
            return Convert.ToBase64String(signatureBytes).Replace("\r", "").Replace("\n", "");
        }
    }
}

测试代码

public void TestOpenSslCompatibleSignature()
{
    string pkcs8PrivateKey = "你的PKCS8私钥Base64字符串";
    string targetData = "my_password";
    
    // 根据你的.NET版本选择方法
    string signature = RsaSignatureHelper.SignRawData(pkcs8PrivateKey, targetData);
    // .NET Framework使用:
    // string signature = RsaSignatureHelper.SignRawDataCsp(pkcs8PrivateKey, targetData);
    
    Console.WriteLine(signature);
    // 输出应该和OpenSSL的结果完全一致
}

关键注意事项

  • 数据编码:OpenSSL的echo -n "my_password"输出的是UTF-8编码的字节(无换行),所以C#中必须用Encoding.UTF8.GetBytes("my_password"),不要用默认的ASCII编码。
  • 填充类型:必须使用PKCS1-v1_5类型1填充,这是OpenSSL无-digest参数时的默认行为,不要混淆成加密用的类型2填充。
  • 私钥导入:使用标准的PKCS8解析逻辑,避免手动跳过字节的hack操作,确保RSA参数完全正确。

内容的提问来源于stack exchange,提问作者Ramunas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 09:45:33