Converting JWKS JSON to PEM using Python
Following on from my earlier exploration of JWKS (RFC7517), I found myself needing to convert the JWKS into PEM format.
This time I turned to Python with my preference of using uv with inline script metadata and created jwks-to-pem.py.
The really nice thing about inline script metadata is that we can use the cryptography package to do all the hard work with RSA and serialisation. We just have to remember that the base64 encoded values are base64 URL encoded and account for it.
As a single file python script, I make it executable with chmod +x jwks-to-pem.py and made it so that I can pipe the output of a curl call to it, or pass in a JSON file. I prefer to use the curl solution though with:
curl -s https://example.com/.well-known/jwks.json | jwks-to-pem.py
Example
Here’s an example from the University of Cambridge.
On the day I wrote this article, the JWKS looks like this:
$ curl -s https://api.apps.cam.ac.uk/oauth2/v1/.well-known/jwks.json { "keys": [ { "alg": "RS256", "e": "AQAB", "n": "6kKjjctVPalX0ypJ2irwog8xIXS9JTABqrSnK_n3YJ4q0aH2-1bjGbWz8p1CaCUqDxQSDuqzvOgMdNGvZrxlNJ-G8hfc39jrb_KnB0T3ZsuxFz6X0mDzmHhdiPjSDK3M0syC4qg5_PB7xwKail5VWOcY0SypIYCPD6Ct5DGnQ_XONGXIVG7eaJAHdxJp2BOz0n3BVEFnZUgM5JcfGrSFfGqb0ZotX2AblwjZKQc58E0EVVykJw8gxW1Bob8rbaVXlMHssfY-9Jx0zua7ZrjO5C4OMmt9J6zYbVnGVwf62ehGtcLSP6iCG4_XM2sAMQwqJnPBss0U9WwDERk17FMHvb_FBwxAFxRygd0DclWmQmCYr5uFYck57KGARtyoxrNNAf4AFUHuObjbV24TyInYEgMhKi3SAML_4ke3dbbG-mjchXPN9OqNd4fydnQIP39WFHmFNk_nIlqvYnALI4xPE-w09T9jCvjU8hYHHlVMRvRluBnUzJkFnxLse5W-agC6ITe3wYvKH7SHVp6MYQWVD_0I2rCLV4gqjSpXzKIMs5eejjTQQq0VYumgL_f1ETvzDoewzXLOC8GGu2LZDwDbP0ea6DchReWjZfj4nJx23uQyGAj1h_uPI1jCd9oeJhbN8jFz2ltYgXYBp51qdSzsbtdec9BPPBVeXjI--c0AWU8", "kid": "70e0ed3c", "kty": "RSA", "use": "sig" } ] }
They very kindly pretty-print it too!
We can then get the PEM version by piping to jwks-to-pem.py:
$ curl -s https://api.apps.cam.ac.uk/oauth2/v1/.well-known/jwks.json | jwks-to-pem.py # Key 0 (kid: 70e0ed3c) -----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6kKjjctVPalX0ypJ2irw og8xIXS9JTABqrSnK/n3YJ4q0aH2+1bjGbWz8p1CaCUqDxQSDuqzvOgMdNGvZrxl NJ+G8hfc39jrb/KnB0T3ZsuxFz6X0mDzmHhdiPjSDK3M0syC4qg5/PB7xwKail5V WOcY0SypIYCPD6Ct5DGnQ/XONGXIVG7eaJAHdxJp2BOz0n3BVEFnZUgM5JcfGrSF fGqb0ZotX2AblwjZKQc58E0EVVykJw8gxW1Bob8rbaVXlMHssfY+9Jx0zua7ZrjO 5C4OMmt9J6zYbVnGVwf62ehGtcLSP6iCG4/XM2sAMQwqJnPBss0U9WwDERk17FMH vb/FBwxAFxRygd0DclWmQmCYr5uFYck57KGARtyoxrNNAf4AFUHuObjbV24TyInY EgMhKi3SAML/4ke3dbbG+mjchXPN9OqNd4fydnQIP39WFHmFNk/nIlqvYnALI4xP E+w09T9jCvjU8hYHHlVMRvRluBnUzJkFnxLse5W+agC6ITe3wYvKH7SHVp6MYQWV D/0I2rCLV4gqjSpXzKIMs5eejjTQQq0VYumgL/f1ETvzDoewzXLOC8GGu2LZDwDb P0ea6DchReWjZfj4nJx23uQyGAj1h/uPI1jCd9oeJhbN8jFz2ltYgXYBp51qdSzs btdec9BPPBVeXjI++c0AWU8CAwEAAQ== -----END PUBLIC KEY-----
The script
This is the script in case anyone else finds it useful:
#!/usr/bin/env -S uv run --script --quiet
# /// script
# dependencies = [
# "cryptography",
# ]
# ///
"""Convert JWK keys to PEM format.
This script reads .well-known/jwks.json and outputs PEM encoded versions
of the public keys in that file.
Usage:
curl -s https://example.com/.well-known/jwks.json | jwks-to-pem.py
uv run jwks-to-pem.py jwks.json
uv run jwks-to-pem.py < jwks.json
Requirements:
- uv (https://github.com/astral-sh/uv)
- cryptography library
Author:
Rob Allen
Copyright 2025
License:
MIT License - https://opensource.org/licenses/MIT
"""
import json
import base64
import sys
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
def base64url_decode(data):
"""Decode base64url to bytes"""
# Add padding if needed
padding = 4 - len(data) % 4
if padding != 4:
data += '=' * padding
# Replace URL-safe chars
data = data.replace('-', '+').replace('_', '/')
# Decode
return base64.b64decode(data)
def jwk_to_pem(jwk_key):
"""Convert JWK to PEM format"""
if jwk_key['kty'] != 'RSA':
raise ValueError("Only RSA keys are supported")
# Decode the modulus (n) and exponent (e) to int
n = int.from_bytes(base64url_decode(jwk_key['n']), 'big')
e = int.from_bytes(base64url_decode(jwk_key['e']), 'big')
# Create RSA public key
public_key = rsa.RSAPublicNumbers(e, n).public_key()
# Serialize to PEM
pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
return pem.decode()
def main():
if len(sys.argv) > 2:
print("Usage: jwk_to_pem.py [jwks.json]")
print("If no file is provided, reads from stdin")
sys.exit(1)
if len(sys.argv) == 2 and sys.argv[1] != '-':
# Read from file
with open(sys.argv[1], 'r') as f:
jwks = json.load(f)
else:
# Read from stdin
jwks = json.load(sys.stdin)
# Convert each key
for i, key in enumerate(jwks['keys']):
kid = key.get('kid', f'key-{i}')
print(f"# Key {i} (kid: {kid})")
print(jwk_to_pem(key))
if __name__ == "__main__":
main()