Class: JWT::PQ::JWKSet

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/jwt/pq/jwk_set.rb,
lib/jwt/pq/jwks_loader.rb

Overview

A set of JWKs (RFC 7517 §5) for publication and kid-based lookup.

Typical producer flow — publish verification keys on /.well-known/jwks.json:

Typical consumer flow — pick the right key for an incoming JWT by the kid header:

The set indexes members by their RFC 7638 JWK Thumbprint, which is the kid JWT::PQ::JWK#export emits. If you import a JWKS that uses custom (non- thumbprint) kid values, lookup by those custom values is not supported — rely on thumbprints when generating the set.

Examples:

Publish a JWKS

jwks = JWT::PQ::JWKSet.new([key_current, key_next])
File.write("jwks.json", jwks.to_json)

Resolve a verification key by kid

jwks = JWT::PQ::JWKSet.import(JSON.parse(fetch_jwks))
_payload, header = JWT.decode(token, nil, false) # unverified peek
key = jwks[header["kid"]] or raise "unknown kid"
payload, = JWT.decode(token, key, true, algorithms: [header["alg"]])

See Also:

Defined Under Namespace

Classes: Loader

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(keys = []) ⇒ JWKSet

Build a set from zero or more Keys.

Parameters:

Raises:



38
39
40
41
42
# File 'lib/jwt/pq/jwk_set.rb', line 38

def initialize(keys = [])
  @keys = []
  @kid_index = {}
  Array(keys).each { |k| add(k) }
end

Class Method Details

.fetch(url) ⇒ JWKSet

Fetch a JWKS from a URL, honouring the process-global cache.

Convenience wrapper around JWT::PQ::JWKSet::Loader#fetch using JWT::PQ::JWKSet::Loader.default — the cache is shared across all callers, so repeated hits on the same URL within cache_ttl seconds return the in-memory set without touching the network.

See Loader for the full option reference (cache TTL, timeouts, body-size cap, HTTPS enforcement, ETag-based revalidation).

Examples:

Verify a token using a remote JWKS

jwks = JWT::PQ::JWKSet.fetch("https://issuer.example/.well-known/jwks.json")
_payload, header = JWT.decode(token, nil, false)
key = jwks[header["kid"]] or raise "unknown kid"
payload, = JWT.decode(token, key, true, algorithms: [header["alg"]])

Parameters:

  • url (String)

    absolute JWKS URL.

Returns:

  • (JWKSet)

    the parsed set of verification keys.

Raises:



191
192
193
# File 'lib/jwt/pq/jwk_set.rb', line 191

def self.fetch(url, **)
  Loader.default.fetch(url, **)
end

.import(source) ⇒ JWKSet

Import a JWKS from a Hash or JSON string.

Each member is reconstructed via JWT::PQ::JWK.import; malformed members raise KeyError.

ML-DSA public keys are ~1.3–2.6 KB each, so a JWKS with N keys is at least N × ~2 KB. When ingesting untrusted JWKS payloads (e.g. a remote /.well-known/jwks.json), bound the HTTP body size before calling import — this method does not cap the number of members.

Parameters:

  • source (Hash, String)

    a JWKS hash or JSON string with a "keys" array.

Returns:

  • (JWKSet)

    a new set with all parsed members.

Raises:

  • (KeyError)

    if source is not a Hash/String, if the keys field is missing or not an Array, or if any member fails to import.



144
145
146
147
148
149
150
151
152
153
154
# File 'lib/jwt/pq/jwk_set.rb', line 144

def self.import(source)
  hash = coerce_to_hash(source)
  raise KeyError, "Expected Hash for JWKS body, got #{hash.class}" unless hash.is_a?(Hash)

  hash = hash.transform_keys(&:to_s)
  raise KeyError, "Missing 'keys' in JWKS" unless hash.key?("keys")
  raise KeyError, "'keys' must be an Array" unless hash["keys"].is_a?(Array)

  members = hash["keys"].map { |jwk| JWT::PQ::JWK.import(jwk) }
  new(members)
end

Instance Method Details

#add(key) ⇒ JWKSet

Add a key to the set.

Idempotent: if a key with the same RFC 7638 thumbprint is already in the set, the call is a no-op (Set semantics). The thumbprint is computed before any mutation, so a failure to derive the kid leaves the set unchanged.

Parameters:

Returns:

  • (JWKSet)

    self, for chaining.

Raises:



54
55
56
57
58
59
60
61
62
63
# File 'lib/jwt/pq/jwk_set.rb', line 54

def add(key)
  raise KeyError, "Expected a JWT::PQ::Key, got #{key.class}" unless key.is_a?(JWT::PQ::Key)

  kid = key.jwk_thumbprint
  return self if @kid_index.key?(kid)

  @keys << key
  @kid_index[kid] = key
  self
end

#each {|key| ... } ⇒ Enumerator

Iterate over the keys in insertion order.

Yield Parameters:

Returns:

  • (Enumerator)

    when called without a block.



69
70
71
# File 'lib/jwt/pq/jwk_set.rb', line 69

def each(&)
  @keys.each(&)
end

#empty?Boolean

Returns true if the set is empty.

Returns:

  • (Boolean)

    true if the set is empty.



80
81
82
# File 'lib/jwt/pq/jwk_set.rb', line 80

def empty?
  @keys.empty?
end

#export(include_private: false) ⇒ Hash{Symbol=>Array<Hash>}

Export the set as a JWKS hash.

Parameters:

  • include_private (Boolean) (defaults to: false)

    include the priv field on each member that has a private component. Default: false.

Returns:

  • (Hash{Symbol=>Array<Hash>})

    a hash with a single :keys member, suitable for serialization with #to_json.



104
105
106
# File 'lib/jwt/pq/jwk_set.rb', line 104

def export(include_private: false)
  { keys: @keys.map { |k| JWK.new(k).export(include_private: include_private) } }
end

#find(kid) ⇒ JWT::PQ::Key? Also known as: []

Look up a key by its RFC 7638 thumbprint (the kid from JWT::PQ::JWK#export).

Parameters:

  • kid (String)

    the thumbprint to match.

Returns:

  • (JWT::PQ::Key, nil)

    the matching key, or nil if not in the set.



88
89
90
# File 'lib/jwt/pq/jwk_set.rb', line 88

def find(kid)
  @kid_index[kid]
end

#inspectString Also known as: to_s

Returns short diagnostic string — never contains key material.

Returns:

  • (String)

    short diagnostic string — never contains key material.



123
124
125
# File 'lib/jwt/pq/jwk_set.rb', line 123

def inspect
  "#<#{self.class} size=#{size}>"
end

#keysArray<JWT::PQ::Key>

Returns a frozen snapshot of the keys in the set.

Returns:

  • (Array<JWT::PQ::Key>)

    a frozen snapshot of the keys in the set.



94
95
96
# File 'lib/jwt/pq/jwk_set.rb', line 94

def keys
  @keys.dup.freeze
end

#sizeInteger Also known as: length

Returns number of keys in the set.

Returns:

  • (Integer)

    number of keys in the set.



74
75
76
# File 'lib/jwt/pq/jwk_set.rb', line 74

def size
  @keys.size
end

#to_jsonString

Serialize the set as a JWKS JSON document.

Always emits public-only keys — the priv field is never written out. This keeps the method safe for arbitrary nesting inside other JSON (e.g. { jwks: set }.to_json), where Ruby's stdlib JSON passes a generator state as a positional argument. To publish private material (unusual), call JSON.generate(set.export(include_private: true)) explicitly.

Returns:

  • (String)

    a JSON document ready for /.well-known/jwks.json.



118
119
120
# File 'lib/jwt/pq/jwk_set.rb', line 118

def to_json(*)
  export.to_json(*)
end