Skip to content
ecdsa.com

PEM ⇄ JWK · Go

How to convert a PEM public key to a JWK and back in Go

In Go, the standard library parses and writes PEM public keys (encoding/pem, crypto/x509), and go-jose's JSONWebKey marshals any of those keys to a JWK and parses JWKs back. The run converts the RFC 7515 P-256 key to a JWK and back to an identical PEM, and converts an Ed25519 key as well.

Tested with github.com/go-jose/go-jose/v4 v4.1.6 (Go 1.27.3).

main.go — pemToJWK and jwkToPEM with go-jose v4
package main
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"log"
"os"
"github.com/go-jose/go-jose/v4"
)
// pemToJWK reads an SPKI public key (EC, RSA or Ed25519) and returns its JWK JSON.
func pemToJWK(data []byte) ([]byte, error) {
block, _ := pem.Decode(data)
if block == nil || block.Type != "PUBLIC KEY" {
return nil, errors.New("no PUBLIC KEY block")
}
key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
return json.Marshal(jose.JSONWebKey{Key: key})
}
// jwkToPEM reads a public JWK and returns its SPKI PEM.
func jwkToPEM(data []byte) ([]byte, error) {
var jwk jose.JSONWebKey
if err := jwk.UnmarshalJSON(data); err != nil {
return nil, err
}
if !jwk.IsPublic() {
return nil, errors.New("refusing to convert a private JWK")
}
der, err := x509.MarshalPKIXPublicKey(jwk.Key)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der}), nil
}
func main() {
for _, step := range []struct {
in, out string
convert func([]byte) ([]byte, error)
}{
{"pub.pem", "out.jwk.json", pemToJWK},
{"pub.jwk.json", "out.pem", jwkToPEM},
{"ed25519.pem", "", pemToJWK},
} {
data, err := os.ReadFile(step.in)
if err != nil {
log.Fatal(err)
}
result, err := step.convert(data)
if err != nil {
log.Fatalf("%s: %v", step.in, err)
}
fmt.Printf("%s\n", result)
if step.out != "" {
if err := os.WriteFile(step.out, result, 0o644); err != nil {
log.Fatal(err)
}
}
}
}

How it works

  1. pem.Decode and x509.ParsePKIXPublicKey turn a BEGIN PUBLIC KEY block into an *ecdsa.PublicKey, *rsa.PublicKey or ed25519.PublicKey.
  2. json.Marshal(jose.JSONWebKey{Key: key}) writes the JWK; Key, KeyID, Algorithm and Use map to the key and to kid, alg and use.
  3. jwk.UnmarshalJSON parses a JWK into the same Go key types and checks that EC points are on the curve.
  4. x509.MarshalPKIXPublicKey and pem.EncodeToMemory write the SPKI PEM; the result equals the original file.

Gotchas

  • Marshaling a JSONWebKey that holds a private key writes d. IsPublic() tells which kind it holds, and jwk.Public() returns the public half — note that Public has a pointer receiver, so call it on a variable, not on a composite literal.
  • An off-curve point is rejected at parse time: go-jose/go-jose: invalid EC key, X/Y are not on declared curve.
  • pem.Decode returns nil for input without a PEM block, and the remaining bytes as the second value — checking the block type (PUBLIC KEY) catches a certificate or private key file passed by mistake.
  • The Go standard library has no JWK support; without go-jose, the conversion means encoding x and y by hand, including the fixed-length padding of EC coordinates.

Related recipes