Search code examples
c#.net-corepemx509certificate2

Export private/public keys from X509 certificate to PEM


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


Solution

  • 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.

    Update (2023-08-30): And .NET 7 added the direct-to-PEM methods, making the PEM output even easier.

    .NET 7+:

    string certificatePem = cert.ExportCertificatePem();
    
    AsymmetricAlgorithm key = cert.GetRSAPrivateKey() ?? cert.GetECDsaPrivateKey();
    string pubKeyPem = key.ExportSubjectPublicKeyInfoPem();
    string privKeyPem = key.ExportPkcs8PrivateKeyPem();
    

    .NET 7 also added PemEncoding.WriteString, letting the .NET 5-style code go to string instead of char[].

    .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 of 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
             7A 59 BD 02 9A 7A 3A 9D 7C 71 D0 AC 2E FA 54 5F
             1F 5C BA 43 BB 43 E1 3B 78 77 AF 82 EF EB 40 C3
             8D 1E CD 73 7F 5B F9 C8 96 92 B2 9C 87 5E D6 E1
       // 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.