Importing RSA public keys in downlevel .NET and .NET Framework versions
In .NET 5 and 6, we can use RSA.ImportFromPem to import a PEM-formatted RSA public key . Older .NET Core versions and .NET Framework don’t offer that functionality – but with some extension methods and a little help from CryptoAPI, we can fill that gap.
.NET Core 3.1 and below
Although .NET Core 3.1 and older versions don’t support RSA.ImportFromPem
,
they do support 2 other useful methods:
RSA.ImportRSAPublicKey
, which imports a public key from a DER-encoded PKCS#1 RSAPublicKey structureRSA.ImportSubjectPublicKeyInfo
, which imports a public key from a DER-encoded X.509 SubjectPublicKeyInfo structure
Recall that the header of a PEM file indicates whether the file contains a RSAPublicKey
structure, a SubjectPublicKeyInfo
structure, or something else entirely. Therefore, we can reimplement ImportFromPem
by:
- Parsing the PEM header/footer to determine whether we’re dealing with a PKCS#1
RSAPublicKey
structure (identified byBEGIN RSA PUBLIC KEY
) or a X.509SubjectPublicKeyInfo
structure (identified byBEGIN PUBLIC KEY
). - Base64-decoding the body to get the DER blob.
- Importing the DER blob by using
RSA.ImportRSAPublicKey
orRSA.ImportSubjectPublicKeyInfo
.
In C#, this looks like:
public static class RSAExtensions
{
private const string RsaPublickeyPemHeader = "-----BEGIN RSA PUBLIC KEY-----";
private const string RsaPublickeyPemFooter = "-----END RSA PUBLIC KEY-----";
private const string SubjectPublicKeyInfoPemHeader = "-----BEGIN PUBLIC KEY-----";
private const string SubjectPublicKeyInfoPemFooter = "-----END PUBLIC KEY-----";
#if !(NET5_0 || NET5_0_OR_GREATER)
//
// Add missing method.
//
public static void ImportFromPem(
this RSA key,
string source)
=> ImportFromPem(key, source, out var _);
#endif
public static void ImportFromPem(
this RSA key,
string source,
out RsaPublicKeyFormat format)
{
source = source.Trim();
//
// Inspect header to determine format.
//
if (source.StartsWith(SubjectPublicKeyInfoPemHeader) &&
source.EndsWith(SubjectPublicKeyInfoPemFooter))
{
format = RsaPublicKeyFormat.SubjectPublicKeyInfo;
}
else if (source.StartsWith(RsaPublickeyPemHeader) &&
source.EndsWith(RsaPublickeyPemFooter))
{
format = RsaPublicKeyFormat.RsaPublicKey;
}
else
{
throw new FormatException("Missing Public key header/footer");
}
//
// Decode body to get DER blob.
//
var der = Convert.FromBase64String(string.Concat(
source
.Split('\n')
.Select(s => s.Trim())
.Where(line => !line.StartsWith("-----"))));
if (format == RsaPublicKeyFormat.RsaPublicKey)
{
key.ImportRSAPublicKey(der, out var _);
}
else
{
key.ImportSubjectPublicKeyInfo(der, out var _);
}
}
...
}
public enum RsaPublicKeyFormat
{
RsaPublicKey,
SubjectPublicKeyInfo
}
.NET Framework
.NET Framework not only lacks ImportFromPem
, but also RSA.ImportRSAPublicKey
and RSA.ImportSubjectPublicKeyInfo
. So we have a little more work to do.
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? With a bit of help from CryptoAPI, we can reimplement
RSA.ImportRSAPublicKey
and RSA.ImportSubjectPublicKeyInfo
on .NET Framework.
Reimplementing RSA.ImportRSAPublicKey
RSA.ImportRSAPublicKey
takes a DER-encoded PKCS#1 RSAPublicKey structure as input.
The first thing we need to do is convert that DER blob into a CSP Public Key blob.
To do this, we can call CryptDecodeObjectEx
and pass the flag RSA_CSP_PUBLICKEYBLOB
.
The resulting CSP public key blob is understood by both CryptoAPI and CNG, but the process
to import it differs slightly. Recall that RSA
is an abstract base class, so we need to
check whether we’re really dealing with a CNG key (RSACng
) or Crypto API key (RSACryptoServiceProvider
):
- If it’s a CryptoAPI key, we can use
RSACryptoServiceProvider.ImportCspBlob
to import the blob, effectively overriding the existing key. RSACng
doesn’t provide an equivalent method, but we can work around that by first converting the blob into anRSAParameters
object, and then importing that.
In C#, this looks like:
public static class RSAExtensions
{
...
#if NET40_OR_GREATER
private static void ImportCspBlob(
RSA key,
byte[] cspBlob)
{
if (key is RSACng)
{
//
// RSACng.Key is private, so we can't import into
// an existing key directly. But we can do so
// indirectly.
//
var importedKey = CngKey.Import(cspBlob, CngKeyBlobFormat.GenericPublicBlob);
var importedKeyParameters = new RSACng(importedKey).ExportParameters(false);
key.ImportParameters(importedKeyParameters);
}
else if (key is RSACryptoServiceProvider cryptoApiKey)
{
cryptoApiKey.ImportCspBlob(cspBlob);
}
else
{
throw new ArgumentException("Unrecognized key type");
}
}
public static void ImportRSAPublicKey(
this RSA key,
byte[] derBlob,
out int bytesRead)
{
using (var derBlobHandle = LocalAllocHandle.Alloc(derBlob.Length))
{
Marshal.Copy(
derBlob,
0,
derBlobHandle.DangerousGetHandle(),
derBlob.Length);
//
// Decode RSA PublicKey DER -> CSP blob.
//
if (UnsafeNativeMethods.CryptDecodeObjectEx(
UnsafeNativeMethods.X509_ASN_ENCODING |
UnsafeNativeMethods.PKCS_7_ASN_ENCODING,
UnsafeNativeMethods.RSA_CSP_PUBLICKEYBLOB,
derBlobHandle.DangerousGetHandle(),
(uint)derBlob.Length,
UnsafeNativeMethods.CRYPT_DECODE_ALLOC_FLAG,
IntPtr.Zero,
out var keyBlobHandle,
out var keyBlobSize))
{
using (keyBlobHandle)
{
var keyBlobBytes = new byte[keyBlobSize];
Marshal.Copy(
keyBlobHandle.DangerousGetHandle(),
keyBlobBytes,
0,
(int)keyBlobSize);
bytesRead = derBlob.Length;
ImportCspBlob(key, keyBlobBytes);
}
}
else
{
throw new CryptographicException(
"Failed to decode DER blob",
new Win32Exception());
}
}
}
#endif
...
}
Note that the code uses a custom helper class LocalAllocHandle
to handle memory allocations.
Reimplementing RSA.ImportSubjectPublicKeyInfo
As its name suggests, RSA.ImportSubjectPublicKeyInfo
takes a DER-encoded
X.509 SubjectPublicKeyInfo structure as input. To reimplement the method,
we again have to convert that DER blob into a CSP Public Key blob first:
- Call CryptDecodeObjectEx
with flag
X509_PUBLIC_KEY_INFO
to decode the DER blob into a CERT_PUBLIC_KEY_INFO structure. - Verify that the structure really contains an RSA public key (and not something else,
like an ECC key) by verifying that
CERT_PUBLIC_KEY_INFO.Algorithm.pszObjId
contains the RSA OID. If it does, we know thatCERT_PUBLIC_KEY_INFO.PublicKey
contains a DER-encoded PKCS#1 RSAPublicKey structure. - Extract the RSAPublicKey from
CERT_PUBLIC_KEY_INFO.PublicKey
and call CryptDecodeObjectEx with flagRSA_CSP_PUBLICKEYBLOB
to decode it into a public key blob that can be imported into CNG or CryptoAPI.
Once we have the CSP Public Key blob, we can import it using the ImportCspBlob
method we looked at before.
public static class RSAExtensions
{
private const string RsaOid = "1.2.840.113549.1.1.1";
...
#if NET40_OR_GREATER
public static void ImportSubjectPublicKeyInfo(
this RSA key,
byte[] certKeyInfoDer,
out int bytesRead)
{
using (var certKeyInfoDerHandle = LocalAllocHandle.Alloc(certKeyInfoDer.Length))
{
Marshal.Copy(
certKeyInfoDer,
0,
certKeyInfoDerHandle.DangerousGetHandle(),
certKeyInfoDer.Length);
//
// Decode DER -> CERT_PUBLIC_KEY_INFO.
//
if (UnsafeNativeMethods.CryptDecodeObjectEx(
UnsafeNativeMethods.X509_ASN_ENCODING |
UnsafeNativeMethods.PKCS_7_ASN_ENCODING,
UnsafeNativeMethods.X509_PUBLIC_KEY_INFO,
certKeyInfoDerHandle.DangerousGetHandle(),
(uint)certKeyInfoDer.Length,
UnsafeNativeMethods.CRYPT_DECODE_ALLOC_FLAG,
IntPtr.Zero,
out var certKeyInfoHandle,
out var certKeyInfoSize))
{
using (certKeyInfoHandle)
{
//
// Check that the CERT_PUBLIC_KEY_INFO contains an RSA public key.
//
var certInfo = Marshal.PtrToStructure<UnsafeNativeMethods.CERT_PUBLIC_KEY_INFO>(
certKeyInfoHandle.DangerousGetHandle());
if (certInfo.Algorithm.pszObjId != RsaOid)
{
throw new CryptographicException("Not an RSA public key");
}
//
// Decode the RSA public key -> CSP blob.
//
if (UnsafeNativeMethods.CryptDecodeObjectEx(
UnsafeNativeMethods.X509_ASN_ENCODING |
UnsafeNativeMethods.PKCS_7_ASN_ENCODING,
UnsafeNativeMethods.RSA_CSP_PUBLICKEYBLOB,
certInfo.PublicKey.pbData,
certInfo.PublicKey.cbData,
UnsafeNativeMethods.CRYPT_DECODE_ALLOC_FLAG,
IntPtr.Zero,
out var cspKeyBlob,
out var cspKeyBlobSize))
{
using (cspKeyBlob)
{
var keyBlobBytes = new byte[cspKeyBlobSize];
Marshal.Copy(
cspKeyBlob.DangerousGetHandle(),
keyBlobBytes,
0,
(int)cspKeyBlobSize);
bytesRead = certKeyInfoDer.Length;
ImportCspBlob(key, keyBlobBytes);
}
}
else
{
throw new CryptographicException(
"Failed to decode RSA public key from CERT_PUBLIC_KEY_INFO",
new Win32Exception());
}
}
}
else
{
throw new CryptographicException(
"Failed to decode DER blob into CERT_PUBLIC_KEY_INFO",
new Win32Exception());
}
}
}
#endif
}
With these extension methods in place, we can now use RSA.ImportFromPem
on
downlevel .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