Windows 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:

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 use RSA PUBLIC KEY.
  • For the DER-encoded X.509 SubjectPublicKeyInfo structure returned by AsymmetricAlgorithm.ExportSubjectPublicKeyInfo, we have to use PUBLIC 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:

  1. Obtain the CSP Public key blob for the key.
  2. Call CryptEncodeObjectEx to convert the CSP Public Key blob into a a DER-encoded PKCS#1 RSAPublicKey structure.
  3. Wrap the DER-encoded PKCS#1 RSAPublicKey structure in a CERT_PUBLIC_KEY_INFO structure (which corresponds to the definition of SubjectPublicKeyInfo).
  4. 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.

Any opinions expressed on this blog are Johannes' own. Refer to the respective vendor’s product documentation for authoritative information.
« Back to home