Correctly create RSACryptoServiceProvider from public key

After a lot of time, searching and bartonjs's outstanding response, the code to do this is actually straight forward in the end albeit a little unintuitive to anyone not familiar with the structure of a public key.

TL;DR Basically, if your public key is coming from a non-.NET source, this answer won't help as .NET doesn't provide a way to natively parse a correctly formed PEM. However, if the code that generated the PEM is .NET based, then this answer describes the creation of the public key-only PEM and how to load it back in.

A public key PEM can describe a variety of key types, not just RSA so rather than something like new RSACryptoServiceProvider(pemBytes), we have to parse the PEM based on its structure/syntax, ASN.1, and it then tells us if it's an RSA key (it could be a range of others). Knowing that;

const string rsaOid = "1.2.840.113549.1.1.1";   // found under System.Security.Cryptography.CngLightup.RsaOid but it's marked as private
Oid oid = new Oid(rsaOid);
AsnEncodedData keyValue = new AsnEncodedData(publicKeyBytes);           // see question
AsnEncodedData keyParam = new AsnEncodedData(new byte[] { 05, 00 });    // ASN.1 code for NULL
PublicKey pubKeyRdr = new PublicKey(oid, keyParam, keyValue);
var rsaCryptoServiceProvider = (RSACryptoServiceProvider)pubKeyRdr.Key;

NOTE: The above code is not production ready! You'll need to put appropriate guards around the object creation (e.g. the public key might not be RSA), the cast to RSACryptoServiceProvider, etc. The code sample here is short to illustrate that it can be done reasonably cleanly.

How did I get this? Spelunking down through the Cryptographic namespace in ILSpy, I had noticed AsnEncodedData which rang a bell with bartonjs's description. Doing more research, I happened upon this post (look familiar?). This was trying to determine the key size specifically but it creates the necessary RSACryptoServiceProvider along the way.

I'm leaving bartonjs's answer as Accepted, and rightly so. The code above is the result of that research and I'm leaving it here so that others looking to do the same can do so cleanly without any array copying hacks like I had in my OP.

Also, for decoding and testing purposes, you can check if your public key is parsable using the ASN.1 decoder here.

UPDATE

It's on the .NET roadmap to make this easier with ASN.1 parsing for Core >2.1.0.

UPDATE 2

There is now a private implementation in Core .NET 2.1.1. MS is dogfooding until satisfied all is well and we'll (hopefully) see the public API in a subsequent version.

UPDATE 3

As I found out via a question here, the above info is incomplete. What's missing is that the public key being loaded with this solution is one that was generated programmatically from a loaded public+private key pair. Once an RSACryptoServiceProvider is created from a key pair (not just the public key), you can export just the public bytes and encode them as a public key PEM. Doing so will be compatible with the solution here. What's with this?

Load the public + private keypair into an RSACryptoServiceProvider and then export it like so;

var cert = new X509Certificate2(keypairBytes, password,
                                X509KeyStorageFlags.Exportable 
                                | X509KeyStorageFlags.MachineKeySet);
var partialAsnBlockWithPublicKey = cert.GetPublicKey();

// export bytes to PEM format
var base64Encoded = Convert.ToBase64String(partialAsnBlockWithPublicKey, Base64FormattingOptions.InsertLineBreaks);
var pemHeader = "-----BEGIN PUBLIC KEY-----";
var pemFooter = "-----END PUBLIC KEY-----";
var pemFull = string.Format("{0}\r\n{1}\r\n{2}", pemHeader, base64Encoded, pemFooter);

If you create a PEM from this key, you'll be able to load it back in using the method described earlier. Why is this different? The call to cert.GetPublicKey() will actually return the ASN.1 block structure;

SEQUENCE(2 elem)
  INTEGER (2048 bit)
  INTEGER 65537

This is actually an incomplete DER blob but one which .NET can decode (full ASN.1 parsing and generation is not supported by .NET at time of writing - https://github.com/dotnet/designs/issues/11).

A correct DER (ASN.1) encoded public key bytes has the following structure;

SEQUENCE(2 elem)
  SEQUENCE(2 elem)
     OBJECT IDENTIFIER   "1.2.840.113549.1.1.1" - rsaEncryption(PKCS #1)
     NULL
BIT STRING(1 elem)
  SEQUENCE(2 elem)
    INTEGER (2048 bit)
    INTEGER 65537

OK, so the above gets you a public key (kind of) that you can load. It's ugly and technically incomplete but does use .NET's own output from RSACryptoServiceProvider.GetPublicCert() method. The constructor can use those same bytes when loading just the public key later. Unfortunately, it's not a true, fully-formed PEM. We're still awaiting MS's ASN.1 parser in .NET Core 3.0>.


You don't need to export existing parameters then re-import over top of them. That forces your machine to generate an RSA key then throw it away. So specifying a keysize to the constructor doesn't matter (if you don't use the key it won't generate one... usually).

The public key file is a DER encoded blob.

-----BEGIN PUBLIC KEY-----
MIGgMA0GCSqGSIb3DQEBAQUAA4GOADCBigKBggC8rLGlNJ17NaWArDs5mOsV6/kA
7LMpvx91cXoAshmcihjXkbWSt+xSvVry2w07Y18FlXU9/3unyYctv34yJt70SgfK
Vo0QF5ksK0G/5ew1cIJM8fSxWRn+1RP9pWIEryA0otCP8EwsyknRaPoD+i+jL8zT
SEwV8KLlRnx2/HYLVQkCAwEAAQ==
-----END PUBLIC KEY-----

If you take the contents inside the PEM armor, it's a Base64-encoded byte array.

30 81 A0 30 0D 06 09 2A 86 48 86 F7 0D 01 01 01 
05 00 03 81 8E 00 30 81 8A 02 81 82 00 BC AC B1 
A5 34 9D 7B 35 A5 80 AC 3B 39 98 EB 15 EB F9 00 
EC B3 29 BF 1F 75 71 7A 00 B2 19 9C 8A 18 D7 91 
B5 92 B7 EC 52 BD 5A F2 DB 0D 3B 63 5F 05 95 75 
3D FF 7B A7 C9 87 2D BF 7E 32 26 DE F4 4A 07 CA 
56 8D 10 17 99 2C 2B 41 BF E5 EC 35 70 82 4C F1 
F4 B1 59 19 FE D5 13 FD A5 62 04 AF 20 34 A2 D0 
8F F0 4C 2C CA 49 D1 68 FA 03 FA 2F A3 2F CC D3 
48 4C 15 F0 A2 E5 46 7C 76 FC 76 0B 55 09 02 03 
01 00 01 

ITU-T X.690 defines how to read things encoded under Basic Encoding Rules (BER), Canonical Encoding Rules (CER, which I've never seen explicitly used), and Distinguished Encoding Rules (DER). For the most part CER restricts BER and DER restricts CER, making DER the easiest to read. (ITU-T X.680 describes Abstract Syntax Notation One (ASN.1), which is the grammar that DER is a binary encoding for)

We can do a bit of parsing now:

30

This identifies a SEQUENCE (0x10) with the CONSTRUCTED bit set (0x20), which means that it contains other DER/tagged values. (SEQUENCE is always CONSTRUCTED in DER)

81 A0

This next part is a length. Since it has the high bit set (> 0x7F) the first byte is a "length length" value. It indicates that the true length is encoded in the next 1 byte(s) (lengthLength & 0x7F). Therefore the contents of this SEQUENCE are 160 bytes total. (In this case, "the rest of the data", but the SEQUENCE could have been contained within something else). So let's read the contents:

30 0D

We see our CONSTRUCTED SEQUENCE again (0x30), with a length value of 0x0D, so we have a 13 byte payload.

06 09 2A 86 48 86 F7 0D 01 01 01 05 00 

The 06 is OBJECT IDENTIFIER, with a 0x09 byte payload. OID has a slightly non-intuitive encoding, but this one is equivalent to the text representation 1.2.840.113549.1.1.1, which is id-rsaEncryption (http://www.oid-info.com/get/1.2.840.113549.1.1.1).

This still leaves us with two bytes (05 00) which we see is a NULL (with a 0 byte payload, because, well, it's NULL).

So so far we have

SEQUENCE
  SEQUENCE
    OID 1.2.840.113549.1.1.1
    NULL
  143 more bytes.

Continuing on:

03 81 8E 00

The 03 means BIT STRING. BIT STRING is encoded as [tag] [length] [number of unused bits]. The unused bits is essentially always zero. So this is a sequence of bits, 0x8E bytes long, and all of them are used.

Technically we should stop there, because CONSTRUCTED wasn't set. But since we happen to know the format of this structure, we treat the value as if the CONSTRUCTED bit was set anyways:

30 81 8A

Here's our friend CONSTRUCTED SEQUENCE again, 0x8A payload bytes, which conveniently corresponds to "everything that's left".

02 81 82

02 identifies an INTEGER, and this one has 0x82 payload bytes:

00 BC AC B1 A5 34 9D 7B 35 A5 80 AC 3B 39 98 EB 
15 EB F9 00 EC B3 29 BF 1F 75 71 7A 00 B2 19 9C 
8A 18 D7 91 B5 92 B7 EC 52 BD 5A F2 DB 0D 3B 63 
5F 05 95 75 3D FF 7B A7 C9 87 2D BF 7E 32 26 DE 
F4 4A 07 CA 56 8D 10 17 99 2C 2B 41 BF E5 EC 35 
70 82 4C F1 F4 B1 59 19 FE D5 13 FD A5 62 04 AF 
20 34 A2 D0 8F F0 4C 2C CA 49 D1 68 FA 03 FA 2F 
A3 2F CC D3 48 4C 15 F0 A2 E5 46 7C 76 FC 76 0B 
55 09 

The leading 0x00 would be a violation of DER, except the next byte has the high bit set. This means that the 0x00 was there to keep the sign bit from being set, making this a positive number.

02 03 01 00 01

Another INTEGER, 3 bytes, value 01 00 01. And we're done.

SEQUENCE
  SEQUENCE
    OID 1.2.840.113549.1.1.1
    NULL
  BIT STRING
    SEQUENCE
      INTEGER 00 BC AC ... 0B 55 09
      INTEGER 01 00 01

Harvesting https://www.rfc-editor.org/rfc/rfc5280 we see that this looks a lot like a SubjectPublicKeyInfo structure:

SubjectPublicKeyInfo  ::=  SEQUENCE  {
  algorithm            AlgorithmIdentifier,
  subjectPublicKey     BIT STRING  }

AlgorithmIdentifier  ::=  SEQUENCE  {
  algorithm               OBJECT IDENTIFIER,
  parameters              ANY DEFINED BY algorithm OPTIONAL  }
                            -- contains a value of the type
                            -- registered for use with the
                            -- algorithm object identifier value

Of course, it doesn't know what the RSA public key format is. But the oid-info site told us to check out RFC 2313, where we see

An RSA public key shall have ASN.1 type RSAPublicKey:

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

So that says that the first INTEGER we read is the Modulus value, and the second is (public)Exponent.

The DER encoding is big-endian, which is also the RSAParameters encoding, but for RSAParameters you need to remove leading 0x00 values from Modulus.

While that isn't as easy as giving you the code to do it, it should be fairly straightforward to write a parser for RSA keys given this information. I'd recommend that you write it as internal static RSAParameters ReadRsaPublicKey(...), and then you just need to do

RSAParameters rsaParameters = ReadRsaPublicKey(...);

using (RSA rsa = RSA.Create())
{
    rsa.ImportParameters(rsaParameters);
    // things you want to do with the key go here
}