Correctly create RSACryptoServiceProvider from public key

后端 未结 3 744
谎友^
谎友^ 2020-11-29 10:24

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 wo

3条回答
  •  悲&欢浪女
    2020-11-29 10:39

    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>.

提交回复
热议问题