diff --git a/go.mod b/go.mod index f0f2c3ed..8c5651ee 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/hetznercloud/hcloud-go/v2 v2.44.0 github.com/jackc/pgx/v5 v5.10.0 github.com/johannesboyne/gofakes3 v0.0.0-20260208201424-4c385a1f6a73 - github.com/lestrrat-go/jwx/v4 v4.0.2 + github.com/lestrrat-go/jwx/v4 v4.1.0 github.com/magiconair/properties v1.8.10 github.com/moby/moby/api v1.55.0 github.com/peterbourgon/ff/v3 v3.4.0 diff --git a/go.sum b/go.sum index eff545b6..519199ca 100644 --- a/go.sum +++ b/go.sum @@ -306,6 +306,8 @@ github.com/lestrrat-go/dsig v1.3.0 h1:phjMOCXvYzhuIgn7Voe2rex8z166vGfxRxmqM25P9/ github.com/lestrrat-go/dsig v1.3.0/go.mod h1:RD2eOaidyPvpc7IJQoO3Qq52RWdy8ZcJs8lrOnoa1Kc= github.com/lestrrat-go/jwx/v4 v4.0.2 h1:T3lzN2dynOt6SuowT08ZWo/cPs3YsB0GHZSXKvfE0uQ= github.com/lestrrat-go/jwx/v4 v4.0.2/go.mod h1:F2a0rSyXsqLAL0orBZGOXrzQGv018Tx4eiEWWYR7Yzo= +github.com/lestrrat-go/jwx/v4 v4.1.0 h1:UFEq8srss6NnlgtrS+Qyo3ftnlAa4eJ8pFnD3FAgIj0= +github.com/lestrrat-go/jwx/v4 v4.1.0/go.mod h1:7fduHKOUbVCIdbUd5ResxRUiTazDFYCZ0Q65A8KcSzY= github.com/lestrrat-go/option/v3 v3.0.0-alpha1 h1:dvdzLwm/Ba5CJUF3jQP7w/iNYSLfy7yyh9XXNa1WjxI= github.com/lestrrat-go/option/v3 v3.0.0-alpha1/go.mod h1:5KSg20dfsKkNJtjDmaQRLZVXuUrzuCCcz/gbDK0pfKk= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= diff --git a/vendor/github.com/lestrrat-go/jwx/v4/AGENTS.md b/vendor/github.com/lestrrat-go/jwx/v4/AGENTS.md index 5d2c0181..f00ab6a9 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/AGENTS.md +++ b/vendor/github.com/lestrrat-go/jwx/v4/AGENTS.md @@ -244,15 +244,15 @@ Read linked doc BEFORE working in that area. No exceptions. | Trigger | Doc | |---------|-----| -| Looking up package APIs, types, functions | `.claude/docs/packages.md` | -| Running or writing tests, fuzz tests | `.claude/docs/testing.md` | -| Understanding package relationships, imports | `.claude/docs/dependencies.md` | -| Working with errors, error handling patterns | `.claude/docs/error-formatting.md` | -| Code generation, options pattern, extension points, JSON/base64 backends | `.claude/docs/internals.md` | +| Looking up package APIs, types, functions | `agents/docs/packages.md` | +| Running or writing tests, fuzz tests | `agents/docs/testing.md` | +| Understanding package relationships, imports | `agents/docs/dependencies.md` | +| Working with errors, error handling patterns | `agents/docs/error-formatting.md` | +| Code generation, options pattern, extension points, JSON/base64 backends | `agents/docs/internals.md` | | Extension modules (ES256K, Ed448, ML-DSA, ML-KEM, X448, compsig, asmbase64, jwkcache) | `docs/10-extensions.md` | -| Companion modules, CI templates, `/jwx-companion-bulk` | `.claude/docs/companions.md` | -| Reverse-syncing action versions from companion workflows into templates | `.claude/docs/companion-template-sync.md` | -| Cutting a release / tagging a new version | `.claude/docs/release.md` | +| Companion modules, CI templates, `/jwx-companion-bulk` | `agents/docs/companions.md` | +| Reverse-syncing action versions from companion workflows into templates | `agents/docs/companion-template-sync.md` | +| Cutting a release / tagging a new version | `agents/docs/release.md` | ## Cache Maintenance @@ -263,9 +263,9 @@ These docs cache repository state. Still read source before modifying code. | Doc | Update trigger | |-----|----------------| -| `.claude/docs/packages.md` | New/renamed/removed exported functions, types, or packages | -| `.claude/docs/testing.md` | Changes to test infrastructure, build tags, test helpers, fuzz targets | -| `.claude/docs/dependencies.md` | New internal imports between packages, new external dependencies | -| `.claude/docs/error-formatting.md` | New sentinel errors, changes to error wrapping patterns | -| `.claude/docs/internals.md` | Changes to generators, options YAML schema, registration points, multi-module layout | -| `.claude/docs/companions.md` | Changes to companion module tooling, templates, `companions.yaml`, or `.companions/` layout | +| `agents/docs/packages.md` | New/renamed/removed exported functions, types, or packages | +| `agents/docs/testing.md` | Changes to test infrastructure, build tags, test helpers, fuzz targets | +| `agents/docs/dependencies.md` | New internal imports between packages, new external dependencies | +| `agents/docs/error-formatting.md` | New sentinel errors, changes to error wrapping patterns | +| `agents/docs/internals.md` | Changes to generators, options YAML schema, registration points, multi-module layout | +| `agents/docs/companions.md` | Changes to companion module tooling, templates, `companions.yaml`, or `.companions/` layout | diff --git a/vendor/github.com/lestrrat-go/jwx/v4/Changes b/vendor/github.com/lestrrat-go/jwx/v4/Changes index 84b8f30f..2a9976e0 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/Changes +++ b/vendor/github.com/lestrrat-go/jwx/v4/Changes @@ -4,6 +4,81 @@ Changes v4 has many incompatibilities with v3. To see the full list of differences between v3 and v4, please read the [Changes-v4.md file](./Changes-v4.md). Coding Agents should read [MIGRATION-v4.md](./MICRATION-v4.md) +v4.1.0 1 July 2026 + * [jws] `jws.Verify` now rejects a JWS whose protected header `"alg"` + does not match the algorithm used to verify it (previously the + non-fast-path `jws.Verify` ignored this). In particular the polymorphic + `"EdDSA"` and the fully-specified `"Ed25519"`/`"Ed448"` identifiers + (RFC 9864) are now treated as distinct, so cross-identifier verification + that previously succeeded is rejected by default — this is an RFC 9864 + conformance fix. Pass the new `jws.WithSkipAlgorithmMatch(true)` option to + restore the lenient behavior. (#2212) + + * [jws][jwt] Closed a gap where `jwt.Parse` and `jws.VerifyCompactFast` + (the compact-JWS fast path) were more lenient than `jws.Verify` about + the protected header. A token whose protected header has duplicate + parameter names (e.g. two `alg` entries) or a non-string `typ`/`kid`/ + `cty` value used to verify successfully on the fast path even though + `jws.Verify` rejects it; `jwt.Parse` now rejects it too, so such a token + can no longer slip through one entry point but not the other. Well-formed + tokens are unaffected. + + Direct callers of `jws.VerifyCompactFast` see one further change: a + protected header carrying anything beyond `alg` plus an optional + `typ`/`kid`/`cty` — for example `x5t`, `jku`, or a custom parameter — + now returns the new sentinel `jws.ErrNonMinimalHeader()` instead of + verifying. Route those through `jws.Verify` (which is what `jwt.Parse` + now does for you automatically). (#2236) + + * [jws] Passing a non-nil payload together with `jws.WithDetachedPayload` + is now an error instead of being silently accepted; a detached payload + must be supplied only through the option. (#2211) + + * [jwe] `jwe.Decrypt` now rejects a message whose authentication tag or + initialization vector is not the length required by the content + encryption algorithm, rather than attempting to decrypt it. (#2207) + + * [jwe] Messages using AES-CBC-HMAC with a very large amount of additional + authenticated data now decrypt correctly; the AAD bit length is computed + as a 64-bit value rather than potentially overflowing. (#2198) + + * [jwe] The `zip` (compression) parameter is honored only from the + protected header. A `zip` set in a per-recipient or unprotected header + is ignored and can no longer trigger decompression. (#2206) + + * [jwe] PBES2 decryption now rejects a `p2s` (salt) shorter than the + 8-octet minimum required by RFC 7518. (#2208) + + * [jwe] Direct key agreement (`alg: "dir"`, including ML-KEM) now rejects + a message that also carries a non-empty `encrypted_key`, which direct + mode does not allow. (#2209, #2223) + + * [jwk] An Ed25519 key whose byte length is wrong is now rejected at + import/parse, instead of being accepted as an unusable key. (#2201) + + * [jwk] An EC key with a nil curve, or with a private scalar `d` larger + than the curve order, is now rejected at import. (#2199) + + * [jwk] The `use` value is now validated when a key is parsed; an + unrecognized value is rejected. (#2205) + + * [jwk] Computing a thumbprint with a hash whose implementation is not + linked into the binary now returns an error instead of panicking. (#2200) + + * [jwt] `jwt.Validate` no longer panics when given a nil clock or a nil + validator. (#2202) + + * [jwt] Reusing a token value across parses no longer carries over private + claims from a previous parse; they are cleared on unmarshal. (#2203) + + * [jwt] A `jwt.Settings()` call changes only the options you pass; + previously, settings you did not specify (such as flatten-audience or + pedantic mode) were reset to their defaults. (#2204) + + * [jws][jwk][jwe][jwt] A custom JSON field decoder that registers or + unregisters a decoder while it runs no longer deadlocks; the registry + read lock is released before your decoder is invoked. (#2197) + v4.0.2 7 May 2026 * [jwk] BREAKING: `jwk.RegisterKeyImporter` now takes a `jwk.KeyImporter[T]` interface rather than a bare typed diff --git a/vendor/github.com/lestrrat-go/jwx/v4/MIGRATION.md b/vendor/github.com/lestrrat-go/jwx/v4/MIGRATION.md index 90c58cd3..0038294a 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/MIGRATION.md +++ b/vendor/github.com/lestrrat-go/jwx/v4/MIGRATION.md @@ -261,6 +261,8 @@ type Fetcher interface { > > `jwkfetch.Client` applies the whitelist to both the initial URL and every HTTP redirect target, so a hostile JWKS host cannot 302 into an off-allowlist URL. This redirect-hop enforcement only applies when the configured `HTTPClient` is a `*http.Client`; if you supply a custom transport, you are responsible for policing redirects yourself. > +> If you migrate a `RegexpWhitelist`, **anchor your patterns** — they are **not** anchored for you. `example\.com` matches anywhere in the URL and also allows `https://example.com.attacker.com/evil`, reopening the SSRF / key-substitution hole. Write `^https://example\.com/` (anchor the start with `^`, escape the dots, terminate the host with `/`), or use `MapWhitelist` when the issuer URLs are known exactly. +> > `jws.WithVerifyAuto(nil)` / `jwt.WithVerifyAuto(nil)` is no longer supported — both error at jku-verification time rather than silently using any default. **Default behavior is equivalent to v3 for trusted, hard-coded URLs.** Both v3's `jwk.Fetch()` and v4's `jwkfetch.NewClient().Fetch()` permit every URL by default — the right choice when the URL is a compile-time constant or comes from trusted configuration. You generally do not need to pass `WithWhitelist` to migrate a hard-coded-URL call site. The SSRF risk above is specific to `jku`-style verification, where the URL originates in the untrusted JWS header. diff --git a/vendor/github.com/lestrrat-go/jwx/v4/README.md b/vendor/github.com/lestrrat-go/jwx/v4/README.md index 0b77ceb2..88da9457 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/README.md +++ b/vendor/github.com/lestrrat-go/jwx/v4/README.md @@ -1,4 +1,4 @@ -# github.com/lestrrat-go/jwx/v4 [![CI](https://github.com/lestrrat-go/jwx/actions/workflows/ci.yml/badge.svg)](https://github.com/lestrrat-go/jwx/actions/workflows/ci.yml) [![Go Reference](https://pkg.go.dev/badge/github.com/lestrrat-go/jwx/v4.svg)](https://pkg.go.dev/github.com/lestrrat-go/jwx/v4) +# github.com/lestrrat-go/jwx/v4 [![CI](https://github.com/lestrrat-go/jwx/actions/workflows/ci.yml/badge.svg)](https://github.com/lestrrat-go/jwx/actions/workflows/ci.yml) [![Go Reference](https://pkg.go.dev/badge/github.com/lestrrat-go/jwx/v4.svg)](https://pkg.go.dev/github.com/lestrrat-go/jwx/v4) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/lestrrat-go/jwx) Go module implementing various JWx (JWA/JWE/JWK/JWS/JWT, otherwise known as JOSE) technologies. @@ -15,6 +15,17 @@ If you are using this module in your product or your company, please add your pr go get github.com/lestrrat-go/jwx/v4 ``` +## Claude Code Skill + +If you use [Claude Code](https://claude.com/claude-code), install the bundled `jwx-dev-v4` skill so the assistant can guide you through using this library — picking algorithms, parsing/signing JWTs, working with JWS/JWE/JWK, and avoiding common footguns: + +``` +/plugin marketplace add lestrrat-go/jwx +/plugin install jwx-dev-v4 +``` + +The skill is scoped to **v4** only. It is intended for developers *using* jwx, not for working on the library itself. + # Migrating from v3 If you are migrating from `github.com/lestrrat-go/jwx/v3`, see [`MIGRATION.md`](MIGRATION.md) for a step-by-step guide with before/after code examples. For a complete list of breaking changes and new features, see [`Changes-v4.md`](Changes-v4.md). @@ -108,13 +119,13 @@ func Example() { // Encrypt and Decrypt arbitrary payload with JWE! { - encrypted, err := jwe.Encrypt(payloadLoremIpsum, jwe.WithKey(jwa.RSA_OAEP(), jwkRSAPublicKey)) + encrypted, err := jwe.Encrypt(payloadLoremIpsum, jwe.WithKey(jwa.RSA_OAEP_256(), jwkRSAPublicKey)) if err != nil { fmt.Printf("failed to encrypt payload: %s\n", err) return } - decrypted, err := jwe.Decrypt(encrypted, jwe.WithKey(jwa.RSA_OAEP(), jwkRSAPrivateKey)) + decrypted, err := jwe.Decrypt(encrypted, jwe.WithKey(jwa.RSA_OAEP_256(), jwkRSAPrivateKey)) if err != nil { fmt.Printf("failed to decrypt payload: %s\n", err) return diff --git a/vendor/github.com/lestrrat-go/jwx/v4/cert/chain.go b/vendor/github.com/lestrrat-go/jwx/v4/cert/chain.go index ca940836..3e4a175f 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/cert/chain.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/cert/chain.go @@ -94,6 +94,17 @@ func (cc *Chain) UnmarshalJSON(data []byte) error { // Get returns the n-th ASN.1 DER + base64 encoded certificate // stored. `false` will be returned in the second argument if // the corresponding index is out of range. +// +// The returned slice aliases the Chain's internal storage and MUST be +// treated as read-only: writing into it in place corrupts the stored +// chain. This is intentionally not defended with a defensive copy. The +// returned bytes are only ever consumed read-only (base64-decoded, parsed +// via x509, compared, re-marshaled), the alias never crosses a trust +// boundary (a Chain is the caller's own object; untrusted x5c input is +// validated and normalized through Add, not Get), and the only way to +// trigger corruption is the caller writing into bytes it was handed by a +// read accessor — a self-inflicted bug, not an exploitable path. Do NOT +// add a bytes.Clone here. func (cc *Chain) Get(index int) ([]byte, bool) { if index < 0 || index >= len(cc.certificates) { return nil, false diff --git a/vendor/github.com/lestrrat-go/jwx/v4/companions.yaml b/vendor/github.com/lestrrat-go/jwx/v4/companions.yaml index 2abcb03c..59db2c55 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/companions.yaml +++ b/vendor/github.com/lestrrat-go/jwx/v4/companions.yaml @@ -24,7 +24,6 @@ modules: - name: jwkfetch repo: git@github.com:jwx-go/jwkfetch.git branch: develop/v4 - runtests: false - name: jwxfilter repo: git@github.com:jwx-go/jwxfilter.git branch: develop/v4 diff --git a/vendor/github.com/lestrrat-go/jwx/v4/internal/json/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v4/internal/json/BUILD.bazel index 99230eb3..0b622a39 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/internal/json/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v4/internal/json/BUILD.bazel @@ -1,4 +1,4 @@ -load("@rules_go//go:def.bzl", "go_library") +load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "json", @@ -16,3 +16,12 @@ alias( actual = ":json", visibility = ["//:__subpackages__"], ) + +go_test( + name = "json_test", + srcs = ["registry_test.go"], + deps = [ + ":json", + "@com_github_stretchr_testify//require", + ], +) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/internal/json/registry.go b/vendor/github.com/lestrrat-go/jwx/v4/internal/json/registry.go index acc3a753..b8be182b 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/internal/json/registry.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/internal/json/registry.go @@ -143,10 +143,15 @@ func (r *Registry) Unregister(name string) { // given field name. If no decoder is registered, the raw value is decoded // into any. func (r *Registry) Decode(name string, raw RawMessage) (any, error) { + // Snapshot the decoder under the read lock, then release the lock before + // invoking it. The user-supplied decoder may re-entrantly call + // Register/Unregister on this same registry (which take a write lock), and + // holding the read lock across the call would deadlock (RLock -> Lock). r.mu.RLock() - defer r.mu.RUnlock() + ctr, ok := r.ctrs[name] + r.mu.RUnlock() - if ctr, ok := r.ctrs[name]; ok { + if ok { v, err := ctr.Decode([]byte(raw)) if err != nil { return nil, fmt.Errorf(`failed to decode field %s: %w`, name, err) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/internal/keyconv/keyconv.go b/vendor/github.com/lestrrat-go/jwx/v4/internal/keyconv/keyconv.go index e0eb8d45..3e935394 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/internal/keyconv/keyconv.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/internal/keyconv/keyconv.go @@ -103,16 +103,30 @@ func Ed25519PrivateKey(src any) (*ed25519.PrivateKey, error) { if !ok { // Export may return ed25519.PrivateKey (not pointer) if v, ok := rawV.(ed25519.PrivateKey); ok { + if len(v) != ed25519.PrivateKeySize { + return nil, fmt.Errorf(`keyconv: invalid ed25519.PrivateKey length %d from export, expected %d`, len(v), ed25519.PrivateKeySize) + } return &v, nil } return nil, fmt.Errorf(`keyconv: expected ed25519.PrivateKey from export, got %T`, rawV) } + // Guard against a malformed exported key reaching ed25519.PrivateKey.Public(), + // which slices priv[32:] and panics when the key is not the expected size. + if ptr == nil || len(*ptr) != ed25519.PrivateKeySize { + return nil, fmt.Errorf(`keyconv: invalid ed25519.PrivateKey from export, expected length %d`, ed25519.PrivateKeySize) + } return ptr, nil } switch src := src.(type) { case *ed25519.PrivateKey: + if src == nil || len(*src) != ed25519.PrivateKeySize { + return nil, fmt.Errorf(`keyconv: invalid ed25519.PrivateKey length, expected %d`, ed25519.PrivateKeySize) + } return src, nil case ed25519.PrivateKey: + if len(src) != ed25519.PrivateKeySize { + return nil, fmt.Errorf(`keyconv: invalid ed25519.PrivateKey length %d, expected %d`, len(src), ed25519.PrivateKeySize) + } return &src, nil default: return nil, fmt.Errorf(`keyconv: expected ed25519.PrivateKey or *ed25519.PrivateKey, got %T`, src) @@ -128,23 +142,43 @@ func Ed25519PublicKey(src any) (*ed25519.PublicKey, error) { src = pk } + // Guard against malformed private keys before calling Public(), which + // slices priv[32:] and panics when the key is not ed25519.PrivateKeySize. switch key := src.(type) { case ed25519.PrivateKey: + if len(key) != ed25519.PrivateKeySize { + return nil, fmt.Errorf(`keyconv: invalid ed25519.PrivateKey length %d, expected %d`, len(key), ed25519.PrivateKeySize) + } src = key.Public() case *ed25519.PrivateKey: + if key == nil || len(*key) != ed25519.PrivateKeySize { + return nil, fmt.Errorf(`keyconv: invalid ed25519.PrivateKey length, expected %d`, ed25519.PrivateKeySize) + } src = key.Public() } switch src := src.(type) { case ed25519.PublicKey: + if len(src) != ed25519.PublicKeySize { + return nil, fmt.Errorf(`keyconv: invalid ed25519.PublicKey length %d, expected %d`, len(src), ed25519.PublicKeySize) + } return &src, nil case *ed25519.PublicKey: + if src == nil || len(*src) != ed25519.PublicKeySize { + return nil, fmt.Errorf(`keyconv: invalid ed25519.PublicKey length, expected %d`, ed25519.PublicKeySize) + } return src, nil case *crypto.PublicKey: + if src == nil { + return nil, fmt.Errorf(`failed to retrieve ed25519.PublicKey out of nil *crypto.PublicKey`) + } tmp, ok := (*src).(ed25519.PublicKey) if !ok { return nil, fmt.Errorf(`failed to retrieve ed25519.PublicKey out of *crypto.PublicKey`) } + if len(tmp) != ed25519.PublicKeySize { + return nil, fmt.Errorf(`keyconv: invalid ed25519.PublicKey length %d, expected %d`, len(tmp), ed25519.PublicKeySize) + } return &tmp, nil case crypto.PublicKey: tmp, ok := src.(ed25519.PublicKey) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/internal/pool/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v4/internal/pool/BUILD.bazel index f93c8f24..99dddc5f 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/internal/pool/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v4/internal/pool/BUILD.bazel @@ -21,9 +21,10 @@ alias( go_test( name = "pool_test", - srcs = ["byte_slice_test.go"], - deps = [ - ":pool", - "@com_github_stretchr_testify//require", + srcs = [ + "byte_slice_test.go", + "error_slice_test.go", ], + embed = [":pool"], + deps = ["@com_github_stretchr_testify//require"], ) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/internal/pool/error_slice.go b/vendor/github.com/lestrrat-go/jwx/v4/internal/pool/error_slice.go index 4f1675c1..cf44ec59 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/internal/pool/error_slice.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/internal/pool/error_slice.go @@ -7,7 +7,12 @@ func allocErrorSlice() []error { } func freeErrorSlice(s []error) []error { - // Reset the slice to its zero value + // Defensive: scrub the entire backing array, not just s[:len(s)]. The + // pooled errors may reference request data; clearing the full capacity + // keeps stale values from staying reachable in the pool's backing + // storage until they happen to be overwritten. + s = s[:cap(s)] + clear(s) return s[:0] } diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwa/jwa.go b/vendor/github.com/lestrrat-go/jwx/v4/jwa/jwa.go index 8b2825b6..f9d72a85 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwa/jwa.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwa/jwa.go @@ -35,12 +35,31 @@ func ErrInvalidKeyAlgorithm() error { } func formatInvalidKeyAlgorithmValue(v string) string { - runes := []rune(v) - if len(runes) <= maxKeyAlgorithmErrorPreview { + // Collect at most maxKeyAlgorithmErrorPreview decoded runes without + // materializing []rune(v) for the whole string. For an + // attacker-controllable v this bounds the transient allocation to + // the preview window rather than the entire input. + // + // Ranging over a string decodes invalid UTF-8 to utf8.RuneError + // (U+FFFD), so the preview is byte-for-byte identical to the naive + // string([]rune(v)[:maxKeyAlgorithmErrorPreview]) implementation, + // including the U+FFFD substitution for malformed bytes within the + // window. + preview := make([]rune, 0, maxKeyAlgorithmErrorPreview) + truncated := false + for _, r := range v { + if len(preview) == maxKeyAlgorithmErrorPreview { + truncated = true + break + } + preview = append(preview, r) + } + if !truncated { + // v has maxKeyAlgorithmErrorPreview runes or fewer: render in full. return fmt.Sprintf("%q", v) } - return fmt.Sprintf("%q", string(runes[:maxKeyAlgorithmErrorPreview])+`...`) + return fmt.Sprintf("%q", string(preview)+`...`) } // algorithmKind tags entries in the shared algRegistry so the diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwe/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v4/jwe/BUILD.bazel index d221b83d..ca4e2dc9 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwe/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwe/BUILD.bazel @@ -40,7 +40,10 @@ go_library( go_test( name = "jwe_test", srcs = [ + "accessors_test.go", + "aead_tag_length_test.go", "bench_encrypt_test.go", + "directcek_test.go", "fuzz_test.go", "gh402_test.go", "headers_nil_test.go", @@ -53,6 +56,7 @@ go_test( "options_gen_test.go", "recipient_headers_test.go", "speed_test.go", + "zip_protected_only_test.go", ], embed = [":jwe"], deps = [ diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwe/decrypt.go b/vendor/github.com/lestrrat-go/jwx/v4/jwe/decrypt.go index 98384798..174c29b1 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwe/decrypt.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwe/decrypt.go @@ -28,6 +28,27 @@ func decryptCEK(alg jwa.KeyEncryptionAlgorithm, key any, msg *Message, recipient algStr := alg.String() recipientKey := recipient.EncryptedKey() + // Direct-mode key management (RFC 7518 §4.5 "dir", §4.6 bare ECDH-ES, and + // direct ML-KEM) derives the CEK without an encrypted key, so the JWE + // Encrypted Key must be the empty octet sequence. Enforce this here, before + // the KeyDecrypter branch, so a tampered message carrying a stray + // encrypted_key is rejected on every path -- including caller-supplied + // custom decrypters. IsMLKEMDirect consults a runtime registry that is + // empty unless the ML-KEM companion module is imported, so this clause is a + // no-op in builds without ML-KEM. + directMode := jwebb.IsDirect(algStr) || jwebb.IsMLKEMDirect(algStr) + if !directMode && jwebb.IsECDHES(algStr) { + // ECDH-ES+A*KW (keywrap == true) legitimately carries an encrypted_key; + // only bare ECDH-ES is direct. Reuse the same helper the ECDH-ES path + // uses to determine keywrap. + if _, _, keywrap, err := jwebb.KeyEncryptionECDHESKeySize(algStr, ctx.ctalg.String()); err == nil { + directMode = !keywrap + } + } + if directMode && len(recipientKey) != 0 { + return nil, fmt.Errorf(`jwe: decrypt key: %q requires an empty encrypted_key`, algStr) + } + if kd, ok := key.(KeyDecrypter); ok { return kd.DecryptKey(alg, recipientKey, recipient, msg) } @@ -57,6 +78,8 @@ func decryptCEK(alg jwa.KeyEncryptionAlgorithm, key any, msg *Message, recipient } func decryptKeyDirect(recipientKey []byte, alg string, key any) ([]byte, error) { + // The empty-encrypted_key invariant for "dir" (RFC 7518 §4.5) is enforced + // in decryptCEK ahead of the KeyDecrypter branch, so it covers every path. cek, err := requireByteKey(key, alg) if err != nil { return nil, err @@ -132,6 +155,12 @@ func decryptKeyPBES2(recipientKey []byte, alg string, key any, headers Headers, return nil, fmt.Errorf(`jwe: decrypt key: failed to decode 'p2s': %w`, err) } + // RFC 7518 §4.8.1.1 requires the salt input to be at least 8 octets. + // This is a hard floor and is not loosenable via an option. + if len(saltBytes) < 8 { + return nil, fmt.Errorf(`jwe: decrypt key: invalid 'p2s' value: salt is %d octets, RFC 7518 §4.8.1.1 requires at least 8`, len(saltBytes)) + } + salt := []byte(alg) salt = append(salt, byte(0)) salt = append(salt, saltBytes...) @@ -175,6 +204,12 @@ func decryptKeyECDHES(recipientKey []byte, alg string, ctalg jwa.ContentEncrypti return nil, fmt.Errorf(`jwe: decrypt key: failed to determine ECDH-ES key size: %w`, err) } + // RFC 7518 §4.6: for bare ECDH-ES (keywrap == false) the CEK is derived + // directly and the JWE Encrypted Key must be the empty octet sequence. + // That invariant is enforced in decryptCEK ahead of the KeyDecrypter + // branch so it covers every path; ECDH-ES+A*KW legitimately carries an + // encrypted_key. + // Extract ephemeral public key from headers epkV, ok := headers.Field(EphemeralPublicKeyKey) if !ok { diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwe/internal/aescbc/aescbc.go b/vendor/github.com/lestrrat-go/jwx/v4/jwe/internal/aescbc/aescbc.go index 14bbd5fa..b7d94feb 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwe/internal/aescbc/aescbc.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwe/internal/aescbc/aescbc.go @@ -172,9 +172,17 @@ func (c Hmac) Overhead() int { return c.blockCipher.BlockSize() + c.tlen } -func (c Hmac) ComputeAuthTag(aad, nonce, ciphertext []byte) ([]byte, error) { +// aadBitLength returns the big-endian 64-bit AAD bit-length ("AL") field for +// CBC-HMAC. The multiply is done in uint64 so a large AAD (>256MiB) does not +// overflow the int multiplication on 32-bit platforms before the cast. +func aadBitLength(n int) [8]byte { var buf [8]byte - binary.BigEndian.PutUint64(buf[:], uint64(len(aad)*8)) + binary.BigEndian.PutUint64(buf[:], uint64(n)*8) + return buf +} + +func (c Hmac) ComputeAuthTag(aad, nonce, ciphertext []byte) ([]byte, error) { + buf := aadBitLength(len(aad)) h := hmac.New(c.hash, c.integrityKey) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwe/internal/cipher/cipher.go b/vendor/github.com/lestrrat-go/jwx/v4/jwe/internal/cipher/cipher.go index 0351482a..6ddd071f 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwe/internal/cipher/cipher.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwe/internal/cipher/cipher.go @@ -149,6 +149,17 @@ func (c AesContentCipher) Decrypt(cek, iv, ciphertxt, tag, aad []byte) (plaintex } }() + // Enforce the AEAD wire tag and IV lengths before concatenating. Without + // this, bytes can be shifted between the wire ciphertext and tag fields to + // produce a byte-identical AEAD input, yielding serialized ciphertext<->tag + // malleability (exploitable on AES-GCM). + if len(tag) != c.TagSize() { + return nil, fmt.Errorf(`failed to decrypt: invalid tag size (got %d, expected %d)`, len(tag), c.TagSize()) + } + if len(iv) != aead.NonceSize() { + return nil, fmt.Errorf(`failed to decrypt: invalid iv size (got %d, expected %d)`, len(iv), aead.NonceSize()) + } + combined := make([]byte, len(ciphertxt)+len(tag)) copy(combined, ciphertxt) copy(combined[len(ciphertxt):], tag) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwe/jwe.go b/vendor/github.com/lestrrat-go/jwx/v4/jwe/jwe.go index 1420f900..3f36af24 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwe/jwe.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwe/jwe.go @@ -774,7 +774,10 @@ func (dc *decryptContext) decryptContent(msg *Message, alg jwa.KeyEncryptionAlgo return nil, fmt.Errorf(`jwe.Decrypt: failed to decrypt payload: %w`, err) } - if v, ok := h2.Compression(); ok && v == jwa.Deflate() { + // Read compression only from the protected header. The "zip" header in + // the unprotected/per-recipient header is not covered by the AEAD, so + // honoring it would let an attacker flip post-decryption decompression. + if v, ok := protectedHeaders.Compression(); ok && v == jwa.Deflate() { buf, err := uncompress(plaintext, dc.maxDecompressBufferSize) if err != nil { return nil, fmt.Errorf(`jwe.Decrypt: failed to uncompress payload: %w`, err) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v4/jwk/BUILD.bazel index 732751f7..96d8a3b5 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/BUILD.bazel @@ -27,6 +27,7 @@ go_library( "set.go", "symmetric.go", "symmetric_gen.go", + "thumbprint.go", "usage.go", "x509.go", ], @@ -59,6 +60,7 @@ go_test( "options_gen_test.go", "probe_bench_test.go", "set_test.go", + "usage_parse_test.go", "x5c_test.go", ], data = glob(["testdata/**"]), diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/akp.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/akp.go index edd8b317..a85494bb 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/akp.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/akp.go @@ -82,6 +82,9 @@ func akpThumbprint(hash crypto.Hash, alg, pub string) []byte { // types tolerate a missing `alg` because their canonical thumbprint // input doesn't include it. func (k *akpPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { + if err := availableHash(hash); err != nil { + return nil, err + } k.mu.RLock() defer k.mu.RUnlock() @@ -101,6 +104,9 @@ func (k *akpPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { // [akpPublicKey]. AKP keys hash the canonical JSON form `{alg, kty, pub}` // per RFC 9802 §7; both `alg` and `pub` are required at thumbprint time. func (k *akpPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) { + if err := availableHash(hash); err != nil { + return nil, err + } k.mu.RLock() defer k.mu.RUnlock() diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/akp_gen.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/akp_gen.go index 3cd11f76..85567a49 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/akp_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/akp_gen.go @@ -477,6 +477,7 @@ func (h *akpPublicKey) UnmarshalJSON(buf []byte) error { return fmt.Errorf(`invalid kty value for RSAPublicKey (%s)`, val) } case AlgorithmKey: + // "alg" is an informational hint stored as-is, not validated against the key type here; see [ParseKey] for rationale. var s string if err := json.UnmarshalDecode(dec, &s); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err) @@ -497,9 +498,16 @@ func (h *akpPublicKey) UnmarshalJSON(buf []byte) error { } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { + val, err := json.ReadNextStringToken(dec, h.dc) + if err != nil { + return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) + } + var acceptor KeyUsageType + if err := acceptor.Accept(val); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } + tmp := acceptor.String() + h.keyUsage = &tmp case AKPPubKey: if err := json.AssignNextBytesToken(&h.pub, dec); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, AKPPubKey, err) @@ -1224,6 +1232,7 @@ func (h *akpPrivateKey) UnmarshalJSON(buf []byte) (retErr error) { return fmt.Errorf(`invalid kty value for RSAPublicKey (%s)`, val) } case AlgorithmKey: + // "alg" is an informational hint stored as-is, not validated against the key type here; see [ParseKey] for rationale. var s string if err := json.UnmarshalDecode(dec, &s); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err) @@ -1244,9 +1253,16 @@ func (h *akpPrivateKey) UnmarshalJSON(buf []byte) (retErr error) { } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { + val, err := json.ReadNextStringToken(dec, h.dc) + if err != nil { + return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) + } + var acceptor KeyUsageType + if err := acceptor.Accept(val); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } + tmp := acceptor.String() + h.keyUsage = &tmp case AKPPrivKey: if err := json.AssignNextBytesToken(&h.priv, dec); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, AKPPrivKey, err) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/ecdsa.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/ecdsa.go index 3af636e4..f32bdad3 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/ecdsa.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/ecdsa.go @@ -103,6 +103,19 @@ func (k *ecdsaPrivateKey) Import(rawKey *ecdsa.PrivateKey) error { return fmt.Errorf(`jwk: %w`, err) } + // validateECDSAPoint guarantees a non-nil Curve. Bound D the same way + // the X/Y coordinates are bounded: AllocECPointBuffer writes D into a + // fixed-size buffer via big.Int.FillBytes, which panics on oversized + // input. A valid private scalar is in [1, n-1], so reject non-positive + // or over-wide values here. + bits := rawKey.Curve.Params().BitSize + if rawKey.D.Sign() <= 0 { + return fmt.Errorf(`jwk: invalid ECDSA private key: d must be a positive scalar`) + } + if rawKey.D.BitLen() > bits { + return fmt.Errorf(`jwk: invalid ECDSA private key: d is %d bits, exceeds curve %q field size of %d bits`, rawKey.D.BitLen(), rawKey.Curve.Params().Name, bits) + } + xbuf := ecutil.AllocECPointBuffer(rawKey.PublicKey.X, rawKey.Curve) ybuf := ecutil.AllocECPointBuffer(rawKey.PublicKey.Y, rawKey.Curve) dbuf := ecutil.AllocECPointBuffer(rawKey.D, rawKey.Curve) @@ -169,6 +182,13 @@ func buildECDSAPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf, ybuf []byte) (*ec // failing closed is preferable to silently accepting unvalidated // points. func validateECDSAPoint(crv elliptic.Curve, x, y *big.Int) error { + // A nil curve has no field parameters; every check below dereferences + // crv.Params(), so guard here to avoid a nil-pointer panic on + // hand-crafted keys with an unset Curve. + if crv == nil { + return fmt.Errorf(`invalid ECDSA key: nil curve`) + } + if x.Sign() == 0 && y.Sign() == 0 { return fmt.Errorf(`invalid ECDSA public key: identity point is not a valid public key`) } @@ -424,6 +444,9 @@ func ecdsaThumbprint(hash crypto.Hash, crv, x, y string) []byte { // Thumbprint returns the JWK thumbprint using the indicated // hashing algorithm, according to RFC 7638 func (k *ecdsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { + if err := availableHash(hash); err != nil { + return nil, err + } k.mu.RLock() defer k.mu.RUnlock() @@ -448,6 +471,9 @@ func (k *ecdsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { // Thumbprint returns the JWK thumbprint using the indicated // hashing algorithm, according to RFC 7638 func (k *ecdsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) { + if err := availableHash(hash); err != nil { + return nil, err + } k.mu.RLock() defer k.mu.RUnlock() @@ -503,6 +529,26 @@ func ecdsaValidateKey(k interface { } if checkPrivate { + // This validates only the length of "d"; it intentionally does NOT + // verify that "d" derives the stored public point (d*G == (x, y)), nor + // that the scalar is in canonical range (0 < d < N). Neither omission is + // exploitable: + // + // - A public point that does not match "d" is self-defeating: every + // operation uses "d" (ECDSA signs under d*G; ECDH derives the shared + // secret from d) while (x, y) is advertised metadata, so the key + // produces signatures/ciphertext that fail to verify/decrypt against + // its own advertised public key. It cannot make a verification or + // decryption wrongly succeed. + // - An out-of-range scalar is left for the signing path (crypto/ecdsa) + // to handle as the authority on scalar validity: depending on the Go + // version and key path it either rejects the key or treats the scalar + // as its in-range equivalent. Either way the only outcomes are that + // the key's own operations fail or behave as some in-range key — + // never that an invalid scalar forges a signature or decryption that + // verifies against a key the holder does not legitimately control. + // + // Do NOT add a d*G == (x, y) or a 0 < d < N check here. if priv, ok := k.(keyWithD); ok { if d, ok := priv.D(); !ok || len(d) != keySize { return fmt.Errorf(`invalid "d" length (%d) for curve %q`, len(d), crv.Params().Name) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/ecdsa_gen.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/ecdsa_gen.go index 98668e9d..ab5df864 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/ecdsa_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/ecdsa_gen.go @@ -546,6 +546,7 @@ func (h *ecdsaPublicKey) UnmarshalJSON(buf []byte) error { return fmt.Errorf(`invalid kty value for RSAPublicKey (%s)`, val) } case AlgorithmKey: + // "alg" is an informational hint stored as-is, not validated against the key type here; see [ParseKey] for rationale. var s string if err := json.UnmarshalDecode(dec, &s); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err) @@ -572,9 +573,16 @@ func (h *ecdsaPublicKey) UnmarshalJSON(buf []byte) error { } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { + val, err := json.ReadNextStringToken(dec, h.dc) + if err != nil { + return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) + } + var acceptor KeyUsageType + if err := acceptor.Accept(val); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } + tmp := acceptor.String() + h.keyUsage = &tmp case ECDSAXKey: if err := json.AssignNextBytesToken(&h.x, dec); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, ECDSAXKey, err) @@ -1402,6 +1410,7 @@ func (h *ecdsaPrivateKey) UnmarshalJSON(buf []byte) (retErr error) { return fmt.Errorf(`invalid kty value for RSAPublicKey (%s)`, val) } case AlgorithmKey: + // "alg" is an informational hint stored as-is, not validated against the key type here; see [ParseKey] for rationale. var s string if err := json.UnmarshalDecode(dec, &s); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err) @@ -1432,9 +1441,16 @@ func (h *ecdsaPrivateKey) UnmarshalJSON(buf []byte) (retErr error) { } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { + val, err := json.ReadNextStringToken(dec, h.dc) + if err != nil { + return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) + } + var acceptor KeyUsageType + if err := acceptor.Accept(val); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } + tmp := acceptor.String() + h.keyUsage = &tmp case ECDSAXKey: if err := json.AssignNextBytesToken(&h.x, dec); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, ECDSAXKey, err) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/interface_gen.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/interface_gen.go index d138668b..026b1549 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/interface_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/interface_gen.go @@ -65,7 +65,9 @@ type Key interface { Validate() error // Thumbprint returns the JWK thumbprint using the indicated - // hashing algorithm, according to RFC 7638 + // hashing algorithm, according to RFC 7638. An error is returned if the + // hash is unavailable (e.g. crypto.Hash(0) or a hash whose package has + // not been imported); it does not panic. Thumbprint(crypto.Hash) ([]byte, error) // Keys returns a list of the keys contained in this jwk.Key. diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/jwk.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/jwk.go index e8c77468..df85347c 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/jwk.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/jwk.go @@ -83,6 +83,12 @@ func init() { // // Import validates the populated JWK before returning it. Malformed raw // keys fail at import time instead of being returned for later validation. +// +// Import expects well-formed raw keys, such as those produced by the +// standard library generators (e.g. ecdsa.GenerateKey, rsa.GenerateKey) +// or by parsing an encoded key. Hand-crafting raw key structs with +// internally inconsistent or out-of-range fields is not supported and may +// cause the underlying crypto primitives to panic; avoid doing so. func Import[T Key](raw any) (T, error) { var zero T key, err := doImport(raw) @@ -361,6 +367,14 @@ func (ctx *setDecodeCtx) IgnoreParseError() bool { // are performed for certificate expiration, no checks against missing // parameters are performed, etc. // +// In particular, the "alg" value is stored as-is and is NOT validated +// against the key type, curve, or key size at parse time. This is +// intentional: per RFC 7517 section 4.4, "alg" is an OPTIONAL, +// informational hint identifying the algorithm intended for use with the +// key, not a constraint on the key itself. An incompatible "alg" is +// rejected when the key is used in a JOSE operation, not at parse time. +// Parse-time alg-vs-kty validation must NOT be added. +// // Use [ParseKeyAs] when a concrete key subtype (e.g. [RSAPrivateKey], // [ECDSAPublicKey]) is required. func ParseKey(data []byte, options ...ParseOption) (Key, error) { diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/okp.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/okp.go index 04ac0885..6078bfda 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/okp.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/okp.go @@ -417,6 +417,9 @@ func okpThumbprint(hash crypto.Hash, crv, x string) []byte { // Thumbprint returns the JWK thumbprint using the indicated // hashing algorithm, according to RFC 7638 / 8037 func (k *okpPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { + if err := availableHash(hash); err != nil { + return nil, err + } k.mu.RLock() defer k.mu.RUnlock() @@ -434,6 +437,9 @@ func (k *okpPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { // Thumbprint returns the JWK thumbprint using the indicated // hashing algorithm, according to RFC 7638 / 8037 func (k *okpPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) { + if err := availableHash(hash); err != nil { + return nil, err + } k.mu.RLock() defer k.mu.RUnlock() @@ -467,6 +473,15 @@ func validateOKPKey(key interface { } if priv, ok := key.(keyWithD); ok { + // This validates the length of "d" but intentionally does NOT verify + // that the public key "x" is derived from "d". A mismatch is + // self-defeating, not exploitable: it can never make a signature + // verification or a decryption wrongly succeed. Ed25519 signs under d's + // real public key, so a signature fails to verify against a mismatched + // advertised "x". X25519 derives the shared secret from "d" (against the + // peer's key), so the local "x" is never consulted in the operation and + // cannot influence its outcome; "x" is public metadata either way. Do + // NOT add an "x derives from d" check here. d, ok := priv.D() if !ok || len(d) == 0 { return fmt.Errorf(`missing "d" field`) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/okp_gen.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/okp_gen.go index ab6a90d2..f4ae7f40 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/okp_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/okp_gen.go @@ -508,6 +508,7 @@ func (h *okpPublicKey) UnmarshalJSON(buf []byte) error { return fmt.Errorf(`invalid kty value for RSAPublicKey (%s)`, val) } case AlgorithmKey: + // "alg" is an informational hint stored as-is, not validated against the key type here; see [ParseKey] for rationale. var s string if err := json.UnmarshalDecode(dec, &s); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err) @@ -534,9 +535,16 @@ func (h *okpPublicKey) UnmarshalJSON(buf []byte) error { } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { + val, err := json.ReadNextStringToken(dec, h.dc) + if err != nil { + return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) + } + var acceptor KeyUsageType + if err := acceptor.Accept(val); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } + tmp := acceptor.String() + h.keyUsage = &tmp case OKPXKey: if err := json.AssignNextBytesToken(&h.x, dec); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, OKPXKey, err) @@ -1306,6 +1314,7 @@ func (h *okpPrivateKey) UnmarshalJSON(buf []byte) (retErr error) { return fmt.Errorf(`invalid kty value for RSAPublicKey (%s)`, val) } case AlgorithmKey: + // "alg" is an informational hint stored as-is, not validated against the key type here; see [ParseKey] for rationale. var s string if err := json.UnmarshalDecode(dec, &s); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err) @@ -1336,9 +1345,16 @@ func (h *okpPrivateKey) UnmarshalJSON(buf []byte) (retErr error) { } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { + val, err := json.ReadNextStringToken(dec, h.dc) + if err != nil { + return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) + } + var acceptor KeyUsageType + if err := acceptor.Accept(val); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } + tmp := acceptor.String() + h.keyUsage = &tmp case OKPXKey: if err := json.AssignNextBytesToken(&h.x, dec); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, OKPXKey, err) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/options.yaml b/vendor/github.com/lestrrat-go/jwx/v4/jwk/options.yaml index dc762090..6181262c 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/options.yaml +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/options.yaml @@ -25,6 +25,11 @@ options: - ident: ThumbprintHash interface: AssignKeyIDOption argument_type: crypto.Hash + comment: | + WithThumbprintHash specifies the hash algorithm `jwk.AssignKeyID` uses + to compute the thumbprint. If the hash is unavailable (e.g. its package + has not been imported), `AssignKeyID` returns an error rather than + panicking. - ident: ForceAssign interface: AssignKeyIDOption argument_type: bool diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/options_gen.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/options_gen.go index c4270cfb..45780d10 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/options_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/options_gen.go @@ -261,6 +261,10 @@ func WithStrictKeyUsage(v bool) GlobalOption { return &globalOption{option.New(identStrictKeyUsage{}, v)} } +// WithThumbprintHash specifies the hash algorithm `jwk.AssignKeyID` uses +// to compute the thumbprint. If the hash is unavailable (e.g. its package +// has not been imported), `AssignKeyID` returns an error rather than +// panicking. func WithThumbprintHash(v crypto.Hash) AssignKeyIDOption { return &assignKeyIDOption{option.New(identThumbprintHash{}, v)} } diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/rsa.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/rsa.go index c73a674f..78de5267 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/rsa.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/rsa.go @@ -386,6 +386,9 @@ func trimLeadingZeroBytes(b []byte) []byte { } func rsaThumbprint(hash crypto.Hash, n, e []byte) ([]byte, error) { + if err := availableHash(hash); err != nil { + return nil, err + } n = trimLeadingZeroBytes(n) e = trimLeadingZeroBytes(e) if len(n) == 0 { @@ -428,6 +431,13 @@ func validateRSAKey(key interface { return err } if checkPrivate { + // For private keys we only require a non-empty "d". We intentionally do + // NOT validate p, q, the CRT values, primality, n == p*q, or e*d ≡ 1 + // (mod λ(n)) here: RFC 7517/7518 do not mandate such parse-time checks and + // they are expensive. A malformed private key is caught by crypto/rsa's own + // consistency checks (p*q == n, d*e ≡ 1) the moment it is used to sign or + // decrypt, and the public thumbprint/export uses only n and e (validated + // just above). Do not add full RSA private-parameter validation here. if priv, ok := key.(keyWithD); ok { if d, ok := priv.D(); !ok || len(d) == 0 { return fmt.Errorf(`missing "d" value`) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/rsa_gen.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/rsa_gen.go index bcc98798..ec22a6dc 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/rsa_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/rsa_gen.go @@ -517,6 +517,7 @@ func (h *rsaPublicKey) UnmarshalJSON(buf []byte) error { return fmt.Errorf(`invalid kty value for RSAPublicKey (%s)`, val) } case AlgorithmKey: + // "alg" is an informational hint stored as-is, not validated against the key type here; see [ParseKey] for rationale. var s string if err := json.UnmarshalDecode(dec, &s); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err) @@ -541,9 +542,16 @@ func (h *rsaPublicKey) UnmarshalJSON(buf []byte) error { } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { + val, err := json.ReadNextStringToken(dec, h.dc) + if err != nil { + return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) + } + var acceptor KeyUsageType + if err := acceptor.Accept(val); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } + tmp := acceptor.String() + h.keyUsage = &tmp case RSANKey: if err := json.AssignNextBytesToken(&h.n, dec); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, RSANKey, err) @@ -1514,6 +1522,7 @@ func (h *rsaPrivateKey) UnmarshalJSON(buf []byte) (retErr error) { return fmt.Errorf(`invalid kty value for RSAPublicKey (%s)`, val) } case AlgorithmKey: + // "alg" is an informational hint stored as-is, not validated against the key type here; see [ParseKey] for rationale. var s string if err := json.UnmarshalDecode(dec, &s); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err) @@ -1550,9 +1559,16 @@ func (h *rsaPrivateKey) UnmarshalJSON(buf []byte) (retErr error) { } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { + val, err := json.ReadNextStringToken(dec, h.dc) + if err != nil { + return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) + } + var acceptor KeyUsageType + if err := acceptor.Accept(val); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } + tmp := acceptor.String() + h.keyUsage = &tmp case RSANKey: if err := json.AssignNextBytesToken(&h.n, dec); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, RSANKey, err) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/symmetric.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/symmetric.go index 46bde6cf..07564ca1 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/symmetric.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/symmetric.go @@ -76,6 +76,9 @@ func octetSeqToRaw(keyif Key, _ any) (any, error) { // Thumbprint returns the JWK thumbprint using the indicated // hashing algorithm, according to RFC 7638 func (k *symmetricKey) Thumbprint(hash crypto.Hash) ([]byte, error) { + if err := availableHash(hash); err != nil { + return nil, err + } k.mu.RLock() defer k.mu.RUnlock() octets, err := Export[[]byte](k) @@ -106,6 +109,15 @@ func (k *symmetricKey) PublicKey() (Key, error) { } func (k *symmetricKey) Validate() error { + // Validate only checks that "k" is non-empty. It intentionally does NOT + // enforce algorithm-specific key lengths even when "alg" is present: + // RFC 7517/7518 do not require parse-time key-length validation, and "alg" + // is an informational hint (RFC 7517 §4.4). Exact AES key sizes (for + // A128/192/256GCM/KW) are enforced at use time (aes.NewCipher rejects a bad + // length). HMAC minimum key sizes (RFC 7518 §3.2) are NOT enforced here OR + // at use time — a short non-empty HMAC key is accepted and yields a weak + // MAC, which is the caller's responsibility. Do NOT add alg-specific + // length checks below. octets, ok := k.Octets() if !ok || len(octets) == 0 { return NewKeyValidationError(fmt.Errorf(`jwk.SymmetricKey: missing "k" field`)) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/symmetric_gen.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/symmetric_gen.go index a9c2172a..3cfb6f39 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwk/symmetric_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/symmetric_gen.go @@ -475,6 +475,7 @@ func (h *symmetricKey) UnmarshalJSON(buf []byte) (retErr error) { return fmt.Errorf(`invalid kty value for RSAPublicKey (%s)`, val) } case AlgorithmKey: + // "alg" is an informational hint stored as-is, not validated against the key type here; see [ParseKey] for rationale. var s string if err := json.UnmarshalDecode(dec, &s); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err) @@ -495,9 +496,16 @@ func (h *symmetricKey) UnmarshalJSON(buf []byte) (retErr error) { } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { + val, err := json.ReadNextStringToken(dec, h.dc) + if err != nil { + return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) + } + var acceptor KeyUsageType + if err := acceptor.Accept(val); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } + tmp := acceptor.String() + h.keyUsage = &tmp case SymmetricOctetsKey: if err := json.AssignNextBytesToken(&h.octets, dec); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, SymmetricOctetsKey, err) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwk/thumbprint.go b/vendor/github.com/lestrrat-go/jwx/v4/jwk/thumbprint.go new file mode 100644 index 00000000..1338bb2f --- /dev/null +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwk/thumbprint.go @@ -0,0 +1,18 @@ +package jwk + +import ( + "crypto" + "fmt" +) + +// availableHash guards the Thumbprint code paths against an unusable hash. +// crypto.Hash(0) and registered-but-unlinked hashes (e.g. crypto.MD4 without +// importing its package) make hash.New() panic with "requested hash function +// is unavailable"; returning an error instead keeps Thumbprint and its callers +// (such as AssignKeyID) panic-free. +func availableHash(h crypto.Hash) error { + if !h.Available() { + return fmt.Errorf(`jwk: thumbprint hash %v is not available (is its package imported?)`, h) + } + return nil +} diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v4/jws/BUILD.bazel index f82be351..1e07de03 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/BUILD.bazel @@ -32,6 +32,7 @@ go_library( "//internal/tokens", "//jwa", "//jwk", + "//jws/internal/jwsbb", "//jws/jwsbb", "@com_github_lestrrat_go_dsig//:dsig", "@com_github_lestrrat_go_option_v3//:option", @@ -43,6 +44,8 @@ go_test( srcs = [ "bench_marshal_test.go", "bench_serialize_test.go", + "detached_payload_presence_test.go", + "ed25519_keylength_test.go", "fuzz_test.go", "headers_nil_test.go", "headers_test.go", @@ -52,6 +55,7 @@ go_test( "options_gen_test.go", "signer_test.go", "streaming_detached_test.go", + "verify_alg_match_test.go", ], embed = [":jws"], deps = [ diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/errors.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/errors.go index 5eb50e14..14a8ab9f 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/errors.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/errors.go @@ -5,42 +5,78 @@ import ( "fmt" ) -// errCritPresent is returned by VerifyCompactFast when the protected -// header carries a "crit" list. The fast path cannot enforce RFC 7515 -// §4.1.11 (it has no WithCritExtension allowlist), so it refuses rather -// than silently accepting. The sentinel is wrapped in verifyError at the -// return site so the resulting error matches BOTH errors.Is(err, -// jws.ErrCritPresent()) (the specific reason) AND errors.Is(err, -// jws.VerifyError()) (the general class), letting callers choose the -// classification granularity that fits their code path. -var errCritPresent = errors.New("VerifyCompactFast: protected header contains \"crit\"; use jws.Verify") +// errNonMinimalHeader is the umbrella sentinel for every VerifyCompactFast +// header refusal: the protected header is not in the minimal shape the fast +// path handles ("alg" exactly once, an optional single "typ"/"kid"/"cty", no +// JSON escape sequences, and no other parameters). fastjson (used by the fast +// path) keeps duplicate object members and resolves them first-wins, whereas +// encoding/json/v2 (used by jws.Verify) rejects duplicate names outright — so +// a header carrying a duplicate, a nested object, an unknown or key-source +// parameter, or an escaped key could be read differently by the two paths +// (see issue #2234). Refusing such headers defers them to jws.Verify, whose +// strict, recursive duplicate rejection and full header handling are the +// authoritative behavior. +// +// The crit and b64 refusals (errCritPresent, errB64Present) are specific +// reasons that wrap this sentinel, so a single errors.Is(err, +// jws.ErrNonMinimalHeader()) check classifies every fast-path header refusal, +// while errors.Is(err, jws.ErrCritPresent()) still identifies the precise +// cause. The sentinel is wrapped in verifyError at the return site, so the +// resulting error also matches errors.Is(err, jws.VerifyError()). +var errNonMinimalHeader = errors.New(`VerifyCompactFast: protected header is not in the fast-path minimal shape; use jws.Verify`) + +// ErrNonMinimalHeader returns the umbrella sentinel that VerifyCompactFast +// returns for every protected-header refusal: a duplicate, a nested object, +// an unknown or key-source parameter, an escaped key, or the more specific +// "crit"/"b64" cases (see ErrCritPresent / ErrB64Present, which both match +// this sentinel too). Treat it as "the fast path declined this header; retry +// through jws.Verify". Errors returned from VerifyCompactFast that wrap this +// sentinel additionally match jws.VerifyError() (they are wrapped in +// verifyError at the return site); the bare sentinel value returned here does +// not. +func ErrNonMinimalHeader() error { + return errNonMinimalHeader +} + +// errCritPresent is the specific case of errNonMinimalHeader where the +// protected header carries a "crit" list. The fast path cannot enforce +// RFC 7515 §4.1.11 (it has no WithCritExtension allowlist), so it refuses +// rather than silently accepting. It wraps errNonMinimalHeader, so the +// returned error matches errors.Is(err, jws.ErrCritPresent()) (the specific +// reason), errors.Is(err, jws.ErrNonMinimalHeader()) (the umbrella), and — +// via the verifyError wrapper at the return site — errors.Is(err, +// jws.VerifyError()) (the general class). +var errCritPresent = fmt.Errorf(`%w (header contains "crit")`, errNonMinimalHeader) // ErrCritPresent returns the sentinel error returned by VerifyCompactFast -// when the protected header contains a "crit" list. The error returned -// from VerifyCompactFast also matches jws.VerifyError(), so callers that -// only branch on the general class still classify the refusal correctly. +// when the protected header contains a "crit" list. This sentinel itself also +// matches jws.ErrNonMinimalHeader() (it wraps the umbrella refusal). Errors +// returned from VerifyCompactFast that wrap it additionally match +// jws.VerifyError() (the general class), so callers can branch at whatever +// granularity fits. func ErrCritPresent() error { return errCritPresent } -// errB64Present is returned by VerifyCompactFast when the protected -// header carries a "b64" entry (typically b64=false per RFC 7797). The -// fast path assumes the default b64=true encoding for both the +// errB64Present is the specific case of errNonMinimalHeader where the +// protected header carries a "b64" entry (typically b64=false per RFC 7797). +// The fast path assumes the default b64=true encoding for both the // signing-input reconstruction and the post-verify payload decode; a -// b64=false message signed under non-conformant rules (b64 not declared -// in "crit") would otherwise verify cryptographically while returning -// a decoded payload that differs from the producer's intent. Refusing -// here defers such messages to jws.Verify, which has the -// WithDetachedPayload and WithCritExtension machinery to handle b64=false -// correctly. As with errCritPresent, the sentinel is wrapped in -// verifyError at the return site so the resulting error matches both -// errors.Is(err, jws.ErrB64Present()) and errors.Is(err, jws.VerifyError()). -var errB64Present = errors.New("VerifyCompactFast: protected header contains \"b64\"; use jws.Verify") - -// ErrB64Present returns the sentinel error returned by VerifyCompactFast -// when the protected header contains a "b64" entry. The error returned -// from VerifyCompactFast also matches jws.VerifyError(), so callers that -// only branch on the general class still classify the refusal correctly. +// b64=false message signed under non-conformant rules (b64 not declared in +// "crit") would otherwise verify cryptographically while returning a decoded +// payload that differs from the producer's intent. Refusing here defers such +// messages to jws.Verify, which has the WithDetachedPayload and +// WithCritExtension machinery to handle b64=false correctly. Like +// errCritPresent it wraps errNonMinimalHeader and is wrapped in verifyError at +// the return site, so it matches ErrB64Present(), ErrNonMinimalHeader(), and +// VerifyError(). +var errB64Present = fmt.Errorf(`%w (header contains "b64")`, errNonMinimalHeader) + +// ErrB64Present returns the sentinel error returned by VerifyCompactFast when +// the protected header contains a "b64" entry. This sentinel itself also +// matches jws.ErrNonMinimalHeader() (it wraps the umbrella refusal). Errors +// returned from VerifyCompactFast that wrap it additionally match +// jws.VerifyError() (the general class). func ErrB64Present() error { return errB64Present } diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/interface.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/interface.go index 732216ee..a1c208bb 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/interface.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/interface.go @@ -74,12 +74,13 @@ type DecodeCtx interface { // so callers that hash or dedup JWS messages by their JSON encoding will // not recognize a round-tripped message as equal to its input. type Message struct { - dc DecodeCtx - payload []byte - signatures []*Signature - detached bool - b64 bool // true if payload should be base64 encoded - maxSignatures int // scratch cap enforced during UnmarshalJSON; 0 means use global default + dc DecodeCtx + payload []byte + signatures []*Signature + detached bool + payloadPresent bool // true if a "payload" member was present on the wire (even if empty); distinguishes JSON "payload":"" from an omitted member + b64 bool // true if payload should be base64 encoded + maxSignatures int // scratch cap enforced during UnmarshalJSON; 0 means use global default } type Signature struct { diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/internal/jwsbb/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v4/jws/internal/jwsbb/BUILD.bazel new file mode 100644 index 00000000..55eca670 --- /dev/null +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/internal/jwsbb/BUILD.bazel @@ -0,0 +1,27 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "jwsbb", + srcs = ["header.go"], + importpath = "github.com/lestrrat-go/jwx/v4/jws/internal/jwsbb", + visibility = ["//jws:__subpackages__"], + deps = [ + "//internal/base64", + "@com_github_valyala_fastjson//:fastjson", + ], +) + +alias( + name = "go_default_library", + actual = ":jwsbb", + visibility = ["//jws:__subpackages__"], +) + +go_test( + name = "jwsbb_test", + srcs = ["header_test.go"], + deps = [ + ":jwsbb", + "@com_github_stretchr_testify//require", + ], +) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/internal/jwsbb/header.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/internal/jwsbb/header.go new file mode 100644 index 00000000..fac24781 --- /dev/null +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/internal/jwsbb/header.go @@ -0,0 +1,293 @@ +// Package jwsbb holds the implementation of the jws/jwsbb building blocks. +// +// The public, end-user-facing API lives in github.com/lestrrat-go/jwx/v4/jws/jwsbb, +// which is a thin facade over this package. Symbols that should be usable by +// jwx-internal code but NOT exposed to end users (e.g. HeaderForEachKey) live +// here and are simply not re-exported by the facade. +package jwsbb + +import ( + "fmt" + + "github.com/lestrrat-go/jwx/v4/internal/base64" + "github.com/valyala/fastjson" +) + +type headerNotFoundError struct { + key string +} + +func (e headerNotFoundError) Error() string { + return fmt.Sprintf(`jwsbb: header "%s" not found`, e.key) +} + +func (e headerNotFoundError) Is(target error) bool { + switch target.(type) { + case headerNotFoundError, *headerNotFoundError: + // If the target is a headerNotFoundError or a pointer to it, we + // consider it a match + return true + default: + return false + } +} + +// ErrHeaderNotFound returns an error that can be passed to `errors.Is` to check if the error is +// the result of the field not being found +func ErrHeaderNotFound() error { + return headerNotFoundError{} +} + +// Header is an object that allows you to access the JWS header in a quick and +// dirty way. It does not verify anything, it does not know anything about what +// each header field means, and it does not care about the JWS specification. +// But when you need to access the JWS header for that one field that you +// need, this is the object you want to use. +// +// As of this writing, Header cannot be used from concurrent goroutines. +// You will need to create a new instance for each goroutine that needs to parse a JWS header. +// Also, in general values obtained from this object should only be used +// while the Header object is still in scope. +// +// This type is experimental and may change or be removed in the future. +type Header interface { + // I'm hiding this behind an interface so that users won't accidentally + // rely on the underlying json handler implementation, nor the concrete + // type name that jwsbb provides, as we may choose a different one in the future. + jwsbbHeader() +} + +type header struct { + v *fastjson.Value + err error +} + +func (h *header) jwsbbHeader() {} + +// HeaderParseCompact parses a JWS header from a compact serialization format. +// You will need to call HeaderGet* functions to extract the values from the header. +// +// This function is experimental and may change or be removed in the future. +func HeaderParseCompact(buf []byte) Header { + decoded, err := base64.Decode(buf) + if err != nil { + return &header{err: err} + } + return HeaderParse(decoded) +} + +// HeaderParse parses a JWS header from a byte slice containing the decoded JSON. +// You will need to call HeaderGet* functions to extract the values from the header. +// +// Unlike HeaderParseCompact, this function does not perform any base64 decoding. +// This function is experimental and may change or be removed in the future. +func HeaderParse(decoded []byte) Header { + var p fastjson.Parser + v, err := p.ParseBytes(decoded) + if err != nil { + return &header{err: err} + } + return &header{ + v: v, + } +} + +// HeaderForEachKey calls fn once for each top-level parameter name in the +// parsed header, in document order. Duplicate parameter names are reported +// once per occurrence, so callers can detect duplicates. The name slice +// passed to fn is only valid for the duration of the call; do not retain it. +// +// An error is returned if the header failed to parse or does not decode to a +// JSON object. +// +// This enumeration primitive exists so that jwx-internal callers which need +// RFC-level header rules — e.g. rejecting duplicate parameter names per +// RFC 7515 §4, or restricting a fast path to a known set of parameters — can +// enforce those rules themselves. HeaderParse stays deliberately spec-agnostic +// (see the [Header] doc): the policy lives in the caller, not in this generic, +// shared field-probe. It is deliberately NOT re-exported by the public jwsbb +// facade — enumerating raw header parameter names is a jwx-internal concern, +// not an end-user one. +func HeaderForEachKey(h Header, fn func(name []byte)) error { + //nolint:forcetypeassert + hh := h.(*header) // we _know_ this can't be another type + if hh.err != nil { + return hh.err + } + obj, err := hh.v.Object() + if err != nil { + return err + } + obj.Visit(func(key []byte, _ *fastjson.Value) { + fn(key) + }) + return nil +} + +func headerGet(h Header, key string) (*fastjson.Value, error) { + //nolint:forcetypeassert + hh := h.(*header) // we _know_ this can't be another type + if hh.err != nil { + return nil, hh.err + } + + v := hh.v.Get(key) + if v == nil { + return nil, headerNotFoundError{key: key} + } + return v, nil +} + +// HeaderGetString returns the string value for the given key from the JWS header. +// An error is returned if the JSON was not valid, if the key does not exist, +// or if the value is not a string. +// +// This function is experimental and may change or be removed in the future. +func HeaderGetString(h Header, key string) (string, error) { + v, err := headerGet(h, key) + if err != nil { + return "", err + } + + sb, err := v.StringBytes() + if err != nil { + return "", err + } + + return string(sb), nil +} + +// HeaderGetBool returns the boolean value for the given key from the JWS header. +// An error is returned if the JSON was not valid, if the key does not exist, +// or if the value is not a boolean. +// +// This function is experimental and may change or be removed in the future. +func HeaderGetBool(h Header, key string) (bool, error) { + v, err := headerGet(h, key) + if err != nil { + return false, err + } + return v.Bool() +} + +// HeaderGetFloat64 returns the float64 value for the given key from the JWS header. +// An error is returned if the JSON was not valid, if the key does not exist, +// or if the value is not a float64. +// +// This function is experimental and may change or be removed in the future. +func HeaderGetFloat64(h Header, key string) (float64, error) { + v, err := headerGet(h, key) + if err != nil { + return 0, err + } + return v.Float64() +} + +// HeaderGetInt returns the int value for the given key from the JWS header. +// An error is returned if the JSON was not valid, if the key does not exist, +// or if the value is not an int. +// +// This function is experimental and may change or be removed in the future. +func HeaderGetInt(h Header, key string) (int, error) { + v, err := headerGet(h, key) + if err != nil { + return 0, err + } + return v.Int() +} + +// HeaderGetInt64 returns the int64 value for the given key from the JWS header. +// An error is returned if the JSON was not valid, if the key does not exist, +// or if the value is not an int64. +// +// This function is experimental and may change or be removed in the future. +func HeaderGetInt64(h Header, key string) (int64, error) { + v, err := headerGet(h, key) + if err != nil { + return 0, err + } + return v.Int64() +} + +// HeaderGetStringBytes returns the JSON string bytes for the given key +// from the JWS header, without copying. An error is returned if the JSON +// was not valid, if the key does not exist, or if the value is not a +// JSON string. +// +// WARNING: the returned slice aliases memory owned by h. It becomes +// invalid as soon as h is reused, re-parsed, or goes out of scope and is +// garbage collected. Do not retain the slice, share it across +// goroutines, or use it after any further call on h. If you need a value +// that outlives h, use [HeaderGetString], which returns a string copy. +// +// This function is experimental and may change or be removed in the future. +func HeaderGetStringBytes(h Header, key string) ([]byte, error) { + v, err := headerGet(h, key) + if err != nil { + return nil, err + } + + return v.StringBytes() +} + +// HeaderHas returns true if the given key exists in the JWS header. +// +// This function is experimental and may change or be removed in the future. +func HeaderHas(h Header, key string) bool { + _, err := headerGet(h, key) + return err == nil +} + +// HeaderGetStringArray returns a string array for the given key from the JWS header. +// An error is returned if the JSON was not valid, if the key does not exist, +// or if the value is not a JSON array of strings. +// +// This function is experimental and may change or be removed in the future. +func HeaderGetStringArray(h Header, key string) ([]string, error) { + v, err := headerGet(h, key) + if err != nil { + return nil, err + } + + arr, err := v.Array() + if err != nil { + return nil, err + } + + result := make([]string, len(arr)) + for i, item := range arr { + sb, err := item.StringBytes() + if err != nil { + return nil, err + } + result[i] = string(sb) + } + return result, nil +} + +// HeaderGetUint returns the uint value for the given key from the JWS header. +// An error is returned if the JSON was not valid, if the key does not exist, +// or if the value is not a uint. +// +// This function is experimental and may change or be removed in the future. +func HeaderGetUint(h Header, key string) (uint, error) { + v, err := headerGet(h, key) + if err != nil { + return 0, err + } + return v.Uint() +} + +// HeaderGetUint64 returns the uint64 value for the given key from the JWS header. +// An error is returned if the JSON was not valid, if the key does not exist, +// or if the value is not a uint64. +// +// This function is experimental and may change or be removed in the future. +func HeaderGetUint64(h Header, key string) (uint64, error) { + v, err := headerGet(h, key) + if err != nil { + return 0, err + } + + return v.Uint64() +} diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/jws.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/jws.go index 29b91cdd..67c4428f 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/jws.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/jws.go @@ -56,6 +56,7 @@ import ( "github.com/lestrrat-go/jwx/v4/internal/tokens" "github.com/lestrrat-go/jwx/v4/jwa" "github.com/lestrrat-go/jwx/v4/jwk" + jwsbbi "github.com/lestrrat-go/jwx/v4/jws/internal/jwsbb" "github.com/lestrrat-go/jwx/v4/jws/jwsbb" ) @@ -240,6 +241,21 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) { // accept messages with "none" signature algorithm, use `jws.Parse` to get the // raw JWS message. // +// By default, Verify rejects a JWS whose protected header "alg" does not +// exactly equal the algorithm actually used to verify it. The verification +// algorithm is resolved from the key or provider you supply (jws.WithKey, +// jws.WithKeySet, jws.WithVerifyAuto, or a custom jws.WithKeyProvider); if the +// protected header advertises a different "alg", verification fails even when +// the signature would otherwise be cryptographically valid. The match is plain +// string equality, with no aliasing: the deprecated polymorphic "EdDSA" and the +// fully-specified "Ed25519"/"Ed448" identifiers are distinct per RFC 9864 and +// are not interchangeable. This check fires only when the protected header +// carries an "alg" — messages that place "alg" only in the unprotected header +// (or omit it) are unaffected. Pass jws.WithSkipAlgorithmMatch(true) to bypass +// the check for non-conforming producers. The compact fast path +// [VerifyCompactFast] performs an equivalent cross-check against its explicitly +// supplied algorithm. +// // The error returned by this function is of type can be checked against // `jws.VerifyError()` and `jws.VerificationError()`. The latter is returned // when the verification process itself fails (e.g. invalid signature, wrong key), @@ -430,6 +446,21 @@ func parse(protected, payload, signature []byte) (*Message, error) { decodedPayload = v } + // The payload decode above and the signature decode below intentionally use + // the auto-detecting base64 decoder, which tolerates non-standard variants + // (e.g. padded base64url, standard base64) in addition to RFC 7515's raw + // base64url. This leniency is deliberate: jws.Verify is the interop path, + // whereas VerifyCompactFast strictly decodes the payload and signature + // (RFC 4648 §5 raw base64url, no padding) and its godoc directs callers whose + // JWS uses non-standard encoding to use jws.Verify instead. The cost is that + // serialized-JWS strings are + // non-canonical/malleable (a signature re-encoded in a different base64 + // variant decodes to the same bytes but yields a different compact string). + // This does NOT affect signature validity or enable forgery; it only matters + // to systems that key replay/dedup on the raw compact-JWS string, which should + // instead key on the verified payload/claims. This is a deliberate won't-fix: + // do NOT switch this to strict decoding; callers needing strict raw base64url + // decoding of the payload and signature should use VerifyCompactFast. decodedSignature, err := base64.Decode(signature) if err != nil { return nil, fmt.Errorf(`failed to decode signature: %w`, err) @@ -443,6 +474,10 @@ func parse(protected, payload, signature []byte) (*Message, error) { var msg Message msg.payload = decodedPayload + // Compact serialization has no way to express "present but empty": + // an empty middle segment is genuinely absent (detached). Presence is + // therefore simply whether the middle segment carried any bytes. + msg.payloadPresent = len(payload) > 0 msg.signatures = append(msg.signatures, &Signature{ protected: hdr, signature: decodedSignature, @@ -632,6 +667,24 @@ func AlgorithmsForKey(key any) ([]jwa.SignatureAlgorithm, error) { case ecdsa.PublicKey, *ecdsa.PublicKey, ecdsa.PrivateKey, *ecdsa.PrivateKey: kty = jwa.EC() case ed25519.PublicKey, ed25519.PrivateKey: + // AlgorithmsForKey classifies by key type to report which algorithms a + // key *could* be used with; it is not a key validator. Value-form + // ed25519 keys are []byte aliases with no length invariant, so a + // wrong-length key is intentionally NOT rejected here — it would still + // be reported as [EdDSA Ed25519]. Key validity (correct length) is + // enforced where it matters, at Sign/Verify time. Do NOT add a length + // check to this advisory classifier. + kty = jwa.OKP() + crv = jwa.Ed25519() + hasCrv = true + case *ed25519.PublicKey, *ed25519.PrivateKey: + // Pointer-form ed25519 keys satisfy crypto.Signer, so without an + // explicit case here a typed-nil or wrong-length pointer would + // fall through to the default branch and panic inside + // signer.Public(). Validate length/nil up front instead. + if err := validateEd25519KeyShape(key); err != nil { + return nil, fmt.Errorf(`%w: %w`, errUnclassifiableKey, err) + } kty = jwa.OKP() crv = jwa.Ed25519() hasCrv = true @@ -650,6 +703,13 @@ func AlgorithmsForKey(key any) ([]jwa.SignatureAlgorithm, error) { var signerPubErr error if signer, ok := key.(crypto.Signer); ok { pub := signer.Public() + // A custom crypto.Signer may hand back a malformed (wrong-length or + // typed-nil) ed25519.PublicKey. Classifying that as OKP would let it + // reach the EdDSA verify path, which panics ("ed25519: bad public key + // length"). Reject it here instead. + if err := validateEd25519KeyShape(pub); err != nil { + return nil, fmt.Errorf(`%w: %w`, errUnclassifiableKey, err) + } // Guard: only recurse if the public key is not itself a crypto.Signer, // to prevent infinite recursion from pathological implementations. if _, isSigner := pub.(crypto.Signer); !isSigner { @@ -759,6 +819,12 @@ func validateAlgorithmForKey(alg jwa.SignatureAlgorithm, key any) error { if hasCustomSigVerifier(alg) { return nil } + // A malformed ed25519 key (typed-nil or wrong-length, value or + // pointer form) satisfies crypto.Signer but panics in Public(). + // Surface the classification error directly instead of probing it. + if shapeErr := validateEd25519KeyShape(key); shapeErr != nil { + return fmt.Errorf(`jws.WithKey: %w`, err) + } if signer, ok := key.(crypto.Signer); ok { if _, isSigner := signer.Public().(crypto.Signer); isSigner { return nil @@ -775,6 +841,37 @@ func validateAlgorithmForKey(alg jwa.SignatureAlgorithm, key any) error { return nil } +// validateEd25519KeyShape reports whether key is a malformed ed25519 key. +// It returns a non-nil error when key is an ed25519 private/public key (value +// or pointer form) that is typed-nil or not the expected length, and nil for +// everything else — including non-ed25519 keys and well-formed ed25519 keys. +// +// Concrete ed25519 keys (and their pointer forms) satisfy crypto.Signer, but +// their Public() method panics ("slice bounds out of range" / nil pointer +// dereference) when the key is not exactly the right size. Callers use this to +// reject malformed keys with an error before any code path reaches Public(). +func validateEd25519KeyShape(key any) error { + switch k := key.(type) { + case ed25519.PrivateKey: + if len(k) != ed25519.PrivateKeySize { + return fmt.Errorf(`invalid ed25519.PrivateKey length %d, expected %d`, len(k), ed25519.PrivateKeySize) + } + case *ed25519.PrivateKey: + if k == nil || len(*k) != ed25519.PrivateKeySize { + return fmt.Errorf(`invalid *ed25519.PrivateKey, expected length %d`, ed25519.PrivateKeySize) + } + case ed25519.PublicKey: + if len(k) != ed25519.PublicKeySize { + return fmt.Errorf(`invalid ed25519.PublicKey length %d, expected %d`, len(k), ed25519.PublicKeySize) + } + case *ed25519.PublicKey: + if k == nil || len(*k) != ed25519.PublicKeySize { + return fmt.Errorf(`invalid *ed25519.PublicKey, expected length %d`, ed25519.PublicKeySize) + } + } + return nil +} + // hasCustomSigVerifier reports whether a non-default Signer or // Verifier has been registered for alg. When this is true, key-type // validation must be skipped: the custom implementation decides what @@ -829,10 +926,10 @@ func Settings(options ...GlobalOption) error { // // Returns the original payload that was signed if verification succeeds. // -// Unlike jws.Verify(), this function requires you to specify the -// algorithm explicitly rather than extracting it from the JWS headers. -// This can be useful for performance-critical applications where the -// algorithm is known in advance. +// Unlike jws.Verify() — which resolves the verification algorithm from the +// key or provider you supply (e.g. WithKey, WithKeySet) — this function takes +// the algorithm as an explicit argument. It is useful for performance-critical +// applications where the algorithm is known in advance. // // This function uses strict base64url encoding without padding (RFC 4648 §5) // for decoding the signature and payload. It does not auto-detect other @@ -868,6 +965,35 @@ func Settings(options ...GlobalOption) error { // Detached-payload callers must use jws.Verify with jws.WithDetachedPayload // regardless, since VerifyCompactFast has no way to accept a detached // payload. +// +// VerifyCompactFast only handles a "minimal" protected header. It proceeds +// with verification only when ALL of the following hold; otherwise it refuses +// with jws.ErrNonMinimalHeader() and the caller should retry through +// jws.Verify: +// +// - "alg" is present exactly once (a missing "alg" is reported separately, +// via the cross-check described above, not as ErrNonMinimalHeader); +// - "typ", "kid", and "cty" each appear at most once; +// - "typ", "kid", and "cty", when present, have JSON string values (a +// non-string value, which jws.Verify rejects, is refused here too); +// - no other parameter is present — this excludes "crit" and "b64" (which +// have their own dedicated refusals, see above), key-source parameters +// such as "jwk"/"jku"/"x5u"/"x5c", and any unknown parameter; +// - the header contains no JSON escape sequences. +// +// The restriction exists because the fast path reads the header with a parser +// that keeps duplicate object members and resolves them first-wins, whereas +// jws.Verify uses encoding/json/v2, which rejects duplicate names outright. +// Limiting the fast path to the minimal shape — and deferring everything else +// to jws.Verify, whose strict, recursive header handling is authoritative — +// makes the two entry points agree on duplicate-name and header-shape handling +// (see issue #2234). It is not a byte-for-byte mirror, though: the fast parser +// does not reproduce all of encoding/json/v2's in-string validation, so a +// header whose "typ"/"kid"/"cty" string value contains e.g. a raw control +// character or invalid UTF-8 is accepted here but rejected by jws.Verify. The +// signature is always verified, so this is a parser-strictness nuance, not a +// bypass; for byte-for-byte parity call jws.Verify. Like the crit refusal, +// ErrNonMinimalHeader means "retry through jws.Verify". func VerifyCompactFast(key any, compact []byte, alg jwa.SignatureAlgorithm) ([]byte, error) { if err := validateAlgorithmForKey(alg, key); err != nil { return nil, makeVerifyError(`%w`, err) @@ -881,7 +1007,30 @@ func VerifyCompactFast(key any, compact []byte, alg jwa.SignatureAlgorithm) ([]b return nil, makeVerifyError("failed to split compact: %w", err) } - parsedHdr := jwsbb.HeaderParseCompact(hdr) + // Decode the protected header ourselves (rather than via + // HeaderParseCompact) so we can inspect the raw JSON for escape + // sequences before parsing it. + decodedHdr, err := base64.Decode(hdr) + if err != nil { + return nil, makeVerifyError("failed to decode protected header: %w", err) + } + + // Refuse any protected header containing a JSON escape sequence. For + // literal keys fastjson resolves duplicates first-wins deterministically, + // but for *escaped* keys its resolution becomes order/state-dependent and + // can diverge from encoding/json/v2 (which jws.Verify uses). Rather than + // reason about that, defer any escape-bearing header to jws.Verify. The + // header parameter names the fast path handles (alg/typ/kid/cty) never + // require escaping; an escape in a value (e.g. a "kid" containing a quote + // or a control char) is simply deferred to jws.Verify, which handles it. + if bytes.IndexByte(decodedHdr, '\\') >= 0 { + return nil, verifyError{fmt.Errorf(`%w (header contains a JSON escape sequence)`, errNonMinimalHeader)} + } + + // Header probing uses the jwx-internal jwsbb package directly: the + // enumeration primitive (HeaderForEachKey) the minimal-shape gate below needs + // is intentionally not part of the public jwsbb facade. + parsedHdr := jwsbbi.HeaderParse(decodedHdr) // Refuse crit-bearing messages: the fast path has no WithCritExtension // allowlist, so accepting them would silently violate RFC 7515 §4.1.11. @@ -890,7 +1039,7 @@ func VerifyCompactFast(key any, compact []byte, alg jwa.SignatureAlgorithm) ([]b // The sentinel is wrapped in verifyError so the same error also matches // errors.Is(err, jws.VerifyError()) — fast-path refusals are a verify // error, just one with a more specific classification available. - if jwsbb.HeaderHas(parsedHdr, CriticalKey) { + if jwsbbi.HeaderHas(parsedHdr, CriticalKey) { return nil, verifyError{errCritPresent} } @@ -904,17 +1053,117 @@ func VerifyCompactFast(key any, compact []byte, alg jwa.SignatureAlgorithm) ([]b // / WithCritExtension machinery to handle b64=false correctly. As with // the crit refusal above, the sentinel is wrapped in verifyError so the // same error matches both jws.ErrB64Present() and jws.VerifyError(). - if jwsbb.HeaderHas(parsedHdr, "b64") { + if jwsbbi.HeaderHas(parsedHdr, B64Key) { return nil, verifyError{errB64Present} } + // Minimal-shape gate. fastjson keeps duplicate object members and resolves + // them first-wins, whereas encoding/json/v2 (jws.Verify) rejects duplicate + // names — so a header with a duplicate or otherwise unusual parameter + // could be read differently by the two paths (issue #2234). The fast path + // therefore only handles the common minimal shape: "alg" exactly once, an + // optional single "typ"/"kid"/"cty", and nothing else. Anything outside + // that — a duplicate, a nested object, an unknown or key-source parameter + // — is deferred to jws.Verify via errNonMinimalHeader, where json/v2's + // strict recursive duplicate rejection and full header handling apply. A + // *missing* "alg" is left to the cross-check below so the caller still + // gets the specific diagnostic. + // + // Why gate-here-and-defer rather than teach the header parser to reject + // duplicates itself: + // - The jwsbb header parser is deliberately spec-agnostic: it is a + // generic, reusable field-probe (shared with other call sites) that + // "does not care about the JWS specification". Baking RFC 7515 §4 + // header rules into it would break that contract and silently change + // behavior for every other consumer. + // - A duplicate check inside the parser runs on *every* parse and needs + // a per-call set/map allocation, taxing the very hot path the fast + // path exists to keep cheap. The shape gate here is a single key sweep + // with fixed counters — allocation-free. + // - A parser-level top-level dedup would still miss *nested* duplicate + // names; deferring unusual headers to jws.Verify inherits json/v2's + // strict *recursive* rejection for free, so the two paths agree on + // more than just the top level. + // Gating on shape and handing anything unusual to the authoritative slow + // path is both cheaper and more complete than making the parser spec-aware. + var algN, typN, kidN, ctyN, others int + var firstOther string + var haveOther bool + if err := jwsbbi.HeaderForEachKey(parsedHdr, func(name []byte) { + switch string(name) { + case AlgorithmKey: + algN++ + case TypeKey: + typN++ + case KeyIDKey: + kidN++ + case ContentTypeKey: + ctyN++ + default: + if !haveOther { + // Capture the first unknown parameter for the diagnostic. + // Use a bool sentinel (not firstOther == "") so a literal + // empty-string key is still reported as the first one. + firstOther = string(name) + haveOther = true + } + others++ + } + }); err != nil { + // Header failed to parse or is not a JSON object; let jws.Verify + // produce the authoritative error. + return nil, verifyError{fmt.Errorf(`%w (protected header is not a valid JSON object)`, errNonMinimalHeader)} + } + // Refuse, naming the specific trigger so the refusal is debuggable. The + // error still wraps errNonMinimalHeader, so errors.Is classification + // (ErrNonMinimalHeader / VerifyError) is unchanged. + if others > 0 || algN > 1 || typN > 1 || kidN > 1 || ctyN > 1 { + var reason string + switch { + case others > 0: + reason = fmt.Sprintf(`unexpected protected header parameter %q`, firstOther) + case algN > 1: + reason = `duplicate "alg"` + case typN > 1: + reason = `duplicate "typ"` + case kidN > 1: + reason = `duplicate "kid"` + default: // ctyN > 1 + reason = `duplicate "cty"` + } + return nil, verifyError{fmt.Errorf(`%w (%s)`, errNonMinimalHeader, reason)} + } + + // The optional descriptive parameters must be JSON strings, matching what + // jws.Verify's typed encoding/json/v2 header decode accepts. Without this, + // a genuinely-signed header like {"alg":"HS256","typ":123} would pass the + // name-count gate here yet be rejected by jws.Verify — the same fast/slow + // divergence the gate exists to prevent. HeaderGetStringBytes reports a + // non-string value without copying it, so this stays allocation-free; only + // headers that actually carry typ/kid/cty pay the (negligible) lookup. + if typN == 1 { + if _, err := jwsbbi.HeaderGetStringBytes(parsedHdr, TypeKey); err != nil { + return nil, verifyError{fmt.Errorf(`%w (non-string "typ")`, errNonMinimalHeader)} + } + } + if kidN == 1 { + if _, err := jwsbbi.HeaderGetStringBytes(parsedHdr, KeyIDKey); err != nil { + return nil, verifyError{fmt.Errorf(`%w (non-string "kid")`, errNonMinimalHeader)} + } + } + if ctyN == 1 { + if _, err := jwsbbi.HeaderGetStringBytes(parsedHdr, ContentTypeKey); err != nil { + return nil, verifyError{fmt.Errorf(`%w (non-string "cty")`, errNonMinimalHeader)} + } + } + // Cross-check the protected header "alg" against the caller-supplied // alg. RFC 7515 §4.1.1 makes "alg" mandatory in the protected header // for compact serialization, and a mismatch between what the message // advertises and the discipline under which we verify is the sort of // silent divergence that downstream code (e.g. JWT consumers) should // not be asked to re-discover on its own. - hdrAlg, err := jwsbb.HeaderGetString(parsedHdr, AlgorithmKey) + hdrAlg, err := jwsbbi.HeaderGetString(parsedHdr, AlgorithmKey) if err != nil { return nil, verifyError{verificationError{fmt.Errorf(`jws.Verify: failed to extract %q from protected header: %w`, AlgorithmKey, err)}} } diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/BUILD.bazel index 1ae9fa05..63e7c400 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/BUILD.bazel @@ -21,8 +21,8 @@ go_library( "//internal/ecutil", "//internal/keyconv", "//internal/tokens", + "//jws/internal/jwsbb", "@com_github_lestrrat_go_dsig//:dsig", - "@com_github_valyala_fastjson//:fastjson", ], ) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/ecdsa.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/ecdsa.go index 67214b11..7837efdb 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/ecdsa.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/ecdsa.go @@ -105,6 +105,9 @@ func SignECDSA(key *ecdsa.PrivateKey, payload []byte, h crypto.Hash, rr io.Reade // The signature is converted from ASN.1 format to JWS format (r||s). // // rr is an io.Reader that provides randomness for signing. If rr is nil, it defaults to rand.Reader. +// +// As a low-level primitive it assumes a well-formed signer; one returning a +// malformed key may panic. Use jws.Sign for untrusted or unvalidated keys. func SignECDSACryptoSigner(signer crypto.Signer, raw []byte, h crypto.Hash, rr io.Reader) ([]byte, error) { signed, err := SignCryptoSigner(signer, raw, h, h, rr) if err != nil { @@ -130,6 +133,19 @@ func signECDSACryptoSigner(signer crypto.Signer, signed []byte) ([]byte, error) return PackECDSASignature(&r, &s, curveBits) } +// ecdsaVerify uses the standard library's ecdsa.Verify, which intentionally +// accepts both low-S and high-S signatures (it only checks that 0 < r,s < N). +// jwx deliberately does NOT enforce canonical/low-S signatures: RFC 7515 +// imposes no low-S requirement, so a high-S signature is a valid JWS ECDSA +// signature. Rejecting high-S on verify would reject legitimate signatures from +// conformant signers — including Go's own randomized crypto/ecdsa.Sign, which +// does not canonicalize S — which would be a broad interop break. +// +// The resulting malleability ((r,s) vs (r, N-s) both verify) does not affect +// signature validity or enable forgery: an attacker still cannot sign new +// messages. It only matters to systems that replay/dedup on the raw signature +// or compact-JWS bytes, which should instead key on the verified payload/claims. +// This is a deliberate won't-fix: do not "harden" this by enforcing low-S. func ecdsaVerify(key *ecdsa.PublicKey, buf []byte, h crypto.Hash, r, s *big.Int) error { hasher := h.New() hasher.Write(buf) @@ -159,6 +175,9 @@ func VerifyECDSA(key *ecdsa.PublicKey, payload, signature []byte, h crypto.Hash) // This function is useful for verifying signatures created by hardware security modules // or other implementations of the crypto.Signer interface. // The payload parameter should be the pre-computed signing input (typically header.payload). +// +// As a low-level primitive it assumes a well-formed signer; one returning a +// malformed key may panic. Use jws.Verify for untrusted or unvalidated keys. func VerifyECDSACryptoSigner(signer crypto.Signer, payload, signature []byte, h crypto.Hash) error { var pubkey *ecdsa.PublicKey switch cpub := signer.Public(); cpub := cpub.(type) { diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/eddsa.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/eddsa.go index 960cf97d..2e2938ef 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/eddsa.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/eddsa.go @@ -2,16 +2,53 @@ package jwsbb import ( "crypto/ed25519" + "fmt" "github.com/lestrrat-go/dsig" ) +// validateEd25519KeyShape returns a non-nil error when key is an ed25519 +// private/public key (value or pointer form) that is typed-nil or not the +// expected length, and nil for everything else — including non-ed25519 keys +// and well-formed ed25519 keys. +// +// Concrete ed25519 keys (and their pointer forms) satisfy crypto.Signer, but +// their Public() method panics ("slice bounds out of range" / nil pointer +// dereference) when the key is not exactly the right size. Dispatchers call +// this before any code path that may reach Public() — including the RSA/ECDSA +// dispatchers, where a cross-family ed25519 key would otherwise panic inside +// the crypto.Signer probe. +func validateEd25519KeyShape(key any) error { + switch k := key.(type) { + case ed25519.PrivateKey: + if len(k) != ed25519.PrivateKeySize { + return fmt.Errorf(`invalid ed25519.PrivateKey length %d, expected %d`, len(k), ed25519.PrivateKeySize) + } + case *ed25519.PrivateKey: + if k == nil || len(*k) != ed25519.PrivateKeySize { + return fmt.Errorf(`invalid *ed25519.PrivateKey, expected length %d`, ed25519.PrivateKeySize) + } + case ed25519.PublicKey: + if len(k) != ed25519.PublicKeySize { + return fmt.Errorf(`invalid ed25519.PublicKey length %d, expected %d`, len(k), ed25519.PublicKeySize) + } + case *ed25519.PublicKey: + if k == nil || len(*k) != ed25519.PublicKeySize { + return fmt.Errorf(`invalid *ed25519.PublicKey, expected length %d`, ed25519.PublicKeySize) + } + } + return nil +} + // SignEdDSA generates an EdDSA (Ed25519) signature for the given payload. // The raw parameter should be the pre-computed signing input (typically header.payload). // EdDSA is deterministic and doesn't require additional hashing of the input. // // This function is now a thin wrapper around dsig.SignEdDSA. For new projects, you should // consider using dsig instead of this function. +// +// As a low-level primitive it assumes a well-formed, correctly-sized key; a +// wrong-length key may panic. Use jws.Sign for untrusted or unvalidated keys. func SignEdDSA(key ed25519.PrivateKey, payload []byte) ([]byte, error) { // Use dsig.Sign with EdDSA algorithm constant return dsig.Sign(key, dsig.EdDSA, payload, nil) @@ -24,6 +61,9 @@ func SignEdDSA(key ed25519.PrivateKey, payload []byte) ([]byte, error) { // // This function is now a thin wrapper around dsig.VerifyEdDSA. For new projects, you should // consider using dsig instead of this function. +// +// As a low-level primitive it assumes a well-formed, correctly-sized key; a +// wrong-length key may panic. Use jws.Verify for untrusted or unvalidated keys. func VerifyEdDSA(key ed25519.PublicKey, payload, signature []byte) error { // Use dsig.Verify with EdDSA algorithm constant return dsig.Verify(key, dsig.EdDSA, payload, signature) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/header.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/header.go index 5368d25a..b878cc45 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/header.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/header.go @@ -1,36 +1,13 @@ package jwsbb import ( - "fmt" - - "github.com/lestrrat-go/jwx/v4/internal/base64" - "github.com/valyala/fastjson" + impl "github.com/lestrrat-go/jwx/v4/jws/internal/jwsbb" ) -type headerNotFoundError struct { - key string -} - -func (e headerNotFoundError) Error() string { - return fmt.Sprintf(`jwsbb: header "%s" not found`, e.key) -} - -func (e headerNotFoundError) Is(target error) bool { - switch target.(type) { - case headerNotFoundError, *headerNotFoundError: - // If the target is a headerNotFoundError or a pointer to it, we - // consider it a match - return true - default: - return false - } -} - -// ErrHeaderNotFound returns an error that can be passed to `errors.Is` to check if the error is -// the result of the field not being found -func ErrHeaderNotFound() error { - return headerNotFoundError{} -} +// This file is a thin public facade over jws/internal/jwsbb. The header probe +// implementation lives in the internal package so that jwx-internal-only +// helpers (e.g. HeaderForEachKey) can be shared across the module without being +// exposed to end users. Everything below simply re-exports the internal API. // Header is an object that allows you to access the JWS header in a quick and // dirty way. It does not verify anything, it does not know anything about what @@ -38,36 +15,26 @@ func ErrHeaderNotFound() error { // But when you need to access the JWS header for that one field that you // need, this is the object you want to use. // -// As of this writing, HeaderParser cannot be used from concurrent goroutines. +// As of this writing, Header cannot be used from concurrent goroutines. // You will need to create a new instance for each goroutine that needs to parse a JWS header. // Also, in general values obtained from this object should only be used // while the Header object is still in scope. // // This type is experimental and may change or be removed in the future. -type Header interface { - // I'm hiding this behind an interface so that users won't accidentally - // rely on the underlying json handler implementation, nor the concrete - // type name that jwsbb provides, as we may choose a different one in the future. - jwsbbHeader() -} +type Header = impl.Header -type header struct { - v *fastjson.Value - err error +// ErrHeaderNotFound returns an error that can be passed to `errors.Is` to check if the error is +// the result of the field not being found. +func ErrHeaderNotFound() error { + return impl.ErrHeaderNotFound() } -func (h *header) jwsbbHeader() {} - // HeaderParseCompact parses a JWS header from a compact serialization format. // You will need to call HeaderGet* functions to extract the values from the header. // // This function is experimental and may change or be removed in the future. func HeaderParseCompact(buf []byte) Header { - decoded, err := base64.Decode(buf) - if err != nil { - return &header{err: err} - } - return HeaderParse(decoded) + return impl.HeaderParseCompact(buf) } // HeaderParse parses a JWS header from a byte slice containing the decoded JSON. @@ -76,28 +43,7 @@ func HeaderParseCompact(buf []byte) Header { // Unlike HeaderParseCompact, this function does not perform any base64 decoding. // This function is experimental and may change or be removed in the future. func HeaderParse(decoded []byte) Header { - var p fastjson.Parser - v, err := p.ParseBytes(decoded) - if err != nil { - return &header{err: err} - } - return &header{ - v: v, - } -} - -func headerGet(h Header, key string) (*fastjson.Value, error) { - //nolint:forcetypeassert - hh := h.(*header) // we _know_ this can't be another type - if hh.err != nil { - return nil, hh.err - } - - v := hh.v.Get(key) - if v == nil { - return nil, headerNotFoundError{key: key} - } - return v, nil + return impl.HeaderParse(decoded) } // HeaderGetString returns the string value for the given key from the JWS header. @@ -106,17 +52,7 @@ func headerGet(h Header, key string) (*fastjson.Value, error) { // // This function is experimental and may change or be removed in the future. func HeaderGetString(h Header, key string) (string, error) { - v, err := headerGet(h, key) - if err != nil { - return "", err - } - - sb, err := v.StringBytes() - if err != nil { - return "", err - } - - return string(sb), nil + return impl.HeaderGetString(h, key) } // HeaderGetBool returns the boolean value for the given key from the JWS header. @@ -125,11 +61,7 @@ func HeaderGetString(h Header, key string) (string, error) { // // This function is experimental and may change or be removed in the future. func HeaderGetBool(h Header, key string) (bool, error) { - v, err := headerGet(h, key) - if err != nil { - return false, err - } - return v.Bool() + return impl.HeaderGetBool(h, key) } // HeaderGetFloat64 returns the float64 value for the given key from the JWS header. @@ -138,11 +70,7 @@ func HeaderGetBool(h Header, key string) (bool, error) { // // This function is experimental and may change or be removed in the future. func HeaderGetFloat64(h Header, key string) (float64, error) { - v, err := headerGet(h, key) - if err != nil { - return 0, err - } - return v.Float64() + return impl.HeaderGetFloat64(h, key) } // HeaderGetInt returns the int value for the given key from the JWS header. @@ -151,11 +79,7 @@ func HeaderGetFloat64(h Header, key string) (float64, error) { // // This function is experimental and may change or be removed in the future. func HeaderGetInt(h Header, key string) (int, error) { - v, err := headerGet(h, key) - if err != nil { - return 0, err - } - return v.Int() + return impl.HeaderGetInt(h, key) } // HeaderGetInt64 returns the int64 value for the given key from the JWS header. @@ -164,11 +88,7 @@ func HeaderGetInt(h Header, key string) (int, error) { // // This function is experimental and may change or be removed in the future. func HeaderGetInt64(h Header, key string) (int64, error) { - v, err := headerGet(h, key) - if err != nil { - return 0, err - } - return v.Int64() + return impl.HeaderGetInt64(h, key) } // HeaderGetStringBytes returns the JSON string bytes for the given key @@ -184,20 +104,14 @@ func HeaderGetInt64(h Header, key string) (int64, error) { // // This function is experimental and may change or be removed in the future. func HeaderGetStringBytes(h Header, key string) ([]byte, error) { - v, err := headerGet(h, key) - if err != nil { - return nil, err - } - - return v.StringBytes() + return impl.HeaderGetStringBytes(h, key) } // HeaderHas returns true if the given key exists in the JWS header. // // This function is experimental and may change or be removed in the future. func HeaderHas(h Header, key string) bool { - _, err := headerGet(h, key) - return err == nil + return impl.HeaderHas(h, key) } // HeaderGetStringArray returns a string array for the given key from the JWS header. @@ -206,25 +120,7 @@ func HeaderHas(h Header, key string) bool { // // This function is experimental and may change or be removed in the future. func HeaderGetStringArray(h Header, key string) ([]string, error) { - v, err := headerGet(h, key) - if err != nil { - return nil, err - } - - arr, err := v.Array() - if err != nil { - return nil, err - } - - result := make([]string, len(arr)) - for i, item := range arr { - sb, err := item.StringBytes() - if err != nil { - return nil, err - } - result[i] = string(sb) - } - return result, nil + return impl.HeaderGetStringArray(h, key) } // HeaderGetUint returns the uint value for the given key from the JWS header. @@ -233,11 +129,7 @@ func HeaderGetStringArray(h Header, key string) ([]string, error) { // // This function is experimental and may change or be removed in the future. func HeaderGetUint(h Header, key string) (uint, error) { - v, err := headerGet(h, key) - if err != nil { - return 0, err - } - return v.Uint() + return impl.HeaderGetUint(h, key) } // HeaderGetUint64 returns the uint64 value for the given key from the JWS header. @@ -246,10 +138,5 @@ func HeaderGetUint(h Header, key string) (uint, error) { // // This function is experimental and may change or be removed in the future. func HeaderGetUint64(h Header, key string) (uint64, error) { - v, err := headerGet(h, key) - if err != nil { - return 0, err - } - - return v.Uint64() + return impl.HeaderGetUint64(h, key) } diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/jwsbb.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/jwsbb.go index 6a94d615..ae58535b 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/jwsbb.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/jwsbb.go @@ -13,6 +13,12 @@ // 2. All exported functions are strongly typed (i.e. they do not take `any` types unless they absolutely have to). // 3. Does not rely on other public jwx packages (they are standalone, except for internal packages). // +// As a corollary of (1), these primitives assume well-formed, correctly-sized +// key material. Passing malformed or wrong-length keys (e.g. a short +// ed25519.PublicKey) may panic. Callers handling untrusted or unvalidated keys +// should use the high-level jws.Sign / jws.Verify API, which validates key +// shape before any cryptographic operation. +// // This implementation uses github.com/lestrrat-go/dsig as the underlying signature provider. package jwsbb diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/sign.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/sign.go index 6af999e3..f6027d0b 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/sign.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/sign.go @@ -83,6 +83,13 @@ func dispatchHMACSign(key any, dsigAlg string, payload []byte) ([]byte, error) { } func dispatchRSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([]byte, error) { + // A malformed ed25519 key (value or pointer) satisfies crypto.Signer but + // panics in Public(). Reject it before the crypto.Signer probe below, which + // a cross-family caller (ed25519 key + RSA alg) could otherwise reach. + if err := validateEd25519KeyShape(key); err != nil { + return nil, fmt.Errorf(`jwsbb.Sign: %w`, err) + } + // Try crypto.Signer first (dsig can handle it directly) if signer, ok := key.(crypto.Signer); ok { // Verify it's an RSA key @@ -101,6 +108,12 @@ func dispatchRSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([]b } func dispatchECDSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([]byte, error) { + // See dispatchRSASign: reject malformed ed25519 keys before the + // crypto.Signer probe to avoid a cross-family Public() panic. + if err := validateEd25519KeyShape(key); err != nil { + return nil, fmt.Errorf(`jwsbb.Sign: %w`, err) + } + // Try crypto.Signer first (dsig can handle it directly) if signer, ok := key.(crypto.Signer); ok { // Verify it's an ECDSA key @@ -122,10 +135,23 @@ func dispatchEdDSASign(key any, jwsAlg, dsigAlg string, payload []byte, rr io.Re // Note: Extension algorithms (e.g. Ed448) are registered as dsig.Custom family, // so they take the dsig.Custom branch in Sign() and never reach this function. + // A concrete ed25519.PrivateKey satisfies crypto.Signer, but its Public() + // method panics ("slice bounds out of range") when the key is not exactly + // ed25519.PrivateKeySize bytes. Reject malformed keys here so we return an + // error instead of panicking inside the crypto.Signer branch below. + if err := validateEd25519KeyShape(key); err != nil { + return nil, fmt.Errorf(`jwsbb.Sign: %w`, err) + } + // Try crypto.Signer first (dsig can handle it directly) if signer, ok := key.(crypto.Signer); ok { // Verify it's an EdDSA key if pub, ok := signer.Public().(ed25519.PublicKey); ok { + // A custom signer may hand back a wrong-length ed25519.PublicKey, + // which would panic inside dsig. Reject it before any crypto call. + if err := validateEd25519KeyShape(pub); err != nil { + return nil, fmt.Errorf(`jwsbb.Sign: %w`, err) + } if err := validateEdDSACurve(jwsAlg, pub); err != nil { return nil, fmt.Errorf(`jwsbb.Sign: %w`, err) } diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/verify.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/verify.go index 641aef0b..0ce02097 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/verify.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/jwsbb/verify.go @@ -69,6 +69,13 @@ func dispatchHMACVerify(key any, dsigAlg string, payload, signature []byte) erro } func dispatchRSAVerify(key any, dsigAlg string, payload, signature []byte) error { + // A malformed ed25519 key (value or pointer) satisfies crypto.Signer but + // panics in Public(). Reject it before the crypto.Signer probe below, which + // a cross-family caller (ed25519 key + RSA alg) could otherwise reach. + if err := validateEd25519KeyShape(key); err != nil { + return fmt.Errorf(`jwsbb.Verify: %w`, err) + } + // Try crypto.Signer first (dsig can handle it directly) if signer, ok := key.(crypto.Signer); ok { // Verify it's an RSA key @@ -87,6 +94,12 @@ func dispatchRSAVerify(key any, dsigAlg string, payload, signature []byte) error } func dispatchECDSAVerify(key any, dsigAlg string, payload, signature []byte) error { + // See dispatchRSAVerify: reject malformed ed25519 keys before the + // crypto.Signer probe to avoid a cross-family Public() panic. + if err := validateEd25519KeyShape(key); err != nil { + return fmt.Errorf(`jwsbb.Verify: %w`, err) + } + // Try crypto.Signer first (dsig can handle it directly) if signer, ok := key.(crypto.Signer); ok { // Verify it's an ECDSA key @@ -108,10 +121,23 @@ func dispatchEdDSAVerify(key any, jwsAlg, dsigAlg string, payload, signature []b // Note: Extension algorithms (e.g. Ed448) are registered as dsig.Custom family, // so they take the dsig.Custom branch in Verify() and never reach this function. + // A concrete ed25519.PrivateKey satisfies crypto.Signer, but its Public() + // method panics ("slice bounds out of range") when the key is not exactly + // ed25519.PrivateKeySize bytes. Reject malformed keys here so we return an + // error instead of panicking inside the crypto.Signer branch below. + if err := validateEd25519KeyShape(key); err != nil { + return fmt.Errorf(`jwsbb.Verify: %w`, err) + } + // Try crypto.Signer first (dsig can handle it directly) if signer, ok := key.(crypto.Signer); ok { // Verify it's an EdDSA key if pub, ok := signer.Public().(ed25519.PublicKey); ok { + // A custom signer may hand back a wrong-length ed25519.PublicKey, + // which would panic inside dsig. Reject it before any crypto call. + if err := validateEd25519KeyShape(pub); err != nil { + return fmt.Errorf(`jwsbb.Verify: %w`, err) + } if err := validateEdDSACurve(jwsAlg, pub); err != nil { return fmt.Errorf(`jwsbb.Verify: %w`, err) } diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/message.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/message.go index 984d7052..33a4bfed 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/message.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/message.go @@ -176,7 +176,7 @@ func (m Message) LookupSignature(kid string) []*Signature { // incoming JSON object. We then decide how to parse it // from the fields that are populated. type messageUnmarshalProbe struct { - Payload *string `json:"payload"` + Payload json.RawMessage `json:"payload"` Signatures []json.RawMessage `json:"signatures,omitempty"` Header json.RawMessage `json:"header,omitempty"` Protected *string `json:"protected,omitempty"` @@ -187,6 +187,7 @@ func (m *Message) UnmarshalJSON(buf []byte) error { m.payload = nil m.signatures = nil m.detached = false + m.payloadPresent = false m.b64 = true var mup messageUnmarshalProbe @@ -293,13 +294,38 @@ func (m *Message) UnmarshalJSON(buf []byte) error { b64 = getB64Value(sig.protected) } + // mup.Payload is json.RawMessage, so we can distinguish an omitted + // "payload" member (nil) from one that is present on the wire (non-nil), + // including the present-but-null case. encoding/json decodes both an + // omitted member and "payload":null to a nil *string, so a *string field + // could not tell "payload":null apart from a true detached JWS. if mup.Payload == nil { m.detached = true } else { + // The "payload" member was present on the wire. Track this + // independently of whether it decodes to empty bytes: a JSON + // JWS that carries "payload":"" is an in-band JWS over an empty + // payload, NOT a detached JWS (RFC 7515 Appendix F omits the + // member entirely for detached). Detached verification must be + // able to tell the two apart. + m.payloadPresent = true + + // RFC 7515 requires the "payload" member value to be a base64url + // (or, with b64=false, raw) string. Reject JSON null and any + // non-string value (number/object/array/bool) rather than letting + // a null be silently mistaken for a detached payload. + var payload *string + if err := json.Unmarshal(mup.Payload, &payload); err != nil { + return fmt.Errorf(`invalid "payload" value: must be a string: %w`, err) + } + if payload == nil { + return fmt.Errorf(`invalid "payload" value: must be a string`) + } + if !b64 { // NOT base64 encoded - m.payload = []byte(*mup.Payload) + m.payload = []byte(*payload) } else { - decoded, err := base64.DecodeString(*mup.Payload) + decoded, err := base64.DecodeString(*payload) if err != nil { return fmt.Errorf(`failed to base64 decode payload: %w`, err) } diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/options.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/options.go index fb8563ac..03bc2d05 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/options.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/options.go @@ -141,6 +141,9 @@ func (w *withKey) SetProtectedDefault(v Headers) Headers { // family of algorithms. You may consider using `github.com/jwx-go/crypto-signer` // if you would like to use keys stored in GCP/AWS KMS services. // +// A malformed (wrong-length) ed25519.PrivateKey/PublicKey causes +// `jws.Sign()`/`jws.Verify()` to return an error rather than panicking. +// // If the key is a jwk.Key and the key contains a key ID (`kid` field), // then it is added to the protected header generated by the signature. // diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/options.yaml b/vendor/github.com/lestrrat-go/jwx/v4/jws/options.yaml index 06a46601..63f6cbf9 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/options.yaml +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/options.yaml @@ -376,6 +376,33 @@ options: This option can be passed to `jws.Settings()` to change the default globally, or to `jws.Parse()` / `jws.ParseFS()` for a per-call override. + - ident: SkipAlgorithmMatch + interface: VerifyOption + argument_type: bool + comment: | + WithSkipAlgorithmMatch disables the check that the protected header's + "alg" parameter exactly equals the algorithm actually used to verify + the signature. By default jws.Verify() rejects a JWS whose protected + header advertises one algorithm while it is verified under another + (for example, a custom jws.WithKeyProvider that returns an HS256 key + for a message whose protected header claims RS256). The match is plain + string equality, with no aliasing: the deprecated polymorphic "EdDSA" + and the fully-specified "Ed25519"/"Ed448" identifiers are distinct per + RFC 9864 and are not interchangeable. This guard runs for every key + source — jws.WithKey(), jws.WithKeySet(), jws.WithVerifyAuto(), and + custom jws.WithKeyProvider() — so the algorithm a message advertises + always matches the discipline under which it was accepted. The check + only fires when the protected header carries an "alg"; messages that + place "alg" only in the unprotected header (or omit it) are unaffected. + + Pass jws.WithSkipAlgorithmMatch(true) to bypass this check. It is + intended for recovery or interoperability with non-conforming + producers that emit a protected "alg" inconsistent with the actual + signature algorithm. It weakens a safety check: with it enabled, a + message can be accepted under an algorithm that contradicts its own + advertised "alg", so use it only when you understand and accept that + risk. + - ident: CritValidation interface: VerifyOption argument_type: bool diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/options_gen.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/options_gen.go index 92ec94f7..d38cbdd9 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/options_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/options_gen.go @@ -202,6 +202,7 @@ type identProtectedHeaders struct{} type identPublicHeaders struct{} type identRequireKid struct{} type identSerialization struct{} +type identSkipAlgorithmMatch struct{} type identUseDefault struct{} type identValidateKey struct{} @@ -277,6 +278,10 @@ func (identSerialization) String() string { return "WithSerialization" } +func (identSkipAlgorithmMatch) String() string { + return "WithSkipAlgorithmMatch" +} + func (identUseDefault) String() string { return "WithUseDefault" } @@ -598,6 +603,32 @@ func WithCompact() SignVerifyParseOption { return &signVerifyParseOption{option.New(identSerialization{}, fmtCompact)} } +// WithSkipAlgorithmMatch disables the check that the protected header's +// "alg" parameter exactly equals the algorithm actually used to verify +// the signature. By default jws.Verify() rejects a JWS whose protected +// header advertises one algorithm while it is verified under another +// (for example, a custom jws.WithKeyProvider that returns an HS256 key +// for a message whose protected header claims RS256). The match is plain +// string equality, with no aliasing: the deprecated polymorphic "EdDSA" +// and the fully-specified "Ed25519"/"Ed448" identifiers are distinct per +// RFC 9864 and are not interchangeable. This guard runs for every key +// source — jws.WithKey(), jws.WithKeySet(), jws.WithVerifyAuto(), and +// custom jws.WithKeyProvider() — so the algorithm a message advertises +// always matches the discipline under which it was accepted. The check +// only fires when the protected header carries an "alg"; messages that +// place "alg" only in the unprotected header (or omit it) are unaffected. +// +// Pass jws.WithSkipAlgorithmMatch(true) to bypass this check. It is +// intended for recovery or interoperability with non-conforming +// producers that emit a protected "alg" inconsistent with the actual +// signature algorithm. It weakens a safety check: with it enabled, a +// message can be accepted under an algorithm that contradicts its own +// advertised "alg", so use it only when you understand and accept that +// risk. +func WithSkipAlgorithmMatch(v bool) VerifyOption { + return &verifyOption{option.New(identSkipAlgorithmMatch{}, v)} +} + // WithUseDefault specifies that if and only if a jwk.Key contains // exactly one jwk.Key, that key should be used. func WithUseDefault(v bool) WithKeySetSuboption { diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/streaming_detached.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/streaming_detached.go index 648d48d9..f4c57d82 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/streaming_detached.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/streaming_detached.go @@ -255,12 +255,33 @@ func (vc *verifyContext) verifyStreaming(buf []byte) ([]byte, error) { if len(msg.signatures) != 1 { return nil, makeVerifyError(`jws.WithDetachedPayloadReader() supports only single-signature JWS, got %d`, len(msg.signatures)) } - if len(msg.payload) != 0 { + if msg.payloadPresent { + // As with the non-streaming detached path, a present-but-empty + // JSON "payload":"" member counts as an embedded (in-band) + // payload and must be rejected; only a truly omitted member is a + // detached JWS. return nil, makeVerifyError(`JWS must not have an embedded payload when jws.WithDetachedPayloadReader() is used`) } sig := msg.signatures[0] + // Enforce that the algorithm we are about to verify under exactly matches + // the "alg" advertised in this signature's protected header. The streaming + // path bypasses verifyContext.tryKey (it talks to dsig directly for + // incremental hashing), so without this guard a detached JWS whose + // protected header advertises one algorithm would still verify under the + // single pinned WithKey algorithm — the same algorithm-confusion the + // tryKey guard prevents on the non-streaming path. The single staticKP + // alg above is the verification algorithm, and the match is plain string + // equality. The check fires only when the protected header carries an + // "alg"; use WithSkipAlgorithmMatch to bypass it for non-conforming + // producers. + if !vc.skipAlgorithmMatch && sig.protected != nil { + if hdrAlg, ok := sig.protected.Algorithm(); ok && hdrAlg.String() != alg.String() { + return nil, verifyError{verificationError{fmt.Errorf(`protected header %q %q does not match verification algorithm %q`, AlgorithmKey, hdrAlg, alg)}} + } + } + var rawHeaders []byte if rbp, ok := sig.protected.(interface{ rawBuffer() []byte }); ok { rawHeaders = rbp.rawBuffer() diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jws/verify_context.go b/vendor/github.com/lestrrat-go/jwx/v4/jws/verify_context.go index 680569a1..736a010f 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jws/verify_context.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jws/verify_context.go @@ -27,6 +27,7 @@ type verifyContext struct { keyUsed *any validateKey bool critValidation bool + skipAlgorithmMatch bool criticalExtensions []string encoder Base64Encoder //nolint:containedctx @@ -52,6 +53,7 @@ func freeVerifyContext(vc *verifyContext) *verifyContext { vc.keyUsed = nil vc.validateKey = false vc.critValidation = true + vc.skipAlgorithmMatch = false vc.criticalExtensions = vc.criticalExtensions[:0] vc.encoder = base64.DefaultEncoder() vc.ctx = context.Background() @@ -118,6 +120,8 @@ func (vc *verifyContext) ProcessOptions(options []VerifyOption) error { vc.validateKey = option.MustGet[bool](opt) case identCritValidation{}: vc.critValidation = option.MustGet[bool](opt) + case identSkipAlgorithmMatch{}: + vc.skipAlgorithmMatch = option.MustGet[bool](opt) case identCritExtension{}: vc.criticalExtensions = append(vc.criticalExtensions, option.MustGet[[]string](opt)...) case identSerialization{}: @@ -169,7 +173,12 @@ func (vc *verifyContext) VerifyMessage(buf []byte) ([]byte, error) { defer msg.clearRaw() if vc.detachedPayload != nil { - if len(msg.payload) != 0 { + // Reject when a "payload" member was present on the wire, even if + // it decoded to empty bytes. A JSON JWS carrying "payload":"" is + // an in-band JWS over an empty payload, not a detached one; only a + // truly absent payload member (RFC 7515 Appendix F) may be + // satisfied by a caller-supplied detached payload. + if msg.payloadPresent { return nil, makeVerifyError(`can't specify detached payload for JWS with payload`) } @@ -282,6 +291,25 @@ func (vc *verifyContext) VerifyMessage(buf []byte) ([]byte, error) { } func (vc *verifyContext) tryKey(verifyBuf []byte, alg jwa.SignatureAlgorithm, key any, msg *Message, sig *Signature) error { + // Enforce that the algorithm we are about to verify under exactly matches + // the "alg" advertised in this signature's protected header. tryKey is the + // single chokepoint every (alg, key) candidate funnels through, so this + // covers all key sources (WithKey, WithKeySet, WithVerifyAuto, and + // custom WithKeyProvider) — a provider cannot verify a message under an + // algorithm that contradicts the message's own protected "alg". The match + // is plain string equality: the deprecated polymorphic "EdDSA" and the + // fully-specified "Ed25519"/"Ed448" identifiers are distinct per RFC 9864 + // and do NOT alias one another. The check fires only when the protected + // header carries an "alg"; if "alg" is absent (for example a JSON JWS that + // places it only in the unprotected header) we fall through unchanged. + // VerifyCompactFast performs the equivalent cross-check on the fast path. + // Use WithSkipAlgorithmMatch to bypass this for non-conforming producers. + if !vc.skipAlgorithmMatch && sig.protected != nil { + if hdrAlg, ok := sig.protected.Algorithm(); ok && hdrAlg.String() != alg.String() { + return verifyError{verificationError{fmt.Errorf(`protected header %q %q does not match verification algorithm %q`, AlgorithmKey, hdrAlg, alg)}} + } + } + if vc.validateKey { if err := validateKeyBeforeUse(key); err != nil { return fmt.Errorf(`failed to validate key before verification: %w`, err) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwt/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v4/jwt/BUILD.bazel index dfdf1ee1..35b0a1f1 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwt/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwt/BUILD.bazel @@ -42,11 +42,13 @@ go_test( name = "jwt_test", srcs = [ "bench_parse_test.go", + "fastpath_header_test.go", "fastpath_test.go", "fuzz_test.go", "jwt_crit_test.go", "jwt_test.go", "options_gen_test.go", + "settings_test.go", "structured_errors_test.go", "token_options_test.go", "token_test.go", diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwt/jwt.go b/vendor/github.com/lestrrat-go/jwx/v4/jwt/jwt.go index 604a2c2e..86a3b72b 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwt/jwt.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwt/jwt.go @@ -26,9 +26,12 @@ var muSettings sync.Mutex var defaultTruncation atomic.Int64 // Settings controls global settings that are specific to JWTs. +// +// Each call adjusts only the options explicitly provided; settings that are +// not specified are left unchanged from their previous value. func Settings(options ...GlobalOption) error { - var flattenAudience bool - var parsePedantic bool + var flattenAudience *bool + var parsePedantic *bool var parsePrecision = types.MaxPrecision + 1 // illegal value, so we can detect nothing was set var formatPrecision = types.MaxPrecision + 1 // illegal value, so we can detect nothing was set truncation := time.Duration(-1) @@ -37,9 +40,11 @@ func Settings(options ...GlobalOption) error { case identTruncation{}: truncation = option.MustGet[time.Duration](opt) case identFlattenAudience{}: - flattenAudience = option.MustGet[bool](opt) + b := option.MustGet[bool](opt) + flattenAudience = &b case identNumericDateParsePedantic{}: - parsePedantic = option.MustGet[bool](opt) + b := option.MustGet[bool](opt) + parsePedantic = &b case identNumericDateParsePrecision{}: v := option.MustGet[int](opt) if v < 0 || v > int(types.MaxPrecision) { @@ -66,17 +71,17 @@ func Settings(options ...GlobalOption) error { types.FormatPrecision.Store(formatPrecision) } - { + if parsePedantic != nil { var newVal uint32 - if parsePedantic { + if *parsePedantic { newVal = 1 } types.Pedantic.Store(newVal) } - { + if flattenAudience != nil { opts := TokenOptionSet(defaultOptions.Load()) - if flattenAudience { + if *flattenAudience { opts.Enable(FlattenAudience) } else { opts.Disable(FlattenAudience) @@ -140,6 +145,29 @@ func ParseString(s string, options ...ParseOption) (Token, error) { // ParseOptions control parsing and verification behavior, and // ValidateOptions are passed to `Validate()` when automatic validation is // enabled. +// +// For the common case — a single `jwt.WithKey()` naming a concrete signature +// algorithm (no per-key suboptions), over a compact JWS — Parse verifies +// through an internal fast path (see `jws.VerifyCompactFast`) that avoids +// fully materializing the JWS message. This fast path is transparent: it +// applies only to a minimal protected header (`alg` once, an optional single +// `typ`/`kid`/`cty`, nothing else, no JSON escapes — see +// `jws.VerifyCompactFast` for the exact shape), and any other header — +// including one carrying duplicate, unknown, or key-source parameters, +// `crit`, or `b64` — is automatically reverified through the full +// `jws.Verify` path, so it is never accepted more leniently than `jws.Verify` +// would. In particular a protected header with duplicate parameter names is +// rejected, matching `jws.Verify`. +// +// The fast path is a close but not byte-for-byte mirror of `jws.Verify`'s +// header parsing. It validates parameter names and that `alg`/`typ`/`kid`/`cty` +// are JSON strings, so the common divergences — duplicate names, non-string +// values — are rejected, matching `jws.Verify`. A residual gap remains only +// for value-level leniency the fast JSON parser tolerates but +// `encoding/json/v2` does not (e.g. a raw control character or invalid UTF-8 +// inside a JSON string value); for byte-for-byte `jws.Verify` header +// validation, call `jws.Verify` directly. The signature is always verified, so +// any residual difference is a parser-strictness nuance, not a security bypass. func Parse(s []byte, options ...ParseOption) (Token, error) { tok, err := parseBytes(s, options...) if err != nil { @@ -318,10 +346,12 @@ func verifyJWS(ctx *parseCtx, payload []byte) ([]byte, int, error) { if err == nil { return verified, peekJWSNestedState(ctx, payload), nil } - // VerifyCompactFast refuses crit-bearing messages; on - // that sentinel, fall through to jws.Verify below so - // the full validateCritical rule set applies. - if !errors.Is(err, jws.ErrCritPresent()) { + // VerifyCompactFast refuses any header outside its minimal + // shape (crit and b64 are specific cases of this); on that + // umbrella sentinel, fall through to jws.Verify below so the + // full validateCritical rule set and json/v2's strict header + // handling (e.g. duplicate-name rejection) apply. + if !errors.Is(err, jws.ErrNonMinimalHeader()) { // The fast path uses strict base64url (RFC 7515). // On a strict-decode failure, surface a diagnosis // first ("input is not strict RFC 7515 base64url") @@ -448,7 +478,7 @@ OUTER: if state != _JwsVerifySkipped { payload = v - // We only check for cty and typ if the pedantic flag is enabled + // We only check for cty (to detect nested JWTs) if the pedantic flag is enabled if !ctx.pedantic { continue } diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwt/options.yaml b/vendor/github.com/lestrrat-go/jwx/v4/jwt/options.yaml index aeb3bb90..7dd9a066 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwt/options.yaml +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwt/options.yaml @@ -80,6 +80,8 @@ options: comment: | WithClock specifies the `Clock` to be used when verifying exp, iat and nbf claims. + + If v is nil, the default clock (the system clock, time.Now) is used. - ident: Context interface: ValidateOption argument_type: context.Context @@ -173,7 +175,8 @@ options: argument_type: Token comment: | WithToken specifies the token instance in which the resulting JWT is stored - when parsing JWT tokens + when parsing JWT tokens. The supplied token is fully reset (all claims, + including private claims, are cleared) before the parsed token is stored into it. - ident: Validate interface: ParseOption argument_type: bool @@ -213,7 +216,10 @@ options: argument_type: bool comment: | WithPedantic enables pedantic mode for parsing JWTs. Currently this only - applies to checking for the correct `typ` and/or `cty` when necessary. + applies to detecting nested JWTs via the `cty` (content type) header and + rejecting payloads that are not recognizable as a JWT. The `typ` header is + not inspected: it is OPTIONAL per RFC 7519 §5.1, so its absence or value is + not treated as an error. - ident: EncryptOption interface: EncryptOption argument_type: jwe.EncryptOption @@ -233,7 +239,9 @@ options: argument_type: Validator comment: | WithValidator validates the token with the given Validator. - + + Passing a nil Validator causes Validate to return an error. + For example, in order to validate tokens that are only valid during August, you would write validator := jwt.ValidatorFunc(func(_ context.Context, t jwt.Token) error { diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwt/options_gen.go b/vendor/github.com/lestrrat-go/jwx/v4/jwt/options_gen.go index c85dbdef..cfb6bacd 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwt/options_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwt/options_gen.go @@ -289,6 +289,8 @@ func WithBase64Encoder(v jws.Base64Encoder) SignParseOption { // WithClock specifies the `Clock` to be used when verifying // exp, iat and nbf claims. +// +// If v is nil, the default clock (the system clock, time.Now) is used. func WithClock(v Clock) ValidateOption { return &validateOption{option.New(identClock{}, v)} } @@ -409,7 +411,10 @@ func WithNumericDateParsePrecision(v int) GlobalOption { } // WithPedantic enables pedantic mode for parsing JWTs. Currently this only -// applies to checking for the correct `typ` and/or `cty` when necessary. +// applies to detecting nested JWTs via the `cty` (content type) header and +// rejecting payloads that are not recognizable as a JWT. The `typ` header is +// not inspected: it is OPTIONAL per RFC 7519 §5.1, so its absence or value is +// not treated as an error. func WithPedantic(v bool) ParseOption { return &parseOption{option.New(identPedantic{}, v)} } @@ -475,7 +480,8 @@ func WithStrictStringClaims(v bool) ParseOption { } // WithToken specifies the token instance in which the resulting JWT is stored -// when parsing JWT tokens +// when parsing JWT tokens. The supplied token is fully reset (all claims, +// including private claims, are cleared) before the parsed token is stored into it. func WithToken(v Token) ParseOption { return &parseOption{option.New(identToken{}, v)} } @@ -508,6 +514,8 @@ func WithValidate(v bool) ParseOption { // WithValidator validates the token with the given Validator. // +// Passing a nil Validator causes Validate to return an error. +// // For example, in order to validate tokens that are only valid during August, you would write // // validator := jwt.ValidatorFunc(func(_ context.Context, t jwt.Token) error { diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwt/parse_fast.go b/vendor/github.com/lestrrat-go/jwx/v4/jwt/parse_fast.go index fd3bef3c..d1ddfdf9 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwt/parse_fast.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwt/parse_fast.go @@ -75,12 +75,15 @@ func tryFastPath(ctx *fastParseCtx, data []byte, options []ParseOption) bool { func parseCompactFast(data []byte, ctx *fastParseCtx) (Token, error) { payload, err := jws.VerifyCompactFast(ctx.key, data, ctx.alg) if err != nil { - // VerifyCompactFast refuses crit-bearing messages. jwt.Parse - // must not be laxer than jws.Verify, so fall through to the - // full jws.Verify path which enforces validateCritical with - // the default-strict (empty) WithCritExtension allowlist. - if errors.Is(err, jws.ErrCritPresent()) { - return parseCompactCritFallback(data, ctx) + // VerifyCompactFast refuses any header outside its minimal shape + // (crit and b64 are specific cases of this umbrella). jwt.Parse must + // not be laxer than jws.Verify, so fall through to the full + // jws.Verify path: it enforces validateCritical with the + // default-strict (empty) WithCritExtension allowlist, plus json/v2's + // strict header decoding (e.g. duplicate-name rejection, issue + // #2234) that the fast path's minimal-shape gate defers to it. + if errors.Is(err, jws.ErrNonMinimalHeader()) { + return parseCompactSlowFallback(data, ctx) } // The fast path uses strict base64url (RFC 7515). On a // strict-decode failure, surface a diagnosis first ("input @@ -111,12 +114,14 @@ func parseCompactFast(data []byte, ctx *fastParseCtx) (Token, error) { return token, nil } -// parseCompactCritFallback routes a fast-path-eligible input through -// jws.Verify so the full RFC 7515 §4.1.11 "crit" rule set applies. -// Reached only when the protected header actually contains "crit" (or -// fails to split), so the extra cost is limited to adversarial / RFC 7797 +// parseCompactSlowFallback routes a fast-path-eligible input through +// jws.Verify so the full RFC 7515 rule set applies: the §4.1.11 "crit" +// handling, and json/v2's strict header decoding (duplicate-name rejection +// etc.) that the fast path's minimal-shape gate defers here. Reached only +// when the protected header actually contains "crit", is non-minimal, or +// fails to split, so the extra cost is limited to adversarial / unusual // inputs. -func parseCompactCritFallback(data []byte, ctx *fastParseCtx) (Token, error) { +func parseCompactSlowFallback(data []byte, ctx *fastParseCtx) (Token, error) { payload, err := jws.Verify(data, jws.WithCompact(), jws.WithKey(ctx.alg, ctx.key)) if err != nil { return nil, parseErrorf(`jwt.Parse`, `%w`, err) diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwt/token_gen.go b/vendor/github.com/lestrrat-go/jwx/v4/jwt/token_gen.go index 5c57deb2..1f9bd9ee 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwt/token_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwt/token_gen.go @@ -376,6 +376,7 @@ func (t *stdToken) UnmarshalJSON(buf []byte) error { t.jwtID = nil t.notBefore = nil t.subject = nil + clear(t.privateClaims) dec := json.NewDecoder(bytes.NewReader(buf)) tok, err := dec.ReadToken() if err != nil { diff --git a/vendor/github.com/lestrrat-go/jwx/v4/jwt/validate.go b/vendor/github.com/lestrrat-go/jwx/v4/jwt/validate.go index 91a7a1c8..5677e69d 100644 --- a/vendor/github.com/lestrrat-go/jwx/v4/jwt/validate.go +++ b/vendor/github.com/lestrrat-go/jwx/v4/jwt/validate.go @@ -20,6 +20,21 @@ func (f ClockFunc) Now() time.Time { return f() } +// isNilValue reports whether v is nil, including a typed-nil interface value +// that wraps a nil pointer (or other nilable kind). A plain `== nil` check +// misses the latter because the interface itself is non-nil. +func isNilValue(v any) bool { + if v == nil { + return true + } + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.Ptr, reflect.Interface, reflect.Func, reflect.Map, reflect.Slice, reflect.Chan: + return rv.IsNil() + } + return false +} + func isSupportedTimeClaim(c string) error { switch c { case ExpirationKey, IssuedAtKey, NotBeforeKey: @@ -66,7 +81,8 @@ func Validate(t Token, options ...ValidateOption) error { ctx := context.Background() trunc := getDefaultTruncation() - var clock Clock = ClockFunc(time.Now) + var defaultClock Clock = ClockFunc(time.Now) + clock := defaultClock var skew time.Duration var baseValidators = []Validator{ IsIssuedAtValid(), @@ -80,7 +96,19 @@ func Validate(t Token, options ...ValidateOption) error { for _, o := range options { switch o.Ident() { case identClock{}: - clock = option.MustGet[Clock](o) + // A nil Clock falls back to the default clock (the system + // clock, time.Now). WithClock(nil) stores an untyped nil, so + // option.Get reports ok=false; a typed-nil interface wrapping a + // nil pointer reports ok=true but isNilValue catches it. In both + // cases reset to the default so a later WithClock(nil) undoes an + // earlier custom clock, matching the documented behavior and + // avoiding a panic in clock.Now(). + c, ok := option.Get[Clock](o) + if !ok || isNilValue(c) { + clock = defaultClock + } else { + clock = c + } case identAcceptableSkew{}: skew = option.MustGet[time.Duration](o) if skew < 0 { @@ -95,7 +123,13 @@ func Validate(t Token, options ...ValidateOption) error { case identCollectErrors{}: collectErrors = option.MustGet[bool](o) case identValidator{}: - v := option.MustGet[Validator](o) + // A nil Validator (including a typed-nil interface wrapping a + // nil pointer) would panic when its Validate method is called, + // so reject it up front with a clear error. + v, ok := option.Get[Validator](o) + if !ok || isNilValue(v) { + return validateErrorf(`jwt.WithValidator: nil Validator`) + } switch v := v.(type) { case *isInTimeRange: if v.c1 != "" { diff --git a/vendor/modules.txt b/vendor/modules.txt index 650d009e..da0e0c90 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -549,7 +549,7 @@ github.com/klauspost/cpuid/v2 ## explicit; go 1.23.0 github.com/lestrrat-go/dsig github.com/lestrrat-go/dsig/internal/ecutil -# github.com/lestrrat-go/jwx/v4 v4.0.2 +# github.com/lestrrat-go/jwx/v4 v4.1.0 ## explicit; go 1.26.0 github.com/lestrrat-go/jwx/v4 github.com/lestrrat-go/jwx/v4/cert @@ -572,6 +572,7 @@ github.com/lestrrat-go/jwx/v4/jwk/ecdsa github.com/lestrrat-go/jwx/v4/jwk/internal/registry github.com/lestrrat-go/jwx/v4/jwk/jwkbb github.com/lestrrat-go/jwx/v4/jws +github.com/lestrrat-go/jwx/v4/jws/internal/jwsbb github.com/lestrrat-go/jwx/v4/jws/jwsbb github.com/lestrrat-go/jwx/v4/jwt github.com/lestrrat-go/jwx/v4/jwt/internal/types