AAuth: HTTP Message Signature verification (RFC 9421 + draft-hardt-httpbis-signature-key)#2276
Draft
christian-posta wants to merge 5 commits into
Draft
AAuth: HTTP Message Signature verification (RFC 9421 + draft-hardt-httpbis-signature-key)#2276christian-posta wants to merge 5 commits into
christian-posta wants to merge 5 commits into
Conversation
Contributor
Author
|
@copilot review |
Adds the http-message-sig crate, a self-contained implementation of RFC 9421 (HTTP Message Signatures), RFC 9530 (Content-Digest), and draft-hardt-httpbis-signature-key (the Signature-Key header with hwk, jwks_uri, and jwt schemes). Ed25519 only for the signing side; the JWK type accepts OKP, RSA, and EC for use by higher layers that consume the JWKS of an arbitrary issuer. Key design points: - The verifier rebuilds the @signature-params line byte-for-byte from the raw Signature-Input header via build_signature_base_raw. RFC 9421 §2.5 requires the signature base be reproduced exactly; reconstructing from parsed fields silently reorders parameters and breaks Ed25519 verification against signers that emit them in a different order than ours. - The signer's @authority includes the URL port when present, matching the verifier's authority construction so sign+verify agree on https://example.com:8443/... - Unsupported signature-key schemes are rejected before key resolution to avoid masking the underlying rejection with a key-fetch error. - created is required per RFC 9421 §4.1; absence is rejected rather than silently defaulting to 0. Tested against cross-implementation vectors (lifted from the Go reference at https://github.com/christian-posta/extauth-aauth-resource) plus 39 unit tests covering signing/verification round-trips, parser edge cases, and content-digest computation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Christian Posta <christian.posta@gmail.com>
Adds the aauth crate built on top of http-message-sig. Validates aa-agent+jwt and aa-auth+jwt tokens per draft-hardt-oauth-aauth- protocol: extracts and verifies the JWT, checks required claims, and returns the embedded cnf.jwk for HTTP signature verification. Key behaviors: - The issuer URL check (is_acceptable_jwt_issuer_url) enforces host-only HTTPS for production. The dev-only http branch is loopback-restricted (localhost, 127.0.0.0/8, ::1) so the dev flag cannot be exploited to point at an external HTTP host. - get_scopes() returns None when the scope claim is empty or whitespace-only, so the 'must have at least one of sub or scope' guard in validate_auth_token is not bypassed by Some(vec![]). - validate_agent_token strictly requires aud when the caller passes Some(expected_audience) (mirroring auth-token semantics); the gateway passes None for agent tokens because they are identity assertions, not resource-scoped grants. - jti is captured when present but not required — agentgateway does not maintain a replay cache, and reference implementations omit it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Christian Posta <christian.posta@gmail.com>
Adds the AAuth policy as a route- and gateway-level traffic policy. Policy behavior: - Three signature-key schemes accepted: hwk (pseudonymous, key inline), jwks_uri (identified, key discovered via well-known doc + JWKS), and jwt (authorized, key bound to an aa-agent+jwt or aa-auth+jwt via RFC 7800 cnf.jwk). - Modes Strict / Optional / Permissive control how missing or invalid signatures are handled. - requiredScheme enforces a minimum scheme strength (hwk < jwks_uri < jwt); stronger schemes always satisfy weaker requirements. Insufficient-scheme rejections include an aauth challenge response header. - Verified claims (scheme, agent, agent_delegate, user, scope, thumbprint, jwt_claims) are injected into request extensions and exposed to CEL as aauth.* for downstream authorization rules. JWKS cache: - Keyed by (issuer_id, dwk) so a single issuer can legitimately publish multiple discovery documents (e.g. aauth-agent.json and aauth-issuer.json) without one's keys aliasing the other's. - Single-flight via per-key tokio::sync::Mutex<()> so N concurrent misses on the same issuer fan out to one network fetch, not N. - Lazy eviction on stale-read upgrades to a write lock and removes expired entries so the map doesn't grow unbounded across rotating issuers. Network-egress safety: - metadata.jwks_uri from the well-known doc is validated to be HTTPS before fetching; a compromised CDN or issuer can otherwise inject http://attacker/jwks and downgrade transport for signing keys. - Under allowInsecureHttpIssuer, plaintext JWKS fetches are accepted only when the host is a loopback address. Tests: 20 policy-level + 11 cross-impl test vectors. End-to-end verified against the Go reference at github.com/christian-posta/extauth-aauth- resource (sign-request CLI for hwk, agent-client for jwks_uri and aa-agent+jwt, tamper-rejection cases, mode behaviors). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Christian Posta <christian.posta@gmail.com>
Adds an example config under examples/aauth/ pairing with the Go reference's agent-client for end-to-end smoke tests, and the auto-regenerated schema/config.json and schema/config.md output covering the new policy fields. The Mode and RequiredScheme enums are renamed to AAuthMode and AAuthRequiredScheme in the generated schema (via schemars(rename)) so the JSON Schema $defs use descriptive names instead of schemars' auto-deduplicated Mode2 suffix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Christian Posta <christian.posta@gmail.com>
1644445 to
df0b8ae
Compare
Contributor
Author
|
Force-pushed with |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds AAuth (HTTP Message Signing) verification to agentgateway as a route- and gateway-level
traffic policy.
This is the verifier half of the
AAuth protocol draft —
inbound requests carrying RFC 9421 HTTP Message Signatures are verified against keys conveyed
via the Signature-Key draft.
Three schemes are supported:
hwkjwks_uri{iss}/.well-known/{dwk}→jwks_uri→ JWKSjwtcnf.jwkVerified claims (scheme, agent, agent_delegate, user, scope, thumbprint, jwt_claims) land in
request extensions and are exposed to CEL as
aauth.*for downstream authorization.Layout
crates/http-message-sig/— new crate implementing RFC 9421 + RFC 9530 + the signature-keydraft. Ed25519-only signing; JWK type accepts OKP/RSA/EC for use by higher layers.
crates/aauth/— new crate validating aa-agent+jwt and aa-auth+jwt tokens; built onhttp-message-sig.
crates/agentgateway/src/http/aauth.rs(+ tests) — the policy module that ties everythingtogether; implements
RequestPolicyTrait.TrafficPolicy::AAuthvariant,RoutePolicies/GatewayPoliciesfields,OutboundCallSubtype::AAuthfor JWKS-fetch telemetry, AAuthClaims plumbed into the CELExecutor/RequestSnapshot,
aauth.scheme/aauth.agentlog fields.examples/aauth/— minimal config + README that pairs with the Go reference'sagent-clientfor end-to-end smoke tests.
Notable design choices
(id, dwk)so a single issuer publishing bothaauth-agent.jsonandaauth-issuer.jsondoesn't alias the two key sets.tokio::sync::Mutex<()>) so N concurrentcold requests fan out to one network fetch.
metadata.jwks_uriis HTTPS-only; underallowInsecureHttpIssuer, plaintext isloopback-only (
localhost,127.0.0.0/8,::1). A compromised metadata document canotherwise inject
http://attacker/jwksand downgrade transport for the actual signing keys.createdis required per RFC 9421 §4.1 — the parser rejects its absence rather thansilently defaulting to 0, which would render the freshness check meaningless on misconfigured
deployments.
@authorityincludes port on both sign and verify sohttps://example.com:8443/...round-trips correctly.
scheme=jwksis rejected; only the currentjwks_uriis accepted.Testing
http-message-sig+ 11 cross-impl test vectors (lifted from the Goreference's vectors file).
aauth.agentgateway::http::aauth::testscovering modes, schemeordering, tamper rejection, deserialization, cache behaviors, jwks_uri validation.
https://github.com/christian-posta/extauth-aauth-resource:
sign-requestCLI for hwk,agent-clientfor jwks_uri and aa-agent+jwt, tamper cases, mode behaviors.Limitations / follow-ups
Content-Digestbody re-verification is not performed inside the gateway. The signeddigest header is verified (signature covers it), but the request body itself is not
re-hashed. Backends that care about body integrity should re-verify, or sit behind a
bufferpolicy. Worth a separate PR to integrate with the existing buffer infrastructure.apply_innerclones the entireHeaderMapinto aHashMap<String, String>because the verifier API takes the latter. Real cost butchanging
verify_signature's signature ripples through the crate's public API; deferred.http_message_sig::keys::jwk::JWK(custom) andjsonwebtoken::jwk::Jwk(used byhttp/jwt.rs). Consolidation is a larger reshape anddeserves its own PR.
Test plan
cargo test -p http-message-sig -p aauth(60 tests)cargo test -p agentgateway --lib http::aauth(20 tests)cargo clippy --all-targets -- -D warnings(clean)make generate-schema check-clean-repo(clean)🤖 Generated with Claude Code