@@ -4,6 +4,158 @@ Changes
44v4 has many incompatibilities with v3. To see the full list of differences between
55v3 and v4, please read the [Changes-v4.md file](./Changes-v4.md). Coding Agents should read [MIGRATION-v4.md](./MICRATION-v4.md)
66
7+ v4.0.2 UNRELEASED
8+ * [jwk] BREAKING: `jwk.RegisterKeyImporter` now takes a
9+ `jwk.KeyImporter[T]` interface rather than a bare typed
10+ function. The v4.0.0 release exported `KeyImporter` and
11+ `KeyImportFunc` but neither type was reachable from the
12+ registration call, leaving `RegisterKeyImporter` inconsistent
13+ with every other extension hook in `jwk` (which all anchor on
14+ an interface plus a `FooFunc` adapter). The unused exported
15+ types were the leftover surface of the original design that
16+ didn't make it through to the registration call.
17+
18+ Correcting this is a compile-time break, but at the time of
19+ this release the only known consumers of `RegisterKeyImporter`
20+ are the `github.com/jwx-go/*` extension modules, which have
21+ been updated in lock-step. Landing the intended shape now,
22+ while the surface is still effectively private, costs less
23+ than carrying the inconsistency through the rest of v4's
24+ lifetime.
25+
26+ Wrap typed importer functions in `jwk.KeyImportFunc[T](...)`:
27+
28+ // before
29+ jwk.RegisterKeyImporter(func(src *MyKey) (jwk.Key, error) { ... })
30+
31+ // after
32+ jwk.RegisterKeyImporter(jwk.KeyImportFunc[*MyKey](func(src *MyKey) (jwk.Key, error) { ... }))
33+
34+ `KeyImporter[T any]` is now a generic interface; struct types
35+ implementing `Import(T) (jwk.Key, error)` can be passed
36+ directly without the adapter. (#2152)
37+
38+ * [jws] Coordinated RFC 7797 `b64=false` handling pass:
39+ `jws.Verify` rejects payloads with `b64=false` unless `b64`
40+ is also listed in `crit`; `jws.Sign` auto-declares `b64` in
41+ `crit` when emitting `b64=false`; `Message.MarshalJSON`
42+ honors `b64=false` instead of silently re-encoding;
43+ `jws.VerifyCompactFast` refuses any compact JWS carrying
44+ `b64` (the fast path doesn't process extension headers); and
45+ `b64` is now declared as a typed boolean header field rather
46+ than handled ad-hoc. (#2080, #2086, #2101, #2103, #2105)
47+
48+ * [jws] Reject malformed general-form JSON-serialized JWS:
49+ inputs with a top-level `header` member as a sibling of
50+ `signatures` are rejected (the spec only permits `header`
51+ inside per-signature objects), as are inputs whose `protected`
52+ member is a literal JSON object instead of a base64url-encoded
53+ string. (#2088, #2107)
54+
55+ * [jws] `jws.AlgorithmsForKey` failures from unclassifiable
56+ keys are now wrapped in a typed sentinel so callers can
57+ branch on "couldn't categorize this key" without string
58+ matching the error message. (#2109)
59+
60+ * [jws] Verify error-shape consistency: `VerifyCompactFast`
61+ refusals now match the `jws.VerifyError()` taxonomy used by
62+ the slow path, fan-out verify errors name the loose
63+ `WithKeySet` options that were tried, multi-signature `b64`
64+ mismatches name the offending signature index and conflicting
65+ value, and the compact `b64=false`+payload-contains-`.` error
66+ references RFC 7797 §5.2 and points at `WithDetachedPayload`.
67+ (#2082, #2084, #2113)
68+
69+ * [jws][jwe] `jws.VerifyMessage` and `jwe.DecryptMessage`
70+ observe context cancellation between loop iterations rather
71+ than only at boundaries. Long fan-out verify/decrypt loops
72+ now respond to a cancelled context promptly. (#2111, #2116)
73+
74+ * [jwe] Reject PBES2 messages whose `p2c` (iteration count)
75+ does not parse cleanly into int64 or violates the configured
76+ bound. The error now names the violated bound (min vs max)
77+ instead of the generic "out of range". (#2118)
78+
79+ * [jwe] `jwe.WithKey()` validates the alg-vs-key shape at
80+ option construction time rather than during encryption, so
81+ misuse surfaces at the call site instead of inside the
82+ encrypt loop. (#2120)
83+
84+ * [jwe] Decrypt error-path cleanup: per-key failures from
85+ key-set providers are surfaced via `errors.Join` so each
86+ underlying error remains inspectable; the joined error count
87+ is bounded to keep diagnostics readable; the redundant outer
88+ `Decrypt:` prefix is dropped; and the compression-cap error
89+ names the "decompressed" payload, the option, and the size.
90+ (#2122, #2124, #2126)
91+
92+ * [jwe] Add `jwe.WithDisabledKeyAlgorithms(...)` as a global
93+ policy hook (`jwe.Configure(...)` or per-call) for refusing
94+ specific key-management algorithms across all `jwe.Decrypt`
95+ calls. (#2128)
96+
97+ * [jwk] Stop duplicating JWK fields at the JWKS top level on
98+ parse. A JWKS whose top-level object carries fields with the
99+ same names as JWK members no longer copies those values into
100+ every key in `keys`. (#2132)
101+
102+ * [jwk] The probe pass tolerates duplicate JSON field names in
103+ the input. Previously the probe rejected duplicates even
104+ though the second-pass unmarshal accepts them, so valid
105+ inputs that round-tripped through tolerant decoders were
106+ inconsistently rejected at parse time. (#2138)
107+
108+ * [jwk] A custom `jwk.KeyParser` returning `(nil, nil)` now
109+ signals "continue to the next parser" rather than
110+ "successfully produced a nil key". Callers no longer end up
111+ with a nil `jwk.Key` from a successful parse when an
112+ extension returns the empty pair. (#2139)
113+
114+ * [jwk] Stream the JWKS `keys` array with a cap-before-allocate
115+ strategy. Inputs respect `WithMaxKeys` before any unbounded
116+ slice growth, and bounded-size JWKS no longer over-allocate
117+ based on attacker-controlled length hints. (#2136)
118+
119+ * [jwk] Additional typed errors on parse and export paths:
120+ `UnknownKeyTypeError{KeyType string}` (when input has no
121+ usable `kty` or names an unregistered family),
122+ `KeyTypeMismatchError` surfaced from `jwk.Export` when the
123+ source key doesn't match the destination type, and
124+ `ParseError` sentinel wrapping on `jwk.ParseKey` /
125+ `jwk.ParseKeyAs`. All follow the existing zero-value-struct +
126+ `errors.Is` / `errors.AsType[T]` pattern.
127+ (#2134, #2143, #2151)
128+
129+ * [jwt] `jwt.ParseRequest` no longer skips the request body
130+ when the request uses chunked transfer encoding. The
131+ Content-Length-based fast path previously bypassed
132+ `ParseForm` for chunked requests even when `WithFormKey` was
133+ supplied. (#2090)
134+
135+ * [jwt] `jwt.Settings` rejects out-of-range NumericDate
136+ precision values at configuration time instead of silently
137+ clamping. (#2092)
138+
139+ * [jwt] Pedantic mode (`jwt.WithPedanticParse(true)`) enforces
140+ that nested-envelope JWTs declare `cty=JWT`. Tokens missing
141+ `cty` or carrying a different value are rejected. (#2093)
142+
143+ * [jwt] `jwt.ParseInsecure` parses the loop-local payload split
144+ from the input rather than the original input bytes. Fixes a
145+ case where `ParseInsecure` could return a token assembled
146+ from a different signature segment than the one being
147+ inspected. (#2096)
148+
149+ * [jwt] `jwt.WithMaxDeltaIs(...)` and `jwt.WithMinDeltaIs(...)`
150+ reject tokens whose compared claim is missing rather than
151+ treating it as zero. Validation no longer silently succeeds
152+ when the claim isn't present on the token. (#2098)
153+
154+ * [jwt] `jwt.Validate` fast and slow paths now check
155+ `iat`/`exp`/`nbf` in the same order, so the same token
156+ reaches the same verdict regardless of which path evaluated
157+ it. (#2100)
158+
7159v4.0.1 28 Apr 2026
8160 * [jwt] `jwt.Parse` / `jwt.ParseRequest` only call `ParseForm` when
9161 `WithFormKey` is supplied. Previously the form body could be
0 commit comments