jwt-pq 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da4d3ef14af28991259ed249b3a2e57a809d30729dff75f3a0ca084279552386
4
- data.tar.gz: 6e557b8db0f0c8720ce3fbcae02d489399ccd071e9d8f2785a8fbfad812da254
3
+ metadata.gz: c0c45ba386fb98df768124e32529f0e0d80f574ddf0a0132e57e87a3ce94ea7a
4
+ data.tar.gz: c6071071b4814e40c707179d575447b7d730a1a2a789ba1cee5bdf71929abb47
5
5
  SHA512:
6
- metadata.gz: b59cfa192a9a540949d52700605915a6c5fb86bf9cb7c2a43ad043cdecbab790c6b30d3ee2d70de55d16ce196a98af9dfbaad0dd0a8f03ae2c6c335759c3f5aa
7
- data.tar.gz: 770546c6daf1c6645bef2198a94da9a0a76285429df70039980f894e015ae07a2a469f6627748d568c767ce294653173711798a0c7e2b6394f159010eadbd988
6
+ metadata.gz: 96c10bfd04d93b946f634257b95427a64e39cf23d8a10f330b1b4bdc3d29e212d0a575ddb7486df7ab47d88bd8af73d21ac4ec2620507db9c5289e2bc5a2588b
7
+ data.tar.gz: 88bc0b05ade5ab27f3dfa8b56d7f8bf3d8bb566145468e452c7a570db5db0e57ddabcc9ad452d86a48206633368675b83cf7a28d832f54a157cc80e75559464a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-04-22
11
+
12
+ ### Added
13
+
14
+ - Ruby 4.0 officially supported (CI matrix now covers 3.2, 3.3, 3.4, and 4.0 on Linux and macOS) (#40)
15
+ - YARD reference site: `docs.yml` workflow publishes the generated docs to GitHub Pages on every tag push, served at `docs.jwt-pq.marcelopazzo.com` (#41)
16
+
17
+ ### Changed
18
+
19
+ - `JWT::PQ::JWKSet.import` (and `JWKSet.fetch`) now tolerates mixed JWKSes: members with unknown `kty` (e.g. RSA, EC, OKP) or unsupported `alg` within `kty: "AKP"` are silently skipped instead of aborting the whole set — enabling incremental PQ rollouts where a single `/.well-known/jwks.json` carries both classical and ML-DSA keys. Pass `strict: true` to restore the previous fail-fast behaviour. Recognized-but-malformed AKP members still raise `KeyError` (#39)
20
+
10
21
  ## [0.5.1] - 2026-04-22
11
22
 
12
23
  ### Changed
@@ -140,7 +151,8 @@ Throughput on Ruby 3.4.6, macOS x86_64, liboqs 0.15.0 (benchmark-ips, 2s warmup
140
151
  - Optional dependency on jwt-eddsa / ed25519
141
152
  - Error classes: `LiboqsError`, `KeyError`, `SignatureError`, `MissingDependencyError`
142
153
 
143
- [Unreleased]: https://github.com/marcelopazzo/jwt-pq/compare/v0.5.1...HEAD
154
+ [Unreleased]: https://github.com/marcelopazzo/jwt-pq/compare/v0.6.0...HEAD
155
+ [0.6.0]: https://github.com/marcelopazzo/jwt-pq/compare/v0.5.1...v0.6.0
144
156
  [0.5.1]: https://github.com/marcelopazzo/jwt-pq/compare/v0.5.0...v0.5.1
145
157
  [0.5.0]: https://github.com/marcelopazzo/jwt-pq/compare/v0.4.0...v0.5.0
146
158
  [0.4.0]: https://github.com/marcelopazzo/jwt-pq/compare/v0.3.0...v0.4.0
data/README.md CHANGED
@@ -138,6 +138,29 @@ signing: `JWT.encode(payload, key, alg, { kid: key.jwk_thumbprint })`.
138
138
  `Key#jwk_thumbprint` memoizes the digest, so it's cheap to call
139
139
  repeatedly on the same key.
140
140
 
141
+ #### Mixed classical + PQ JWKSes
142
+
143
+ `JWKSet.import` (and `JWKSet.fetch`) tolerates JWKSes that mix this
144
+ gem's `ML-DSA-{44,65,87}` keys with classical RSA/EC/EdDSA entries or
145
+ future PQ algorithms — members with an unknown `kty` or an
146
+ unsupported `alg` within `kty: "AKP"` are silently dropped. This is
147
+ the realistic shape of an incremental PQ rollout, where an issuer
148
+ publishes both classical and ML-DSA keys on the same
149
+ `/.well-known/jwks.json`.
150
+
151
+ Members that **do** have `kty: "AKP"` with a supported `alg` but are
152
+ otherwise malformed (missing `pub`, invalid base64url, wrong key size,
153
+ etc.) still raise `JWT::PQ::KeyError` — that is a real bug in the
154
+ emitter, not an interop boundary.
155
+
156
+ Pass `strict: true` to restore fail-fast behaviour on any unknown
157
+ `kty`/`alg`:
158
+
159
+ ```ruby
160
+ JWT::PQ::JWKSet.import(body, strict: true)
161
+ JWT::PQ::JWKSet.fetch(url, strict: true)
162
+ ```
163
+
141
164
  ### Fetching a remote JWKS
142
165
 
143
166
  For consuming a JWKS from an identity provider or sibling service,
data/SECURITY.md CHANGED
@@ -6,8 +6,8 @@ jwt-pq is pre-1.0. Only the latest minor release receives security fixes.
6
6
 
7
7
  | Version | Supported |
8
8
  |---------|-----------|
9
- | 0.5.x | Yes |
10
- | < 0.5 | No |
9
+ | 0.6.x | Yes |
10
+ | < 0.6 | No |
11
11
 
12
12
  ## Reporting a vulnerability
13
13
 
data/SPEC.md CHANGED
@@ -4,9 +4,9 @@ This document records which external specifications each jwt-pq release
4
4
  targets, what its current divergences are (if any), and the compatibility
5
5
  policy for drafts in flight.
6
6
 
7
- ## Tracked specifications — jwt-pq 0.5.x
7
+ ## Tracked specifications — jwt-pq 0.6.x
8
8
 
9
- Last reviewed: **2026-04-20**.
9
+ Last reviewed: **2026-04-22**.
10
10
 
11
11
  | Spec | Role in jwt-pq | Status |
12
12
  |-----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|-----------------------------|
data/jwt-pq.gemspec CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
  "source_code_uri" => github_uri,
23
23
  "changelog_uri" => "#{github_uri}/blob/main/CHANGELOG.md",
24
24
  "bug_tracker_uri" => "#{github_uri}/issues",
25
- "documentation_uri" => "https://rubydoc.info/gems/jwt-pq",
25
+ "documentation_uri" => "https://docs.jwt-pq.marcelopazzo.com",
26
26
  "rubygems_mfa_required" => "true"
27
27
  }
28
28
 
data/lib/jwt/pq/jwk.rb CHANGED
@@ -98,6 +98,27 @@ module JWT
98
98
  end
99
99
  end
100
100
 
101
+ # Whether a JWK hash looks like something this gem can import:
102
+ # a JSON object with `kty == "AKP"` and a supported ML-DSA `alg`.
103
+ #
104
+ # Used by {JWT::PQ::JWKSet.import} to tolerate mixed JWKSes during
105
+ # incremental PQ rollouts — a `/.well-known/jwks.json` carrying a
106
+ # blend of RSA/EdDSA and ML-DSA members will not raise on the
107
+ # classical entries. It does **not** validate the remaining fields
108
+ # (`pub`, `priv`, base64url-ness, key sizes); a recognized-but-
109
+ # malformed member still raises from {.import}, since that signals
110
+ # a real bug in the emitter rather than an interop boundary.
111
+ #
112
+ # @api private
113
+ # @param jwk_hash [Object] candidate JWK.
114
+ # @return [Boolean] true iff the member is in this gem's scope.
115
+ def self.recognized?(jwk_hash)
116
+ return false unless jwk_hash.is_a?(Hash)
117
+
118
+ jwk = normalize_keys(jwk_hash)
119
+ jwk["kty"] == KTY && ALGORITHMS.include?(jwk["alg"])
120
+ end
121
+
101
122
  # Compute the JWK Thumbprint (RFC 7638) used as `kid`.
102
123
  #
103
124
  # Delegates to {JWT::PQ::Key#jwk_thumbprint}, which memoizes the
@@ -127,8 +127,20 @@ module JWT
127
127
 
128
128
  # Import a JWKS from a Hash or JSON string.
129
129
  #
130
- # Each member is reconstructed via {JWT::PQ::JWK.import}; malformed
131
- # members raise {KeyError}.
130
+ # By default, members this gem cannot represent — unknown `kty`
131
+ # (e.g. `RSA`, `EC`, `OKP`) or unknown `alg` within `kty: "AKP"`
132
+ # (e.g. a future ML-DSA parameter set or a sibling PQ algorithm not
133
+ # yet implemented here) — are silently dropped. This keeps the set
134
+ # usable during an incremental PQ rollout, where a single
135
+ # `/.well-known/jwks.json` carries both classical and ML-DSA keys.
136
+ #
137
+ # Members that **are** in scope (`kty: "AKP"` with a supported
138
+ # `alg`) but malformed — missing `pub`, wrong field type, invalid
139
+ # base64url, wrong key size — still raise {KeyError}: that is a
140
+ # real bug in the emitter, not an interop boundary.
141
+ #
142
+ # Pass `strict: true` to restore the previous fail-fast behaviour,
143
+ # where any unknown `kty`/`alg` raises.
132
144
  #
133
145
  # ML-DSA public keys are ~1.3–2.6 KB each, so a JWKS with N keys is
134
146
  # at least N × ~2 KB. When ingesting untrusted JWKS payloads (e.g.
@@ -138,10 +150,14 @@ module JWT
138
150
  #
139
151
  # @param source [Hash, String] a JWKS hash or JSON string with a
140
152
  # `"keys"` array.
141
- # @return [JWKSet] a new set with all parsed members.
153
+ # @param strict [Boolean] if true, unknown `kty`/`alg` members
154
+ # raise {KeyError} instead of being skipped. Default: false.
155
+ # @return [JWKSet] a new set with the parsed in-scope members.
142
156
  # @raise [KeyError] if `source` is not a Hash/String, if the `keys`
143
- # field is missing or not an Array, or if any member fails to import.
144
- def self.import(source)
157
+ # field is missing or not an Array, if an in-scope member is
158
+ # malformed, or (only when `strict: true`) if any member has an
159
+ # unknown `kty`/`alg`.
160
+ def self.import(source, strict: false)
145
161
  hash = coerce_to_hash(source)
146
162
  raise KeyError, "Expected Hash for JWKS body, got #{hash.class}" unless hash.is_a?(Hash)
147
163
 
@@ -149,10 +165,18 @@ module JWT
149
165
  raise KeyError, "Missing 'keys' in JWKS" unless hash.key?("keys")
150
166
  raise KeyError, "'keys' must be an Array" unless hash["keys"].is_a?(Array)
151
167
 
152
- members = hash["keys"].map { |jwk| JWT::PQ::JWK.import(jwk) }
168
+ members = hash["keys"].filter_map { |jwk| import_member(jwk, strict: strict) }
153
169
  new(members)
154
170
  end
155
171
 
172
+ # @api private
173
+ def self.import_member(jwk, strict:)
174
+ return nil unless strict || JWT::PQ::JWK.recognized?(jwk)
175
+
176
+ JWT::PQ::JWK.import(jwk)
177
+ end
178
+ private_class_method :import_member
179
+
156
180
  # @api private
157
181
  def self.coerce_to_hash(source)
158
182
  case source
@@ -184,6 +208,10 @@ module JWT
184
208
  # key = jwks[header["kid"]] or raise "unknown kid"
185
209
  # payload, = JWT.decode(token, key, true, algorithms: [header["alg"]])
186
210
  #
211
+ # By default, members with unknown `kty`/`alg` in the fetched
212
+ # body are skipped (see {.import}); pass `strict: true` to make
213
+ # them raise.
214
+ #
187
215
  # @param url [String] absolute JWKS URL.
188
216
  # @return [JWKSet] the parsed set of verification keys.
189
217
  # @raise [JWKSFetchError] on fetch failure (see {Loader#fetch}).
@@ -80,6 +80,9 @@ module JWT
80
80
  # Default: 1 MB.
81
81
  # @param allow_http [Boolean] allow plain `http://` URLs. Default:
82
82
  # false (strongly recommended for production).
83
+ # @param strict [Boolean] forwarded to {JWKSet.import}: if true,
84
+ # members with unknown `kty`/`alg` raise instead of being
85
+ # skipped. Default: false.
83
86
  # @return [JWKSet] the parsed set of verification keys.
84
87
  # @raise [JWKSFetchError] on network error, timeout, non-2xx
85
88
  # response, oversized body, redirect, or non-HTTPS URL.
@@ -89,7 +92,8 @@ module JWT
89
92
  timeout: DEFAULT_TIMEOUT,
90
93
  open_timeout: DEFAULT_OPEN_TIMEOUT,
91
94
  max_body_bytes: DEFAULT_MAX_BODY_BYTES,
92
- allow_http: false)
95
+ allow_http: false,
96
+ strict: false)
93
97
  uri = validate_uri!(url, allow_http: allow_http)
94
98
 
95
99
  fresh = fresh_entry(url, cache_ttl)
@@ -102,7 +106,7 @@ module JWT
102
106
  @mutex.synchronize { existing.fetched_at = now }
103
107
  existing.jwks
104
108
  else
105
- jwks = JWKSet.import(result[:body])
109
+ jwks = JWKSet.import(result[:body], strict: strict)
106
110
  @mutex.synchronize do
107
111
  @cache[url] = CacheEntry.new(jwks, result[:etag], now)
108
112
  end
@@ -3,6 +3,6 @@
3
3
  module JWT
4
4
  module PQ
5
5
  # Current gem version.
6
- VERSION = "0.5.1"
6
+ VERSION = "0.6.0"
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwt-pq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcelo Almeida
@@ -91,7 +91,7 @@ metadata:
91
91
  source_code_uri: https://github.com/marcelopazzo/jwt-pq
92
92
  changelog_uri: https://github.com/marcelopazzo/jwt-pq/blob/main/CHANGELOG.md
93
93
  bug_tracker_uri: https://github.com/marcelopazzo/jwt-pq/issues
94
- documentation_uri: https://rubydoc.info/gems/jwt-pq
94
+ documentation_uri: https://docs.jwt-pq.marcelopazzo.com
95
95
  rubygems_mfa_required: 'true'
96
96
  post_install_message: |
97
97
  jwt-pq compiles liboqs from source during installation.