【问题标题】:Create X509Certificate2 from PEM file in .NET Core从 .NET Core 中的 PEM 文件创建 X509Certificate2
【发布时间】:2018-10-18 01:11:06
【问题描述】:

我想创建一个基于 PEM 文件的 X509Certificate2 对象。问题是设置 X509Certificate2 的 PrivateKey 属性。我看了X509Certificate2.CreateFromCertFile() on .NET Core 然后用了

var rsa = new RSACryptoServiceProvider();

rsa.ImportCspBlob(pvk);

其中pvk 是私钥的字节数组(从 GetBytesFromPEM 读取,如下所示how to get private key from PEM file?),用于设置私钥,但随后我得到一个

Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException 带有消息错误版本的提供程序。

如何根据PEM文件中的私钥正确设置X509Certificate2的PrivateKey?

如果我查看Creating the X509Certificate2,他们会使用

 RSACryptoServiceProvider prov = Crypto.DecodeRsaPrivateKey(keyBuffer);
 certificate.PrivateKey = prov;

这似乎是一种巧妙的方法,但这在 .Net Core 中不起作用...

【问题讨论】:

    标签: c# .net-core x509certificate private-key pem


    【解决方案1】:

    如果您刚刚从私钥文件的 Base64 编码中提取了字节,则您有一个 PKCS#1、PKCS#8 或加密的 PKCS#8 私钥 blob(取决于它是否显示“BEGIN RSA PRIVATE KEY ”、“开始私钥”或“开始加密私钥”)。 ImportCspBlob 想要数据的自定义格式,这就是它抱怨的原因。

    Digital signature in c# without using BouncyCastle 解释了前进的方向。最简单/最公式化的方法是使用证书和密钥制作 PFX,然后让 X509Certificate2 构造函数完成它的工作。

    如果您直接加载密钥对象,那么您将私钥与证书配对的方式是使用新的CopyWithPrivateKey 扩展方法之一。这将返回一个知道私钥的 X509Certificate2 的新实例。

    PrivateKey setter 已从 .NET Core 中“移除”,因为它在 Windows 上具有很多在 Linux 和 macOS 上难以复制的副作用,尤其是当您从 X509Store 实例中检索证书时。


    此代码是对真实 BER 规则过于严格和过度接受的结合,但这应该读取有效编码的 PKCS#8 文件,除非它们包含属性。

    private static readonly byte[] s_derIntegerZero = { 0x02, 0x01, 0x00 };
    
    private static readonly byte[] s_rsaAlgorithmId =
    {
        0x30, 0x0D,
        0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01,
        0x05, 0x00,
    };
    
    private static int ReadLength(byte[] data, ref int offset)
    {
        byte lengthOrLengthLength = data[offset++];
    
        if (lengthOrLengthLength < 0x80)
        {
            return lengthOrLengthLength;
        }
    
        int lengthLength = lengthOrLengthLength & 0x7F;
        int length = 0;
    
        for (int i = 0; i < lengthLength; i++)
        {
            if (length > ushort.MaxValue)
            {
                throw new InvalidOperationException("This seems way too big.");
            }
    
            length <<= 8;
            length |= data[offset++];
        }
    
        return length;
    }
    
    private static byte[] ReadUnsignedInteger(byte[] data, ref int offset, int targetSize = 0)
    {
        if (data[offset++] != 0x02)
        {
            throw new InvalidOperationException("Invalid encoding");
        }
    
        int length = ReadLength(data, ref offset);
    
        // Encoding rules say 0 is encoded as the one byte value 0x00.
        // Since we expect unsigned, throw if the high bit is set.
        if (length < 1 || data[offset] >= 0x80)
        {
            throw new InvalidOperationException("Invalid encoding");
        }
    
        byte[] ret;
    
        if (length == 1)
        {
            ret = new byte[length];
            ret[0] = data[offset++];
            return ret;
        }
    
        if (data[offset] == 0)
        {
            offset++;
            length--;
        }
    
        if (targetSize != 0)
        {
            if (length > targetSize)
            {
                throw new InvalidOperationException("Bad key parameters");
            }
    
            ret = new byte[targetSize];
        }
        else
        {
            ret = new byte[length];
        }
    
        Buffer.BlockCopy(data, offset, ret, ret.Length - length, length);
        offset += length;
        return ret;
    }
    
    private static void EatFullPayloadTag(byte[] data, ref int offset, byte tagValue)
    {
        if (data[offset++] != tagValue)
        {
            throw new InvalidOperationException("Invalid encoding");
        }
    
        int length = ReadLength(data, ref offset);
    
        if (data.Length - offset != length)
        {
            throw new InvalidOperationException("Data does not represent precisely one value");
        }
    }
    
    private static void EatMatch(byte[] data, ref int offset, byte[] toMatch)
    {
        if (data.Length - offset > toMatch.Length)
        {
            if (data.Skip(offset).Take(toMatch.Length).SequenceEqual(toMatch))
            {
                offset += toMatch.Length;
                return;
            }
        }
    
        throw new InvalidOperationException("Bad data.");
    }
    
    private static RSA DecodeRSAPkcs8(byte[] pkcs8Bytes)
    {
        int offset = 0;
    
        // PrivateKeyInfo SEQUENCE
        EatFullPayloadTag(pkcs8Bytes, ref offset, 0x30);
        // PKCS#8 PrivateKeyInfo.version == 0
        EatMatch(pkcs8Bytes, ref offset, s_derIntegerZero);
        // rsaEncryption AlgorithmIdentifier value
        EatMatch(pkcs8Bytes, ref offset, s_rsaAlgorithmId);
        // PrivateKeyInfo.privateKey OCTET STRING
        EatFullPayloadTag(pkcs8Bytes, ref offset, 0x04);
        // RSAPrivateKey SEQUENCE
        EatFullPayloadTag(pkcs8Bytes, ref offset, 0x30);
        // RSAPrivateKey.version == 0
        EatMatch(pkcs8Bytes, ref offset, s_derIntegerZero);
    
        RSAParameters rsaParameters = new RSAParameters();
        rsaParameters.Modulus = ReadUnsignedInteger(pkcs8Bytes, ref offset);
        rsaParameters.Exponent = ReadUnsignedInteger(pkcs8Bytes, ref offset);
        rsaParameters.D = ReadUnsignedInteger(pkcs8Bytes, ref offset, rsaParameters.Modulus.Length);
        int halfModulus = (rsaParameters.Modulus.Length + 1) / 2;
        rsaParameters.P = ReadUnsignedInteger(pkcs8Bytes, ref offset, halfModulus);
        rsaParameters.Q = ReadUnsignedInteger(pkcs8Bytes, ref offset, halfModulus);
        rsaParameters.DP = ReadUnsignedInteger(pkcs8Bytes, ref offset, halfModulus);
        rsaParameters.DQ = ReadUnsignedInteger(pkcs8Bytes, ref offset, halfModulus);
        rsaParameters.InverseQ = ReadUnsignedInteger(pkcs8Bytes, ref offset, halfModulus);
    
        if (offset != pkcs8Bytes.Length)
        {
            throw new InvalidOperationException("Something didn't add up");
        }
    
        RSA rsa = RSA.Create();
        rsa.ImportParameters(rsaParameters);
        return rsa;
    }
    

    【讨论】:

    • 我之所以使用 PEM 格式是因为证书在 Kubernetes 中是作为机密存储的。是否有可能以某种方式将证书作为字符串读取,将内容转换为 PFX 格式 - 然后将其用作 X509Certificate2 的构造函数的输入?
    • 更新:所以,当我尝试:使用 (CngKey key = CngKey.Import(p8bytes, CngKeyBlobFormat.Pkcs8PrivateBlob)) { var rsaCng= new RSACng(key); X509Certificate2 certWithPrivateKey = 证书.CopyWithPrivateKey(rsaCng); },RSACng 对象很好,但是当调用 CopyWithPrivateKey 时,我得到一个异常,指出“不支持请求的操作”.. 你能看到任何明显的错误吗? @bartonjs
    • 这里的另一个评论是,我在 Kubernetes 的 Docker 容器中运行应用程序,所以 CngKey 无论如何都不起作用?
    • @heydy 啊,因为 CngKey.Import 不允许您命名密钥,所以如果不进行不同的导出/导入,它就无法绑定它,但是密钥不可导出 (stackoverflow.com/a/48647314/6535399) .但是,您是对的,CngKey 仅适用于 Windows。
    • @heydy 显然我今天受到了启发,并制作了一个轻量级的 PKCS8 阅读器。享受吧。
    【解决方案2】:

    使用 .NET 5.0,我们终于有了一个很好的方法。

    X509Certificate2 类提供了两个静态方法X509Certificate2.CreateFromPemX509Certificate2.CreateFromPemFile。因此,如果您有文件路径,则可以调用:

    var cert = X509Certificate2.CreateFromPemFile(filePath);
    

    如果在没有文件的情况下创建证书,则可以传入ReadOnlySpan&lt;char&gt; 以获得证书指纹和密钥。如果内容是加密的,还有X509Certificate2.CreateFromEncryptedPemX509Certificate2.CreateFromEncryptedPemFile

    更多信息可以在官方 API 文档中找到:https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2.createfrompemfile?view=net-5.0

    【讨论】:

    • 当我使用它时,我收到以下错误:“TLS 客户端凭据的证书没有附加私钥信息属性。这通常发生在证书备份不正确时,然后再备份已恢复。此消息还可能表示证书注册失败。”你知道为什么会这样吗?
    • .NET core 3.1 不支持该方法。
    猜你喜欢
    • 2022-01-15
    • 2016-06-21
    • 2020-05-04
    • 2021-11-25
    • 2020-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-21
    相关资源
    最近更新 更多