Correctly create RSACryptoServiceProvider from public key

DiskJunky picture DiskJunky · Jan 23, 2017 · Viewed 15.7k times · Source

I'm currently trying to create an RSACryptoServiceProvider object solely from a decoded PEM file. After several days of searching, I did manage to wrangle a working solution but it's not one that would be production ready.

In a nutshell, in order to create an RSACryptoServiceProvider object from the bytes that make up the public key in a PEM file, I must create the object specifying the keysize (currently 2048 using SHA256, specifically) and then importing a RSAParameters object with the Exponent and Modulus set. I'm doing this as so;

byte[] publicKeyBytes = Convert.FromBase64String(deserializedPublicKey.Replace("-----BEGIN PUBLIC KEY-----", "")
                                                                      .Replace("-----END PUBLIC KEY-----", ""));

// extract the modulus and exponent based on the key data
byte[] exponentData = new byte[3];
byte[] modulusData = new byte[256];
Array.Copy(publicKeyBytes, publicKeyBytes.Length - exponentData.Length, exponentData, 0, exponentData.Length);
Array.Copy(publicKeyBytes, 9, modulusData, 0, modulusData.Length);


// import the public key data (base RSA - works)
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(dwKeySize: 2048);
RSAParameters rsaParam = rsa.ExportParameters(false);
rsaParam.Modulus = modulusData;
rsaParam.Exponent = exponentData;
rsa.ImportParameters(rsaParam);

While this works, it's not viable to assume that the deserializedPublicKey will be exactly 270 bytes and that the modulus I need is found at position 9 and always be 256 bytes in length.

How do I change this to correctly pick out the Modulus and Exponent byte given a set of public key bytes? I've tried to make sense of the ASN.1 standard but with little luck finding what I need from it - the standard(s) are somewhat byzantine.

Any help is appreciated.

Answer

bartonjs picture bartonjs · Jan 24, 2017

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://tools.ietf.org/html/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
}