Skip to content
ecdsa.com

JWK thumbprint · Ruby

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

In Ruby, the jwt gem computes a JWK thumbprint with JWT::JWK::Thumbprint, for JWKs imported from JSON and for keys created from an OpenSSL::PKey. The run reproduces the published RFC 7638 thumbprint and shows that the gem's default kid is something else unless configured.

Tested with jwt (ruby-jwt) 3.3.0 (Ruby 4.0.7, openssl gem 4.0.2).

thumbprint.rb — RFC 7638 thumbprints and the kid generator setting
require "jwt" # gem install jwt -v 3.3.0
require "json"
require "openssl"
# From a JWK file
%w[ec.jwk.json rsa.jwk.json].each do |file|
jwk = JWT::JWK.import(JSON.parse(File.read(file)))
puts "#{file}: #{JWT::JWK::Thumbprint.new(jwk)}"
end
# From a PEM public key
%w[ec.pub.pem rsa.pub.pem].each do |file|
jwk = JWT::JWK.new(OpenSSL::PKey.read(File.read(file)))
puts "#{file}: #{JWT::JWK::Thumbprint.new(jwk)}"
end
# Ed25519 (OKP) is not supported by the gem: hash the RFC 8037 members, already in sorted order
okp = JSON.parse(File.read("okp.jwk.json")).slice("crv", "kty", "x")
puts "okp.jwk.json: #{[OpenSSL::Digest::SHA256.digest(JSON.generate(okp))].pack("m0").tr("+/", "-_").delete("=")}"
# The gem's default kid is NOT the RFC 7638 thumbprint unless configured so
puts "default kid: #{JWT::JWK.new(OpenSSL::PKey.read(File.read("ec.pub.pem"))).kid}"
JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint
puts "rfc7638 kid: #{JWT::JWK.new(OpenSSL::PKey.read(File.read("ec.pub.pem"))).kid}"

How it works

  1. JWT::JWK.import(hash) builds a JWK from parsed JSON; JWT::JWK.new(pkey) from an OpenSSL key read from PEM.
  2. JWT::JWK::Thumbprint.new(jwk).to_s sorts the required members, serializes them and returns the base64url SHA-256 digest.
  3. Every JWK object gets a kid when created; kid_generator_type = :rfc7638_thumbprint makes that kid the RFC 7638 thumbprint.
  4. The thumbprint is the same for the JWK and for the PEM form of the key, as the run shows. For Ed25519, which the gem does not support, slice("crv", "kty", "x") keeps the RFC 8037 members in sorted order and JSON.generate writes them without whitespace.

Gotchas

  • The gem's default kid is its own key digest (kid_generator_type = :key_digest), not the RFC 7638 thumbprint — the run prints a different value. Other libraries do not compute it; switch the generator before publishing keys.
  • The configuration is global: setting JWT.configuration.jwk.kid_generator_type affects every JWK created afterwards in the process, as the second kid above shows.
  • OKP keys are not supported: JWT::JWK.import of an Ed25519 JWK raises JWT::UnsupportedKeyType: Key type OKP not supported — hence the hand-built canonical form above. Hash#slice returns the keys in the order they are listed, which is why they are listed sorted.
  • An imported JWK keeps the kid from the JSON; the generator only fills in a missing one.

Related recipes