Exporting RSA public keys in .NET and .NET Framework
.NET 5 and 6 lets us import RSA public keys in PEM format by using
RSA.ImportFromPem
.
Somewhat surprisingly, there’s no corresponding ExportToPem method that would let us export RSA keys in PEM format.
But with some extension methods and a little help from CryptoAPI, we can fill that gap.
.NET Core 3.0 and later
Although .NET Core 3.0 and newer versions don’t provide a RSA.ExportToPem
method, they do provide 2 other useful methods:
- RSA.ExportRSAPublicKey, which exports a public key as a DER-encoded PKCS#1 RSAPublicKey structure.
- AsymmetricAlgorithm.ExportSubjectPublicKeyInfo, which exports a public key as a DER-encoded X.509 SubjectPublicKeyInfo structure.
In a previous post, we saw that PEM, or Privacy Enhanced Mail, is an envelope format and that to encode a piece of binary data as PEM, we merely have to:
- Base64-encode the data
- Add a header (
-----BEGIN [LABEL]-----
) and footer (-----END [LABEL]-----
)
The right label to use in the header and footer is defined in RFC 7468:
- For the DER-encoded PKCS#1 RSAPublicKey structure returned by
RSA.ExportRSAPublicKey
, we have to useRSA PUBLIC KEY
. - For the DER-encoded X.509 SubjectPublicKeyInfo structure returned by
AsymmetricAlgorithm.ExportSubjectPublicKeyInfo
, we have to usePUBLIC KEY
.
Implementing a RSA.ExportToPem
method is therefore straightforward:
public static class RSAExtensions
{
public static string ExportToPem(
this RSA key,
RsaPublicKeyFormat format)
{
var buffer = new StringBuilder();
if (format == RsaPublicKeyFormat.RsaPublicKey)
{
buffer.AppendLine(RsaPublickeyPemHeader);
buffer.AppendLine(Convert.ToBase64String(
key.ExportRSAPublicKey(),
Base64FormattingOptions.InsertLineBreaks));
buffer.AppendLine(RsaPublickeyPemFooter);
}
else if (format == RsaPublicKeyFormat.SubjectPublicKeyInfo)
{
buffer.AppendLine(SubjectPublicKeyInfoPemHeader);
buffer.AppendLine(Convert.ToBase64String(
key.ExportSubjectPublicKeyInfo(),
Base64FormattingOptions.InsertLineBreaks));
buffer.AppendLine(SubjectPublicKeyInfoPemFooter);
}
else
{
throw new ArgumentException(nameof(format));
}
return buffer.ToString();
}
}
public enum RsaPublicKeyFormat
{
RsaPublicKey,
SubjectPublicKeyInfo
}
.NET Framework
But ff we’re using .NET Framework, things aren’t quite as straightforward as .NET Framework lacks both
ExportRSAPublicKey
and ExportSubjectPublicKeyInfo
.
To compensate for these missing methods, we could turn to BouncyCastle.NET. But why add another dependency if we can live off the land by P/Invoking into CryptoAPI instead?
Reimplementing ExportRSAPublicKey
To reimplement ExportRSAPublicKey
, we first need the CSP Public key blob for the key. If the key is
CNG-backed (RSACng
), we can get the key blob by calling CngKey.Export
with the CngKeyBlobFormat.GenericPublicBlob
flag. If we’re dealing with a CryptoAPI-backed key
(RSACryptoServiceProvider
), we have to call ExportCspBlob(false)
instead.
The next step is to convert the CSP Public Key blob into a a DER-encoded PKCS#1 RSAPublicKey structure.
For that, we pass the CSP Public Key blob to CryptEncodeObjectEx
and specify the flag CNG_RSA_PUBLIC_KEY_BLOB
(if it’s a CNG key) or RSA_CSP_PUBLICKEYBLOB
(if it’s a CryptoAPI key).
In C# this looks like:
public static class RSAExtensions
{
....
#if NET40_OR_GREATER
private static byte[] ExportCspBlob(
RSA key,
out uint cspBlobType)
{
//
// CNG and CryptoAPI use different key blob formats, and expose
// different APIs to create them.
//
if (key is RSACng cngKey)
{
cspBlobType = UnsafeNativeMethods.CNG_RSA_PUBLIC_KEY_BLOB;
return cngKey.Key.Export(CngKeyBlobFormat.GenericPublicBlob);
}
else if (key is RSACryptoServiceProvider cryptoApiKey)
{
cspBlobType = UnsafeNativeMethods.RSA_CSP_PUBLICKEYBLOB;
return cryptoApiKey.ExportCspBlob(false);
}
else
{
throw new ArgumentException("Unrecognized key type");
}
}
public static byte[] ExportRSAPublicKey(this RSA key)
{
byte[] cspBlob = ExportCspBlob(key, out uint cspBlobType);
//
// Decode CSP blob -> RSA PublicKey DER.
//
using (var cspBlobHandle = LocalAllocHandle.Alloc(cspBlob.Length))
{
Marshal.Copy(
cspBlob,
0,
cspBlobHandle.DangerousGetHandle(),
cspBlob.Length);
if (UnsafeNativeMethods.CryptEncodeObjectEx(
UnsafeNativeMethods.X509_ASN_ENCODING |
UnsafeNativeMethods.PKCS_7_ASN_ENCODING,
cspBlobType,
cspBlobHandle.DangerousGetHandle(),
UnsafeNativeMethods.CRYPT_DECODE_ALLOC_FLAG,
IntPtr.Zero,
out var derBlobHandle,
out uint derBlobSize))
{
using (derBlobHandle)
{
var derBlob = new byte[derBlobSize];
Marshal.Copy(
derBlobHandle.DangerousGetHandle(),
derBlob,
0,
(int)derBlobSize);
return derBlob;
}
}
else
{
throw new CryptographicException(
"Failed to encode CSP blob",
new Win32Exception());
}
}
}
#endif
...
}
Note that the code uses a custom helper class LocalAllocHandle
to handle memory allocations.
Reimplementing ExportSubjectPublicKeyInfo
In a previous post we looked at the definition of the X.509 SubjectPublicKeyInfo structure and saw that it is a discriminated union:
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING }
We already know how to create a DER-encoded DER-encoded PKCS#1 RSAPublicKey structure. All it takes
to turn this into a X.509 SubjectPublicKeyInfo is wrapping it by a SubjectPublicKeyInfo
structure.
So what we need to do is:
- Obtain the CSP Public key blob for the key.
- Call CryptEncodeObjectEx to convert the CSP Public Key blob into a a DER-encoded PKCS#1 RSAPublicKey structure.
- Wrap the DER-encoded PKCS#1 RSAPublicKey structure in a
CERT_PUBLIC_KEY_INFO
structure (which corresponds to the definition ofSubjectPublicKeyInfo
). - Call CryptEncodeObjectEx
once again to encode the
CERT_PUBLIC_KEY_INFO
structure as a DER blob.
In C#, this looks like:
public static class RSAExtensions
{
....
#if NET40_OR_GREATER
public static byte[] ExportSubjectPublicKeyInfo(this RSA key)
{
byte[] cspBlob = ExportCspBlob(key, out uint cspBlobType);
//
// Decode CSP blob -> RSA PublicKey DER.
//
using (var cspBlobHandle = LocalAllocHandle.Alloc(cspBlob.Length))
{
Marshal.Copy(
cspBlob,
0,
cspBlobHandle.DangerousGetHandle(),
cspBlob.Length);
if (UnsafeNativeMethods.CryptEncodeObjectEx(
UnsafeNativeMethods.X509_ASN_ENCODING |
UnsafeNativeMethods.PKCS_7_ASN_ENCODING,
cspBlobType,
cspBlobHandle.DangerousGetHandle(),
UnsafeNativeMethods.CRYPT_DECODE_ALLOC_FLAG,
IntPtr.Zero,
out var rsaDerHandle,
out uint rsaDerSize))
{
using (rsaDerHandle)
{
//
// Wrap the RSA PublicKey DER blob into a CERT_PUBLIC_KEY_INFO.
//
var certKeyInfo = new UnsafeNativeMethods.CERT_PUBLIC_KEY_INFO()
{
Algorithm = new UnsafeNativeMethods.CRYPT_ALGORITHM_IDENTIFIER()
{
pszObjId = RsaOid
},
PublicKey = new UnsafeNativeMethods.CRYPT_BIT_BLOB()
{
pbData = rsaDerHandle.DangerousGetHandle(),
cbData = rsaDerSize
}
};
//
// Encode CERT_PUBLIC_KEY_INFO -> DER.
//
using (var certKeyInfoHandle = LocalAllocHandle.Alloc(
Marshal.SizeOf<UnsafeNativeMethods.CERT_PUBLIC_KEY_INFO>()))
{
Marshal.StructureToPtr(
certKeyInfo,
certKeyInfoHandle.DangerousGetHandle(),
false);
if (UnsafeNativeMethods.CryptEncodeObjectEx(
UnsafeNativeMethods.X509_ASN_ENCODING |
UnsafeNativeMethods.PKCS_7_ASN_ENCODING,
UnsafeNativeMethods.X509_PUBLIC_KEY_INFO,
certKeyInfoHandle.DangerousGetHandle(),
UnsafeNativeMethods.CRYPT_DECODE_ALLOC_FLAG,
IntPtr.Zero,
out var certKeyInfoDerHandle,
out uint certKeyInfoDerSize))
{
using (certKeyInfoDerHandle)
{
var certKeyInfoDer = new byte[certKeyInfoDerSize];
Marshal.Copy(
certKeyInfoDerHandle.DangerousGetHandle(),
certKeyInfoDer,
0,
(int)certKeyInfoDerSize);
return certKeyInfoDer;
}
}
else
{
throw new CryptographicException(
"Failed to encode CERT_PUBLIC_KEY_INFO",
new Win32Exception());
}
}
}
}
else
{
throw new CryptographicException(
"Failed to encode CSP blob",
new Win32Exception());
}
}
}
#endif
...
}
And that’s all we need.
With these extension methods in place, we can now use RSA.ExportToPem
on
all .NET and .NET Framework versions.
You can find the complete source code on GitHub.
Alternatively, you can use the Jpki.Security.Cryptography
library which also
backports a number of other “missing” PKI-related methods to .NET 4.x