18

I want to create a X509Certificate2 object based on a PEM file. The problem is setting the PrivateKey property of X509Certificate2. I read X509Certificate2.CreateFromCertFile() on .NET Core and then used

var rsa = new RSACryptoServiceProvider();

rsa.ImportCspBlob(pvk);

Where pvk is the byte array of the private key (read from GetBytesFromPEM as shown here how to get private key from PEM file?), to set the private key, but then I get an

Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException with message Bad Version of provider.

How can I properly set the PrivateKey of the X509Certificate2 based on the private key in the PEM file?

If I look at Creating the X509Certificate2, they use

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

which seems like a neat way to do this, but this does not work in .Net Core...

bartonjs
  • 30,352
  • 2
  • 71
  • 111
heydy
  • 273
  • 2
  • 3
  • 10

2 Answers2

9

Using .NET 5.0 we finally have a nice way of doing this.

The X509Certificate2 class provides two static methods X509Certificate2.CreateFromPem and X509Certificate2.CreateFromPemFile. So if you have the file path then can call:

var cert = X509Certificate2.CreateFromPemFile(filePath);

If creating the certificate without the file then can pass in ReadOnlySpan<char> for the certificate thumbprint and key. There are also X509Certificate2.CreateFromEncryptedPem and X509Certificate2.CreateFromEncryptedPemFile if the contents is encrypted.

More info can be found in the official API docs here: https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2.createfrompemfile?view=net-5.0

Tom Dee
  • 2,516
  • 4
  • 17
  • 25
  • 1
    When I use this I get the following error : "The TLS client credential's certificate does not have a private key information property attached to it. This most often occurs when a certificate is backed up incorrectly and then later restored. This message can also indicate a certificate enrollment failure." Would you have any idea why this happens? – Sreejith Krishnadas May 26 '21 at 15:31
  • 1
    .NET core 3.1 doesn't support that method. – ademchenko Sep 06 '21 at 15:43
6

If you've just extracted the bytes from the Base64 encoding of the private key file you have a PKCS#1, PKCS#8, or encrypted PKCS#8 private key blob (depending on if it said "BEGIN RSA PRIVATE KEY", "BEGIN PRIVATE KEY" or "BEGIN ENCRYPTED PRIVATE KEY"). ImportCspBlob wants a custom format for the data, and that's why it's complaining.

Digital signature in c# without using BouncyCastle has an explanation of ways forward. The easiest / most formulaic is to just make a PFX with the cert and key, and let the X509Certificate2 constructor do its thing.

If you go the route of loading the key object directly then the way you would mate a private key with the certificate is to use one of the new CopyWithPrivateKey extension methods. This returns a new instance of X509Certificate2 which knows about the private key.

The PrivateKey setter was "removed" from .NET Core because it has a lot of side effects on Windows that are hard to replicate on Linux and macOS, particularly if you retrieved the certificate out of an instance of X509Store.


This code is a combination of overly strict and overly accepting for real BER rules, but this should read validly encoded PKCS#8 files unless they included attributes.

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;
}
bartonjs
  • 30,352
  • 2
  • 71
  • 111
  • 2
    The reason for why I am using PEM format is that the certificate is stored as a secret in Kubernetes. Would it be possible somehow to read the certificate as a string, convert the content to PFX format - and then use this as input to X509Certificate2's constructor? – heydy May 08 '18 at 20:12
  • 1
    Update: So, when I try: using (CngKey key = CngKey.Import(p8bytes, CngKeyBlobFormat.Pkcs8PrivateBlob)) { var rsaCng= new RSACng(key); X509Certificate2 certWithPrivateKey = certificate.CopyWithPrivateKey(rsaCng); }, the RSACng object is fine, but when CopyWithPrivateKey is called, I get an exception stating 'The requested operation is not supported'.. can you see any obvious mistakes there? @bartonjs – heydy May 09 '18 at 08:41
  • Another comment here is that I am running the application in a Docker container in Kubernetes, so then CngKey won't work anyway? – heydy May 09 '18 at 11:50
  • @heydy Ah, since CngKey.Import doesn't let you name the key it can't bind it without doing a different export/import, but the key isn't exportable (https://stackoverflow.com/a/48647314/6535399). But, you are correct, CngKey only works on Windows. – bartonjs May 09 '18 at 15:25
  • 2
    @heydy Apparently I felt inspired today, and made a lightweight PKCS8 reader. Enjoy. – bartonjs May 09 '18 at 16:47
  • For decoding the parameters, see also : https://github.com/StefH/OpenSSL-X509Certificate2-Provider – Stef Heyenrath Aug 11 '22 at 15:35