在.NET Core中从PEM文件创建X509Certificate2

19

我想基于PEM文件创建一个X509Certificate2对象。问题在于如何设置X509Certificate2的PrivateKey属性。我阅读了.NET Core上的X509Certificate2.CreateFromCertFile(),然后使用了

var rsa = new RSACryptoServiceProvider();

rsa.ImportCspBlob(pvk);

在这里读取pvk作为私钥的字节数组(从GetBytesFromPEM中读取,如此展示:如何从PEM文件获取私钥?),设置私钥,但是我遇到了一个

Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException 错误,错误信息为Bad Version of provider。

如何正确地基于PEM文件中的私钥设置X509Certificate2的PrivateKey?

如果我看一下创建X509Certificate2,它们使用

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

这似乎是一种不错的方法,但在 .Net Core 中无法正常工作...

2个回答

11

使用.NET 5.0,我们终于有了一个不错的方法来完成这个任务。

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

var cert = X509Certificate2.CreateFromPemFile(filePath);

如果创建证书时没有文件,那么可以使用 ReadOnlySpan<char> 传入证书的指纹和密钥。如果内容加密,也可以使用 X509Certificate2.CreateFromEncryptedPemX509Certificate2.CreateFromEncryptedPemFile

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


1
当我使用它时,我会收到以下错误消息:“TLS客户端凭据的证书未附加私钥信息属性。这通常发生在证书备份不正确并稍后恢复时。此消息还可能表示证书注册失败。”您有任何想法为什么会发生这种情况吗? - Sreejith Krishnadas
1
.NET Core 3.1 不支持该方法。 - ademchenko

6
如果您刚刚从私钥文件的Base64编码中提取了字节,则会得到一个PKCS#1、PKCS#8或加密的PKCS#8私钥blob(取决于它是否说“BEGIN RSA PRIVATE KEY”,“BEGIN PRIVATE KEY”或“BEGIN ENCRYPTED PRIVATE KEY”)。ImportCspBlob需要一种自定义格式的数据,这就是它抱怨的原因。 在不使用BouncyCastle的情况下使用c#进行数字签名有解释方法的说明。最简单/最公式化的方法是只需使用证书和密钥创建PFX,然后让X509Certificate2构造函数完成其工作即可。
如果您直接加载密钥对象,则配对私钥与证书的方式是使用新的CopyWithPrivateKey扩展方法之一。这将返回一个新的X509Certificate2实例,该实例知道私钥。 PrivateKey设置器已从.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;
}

2
我使用PEM格式的原因是证书作为Kubernetes中的秘密存储。是否有可能以某种方式将证书作为字符串读取,将内容转换为PFX格式,然后将其用作X509Certificate2构造函数的输入? - heydy
1
更新:所以,当我尝试: using (CngKey key = CngKey.Import(p8bytes, CngKeyBlobFormat.Pkcs8PrivateBlob)) { var rsaCng= new RSACng(key); X509Certificate2 certWithPrivateKey = certificate.CopyWithPrivateKey(rsaCng); }, RSACng对象没问题,但是当调用CopyWithPrivateKey时,我收到一个异常,指出“不支持请求的操作”.. 你能看到任何明显的错误吗?@bartonjs - heydy
另一个评论是我在Kubernetes中的Docker容器中运行该应用程序,那么CngKey也无法工作了? - heydy
@heydy 啊,由于CngKey.Import不允许您命名密钥,因此无法在不进行不同的导出/导入的情况下绑定它,但是该密钥无法导出(https://stackoverflow.com/a/48647314/6535399)。但是,您是正确的,CngKey仅适用于Windows。 - bartonjs
2
@heydy 今天我感到很有灵感,写了一个轻量级的PKCS8读取器。请享用。 - bartonjs
有关参数解码,请参见:https://github.com/StefH/OpenSSL-X509Certificate2-Provider - Stef Heyenrath

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接