How to convert JWK to PEM
Asked Answered
D

2

6

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!

Department answered 3/2, 2022 at 15:32 Comment(1)
I think you can get pretty much of the logic here github.com/Spomky-Labs/jose/blob/master/src/KeyConverter/…, just check the loadJwk then the toPem functionsSelmner
N
7

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.

Nappy answered 12/2, 2022 at 22:34 Comment(3)
This is the answer I've been somewhat looking for, though it still doesn't go over the process in any pseudocode or suggestive or collaborative format. - The first SO question doesn't go over how you convert the hexadecimal to ASN.1 format (nor do you cover this in your question) and neither do the other 2 questions you linked. This is the step in which I get stuck, but it is the best answer on this post (and actually the only answer). I will mark is as an answer, but I do not feel like it really targetted what I was asking to deserve the bounty.Department
Hi @GenericNerd. Thank you very much for the feedback. I totally agree with you, sorry. I tried to clarify that point in the answer. Please, consider read the updated version. I hope it helps.Nappy
See baptistout.net/posts/… for step-by-step openssh commands to convert JWK to PEM.Convert
K
6

I made a website to serve this specific purpose of converting PEM assets to JWK and back: https://jwkset.com/generate

You can see a screenshot of the relevant page below. It's open-source and easy to self-host if you are working with private keys.

docker run --rm -p 8080:8080 micahparks/jwksetcom

This website is apart of the https://github.com/MicahParks/jwkset open source project, which is a Golang package. It also publishes a CLI tool that will accomplish this. More on that below the screenshot of the website.

screenshot of jwkset.com

The CLI tool jwksetinfer can be used to convert one or more PEM encoded assets into a single JWK Set.

Install via the Golang toolchain:

go install github.com/MicahParks/jwkset/cmd/jwksetinfer@latest

Usage:

jwksetinfer mykey.pem mycert.crt

If you're looking for a Golang code example, you can observe this test, which covers the following PEM encoded X.509 certificates, SEC 1, PKCS #1, PKCS #8, and PKIX.

Kathleenkathlene answered 12/12, 2023 at 14:4 Comment(2)
That's absolutely incredible! Thank you so much for creating this.Department
Glad you find it useful :)Kathleenkathlene

© 2022 - 2024 — McMap. All rights reserved.