Search code examples
cryptographyrsapseudocodepemjwk

How to convert JWK to PEM


This question is more generic rather than for a specific language, so I will explain my issue and what I have tried in pseudocode.

I am trying to generate a PEM public key from a JWK Set. The JWK includes the "e" (exponent) and the "n" (modulus) variables. My question is what would be the exact steps to convert this JWK to PEM without using any libraries and without the OpenSSL command-line tool.

Here is the JWK for reference:

{
  "kty": "RSA",
  "alg": "RS512",
  "kid": "26887d3ee3293c526c0e6dd05f122df53aa3f13d7dad06d25e266fa6f51db79fb52422aaf79f121476237e98dcd6640350fee47fec70e783544ec9a36e4605bc",
  "use": "sig",
  "n": "14m79mVwIE0JxQdKrgXVf7dVcBS90U0TvG7Yf7dG4NJocz1PNUrKrzGhe_FryOe0JahL_sjA2_rKw7NBCpuVx_zSPFRw6kqjewGicjXGus5Fmlf3zDuqwV4BWIFHyQexMPOly0agFfcM0M0MgBULXjINgBs9MwnRv7JVfRoGqXHsNM45djFDd3o4liu4LPlge_DquZUFLNu-BYAyAlWkz0H2TepZhGrN9VEPmxzQkNzXc1R4MpZvbxrRRgaAA2z094ik3hk86JhfyFq-LDcueZhtshmrYZ95LWgMlQ7PixkeK1HkeEYMt20lmNzR8B8KabimYmibxA4Ay9gpRwfp-Q",
  "e": "AQAB"
}

The bulk of my research originates from the node-jwk-to-pem library (which can be found here: https://github.com/Brightspace/node-jwk-to-pem) and a StackOverflow question which uses a JOSE library in PHP (which can be found here: How to convert a public key from a JWK into PEM for OpenSSL?)

From what I have figured out through reading the above mentioned libraries (plus a few other articles and questions that didn't quite mention the step-by-step process), I found that the first step would be to convert the modulus and exponents to integers (specifically BigInt). This usually results in the following:

n = 27209154847141595039206198313134388207882403216639061643382959557246344691110642716248339592972939239825436848454712213431917464875221502188500634554426063986720722251339335488215426908010590565758292169712942346285217861588132983738839891250753593240057639347814778952277719613395670962156961452389927728643840215833830614315091417876959205843957512422401240879135352731575182574836052718961865690645602829768565458494497550672252951063585023601307115444743487394113997186698238507983094748342588645472362960665610355698438390751920697759620235642103374737421940385132232531739910444003185620313592808726865629407737
e = 65537

Using this information, how would I generate the PEM public key from the exponent and modulus? Any pseudocode or suggestions would be incredible!


Solution

  • A RSA public key is defined by both the modulus n and the exponent e.

    You can obtain that information directly from a JWK of type RSA using the n and e fields:

    The "n" (modulus) parameter contains the modulus value for the RSA public key. It is represented as a Base64urlUInt-encoded value.

    The "e" (exponent) parameter contains the exponent value for the RSA public key. It is represented as a Base64urlUInt-encoded value.

    To create a PEM file that represent this public key information, you basically need to build its equivalent ASN.1 DER encoded representation, maybe with some preamble. In its original form, it is defined like this in RFC3447:

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

    ASN.1 define rules and types for representing arbitrary data structures, and it is widely used in cryptography for that purpose.

    ASN.1 only gives you a syntax, but the actual encoding of that information is performed using different encoding formats, especially DER.

    The way in which that encoding is performed is non trivial: Let's Encrypt provides an excellent article about the subject. The great lapo.it/asn1js site provides a wonderful tool for inspecting ASN.1 DER encoded information as well.

    Consider for instance the following modulus and exponent, extracted from a SO question mentioned below:

    SEQUENCE (2 elements)
       INTEGER (2048 bit): EB506399F5C612F5A67A09C1192B92FAB53DB28520D859CE0EF6B7D83D40AA1C1DCE2C0720D15A0F531595CAD81BA5D129F91CC6769719F1435872C4BCD0521150A0263B470066489B918BFCA03CE8A0E9FC2C0314C4B096EA30717C03C28CA29E678E63D78ACA1E9A63BDB1261EE7A0B041AB53746D68B57B68BEF37B71382838C95DA8557841A3CA58109F0B4F77A5E929B1A25DC2D6814C55DC0F81CD2F4E5DB95EE70C706FC02C4FCA358EA9A82D8043A47611195580F89458E3DAB5592DEFE06CDE1E516A6C61ED78C13977AE9660A9192CA75CD72967FD3AFAFA1F1A2FF6325A5064D847028F1E6B2329E8572F36E708A549DDA355FC74A32FDD8DBA65
       INTEGER (24 bit): 010001
    

    The corresponding ASN.1 DER encoding is the following:

    30 82 01 0A      ;sequence (0x10A bytes long)
       02 82 01 01   ;integer (0x101 bytes long)
          00 EB506399F5C612F5A67A09C1192B92FAB53DB28520D859CE0EF6B7D83D40AA1C1DCE2C0720D15A0F531595CAD81BA5D129F91CC6769719F1435872C4BCD0521150A0263B470066489B918BFCA03CE8A0E9FC2C0314C4B096EA30717C03C28CA29E678E63D78ACA1E9A63BDB1261EE7A0B041AB53746D68B57B68BEF37B71382838C95DA8557841A3CA58109F0B4F77A5E929B1A25DC2D6814C55DC0F81CD2F4E5DB95EE70C706FC02C4FCA358EA9A82D8043A47611195580F89458E3DAB5592DEFE06CDE1E516A6C61ED78C13977AE9660A9192CA75CD72967FD3AFAFA1F1A2FF6325A5064D847028F1E6B2329E8572F36E708A549DDA355FC74A32FDD8DBA65
       02 03         ;integer (3 bytes long)
          010001
    

    In ASN.1 DER, every element is encoded as type-length-value triplet.

    The type is identified by a tag, usually encoded with one byte.

    For example, 30 means a SEQUENCE whereas 02 corresponds to an INTEGER type.

    The above mentioned article provides a list of the different types defined by ASN.1 with the corresponding tag.

    The length is a bit tricky: it is indicated by a single byte if the associated value is up to 127 (0x7F) bytes long. If the number of bytes is greater than 127, then this first length byte specifies the number of bytes that actually define the length. Please, consider read the Length section of this article, I think it is very explanatory.

    Then, you include the actual value.

    In the example provided, 30 82 01 0A means:

    • 30: A SEQUENCE.
    • 82: the length byte. 0x82 & 0x7F -> 0x02 The actual value length is expressed in 2 bytes.
    • 01 0A: Actual value length, 266 bytes long.

    Now, the value 02 82 01 01 00 EB506399F5C612F5A67A09C119...:

    • 02: An INTEGER
    • 82: the length byte. Again, 0x82 & 0x7F -> 0x02. The actual value length is expressed in 2 bytes.
    • 01 01: Actual value length, 257 bytes long, a modulus of 2048 bits padded with a first byte 0x00 (see the Integer padding section in the aforementioned article).
    • 00 EB506399F5C612F5A67A09C119: padded modulus value.

    Finally, 02 03 010001:

    • 02: An INTEGER.
    • 03: The exponent value is 3 bytes long.
    • 010001: exponent value.

    This result should be then encoded as base64 and enclosed within special text delimiters, -----BEGIN RSA PUBLIC KEY----- and -----END RSA PUBLIC KEY-----, for instance.

    Please, be aware that you have several options to represent the key in PEM format, the one described above, PKCS#1, and the more generic X.509, suitable for the representation of keys in different formats (the one of the form -----BEGIN PUBLIC KEY-----). Consider read this wonderful SO question and related answer, it explains in great detail not only the RSA public key fundamentals, but the caveats of those different representations as well.

    Consider read as well this and especially this other SO questions, I think they can be of help also.