Export private/public keys from X509 certificate to PEM

后端 未结 2 1590
别跟我提以往
别跟我提以往 2020-12-03 17:32

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?

相关标签:
2条回答
  • 2020-12-03 18:04

    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.

    0 讨论(0)
  • 2020-12-03 18:06

    I figured out a solution that works well. I could not find an EXACT example of how to go from certificate store to pem file in windows. Granted, this may not work for some certificates, but if you are working with one you have created yourself (for example, if you just need security between two machines you control that the end user won't see) this is a good way of going back to pem / pk (linux style).

    I utilized the utilities found at http://www.bouncycastle.org/csharp/

    X509Store certStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);
    certStore.Open(OpenFlags.ReadOnly);
    
    X509Certificate2 caCert = certStore.Certificates.Find(X509FindType.FindByThumbprint, "3C97BF2632ACAB5E35B48CB94927C4A7D20BBEBA", true)[0];
    
    
    RSACryptoServiceProvider pkey = (RSACryptoServiceProvider)caCert.PrivateKey;
    
    
    AsymmetricCipherKeyPair keyPair = DotNetUtilities.GetRsaKeyPair(pkey);
    using (TextWriter tw = new StreamWriter("C:\\private.pem"))
    {
        PemWriter pw = new PemWriter(tw);
        pw.WriteObject(keyPair.Private);
        tw.Flush();
    }
    
    0 讨论(0)
提交回复
热议问题