This project has been superseded by a new tool I've written called lokey: https://github.com/jpf/lokey
lokey does everything that this code does, and more.
For example, to convert a JWKs result to PEM with lokey, you'd do one of the following:
$ lokey fetch jwk example.okta.com | lokey to pem
$ lokey fetch jwk login.salesforce.com | lokey to pem
$ lokey fetch jwk accounts.google.com | lokey to pem
Note that With lokey, you could also convert to other RSA key formats:
$ lokey fetch jwk example.okta.com | lokey to pem
$ lokey fetch jwk example.okta.com | lokey to ssh
$ lokey fetch jwk example.okta.com | lokey to pgp --name "John Doe" --email "john.doe@example.com"
I'm keeping this project around because of how thoroughly it is documented in the hope that it'll be useful for somebody wanting to learn about converting between JWK and PEM formats.
However, I will not be doing any further development on this project and encourage you to use lokey instead.
Joël Franusic November 6th, 2017
This is a Python script that fetches JWKS results, and for each jwk, uses the modulus and exponent to generate a PEM encoded public key, suitable for use in tools like jwt.io
Here is an example of using this tool to get the PEM encoded public keys for the "example.okta.com" Okta org:
./jwks_to_pem.py --org example.okta.com
Fetching JWKS from example.okta.com
PEM for KID 're7eOFV6SiygSbCyYHGGdERFCJ_EoNpi9Duv0FIxllo'
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgVdVgO4RogxtWt4XN2vO
9SG3Ekt6Qh+k6Io28dTNjEWuNxCYCtQI2dFtFs2y7OyLxQ2e3491XTDVRxtUx/Kl
RhQCcGDtLM5vTRWtGo39heg9dLWv7mqlk+jVkJrK2vAO+0bfl0x2Ouov4VS4Ixwx
lJfaec8v0cw+xjcJc29Y28WNFYhW/wpf1uEHYAf/pQ9q7S25rhK5yPv23101P7pA
bCNDyFB6PYLuXxqkE7dq7rIZXfw5xgNQBRugBrSmUEjoCFs3XowAXCk2gWhM/1Lg
mSqaaAh/Cu5vvzM0wYoaEi598LWYmtgurQ3C2Nenu8HVjI+zCSg8v7VrcTa1MHua
owIDAQAB
-----END PUBLIC KEY-----
PEM for KID 'kTP2cLZY0qA2qfnedNEgx6rs6yqIEdf4DQYYV2m4KPQ'
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjKb91FLaoZe9/5NEMZrO
1eDn4hdrhtjrvsy+qO1QIbbdhRXJIJoE+qpHmgmq1gK28OZCV51xUAwk8ugw5p7/
m2wIarykHtXuBmhcFPkWez6N/yX30qvdOPPKUGqd05AoGcrzAW6fV07CRROU+5g1
RnTdNasLEMYaq0xPlmCMDjb3usyiafGyyrwg4+tndOTry4uMtF7LeTVLZo9Tnn2x
dJiytWWh+Rq5/KAn1mJ2GgwG8tp8o7SRf65c0LYQenN1d6vXX/Iimq/mg//B5CHP
zIaUrZfoL+2sbRIyQ5AePlDyn8Neg6sIsV9nTkPAcYvvQsS+/8xnfNq6np0zKbua
dQIDAQAB
-----END PUBLIC KEY-----
This Python script depends on the cryptography
and requests
Python
2.7 packages
The easiest way to install these dependencies is to use the Nix package manager, which will automatically install these dependencies for you if you run the commands below.
Note: While Nix is the easiest way to install the dependencies for this script, Nix will download several hundred MiB of packages these dependencies. If you don't want to use the Nix package manager, you can install the dependencies manually using your preferred package manager and then change the interpreter on the first line of included script from "
/usr/bin/env nix-shell
" to "/usr/env/python
"
Here is how to install this script on your system using the Nix package manager on GNU/Linux or Macintosh OS X systems:
-
Install the Nix package manager:
curl https://nixos.org/nix/install | sh
-
Download the
get_id_token.sh
shell script to your systemgit clone git@github.com:jpf/okta-jwks-to-pem.git
-
Change to the directory containing the
get_id_token.sh
shell scriptcd okta-jwks-to-pem
-
Run the script:
./jwks-to-pem.py --org example.okta.org
The most important part of this code is the conversion of RSA public key modulus and exponent integers into a PEM encoded public key.
Thanks to the excellent Python cryptography library, the process of converting an RSA public key modulus and exponent is three step process:
-
Convert the JWK modulus and exponent, which are Base64url encoded, into integers:
exponent = base64_to_long(jwk['e']) modulus = base64_to_long(jwk['n'])
Note: This conversion is actually pretty frustrating, the
base64_to_long
function abstracts this all away. -
Use the RSAPublicNumbers class to store the modulus and exponent
numbers = RSAPublicNumbers(exponent, modulus) public_key = numbers.public_key(backend=default_backend())
-
Serialize the RSAPublicNumbers object to PEM
pem = public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo )
We cover the rest of the script below.
First, we import the libraries that we'll need:
argparse
: For handling the--org=
command line argument and giving help when it isn't present.base64
,six
,struct
: Used to properly decode the Base64url encoded modulus and exponent.cryptography
: For conversion of the modulus and exponent to PEMrequests
: To fetch the JWKS URI
Here is how we import the dependencies listed above:
import argparse
import base64
import six
import struct
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import requests
Next, we set up ArgumentParser
to handle the --org
command line
argument. The required=true
option will cause ArgumentParser
to give
help text if the --org
argument isn't present.
arg_parser = argparse.ArgumentParser(
description='JWK to PEM conversion tool')
arg_parser.add_argument('--org',
dest='org',
help='Domain for Okta org',
required=True)
args = arg_parser.parse_args()
Next up is the the code that handles the ugly job of decoding and properly padding the base64url encoded strings that are used in a JWK.
This is easily the most frustrating part of dealing with a JWK. Particularly annoying is the fact that the keys are not Base64 encoded, the are Base64url encoded. Which means that we need to take special precautions for padding and decoding. Thankfully, I was able to find some code that already does this, written by the prolific and talented Roland Hedberg. The functions below come from: https://github.com/rohe/pyjwkest/blob/master/src/jwkest/__init__.py
def intarr2long(arr):
return int(''.join(["%02x" % byte for byte in arr]), 16)
def base64_to_long(data):
if isinstance(data, six.text_type):
data = data.encode("ascii")
# urlsafe_b64decode will happily convert b64encoded data
_d = base64.urlsafe_b64decode(bytes(data) + b'==')
return intarr2long(struct.unpack('%sB' % len(_d), _d))
Here we fetch and decode the JSON from an Okta jwks_uri
endpoint:
print("Fetching JWKS from {}".format(args.org))
r = requests.get("https://{}/oauth2/v1/keys".format(args.org))
jwks = r.json()
Finally, we process each key, and print out the PEM encoded key for
each JWK Key ID (kid
) that we find:
for jwk in jwks['keys']:
exponent = base64_to_long(jwk['e'])
modulus = base64_to_long(jwk['n'])
numbers = RSAPublicNumbers(exponent, modulus)
public_key = numbers.public_key(backend=default_backend())
pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
print "PEM for KID '{}'".format(jwk['kid'])
print pem
This script depends on the command line tools listed below. These
requirements should be automatically included via the nix-shell
directives in the script, but are listed below for the sake of
completeness.
Name | Version | Description | License |
---|---|---|---|
requests | 2.11.1 | HTTP Requests for Humans | Apache 2.0 or BSD |
cryptography | 1.5.2 | Exposes cryptographic recipes and primitives | Apache 2.0 |
Copyright © 2016, Okta, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.