Skip to content
ecdsa.com

JWK thumbprint · Go

How to compute a JWK thumbprint (RFC 7638) in Go

In Go, go-jose's JSONWebKey.Thumbprint computes the RFC 7638 thumbprint for EC, RSA and Ed25519 keys, whether the JSONWebKey came from a JWK file or wraps a crypto.PublicKey parsed from PEM. The run checks all three test keys against their published thumbprints, from both sources.

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

main.go — thumbprints from JWK files and from PEM keys with go-jose v4
package main
import (
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"log"
"os"
"github.com/go-jose/go-jose/v4"
)
func thumbprint(jwk jose.JSONWebKey) string {
sum, err := jwk.Thumbprint(crypto.SHA256) // RFC 7638 over the required members
if err != nil {
log.Fatal(err)
}
return base64.RawURLEncoding.EncodeToString(sum)
}
func main() {
// From a JWK: kid, use and alg are parsed but do not enter the thumbprint
for _, name := range []string{"ec.jwk.json", "rsa.jwk.json", "okp.jwk.json"} {
data, err := os.ReadFile(name)
if err != nil {
log.Fatal(err)
}
var jwk jose.JSONWebKey
if err := jwk.UnmarshalJSON(data); err != nil {
log.Fatal(err)
}
fmt.Printf("%s: %s\n", name, thumbprint(jwk))
}
// From a PEM public key: wrap the parsed crypto.PublicKey
for _, name := range []string{"ec.pub.pem", "rsa.pub.pem", "okp.pub.pem"} {
data, err := os.ReadFile(name)
if err != nil {
log.Fatal(err)
}
block, _ := pem.Decode(data)
if block == nil {
log.Fatalf("%s: no PEM block", name)
}
key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s: %s\n", name, thumbprint(jose.JSONWebKey{Key: key}))
}
}

How it works

  1. jwk.UnmarshalJSON(data) parses a JWK, including kid, use and alg, which Thumbprint then ignores.
  2. For a PEM key, x509.ParsePKIXPublicKey gives an *ecdsa.PublicKey, *rsa.PublicKey or ed25519.PublicKey; jose.JSONWebKey{Key: key} wraps it.
  3. Thumbprint(crypto.SHA256) returns the raw digest; base64.RawURLEncoding (no padding) turns it into the thumbprint string.
  4. The same digest with crypto.SHA384 or crypto.SHA512 gives the other thumbprints RFC 7638 allows; SHA-256 is the common one.

Gotchas

  • base64.URLEncoding adds = padding; thumbprints use RawURLEncoding.
  • go-jose computes the thumbprint of the public part: wrapping an *ecdsa.PrivateKey gives the same value as its public key, which is what you want for a kid, but make sure the JWK you publish is jwk.Public().
  • The Go standard library has no JWK type; without go-jose (or lestrrat-go/jwx), the canonical JSON must be built by hand — with fixed-length EC coordinates and the members in sorted order.
  • go-jose v2 and v3 are older major versions with different import paths (gopkg.in/square/go-jose.v2, github.com/go-jose/go-jose/v3); v4 is the maintained one.

Related recipes