Export private/public keys from X509 certificate to PEM

rudolfdobias picture rudolfdobias · May 12, 2017 · Viewed 16.9k times · Source

is there any convenient way to export private/public keys from .p12 certificate in PEM format using .NET Core? Without manipulating with bytes at low level? I googled for hours and almost nothing is usable in .net core or it isn't documented anywhere..

Let's have an X509Certificate2

var cert = new X509Certificate2(someBytes, pass);
var privateKey = cert.GetRSAPrivateKey();
var publicKey = cert.GetRSAPublicKey();
// assume everything is fine so far

And now I need to export the keys as two separate PEM keys. I already tried PemWriter in BouncyCastle but the types are not compatibile with System.Security.Cryptography from Core... no luck.

In other words, I'm finding a way how to write this:

$ openssl pkcs12 -in path/to/cert.p12 -out public.pub -clcerts -nokeys
$ openssl pkcs12 -in path/to/cert.p12 -out private.key -nocerts

Does anybody have an idea?

Thanks

Answer

bartonjs picture bartonjs · May 12, 2017

Update (2021-01-12): For .NET 5 this is pretty easy. .NET Core 3.0 can even get most of the way there. The original answer was written when .NET Core 1.1 was the newest version of .NET Core. It explains what these new methods are doing under the covers.

.NET 5+:

byte[] certificateBytes = cert.RawData;
char[] certificatePem = PemEncoding.Write("CERTIFICATE", certificateBytes);

AsymmetricAlgorithm key = cert.GetRSAPrivateKey() ?? cert.GetECDsaPrivateKey();
byte[] pubKeyBytes = key.ExportSubjectPublicKeyInfo();
byte[] privKeyBytes = key.ExportPkcs8PrivateKey();
char[] pubKeyPem = PemEncoding.Write("PUBLIC KEY", pubKeyBytes);
char[] privKeyPem = PemEncoding.Write("PRIVATE KEY", privKeyBytes);

new string(char[]) can turn those char arrays into System.String instances, if desired.

For encrypted PKCS#8 it's still easy, but you have to make some choices for how to encrypt it:

byte[] encryptedPrivKeyBytes = key.ExportEncryptedPkcs8PrivateKey(
    password,
    new PbeParameters(
        PbeEncryptionAlgorithm.Aes256Cbc,
        HashAlgorithmName.SHA256,
        iterationCount: 100_000));

.NET Core 3.0, .NET Core 3.1:

This is the same as the .NET 5 answer, except the PemEncoding class doesn't exist yet. But that's OK, there's a start for a PEM-ifier in the older answer (though "CERTIFICATE" and cert.RawData) would need to come from parameters).

.NET Core 3.0 was the release where the extra key format export and import methods were added.

.NET Core 2.0, .NET Core 2.1:

The same as the original answer, except you don't need to write a DER encoder. You can use the System.Formats.Asn1 NuGet package.

Original answer (.NET Core 1.1 was the newest option):

The answer is somewhere between "no" and "not really".

I'm going to assume that you don't want the p12 output gunk at the top of public.pub and private.key.

public.pub is just the certificate. The openssl commandline utility prefers PEM encoded data, so we'll write a PEM encoded certificate (note, this is a certificate, not a public key. It contains a public key, but isn't itself one):

using (var cert = new X509Certificate2(someBytes, pass))
{
    StringBuilder builder = new StringBuilder();
    builder.AppendLine("-----BEGIN CERTIFICATE-----");
    builder.AppendLine(
        Convert.ToBase64String(cert.RawData, Base64FormattingOptions.InsertLineBreaks));
    builder.AppendLine("-----END CERTIFICATE-----");

    return builder.ToString();
}

The private key is harder. Assuming the key is exportable (which, if you're on Windows or macOS, it isn't, because you didn't assert X509KeyStorageFlags.Exportable) you can get the parameters with privateKey.ExportParameters(true). But now you have to write that down.

An RSA private key gets written into a PEM encoded file whose tag is "RSA PRIVATE KEY" and whose payload is the ASN.1 (ITU-T X.680) RSAPrivateKey (PKCS#1 / RFC3447) structure, usually DER-encoded (ITU-T X.690) -- though since it isn't signed there's not a particular DER restriction, but many readers may be assuming DER.

Or, it can be a PKCS#8 (RFC 5208) PrivateKeyInfo (tag: "PRIVATE KEY"), or EncryptedPrivateKeyInfo (tag: "ENCRYPTED PRIVATE KEY"). Since EncryptedPrivateKeyInfo wraps PrivateKeyInfo, which encapsulates RSAPrivateKey, we'll just start there.

  RSAPrivateKey ::= SEQUENCE {
      version           Version,
      modulus           INTEGER,  -- n
      publicExponent    INTEGER,  -- e
      privateExponent   INTEGER,  -- d
      prime1            INTEGER,  -- p
      prime2            INTEGER,  -- q
      exponent1         INTEGER,  -- d mod (p-1)
      exponent2         INTEGER,  -- d mod (q-1)
      coefficient       INTEGER,  -- (inverse of q) mod p
      otherPrimeInfos   OtherPrimeInfos OPTIONAL
  }

Now ignore the part about otherPrimeInfos. exponent1 is DP, exponent2 is DQ, and coefficient is InverseQ.

Let's work with a pre-published 384-bit RSA key.

RFC 3447 says we want Version=0. Everything else comes from the structure.

// SEQUENCE (RSAPrivateKey)
30 xa [ya [za]]
   // INTEGER (Version=0)
   02 01
         00
   // INTEGER (modulus)
   // Since the most significant bit if the most significant content byte is set,
   // add a padding 00 byte.
   02 31
         00
         DA CC 22 D8 6E 67 15 75 03 2E 31 F2 06 DC FC 19
         2C 65 E2 D5 10 89 E5 11 2D 09 6F 28 82 AF DB 5B
         78 CD B6 57 2F D2 F6 1D B3 90 47 22 32 E3 D9 F5
   // INTEGER publicExponent
   02 03
         01 00 01
   // INTEGER (privateExponent)
   // high bit isn't set, so no padding byte
   02 30
         DA CC 22 D8 6E 67 15 75 03 2E 31 F2 06 DC FC 19
         2C 65 E2 D5 10 89 E5 11 2D 09 6F 28 82 AF DB 5B
         78 CD B6 57 2F D2 F6 1D B3 90 47 22 32 E3 D9 F5
   // INTEGER (prime1)
   // high bit is set, pad.
   02 19
         00
         FA DB D7 F8 A1 8B 3A 75 A4 F6 DF AE E3 42 6F D0
         FF 8B AC 74 B6 72 2D EF
   // INTEGER (prime2)
   // high bit is set, pad.
   02 19
         00
         DF 48 14 4A 6D 88 A7 80 14 4F CE A6 6B DC DA 50
         D6 07 1C 54 E5 D0 DA 5B
   // INTEGER (exponent1)
   // no padding
   02 18
         24 FF BB D0 DD F2 AD 02 A0 FC 10 6D B8 F3 19 8E
         D7 C2 00 03 8E CD 34 5D
   // INTEGER (exponent2)
   // padding required
   02 19
         00
         85 DF 73 BB 04 5D 91 00 6C 2D 45 9B E6 C4 2E 69
         95 4A 02 24 AC FE 42 4D
   // INTEGER (coefficient)
   // no padding
   02 18
         1A 3A 76 9C 21 26 2B 84 CA 9C A9 62 0F 98 D2 F4
         3E AC CC D4 87 9A 6F FD

Now we count up the number of bytes that went into the RSAPrivateKey structure. I count 0xF2 (242). Since that's bigger than 0x7F we need to use multi-byte length encoding: 81 F2.

So now with the byte array 30 81 F2 02 01 00 ... 9A 6F FD you could convert that to multi-line Base64 and wrap it in "RSA PRIVATE KEY" PEM armor. But maybe you want a PKCS#8.

  PrivateKeyInfo ::= SEQUENCE {
    version                   Version,
    privateKeyAlgorithm       PrivateKeyAlgorithmIdentifier,
    privateKey                PrivateKey,
    attributes           [0]  IMPLICIT Attributes OPTIONAL }

  Version ::= INTEGER
  PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
  PrivateKey ::= OCTET STRING

So, let's do it again... The RFC says we want version=0 here, too. AlgorithmIdentifier can be found in RFC5280.

// SEQUENCE (PrivateKeyInfo)
30 xa [ya [za]]
   // INTEGER (Version=0)
   02 01
         00
   // SEQUENCE (PrivateKeyAlgorithmIdentifier / AlgorithmIdentifier)
   30 xb [yb [zb]]
      // OBJECT IDENTIFIER id-rsaEncryption (1.2.840.113549.1.1.1)
      06 09 2A 86 48 86 F7 0D 01 01 01
      // NULL (per RFC 3447 A.1)
      05 00
   // OCTET STRING (aka byte[]) (PrivateKey)
   04 81 F5
      [the previous value here,
       note the length here is F5 because of the tag and length bytes of the payload]

Backfill the lengths:

The "b" series is 13 (0x0D), since it only contains things of pre-determined length.

The "a" series is now (2 + 1) + (2 + 13) + (3 + 0xF5) = 266 (0x010A).

30 82 01 0A  02 01 00 30  0D ...

Now you can PEM that as "PRIVATE KEY".

Encrypting it? That's a whole different ballgame.