Skip to content
ecdsa.com

PEM ⇄ JWK · OpenSSL

How to convert a PEM public key to a JWK and back with OpenSSL

The OpenSSL command line knows nothing about JWK, but a P-256 public key can still be converted both ways with openssl pkey and a few shell tools: the SPKI DER ends with the point 04 ‖ x ‖ y, and a JWK's x and y prepended with the fixed SPKI header become a DER key again. The run converts the RFC 7515 key to a JWK and back to an identical PEM.

Tested with OpenSSL 3.5.7 (Debian 13, with xxd and bash).

to-jwk.sh — the last 64 bytes of the SPKI DER are x and y
# PEM -> JWK for a P-256 key: the SPKI DER ends with the point 04 ‖ x ‖ y
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
spki=$(openssl pkey -pubin -in pub.pem -outform DER | xxd -p | tr -d '\n')
x=$(printf '%s' "${spki: -128:64}" | xxd -r -p | b64url)
y=$(printf '%s' "${spki: -64}" | xxd -r -p | b64url)
printf '{"kty":"EC","crv":"P-256","x":"%s","y":"%s"}\n' "$x" "$y" | tee out.jwk.json
to-pem.sh — fixed SPKI header + 04 + x + y, then openssl pkey
# JWK -> PEM for a P-256 key: the fixed 26-byte SPKI header, then 04 ‖ x ‖ y
b64url_decode() { local v; v=$(tr '_-' '/+' <<< "$1"); while (( ${#v} % 4 )); do v+="="; done; openssl base64 -d -A <<< "$v"; }
json_field() { grep -o "\"$1\" *: *\"[^\"]*\"" pub.jwk.json | sed 's/.*: *"\(.*\)"/\1/'; }
{
printf '3059301306072a8648ce3d020106082a8648ce3d03010703420004' | xxd -r -p
b64url_decode "$(json_field x)"
b64url_decode "$(json_field y)"
} > pub.der
openssl pkey -pubin -inform DER -in pub.der -out out.pem # also checks that the point is on the curve
cat out.pem

How it works

  1. openssl pkey -pubin -outform DER gives the 91-byte SPKI of a P-256 key: a 26-byte header, the byte 04, and the 32-byte coordinates x and y.
  2. xxd -r -p turns the hex back into bytes, and the b64url helper encodes them in the unpadded base64url form JWK uses.
  3. For the reverse, the header 3059…420004 is the same for every P-256 public key; the decoded x and y complete the DER.
  4. openssl pkey -pubin -inform DER parses that DER — which checks that the point is on the curve — and writes the standard PEM.

Gotchas

  • The offsets and the header are specific to P-256 public keys. P-384 keys have a different header and 48-byte coordinates, RSA and Ed25519 keys a different structure altogether.
  • A point that is not on the curve fails with the misleading Could not find private key of Public Key from pub.der (and STORE routines … unsupported): the problem is the point, not a missing private key.
  • Base64url has no padding and uses -_; openssl base64 -d expects standard base64 with padding, which b64url_decode restores first.
  • json_field is a grep over the JSON, good enough for a single well-formed JWK; for JWKS files or arbitrary JSON, use jq or one of the language recipes.

Related recipes