Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/request-signing-authority-rule-inlined.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
---

Three tightenings to close the cross-vhost replay vector on the request-signing side, mirroring the webhook-side fix in PR #2467.

- **Checklist step 10** inlines the `@authority` derivation rule (already load-bearing in the profile's canonicalization section) so a verifier implementer working from the checklist alone cannot miss it.
- **Profile `@authority` canonicalization** (the canonical rule) now pins the `:authority` (HTTP/2+) vs `Host` (HTTP/1.1) source precedence and requires byte-equality when both are present on the as-received request. Pick-one behavior across HTTP/2→HTTP/1.1 translating intermediaries was a silent downgrade surface — RFC 7540 §8.1.2.3 requires equivalence but does not require intermediaries to strip the source header, so verifiers MUST reject `request_target_uri_malformed` on divergence. The canonicalized value MUST byte-for-byte match the authority component of the canonical `@target-uri` (the load-bearing safety gate, because `Host` itself can be rewritten in transit).
- **Error-taxonomy row** for `request_target_uri_malformed` expanded to cover both the syntactic-malformation case (already described) and the authority-mismatch case (already normative in the profile, now visible in the table).

No wire-schema change. No new error codes. Conformant verifiers already applying the profile's `@authority` rule remain conformant.
6 changes: 3 additions & 3 deletions docs/building/implementation/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,7 @@ This profile constrains RFC 9421 to a single canonical shape so cross-implementa
7. **Preserve the query string byte-for-byte** as received. Do not reorder parameters, do not re-encode, do not interpret `+` as space. A trailing `?` with empty query is preserved (`https://host/p?` canonicalizes to `https://host/p?`, distinct from `https://host/p`). A URL with no `?` stays with no `?`.
8. Strip the fragment (fragments are not sent on the wire per RFC 9421 §2.2.2).

**`@authority` canonicalization** produces `host[:port]` from the URL's authority after steps 2–4 above (lowercase host / IDN → ACE / IPv6 bracketing preserved; userinfo stripped; default port stripped). IPv6 hosts retain their brackets in `@authority` (`[::1]:8443`). Verifiers MUST derive `@authority` from the as-received HTTP `Host` header (or HTTP/2+ `:authority` pseudo-header), not from reverse-proxy routing state, and MUST reject with `request_target_uri_malformed` if that value canonicalized does not byte-for-byte match the authority component of the canonical `@target-uri`. This closes a cross-vhost replay vector: an attacker who intercepts a TLS-terminated request and replays it to a second vhost on the same verifier pool (same cert SAN, different `Host`) will fail the authority-match check even though the signature covers `@authority`.
**`@authority` canonicalization** produces `host[:port]` from the URL's authority after steps 2–4 above (lowercase host / IDN → ACE / IPv6 bracketing preserved; userinfo stripped; default port stripped). IPv6 hosts retain their brackets in `@authority` (`[::1]:8443`). Verifiers MUST derive `@authority` from the HTTP/2+ `:authority` pseudo-header when present, otherwise from the as-received HTTP/1.1 `Host` header — not from reverse-proxy routing state, load-balancer metadata, or any `Host` value a forward proxy may have rewritten in transit. **When both `:authority` and `Host` are present on the as-received request** (HTTP/2→HTTP/1.1 translating intermediaries are permitted to leave both by RFC 7540 §8.1.2.3, which requires equivalence but does not require stripping the source), verifiers MUST reject with `request_target_uri_malformed` if they are not byte-equal after canonicalization; pick-one behavior is a silent downgrade surface. Regardless of the source header, the canonicalized value MUST byte-for-byte match the authority component of the canonical `@target-uri` — the byte-match against the signed `@target-uri` is the load-bearing safety gate, because `Host` can itself be rewritten in transit. Mismatch rejects with `request_target_uri_malformed`. This closes a cross-vhost replay vector: an attacker who intercepts a TLS-terminated request and replays it to a second vhost on the same verifier pool (same cert SAN, different `Host`) will fail the authority-match check even though the signature covers `@authority`.

Signers that canonicalize and verifiers that canonicalize MUST produce identical bytes for the same logical request. If your 9421 library applies different rules, either configure it to match this profile or normalize before handing the URL to the library.

Expand Down Expand Up @@ -905,7 +905,7 @@ Otherwise, verifiers MUST perform these checks in order, short-circuiting on the

**9a. Per-keyid cap check.** Check the [per-keyid replay-cache cap](#transport-replay-dedup). Reject with `request_signature_rate_abuse` if the cap has been reached for this `keyid`. Runs before cryptographic verify (step 10) — same rationale as step 9: a compromised or misconfigured signer exhausting its cap MUST NOT force amplified Ed25519/ECDSA work on the verifier. Runs *after* `keyid` resolution (step 7) so the cap-state oracle only responds for keys the verifier has already committed to recognizing — running 9a earlier would let an attacker probe verifier-internal rate-limit state across the full keyid space, including keyids not published in JWKS.

10. Compute the canonical signature base per RFC 9421 §2.5 using the covered components (after applying `@target-uri` canonicalization). Verify the signature against the JWK (`request_signature_invalid` on failure).
10. Compute the canonical signature base per RFC 9421 §2.5 using the covered components, after applying `@target-uri` canonicalization AND `@authority` derivation per [the profile above](#adcp-rfc-9421-profile). **The `@authority` rule is load-bearing:** verifiers MUST derive `@authority` from the HTTP/2+ `:authority` pseudo-header when present, otherwise from the as-received HTTP/1.1 `Host` header — NOT from reverse-proxy routing state, load-balancer metadata, or any `Host` value a forward proxy may have rewritten in transit. If both `:authority` and `Host` are present on the as-received request, they MUST be byte-equal after canonicalization (RFC 7540 §8.1.2.3 equivalence); divergence rejects with `request_target_uri_malformed`. The canonicalized `@authority` MUST byte-for-byte match the authority component of the canonical `@target-uri`; mismatch rejects with `request_target_uri_malformed`. That byte-match against the signed `@target-uri` — not the choice of source header — is the only safe gate, because `Host` itself can be rewritten in transit. Implementers building from this checklist alone — without cross-referencing the profile's canonicalization section — MUST apply this rule; skipping it silently accepts a cross-vhost replay vector (an attacker intercepts a TLS-terminated request and replays it to a second vhost on the same verifier pool: same cert SAN, different `Host`). After canonicalization completes, verify the signature against the JWK (`request_signature_invalid` on failure).
11. If `content-digest` is covered, recompute the digest from the received body bytes and compare (`request_signature_digest_mismatch` on mismatch).
12. Check the nonce against the replay cache (see [Transport replay dedup](#transport-replay-dedup)). Reject if `(keyid, nonce)` has been seen within the replay-cache TTL (`request_signature_replayed`).
13. **Only after steps 1–9, 9a, and 10–12 have all passed**, insert `(keyid, nonce)` into the replay cache with TTL = `(expires − now) + 60 s` (the +60 s matches the skew tolerance applied at step 5).
Expand Down Expand Up @@ -1002,7 +1002,7 @@ Stable codes returned in `WWW-Authenticate: Signature error="<code>"` on 401, an
| Failure | Retry? | Code |
|---|---|---|
| Operation in `required_for` but request is unsigned | No | `request_signature_required` |
| Request `@target-uri` is syntactically malformed (e.g., empty authority, bare IPv6, IPv6 zone identifier, raw non-ASCII host) | No | `request_target_uri_malformed` |
| Request `@target-uri` is syntactically malformed (e.g., empty authority, bare IPv6, IPv6 zone identifier, raw non-ASCII host), OR canonicalized `@authority` does not byte-match the authority component of the canonical `@target-uri` (cross-vhost replay) | No | `request_target_uri_malformed` |
| `Signature` or `Signature-Input` header present but malformed | No | `request_signature_header_malformed` |
| Required sig-param absent (`created`, `expires`, `nonce`, `keyid`, `alg`, or `tag`) | No | `request_signature_params_incomplete` |
| `tag` not `adcp/request-signing/v1` | No | `request_signature_tag_invalid` |
Expand Down
Loading