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):
- Starting in .NET 6 you can use
cert.PublicKey.ExportSubjectPublicKeyInfo()
to get the DER-encoded SubjectPublicKeyInfo.
- In .NET Core 3+/.NET 5+ you can use
cert.GetRSAPublicKey()?.ExportSubjectPublicKeyInfo()
(or whatever algorithm your key is)
- In .NET 5+ you can turn those answers into PEM with
PemEncoding.Write("PUBLIC KEY", spki)
- Regardless of your .NET/.NET Core/.NET Framework version you can use the
System.Formats.Asn1
package with AsnWriter
to avoid the BuildSimpleDerSequence
work (published 2020-11-09).
-- 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.