public-key-encryptionpublic-keypemx509certificate2cer

exporting a public key in pem format from x509certificate2 object


I'm new to this subject, and I got confused of the differences between a public key in PEM format vs CER format.

I'm trying to export a public key from a x509certificate2 object in PEM format in c# code.

As far as I understand, the difference between a certificate in cer format vs pem format, is only the header and footer (if I understand correctly, a certificate in .cer format in base 64 should be someBase64String and in pem format it's the same string including the begin and end header and footer).

but my question is for the public key. let pubKey be a public key exported in .cer format from an x509certificate2 object, is the pem format of this key, will be:

------BEGIN PUBLIC KEY-----
pubKey...
------END PUBLIC KEY------

encoded in base 64?

Thanks :)


Solution

  • for the public key. let pubKey be a public key exported in .cer format from an x509certificate2 object

    Talking about a ".cer format" only applies when you have the whole certificate; and that's all that an X509Certificate2 will export as. (Well, or a collection of certificates, or a collection of certificates with associated private keys).

    Edit (2021-08-20):

    -- Original answer continues --

    Nothing built in to .NET will give you the DER-encoded SubjectPublicKeyInfo block of the certificate, which is what becomes "PUBLIC KEY" under a PEM encoding.

    You can build the data yourself, if you want. For RSA it's not too bad, though not entirely pleasant. The data format is defined in https://www.rfc-editor.org/rfc/rfc3280#section-4.1:

    SubjectPublicKeyInfo  ::=  SEQUENCE  {
        algorithm            AlgorithmIdentifier,
        subjectPublicKey     BIT STRING  }
    
    AlgorithmIdentifier  ::=  SEQUENCE  {
        algorithm               OBJECT IDENTIFIER,
        parameters              ANY DEFINED BY algorithm OPTIONAL  }
    

    https://www.rfc-editor.org/rfc/rfc3279#section-2.3.1 describes how RSA keys, in particular are to be encoded:

    The rsaEncryption OID is intended to be used in the algorithm field of a value of type AlgorithmIdentifier. The parameters field MUST have ASN.1 type NULL for this algorithm identifier.

    The RSA public key MUST be encoded using the ASN.1 type RSAPublicKey:

    RSAPublicKey ::= SEQUENCE {
        modulus            INTEGER,    -- n
        publicExponent     INTEGER  }  -- e
    

    The language behind these structures is ASN.1, defined by ITU X.680, and the way they get encoded to bytes is covered by the Distinguished Encoding Rules (DER) ruleset of ITU X.690.

    .NET actually gives you back a lot of these pieces, but you have to assemble them:

    private static string BuildPublicKeyPem(X509Certificate2 cert)
    {
        byte[] algOid;
    
        switch (cert.GetKeyAlgorithm())
        {
            case "1.2.840.113549.1.1.1":
                algOid = new byte[] { 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 };
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(cert), $"Need an OID lookup for {cert.GetKeyAlgorithm()}");
        }
    
        byte[] algParams = cert.GetKeyAlgorithmParameters();
        byte[] publicKey = WrapAsBitString(cert.GetPublicKey());
    
        byte[] algId = BuildSimpleDerSequence(algOid, algParams);
        byte[] spki = BuildSimpleDerSequence(algId, publicKey);
    
        return PemEncode(spki, "PUBLIC KEY");
    }
    
    private static string PemEncode(byte[] berData, string pemLabel)
    {
        StringBuilder builder = new StringBuilder();
        builder.Append("-----BEGIN ");
        builder.Append(pemLabel);
        builder.AppendLine("-----");
        builder.AppendLine(Convert.ToBase64String(berData, Base64FormattingOptions.InsertLineBreaks));
        builder.Append("-----END ");
        builder.Append(pemLabel);
        builder.AppendLine("-----");
    
        return builder.ToString();
    }
        
    private static byte[] BuildSimpleDerSequence(params byte[][] values)
    {
        int totalLength = values.Sum(v => v.Length);
        byte[] len = EncodeDerLength(totalLength);
        int offset = 1;
    
        byte[] seq = new byte[totalLength + len.Length + 1];
        seq[0] = 0x30;
    
        Buffer.BlockCopy(len, 0, seq, offset, len.Length);
        offset += len.Length;
    
        foreach (byte[] value in values)
        {
            Buffer.BlockCopy(value, 0, seq, offset, value.Length);
            offset += value.Length;
        }
    
        return seq;
    }
    
    private static byte[] WrapAsBitString(byte[] value)
    {
        byte[] len = EncodeDerLength(value.Length + 1);
        byte[] bitString = new byte[value.Length + len.Length + 2];
        bitString[0] = 0x03;
        Buffer.BlockCopy(len, 0, bitString, 1, len.Length);
        bitString[len.Length + 1] = 0x00;
        Buffer.BlockCopy(value, 0, bitString, len.Length + 2, value.Length);
        return bitString;
    }
    
    private static byte[] EncodeDerLength(int length)
    {
        if (length <= 0x7F)
        {
            return new byte[] { (byte)length };
        }
    
        if (length <= 0xFF)
        {
            return new byte[] { 0x81, (byte)length };
        }
    
        if (length <= 0xFFFF)
        {
            return new byte[]
            {
                0x82,
                (byte)(length >> 8),
                (byte)length,
            };
        }
    
        if (length <= 0xFFFFFF)
        {
            return new byte[]
            {
                0x83,
                (byte)(length >> 16),
                (byte)(length >> 8),
                (byte)length,
            };
        }
    
        return new byte[]
        {
            0x84,
            (byte)(length >> 24),
            (byte)(length >> 16),
            (byte)(length >> 8),
            (byte)length,
        };
    }
    

    DSA and ECDSA keys have more complex values for AlgorithmIdentifier.parameters, but X509Certificate's GetKeyAlgorithmParameters() happens to give them back correctly formatted, so you would just need to write down their OID (string) lookup key and their OID (byte[]) encoded value in the switch statement.

    My SEQUENCE and BIT STRING builders can definitely be more efficient (oh, look at all those poor arrays), but this would suffice for something that isn't perf-critical.

    To check your results, you can paste the output to openssl rsa -pubin -text -noout, and if it prints anything other than an error you've made a legally encoded "PUBLIC KEY" encoding for an RSA key.