regent-httpsig 0.5.1__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/CHANGELOG.md +125 -0
  2. regent_httpsig-0.7.0/CONTRIBUTING.md +11 -0
  3. regent_httpsig-0.7.0/GOVERNANCE.md +35 -0
  4. regent_httpsig-0.7.0/MAINTAINERS.md +11 -0
  5. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/PKG-INFO +67 -24
  6. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/README.md +66 -23
  7. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/pyproject.toml +1 -1
  8. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/src/regent_httpsig/__init__.py +33 -7
  9. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/src/regent_httpsig/budget.py +104 -4
  10. regent_httpsig-0.7.0/src/regent_httpsig/config.py +62 -0
  11. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/src/regent_httpsig/fastapi.py +189 -106
  12. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/src/regent_httpsig/sfv.py +58 -6
  13. regent_httpsig-0.7.0/src/regent_httpsig/sign.py +329 -0
  14. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/src/regent_httpsig/usage.py +119 -14
  15. regent_httpsig-0.7.0/src/regent_httpsig/verify.py +744 -0
  16. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/tests/test_budget.py +185 -41
  17. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/tests/test_signer.py +22 -0
  18. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/tests/test_usage.py +7 -5
  19. regent_httpsig-0.7.0/tests/test_verifier.py +512 -0
  20. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/vectors/aauth-budgets-vectors.json +1 -1
  21. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/vectors/generate.py +1 -1
  22. regent_httpsig-0.5.1/src/regent_httpsig/config.py +0 -47
  23. regent_httpsig-0.5.1/src/regent_httpsig/sign.py +0 -121
  24. regent_httpsig-0.5.1/src/regent_httpsig/verify.py +0 -490
  25. regent_httpsig-0.5.1/tests/test_verifier.py +0 -314
  26. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/.github/workflows/ci.yml +0 -0
  27. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/.github/workflows/release.yml +0 -0
  28. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/.gitignore +0 -0
  29. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/LICENSE +0 -0
  30. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/SECURITY.md +0 -0
  31. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/examples/fastapi_verify.py +0 -0
  32. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/examples/httpx_signer.py +0 -0
  33. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/src/regent_httpsig/cli.py +0 -0
  34. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/src/regent_httpsig/jwk.py +0 -0
  35. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/src/regent_httpsig/netguard.py +0 -0
  36. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/tests/test_netguard.py +0 -0
  37. {regent_httpsig-0.5.1 → regent_httpsig-0.7.0}/tests/test_vectors.py +0 -0
@@ -1,5 +1,130 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ **AAuth -11 cut-over** (draft-hardt-oauth-aauth-protocol-11, published
6
+ 25 September 2026; Signature-Key -09; budgets editor's copy of 6 October). One
7
+ dialect, as the editor asked: the -10 forms are gone, not kept behind a flag.
8
+ Breaking for every AAuth signer and resource; Web Bot Auth is untouched.
9
+
10
+ *Resource side (§11.3.4 — a typed answer for every refusal):*
11
+
12
+ - **`HttpsigVerifier.verify_detailed()`** returns a `VerifiedSignature` or a
13
+ **`VerificationError`** carrying the Signature-Error code -11 assigns:
14
+ `invalid_signature`, `invalid_input` (+ `required_input`),
15
+ `unsupported_scheme` (+ `Accept-Signature-Scheme`), `unsupported_algorithm`
16
+ (+ `Accept-Signature-Alg`), `invalid_key`, `unknown_key`, `issuer_missing`,
17
+ `issuer_mismatch`, `invalid_jwt`, `expired_jwt`, `revoked_jwt`, `clock_skew`.
18
+ `error.headers()` is the 401's header set, `error.problem()` the
19
+ `application/problem+json` body (`type: urn:ietf:params:sig-error:<code>`).
20
+ `verify()` keeps its `None`-on-failure contract.
21
+ - **Covered components are enforced** (§11.3.3.1): `@method @authority @path
22
+ signature-key`, plus `content-digest content-type` when the request has a
23
+ body — and the digest is checked against the body you pass. Missing
24
+ components are `invalid_input` with the full `required_input` list.
25
+ - **`created` window** (§11.3.4 step 3): `HttpsigConfig.signature_window_seconds`
26
+ (default 60). Older than the window is `invalid_signature`; further ahead of
27
+ our clock is `clock_skew`; a past `expires` is `invalid_signature`. The
28
+ upstream library's 5-second skew rule no longer pre-empts these.
29
+ - **RS-51 token time**: `exp` has no tolerance; `iat` is not a validity check
30
+ (only an `iat` beyond the window ahead is refused, as `clock_skew`); person
31
+ and auth tokens live at most **3600 s exactly** (the 90 s grace is gone);
32
+ `typ` is read first; an auth token's `dwk` must be `aauth-person.json` or
33
+ `aauth-access.json`.
34
+ - **Fully-specified algorithms by default** (§11.3.1, RFC 9864):
35
+ `require_fully_specified_algs` is now `True`. The polymorphic `EdDSA` and a
36
+ `cnf.jwk` without `alg` are `unsupported_algorithm` with
37
+ `Accept-Signature-Alg: Ed25519, ES256, RS256`. A P-256 possession key
38
+ (ES256) now verifies. The flag stays only for private test rigs.
39
+ - **The `jwks_uri` server scheme** (§11.3.2, Signature-Key §3.6):
40
+ `sig=jwks_uri;id="…";dwk="aauth-person.json";kid="…"` — the metadata
41
+ document at `{id}/.well-known/{dwk}` must name `issuer == id`
42
+ (`issuer_mismatch`), its `jwks_uri` is followed, the `kid` found (one
43
+ refetch on rotation, then `unknown_key`). `verify_server()` is the
44
+ entry point for usage and revocation endpoints; `trusted_ps` may pin a
45
+ JWKS URL or `""` to discover it.
46
+ - **Revocation hook**: `HttpsigVerifier(is_revoked=meter.is_revoked)` — a
47
+ token the application has on record as revoked is answered `revoked_jwt`
48
+ (RS-43, §11.12.5), never "invalid" or "expired".
49
+ - **RS-52**: agent identifiers compare exactly; no case folding (test pinned).
50
+
51
+ *FastAPI layer:*
52
+
53
+ - `RequiredSignatureDep` answers a failed signature with `401` +
54
+ `Signature-Error` (+ `Accept-Signature-*`, + `AAuth-Requirement:
55
+ requirement=person-token` for a revoked/expired person or auth token) and a
56
+ problem+json body; an unsigned request gets `AAuth-Requirement:
57
+ requirement=agent-token` (§6.1). New `SignatureErrorDep`,
58
+ `RequiredServerSignatureDep`, `signature_error_response()`.
59
+ - `BudgetMiddleware`: a **revoked** auth token is `revoked_jwt` +
60
+ `requirement=person-token`, with **no resource token** (the PS would reject
61
+ one naming a revoked token) and no `AAuth-Budget`; an **expired** auth token
62
+ is `expired_jwt` + `requirement=person-token` the same way (budgets editor's
63
+ copy, 6 October). The 0.5.1 final-record challenge and
64
+ `allow_expired_auth_token` are removed — the final figure reaches the
65
+ issuer through the usage endpoint. Any other signed-but-failing request on
66
+ a priced route is refused with `Signature-Error` instead of being treated
67
+ as unsigned.
68
+
69
+ *Revocation endpoint (RS-60, §11.12):*
70
+
71
+ - `make_revocation_endpoint`: the body is **`{"jti", "exp"}`**; the issuer
72
+ comes from the caller's verified server signature, never from the body.
73
+ The pair is recorded whether or not the token was ever seen here; the
74
+ answer is always an empty `200` — **no 404**. Errors are problem+json:
75
+ `invalid_request` 400 (incl. `exp` beyond `max_token_lifetime` + skew),
76
+ `unsupported_iss` 403 (`accepted_issuers=` predicate), a failed signature
77
+ 401 + `Signature-Error`.
78
+ - `InMemoryMeter.record_revocation(iss, jti, exp)` and `is_revoked(iss, jti)`;
79
+ entries live until `exp` + 60 s. `revoke()` remains for tokens the meter
80
+ holds.
81
+ - `make_usage_endpoint` / `make_revocation_endpoint` accept an
82
+ `authenticate_ps` that returns a `VerifiedSignature`, an issuer string, a
83
+ `VerificationError` or `None`; refusals are 401 + `Signature-Error`.
84
+
85
+ *Signers:*
86
+
87
+ - **`EgressSigner.sign_aauth(method, url, headers, token=…, body=…)`**: the
88
+ -11 agent signature — `Signature-Key: sig=jwt;jwt="…"`, the required
89
+ components (+ body components with a computed `Content-Digest`), `created`
90
+ only, no `keyid`. `EgressSigner.public_jwk` now carries `alg: Ed25519`
91
+ (RFC 9864) for `cnf.jwk`; the Web Bot Auth directory is unchanged.
92
+ - **`ServerSigner(seed, server_id, dwk)`**: the jwks_uri scheme for a PS/AS
93
+ (or a resource) calling another server; `.jwks()` is what to publish.
94
+ - **`ResponseSigner(seed, server_id, dwk="aauth-resource.json")`** emits the
95
+ -11 `Signature-Key` form; the `jwks_url` form is gone. Golden vectors
96
+ regenerated.
97
+ - `sign_request()`, `content_digest()`, `parse_signature_key()`,
98
+ `parse_signature_input()`, `build_person_token_requirement()` exported.
99
+
100
+ Dropped: `allow_expired_auth_token`, `VerifiedSignature.expired`,
101
+ `EXPIRED_AUTH_TOKEN_GRACE`, `ResponseSigner(jwks_url=)`, the `{iss, jti}`
102
+ revocation body, the 90 s lifetime grace, `EdDSA` by default.
103
+
104
+ ## 0.6.0
105
+
106
+ **Budgets — revocation reaches the meter** (base protocol §Token Revocation,
107
+ budgets §Token Scope; drain rule per AAuth issue #151, raised from our
108
+ production and not yet in the editor's copy):
109
+
110
+ - **`InMemoryMeter.revoke(iss, jti)`**: withdraws the grant so no new request
111
+ can reserve against the token; requests already in flight complete and are
112
+ committed as usual. Idempotent; `False` for an unknown `(iss, jti)`.
113
+ - **`TokenRevoked(drained)`**: what `reserve()` returns for a revoked token.
114
+ `drained` is true once nothing is in flight on it.
115
+ - **Final record only after the drain**: `consumed_record()` withholds a
116
+ revoked token's record while requests are in flight, so a record the
117
+ resource issues after the revocation is the token's final figure and the
118
+ issuer can settle on it (the ordering is checkable by `iat`).
119
+ - **`make_revocation_endpoint(meter, authenticate_ps=…)`**: the base
120
+ protocol's endpoint — signed `POST {"iss","jti"}`, `200` empty on success or
121
+ already-invalid, `404` unknown, `403` when the caller is not the issuer.
122
+ - **`BudgetMiddleware`** answers a revoked token with the plain
123
+ `requirement=auth-token` challenge (`code: AUTH_TOKEN_REVOKED`, no
124
+ `AAuth-Budget`), the resource token carrying the final record once drained.
125
+ - `revocation_state(key, jti)` for resources that learn of revocation out of
126
+ band (e.g. from their own registry) and want the same challenge.
127
+
3
128
  ## 0.5.1
4
129
 
5
130
  **Budgets — the final-record challenge for expired auth tokens** (draft
@@ -0,0 +1,11 @@
1
+ # Contributing
2
+
3
+ Issues and pull requests are welcome. A few things make review faster.
4
+
5
+ - Say which specification and revision a change targets: RFC 9421, a Web Bot Auth draft revision, or an AAuth draft revision. The library tracks revisions published on the IETF datatracker.
6
+ - Add or update a test for any wire-level change: a known-good signature or a request that must fail, next to the existing tests under `tests/`.
7
+ - Keep the dependency tree as it is unless the change needs a new one; say why in the pull request.
8
+ - Run `ruff check` and the test suite before opening the pull request; CI runs both.
9
+ - Security problems go to the address in SECURITY.md, not to a public issue.
10
+
11
+ Interoperability reports are as useful as code: if the library fails against a real signer or verifier, an issue with the raw request and the expected result is a contribution.
@@ -0,0 +1,35 @@
1
+ # Governance
2
+
3
+ regent-httpsig is a small library with a small governance model. This file says who decides what, how a contributor becomes a maintainer, and how this file itself changes.
4
+
5
+ ## Roles
6
+
7
+ **Contributor.** Anyone who opens an issue or a pull request. No agreement to sign beyond the Apache-2.0 licence of the repository; by submitting a change you licence it under the same terms.
8
+
9
+ **Maintainer.** Has the commit bit, reviews and merges pull requests, cuts releases to PyPI, and answers security reports under SECURITY.md. Maintainers are listed in MAINTAINERS.md with their affiliation.
10
+
11
+ **Security contact.** The address in SECURITY.md. Today it is the maintainer.
12
+
13
+ ## Decisions
14
+
15
+ Changes land through pull requests with a green CI run. A pull request needs approval from one maintainer who is not its author; while there is a single maintainer, that maintainer's own changes are held for 48 hours before merge so that anyone can object on the pull request.
16
+
17
+ Anything that changes the wire behaviour of the library, the set of supported drafts, or a public API is announced in the pull request title with the draft revision it targets, and recorded in CHANGELOG.md.
18
+
19
+ The library implements external specifications: RFC 9421, the Web Bot Auth drafts, and the AAuth drafts. It does not define its own. Where a draft revision changes behaviour, the library tracks the revision published on the IETF datatracker, not the editor's copy, and the version number in CHANGELOG.md names the revision it targets.
20
+
21
+ Disagreements are resolved on the pull request. If maintainers cannot agree, the change does not merge.
22
+
23
+ ## Becoming a maintainer
24
+
25
+ A contributor becomes a maintainer after three merged pull requests of substance over at least sixty days, a nomination by an existing maintainer on a public issue, and no objection from other maintainers within fourteen days. A maintainer from a different organization than the existing maintainers is explicitly welcome; the project's stated goal is at least two maintainers from two organizations.
26
+
27
+ A maintainer who has not reviewed or merged anything for six months is moved to emeritus in MAINTAINERS.md and can return by asking on an issue.
28
+
29
+ ## Releases
30
+
31
+ Any maintainer can cut a release. Versions follow semantic versioning; a change of targeted draft revision is at least a minor version. Releases are published to PyPI from CI through trusted publishing; no maintainer holds a long-lived PyPI token.
32
+
33
+ ## Changing this file
34
+
35
+ Changes to this file follow the same pull request process, with a fourteen-day comment period announced on an issue.
@@ -0,0 +1,11 @@
1
+ # Maintainers
2
+
3
+ | Name | GitHub | Affiliation | Since |
4
+ |---|---|---|---|
5
+ | Abay Aubakirov | abay94 | Regent Protocol | 2026-08-17 |
6
+
7
+ The project is looking for a second maintainer from another organization. See GOVERNANCE.md for how that works and open an issue titled "maintainer: <your name>" if you are interested.
8
+
9
+ ## Emeritus
10
+
11
+ None.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: regent-httpsig
3
- Version: 0.5.1
3
+ Version: 0.7.0
4
4
  Summary: Verify and sign AI agent HTTP traffic in Python — RFC 9421 HTTP Message Signatures: Web Bot Auth (what OpenAI ships) and AAuth.
5
5
  Project-URL: Homepage, https://github.com/regent-protocol/regent-httpsig
6
6
  Project-URL: Repository, https://github.com/regent-protocol/regent-httpsig
@@ -64,13 +64,20 @@ No FastAPI? The core has no framework dependencies:
64
64
 
65
65
  ```python
66
66
  verifier = HttpsigVerifier()
67
- sig = await verifier.verify(method, url, headers) # VerifiedSignature | None
67
+ sig = await verifier.verify(method, url, headers) # VerifiedSignature | None
68
+ out = await verifier.verify_detailed(method, url, headers, body)
69
+ # VerifiedSignature | VerificationError(code="expired_jwt", …) | None
68
70
  ```
69
71
 
70
72
  Verification is **enrichment by default**: no `Signature` header costs nothing, a bad
71
- signature yields `None`, and nothing ever raises on untrusted input. Use
73
+ signature yields `None`, and nothing ever raises on untrusted input. `verify_detailed()`
74
+ says *why* with the `Signature-Error` code of draft-hardt-httpbis-signature-key exactly
75
+ as AAuth -11 §11.3.4 assigns it (`invalid_signature`, `invalid_input`, `unsupported_scheme`,
76
+ `unsupported_algorithm`, `invalid_key`, `unknown_key`, `issuer_missing`, `issuer_mismatch`,
77
+ `invalid_jwt`, `expired_jwt`, `revoked_jwt`, `clock_skew`); `error.headers()` is the 401's
78
+ header set and `error.problem()` its problem+json body. Use
72
79
  `regent_httpsig.fastapi.RequiredSignatureDep` when a signature must be present — the 401
73
- tells the agent exactly how to sign.
80
+ carries that header, or `AAuth-Requirement: requirement=agent-token` when nothing was signed.
74
81
 
75
82
  > **Behind a reverse proxy?** The agent signed the *public* URL
76
83
  > (`https://api.example/…`), but your ASGI server sees `http://container/…`. The FastAPI
@@ -98,6 +105,18 @@ regent-httpsig keygen --agent https://myagent.example --out ./well-known/
98
105
  Publish the directory at `https://myagent.example/.well-known/http-message-signatures-directory`
99
106
  and every Web Bot Auth verifier on the internet can now identify your agent.
100
107
 
108
+ The same key signs as an **AAuth agent** (-11 §11.3.3) — the token in `Signature-Key`,
109
+ the required components covered, `Content-Digest` computed when there is a body:
110
+
111
+ ```python
112
+ headers = signer.sign_aauth("POST", url, {"content-type": "application/json"},
113
+ token=agent_token, body=raw_body)
114
+ ```
115
+
116
+ and a **server** (a PS or AS calling a resource's usage or revocation endpoint, §11.3.2)
117
+ signs under the `jwks_uri` scheme with `ServerSigner(seed=…, server_id="https://ps.example",
118
+ dwk="aauth-person.json")` — publish `.jwks()` at your metadata document's `jwks_uri`.
119
+
101
120
  ## What exactly is verified
102
121
 
103
122
  | Check | Status |
@@ -106,7 +125,9 @@ and every Web Bot Auth verifier on the internet can now identify your agent.
106
125
  | Web Bot Auth draft -05 A.2.2 — sf-dictionary `Signature-Agent` covered with `;key=` | ✅ in CI¹ |
107
126
  | Web Bot Auth A.2.3 — legacy sf-string form (**what OpenAI ships in production**) | ✅ in CI |
108
127
  | Sign → verify roundtrip (fresh keys, full pipeline) | ✅ in CI |
109
- | AAuth identity-mode roundtrip (`aa-agent+jwt` + `cnf.jwk` proof of possession) | ✅ in CI |
128
+ | AAuth -11 agent / person / auth tokens (`cnf.jwk` proof of possession, required components, body digest, 60 s `created` window, RS-51 token time, fully-specified algs, Ed25519 + ES256 possession keys) | ✅ in CI |
129
+ | AAuth -11 server scheme (`jwks_uri` with `id`/`dwk`/`kid`, `issuer_mismatch`, key rotation refetch) | ✅ in CI |
130
+ | Every -11 §11.3.4 Signature-Error code, `revoked_jwt` via the revocation record, RS-52 case-exact ids | ✅ in CI |
110
131
  | Signed by [`aauth-signing`](https://github.com/christian-posta/aauth-python-library) (jwt scheme, keyid-less) → verified | ✅² |
111
132
  | Tampered request / expired signature / wrong directory key rejected | ✅ in CI |
112
133
 
@@ -128,12 +149,16 @@ keyid-less shape is pinned in CI.
128
149
  `{Signature-Agent}/.well-known/http-message-signatures-directory`. Both wire forms of
129
150
  `Signature-Agent` are accepted — the current sf-dictionary and the legacy bare sf-string
130
151
  OpenAI actually sends.
131
- - **AAuth** (`draft-hardt-oauth-aauth-protocol`, identity-based mode): the agent carries a
132
- JWT `agent_token` in `Signature-Key`; the issuer's JWKS verifies the token, the token's
133
- `cnf.jwk` verifies the request signature. Install with `pip install 'regent-httpsig[aauth]'`.
134
- Tracks the **-11 editor's copy**: fully-specified algorithms (RFC 9864, `Ed25519` — with a
135
- transition flag for the -10 ecosystem's `EdDSA`) and **person tokens** (`aa-person+jwt`,
136
- opt-in via `HttpsigConfig.resource_url`).
152
+ - **AAuth** (`draft-hardt-oauth-aauth-protocol-11`): the agent carries its token in
153
+ `Signature-Key` under the `jwt` scheme — an agent token (`aa-agent+jwt`), a person token
154
+ (`aa-person+jwt`, opt-in via `HttpsigConfig.resource_url`) or an auth token
155
+ (`aa-auth+jwt`, pinned via `trusted_ps`); the issuer's JWKS, discovered through
156
+ `{iss}/.well-known/{dwk}`, verifies the token, the token's `cnf.jwk` verifies the request
157
+ signature. Servers sign under `jwks_uri` (`id`/`dwk`/`kid`). Install with
158
+ `pip install 'regent-httpsig[aauth]'`. 0.7.0 is the **-11 cut-over** — one dialect, as
159
+ published: fully-specified algorithms only (RFC 9864; `EdDSA` is refused), the required
160
+ covered components, the 60-second `created` window, no tolerance on `exp`, revoked tokens
161
+ named as revoked. `require_fully_specified_algs=False` survives for private test rigs only.
137
162
  For a full-protocol AAuth implementation (both roles, all token types) see
138
163
  [christian-posta/aauth-python-library](https://github.com/christian-posta/aauth-python-library) —
139
164
  this library is the thin relying-party verifier that handles both dialects.
@@ -174,11 +199,18 @@ production on [get4agent.com](https://get4agent.com).
174
199
 
175
200
  - `insufficient-budget` refusals carry **`required`** — the refused request's
176
201
  maximum cost — so the agent lowers its bound and retries instead of guessing.
177
- - An **expired** auth token gets the base protocol's plain challenge with a
178
- resource token carrying that token's **final** consumption record — the
179
- figure its issuer needs to settle the allocation (a record stated at or
180
- after `exp` is final; one on a live token is a snapshot and releases
181
- nothing).
202
+ - **Revocation** (-11 §11.12): `make_revocation_endpoint(meter, authenticate_ps=…)`
203
+ takes the signed `POST {"jti", "exp"}`, keys the record by the caller's
204
+ verified identity, answers an empty `200` always (no 404) and problem+json
205
+ `invalid_request` / `unsupported_iss`. A revoked token stops spending at
206
+ once, in-flight requests complete, and a later presentation is answered
207
+ `401` + `Signature-Error: error=revoked_jwt` + `AAuth-Requirement:
208
+ requirement=person-token` — no resource token (§11.12.5). Wire
209
+ `HttpsigVerifier(is_revoked=meter.is_revoked)` so the verifier names it first.
210
+ - An **expired** auth token is answered the same way with `expired_jwt`
211
+ (budgets editor's copy, 6 October): nothing is served or metered, and the
212
+ token's final consumption record reaches its issuer through the usage
213
+ endpoint, not on the challenge.
182
214
  - **Streaming** responses run in the draft's cost-omitted mode: `reserved` in
183
215
  the header, commit when the stream ends (set `request.state.budget_cost`
184
216
  mid-stream if you learn the actual), and the agent recovers the exact cost
@@ -190,11 +222,15 @@ production on [get4agent.com](https://get4agent.com).
190
222
  ```python
191
223
  from regent_httpsig import InMemoryMeter, ResponseSigner, make_usage_endpoint
192
224
 
225
+ async def authenticate_ps(request): # the PS signs as a server, §11.3.2
226
+ return await verifier.verify_server(request.method, str(request.url),
227
+ dict(request.headers), await request.body())
228
+
193
229
  handler = make_usage_endpoint(
194
230
  meter,
195
- authenticate_ps=my_ps_authenticator, # verify the PS's jwks_uri signature
231
+ authenticate_ps=authenticate_ps,
196
232
  unit="USD", decimals=6,
197
- signer=ResponseSigner(seed=SEED, jwks_url="https://api.example/jwks.json"),
233
+ signer=ResponseSigner(seed=SEED, server_id="https://api.example"), # dwk aauth-resource.json
198
234
  )
199
235
 
200
236
  @app.post("/usage")
@@ -238,9 +274,12 @@ from regent_httpsig import HttpsigConfig, HttpsigVerifier
238
274
 
239
275
  verifier = HttpsigVerifier(HttpsigConfig(
240
276
  trusted_agents=frozenset({"https://chatgpt.com", "https://operator.openai.com"}),
241
- max_age_hours=25, # reject signatures created earlier than this
242
- cache_ttl=600, # key-directory cache seconds
243
- ))
277
+ max_age_hours=25, # Web Bot Auth: reject signatures created earlier than this
278
+ signature_window_seconds=60, # AAuth -11: the `created` window (advertise as signature_window)
279
+ resource_url="https://api.example", # accept person / auth tokens for us
280
+ trusted_ps={"https://ps.example": ""}, # pinned issuers ("" = discover the JWKS)
281
+ cache_ttl=600, # key-directory cache seconds
282
+ ), is_revoked=meter.is_revoked) # optional: name revoked tokens as revoked
244
283
  ```
245
284
 
246
285
  Pass your app's shared client to reuse its pool: `HttpsigVerifier(http_client=my_async_client)`.
@@ -249,9 +288,13 @@ Pass your app's shared client to reuse its pool: `HttpsigVerifier(http_client=my
249
288
 
250
289
  - Web Bot Auth and AAuth are **IETF drafts** (RFC 9421 itself is a final standard). We track
251
290
  the drafts; breaking draft changes land as minor releases while we're 0.x.
252
- - **Ed25519 only** for now — it's what the agent ecosystem ships.
253
- - Body coverage (`content-digest`) is verified when covered by the signature, but this
254
- library does not require it; decide per-route whether you need it.
291
+ - Web Bot Auth is **Ed25519 only** — it's what the agent ecosystem ships. AAuth
292
+ possession keys may be Ed25519 or P-256 (ES256); RS256 is accepted for issuer keys.
293
+ - On the AAuth path a request with a body **must** cover `content-digest` and
294
+ `content-type` (-11 §11.3.3.1) and the digest is checked against the body you pass;
295
+ the FastAPI layer reads the body for you. Web Bot Auth keeps its per-route choice.
296
+ - Not in 0.7.0: issuing person/resource tokens, missions, the `202` deferred
297
+ `requirement=auth-token`, federated mode — the PS half of the protocol.
255
298
 
256
299
  ## Related projects
257
300
 
@@ -34,13 +34,20 @@ No FastAPI? The core has no framework dependencies:
34
34
 
35
35
  ```python
36
36
  verifier = HttpsigVerifier()
37
- sig = await verifier.verify(method, url, headers) # VerifiedSignature | None
37
+ sig = await verifier.verify(method, url, headers) # VerifiedSignature | None
38
+ out = await verifier.verify_detailed(method, url, headers, body)
39
+ # VerifiedSignature | VerificationError(code="expired_jwt", …) | None
38
40
  ```
39
41
 
40
42
  Verification is **enrichment by default**: no `Signature` header costs nothing, a bad
41
- signature yields `None`, and nothing ever raises on untrusted input. Use
43
+ signature yields `None`, and nothing ever raises on untrusted input. `verify_detailed()`
44
+ says *why* with the `Signature-Error` code of draft-hardt-httpbis-signature-key exactly
45
+ as AAuth -11 §11.3.4 assigns it (`invalid_signature`, `invalid_input`, `unsupported_scheme`,
46
+ `unsupported_algorithm`, `invalid_key`, `unknown_key`, `issuer_missing`, `issuer_mismatch`,
47
+ `invalid_jwt`, `expired_jwt`, `revoked_jwt`, `clock_skew`); `error.headers()` is the 401's
48
+ header set and `error.problem()` its problem+json body. Use
42
49
  `regent_httpsig.fastapi.RequiredSignatureDep` when a signature must be present — the 401
43
- tells the agent exactly how to sign.
50
+ carries that header, or `AAuth-Requirement: requirement=agent-token` when nothing was signed.
44
51
 
45
52
  > **Behind a reverse proxy?** The agent signed the *public* URL
46
53
  > (`https://api.example/…`), but your ASGI server sees `http://container/…`. The FastAPI
@@ -68,6 +75,18 @@ regent-httpsig keygen --agent https://myagent.example --out ./well-known/
68
75
  Publish the directory at `https://myagent.example/.well-known/http-message-signatures-directory`
69
76
  and every Web Bot Auth verifier on the internet can now identify your agent.
70
77
 
78
+ The same key signs as an **AAuth agent** (-11 §11.3.3) — the token in `Signature-Key`,
79
+ the required components covered, `Content-Digest` computed when there is a body:
80
+
81
+ ```python
82
+ headers = signer.sign_aauth("POST", url, {"content-type": "application/json"},
83
+ token=agent_token, body=raw_body)
84
+ ```
85
+
86
+ and a **server** (a PS or AS calling a resource's usage or revocation endpoint, §11.3.2)
87
+ signs under the `jwks_uri` scheme with `ServerSigner(seed=…, server_id="https://ps.example",
88
+ dwk="aauth-person.json")` — publish `.jwks()` at your metadata document's `jwks_uri`.
89
+
71
90
  ## What exactly is verified
72
91
 
73
92
  | Check | Status |
@@ -76,7 +95,9 @@ and every Web Bot Auth verifier on the internet can now identify your agent.
76
95
  | Web Bot Auth draft -05 A.2.2 — sf-dictionary `Signature-Agent` covered with `;key=` | ✅ in CI¹ |
77
96
  | Web Bot Auth A.2.3 — legacy sf-string form (**what OpenAI ships in production**) | ✅ in CI |
78
97
  | Sign → verify roundtrip (fresh keys, full pipeline) | ✅ in CI |
79
- | AAuth identity-mode roundtrip (`aa-agent+jwt` + `cnf.jwk` proof of possession) | ✅ in CI |
98
+ | AAuth -11 agent / person / auth tokens (`cnf.jwk` proof of possession, required components, body digest, 60 s `created` window, RS-51 token time, fully-specified algs, Ed25519 + ES256 possession keys) | ✅ in CI |
99
+ | AAuth -11 server scheme (`jwks_uri` with `id`/`dwk`/`kid`, `issuer_mismatch`, key rotation refetch) | ✅ in CI |
100
+ | Every -11 §11.3.4 Signature-Error code, `revoked_jwt` via the revocation record, RS-52 case-exact ids | ✅ in CI |
80
101
  | Signed by [`aauth-signing`](https://github.com/christian-posta/aauth-python-library) (jwt scheme, keyid-less) → verified | ✅² |
81
102
  | Tampered request / expired signature / wrong directory key rejected | ✅ in CI |
82
103
 
@@ -98,12 +119,16 @@ keyid-less shape is pinned in CI.
98
119
  `{Signature-Agent}/.well-known/http-message-signatures-directory`. Both wire forms of
99
120
  `Signature-Agent` are accepted — the current sf-dictionary and the legacy bare sf-string
100
121
  OpenAI actually sends.
101
- - **AAuth** (`draft-hardt-oauth-aauth-protocol`, identity-based mode): the agent carries a
102
- JWT `agent_token` in `Signature-Key`; the issuer's JWKS verifies the token, the token's
103
- `cnf.jwk` verifies the request signature. Install with `pip install 'regent-httpsig[aauth]'`.
104
- Tracks the **-11 editor's copy**: fully-specified algorithms (RFC 9864, `Ed25519` — with a
105
- transition flag for the -10 ecosystem's `EdDSA`) and **person tokens** (`aa-person+jwt`,
106
- opt-in via `HttpsigConfig.resource_url`).
122
+ - **AAuth** (`draft-hardt-oauth-aauth-protocol-11`): the agent carries its token in
123
+ `Signature-Key` under the `jwt` scheme — an agent token (`aa-agent+jwt`), a person token
124
+ (`aa-person+jwt`, opt-in via `HttpsigConfig.resource_url`) or an auth token
125
+ (`aa-auth+jwt`, pinned via `trusted_ps`); the issuer's JWKS, discovered through
126
+ `{iss}/.well-known/{dwk}`, verifies the token, the token's `cnf.jwk` verifies the request
127
+ signature. Servers sign under `jwks_uri` (`id`/`dwk`/`kid`). Install with
128
+ `pip install 'regent-httpsig[aauth]'`. 0.7.0 is the **-11 cut-over** — one dialect, as
129
+ published: fully-specified algorithms only (RFC 9864; `EdDSA` is refused), the required
130
+ covered components, the 60-second `created` window, no tolerance on `exp`, revoked tokens
131
+ named as revoked. `require_fully_specified_algs=False` survives for private test rigs only.
107
132
  For a full-protocol AAuth implementation (both roles, all token types) see
108
133
  [christian-posta/aauth-python-library](https://github.com/christian-posta/aauth-python-library) —
109
134
  this library is the thin relying-party verifier that handles both dialects.
@@ -144,11 +169,18 @@ production on [get4agent.com](https://get4agent.com).
144
169
 
145
170
  - `insufficient-budget` refusals carry **`required`** — the refused request's
146
171
  maximum cost — so the agent lowers its bound and retries instead of guessing.
147
- - An **expired** auth token gets the base protocol's plain challenge with a
148
- resource token carrying that token's **final** consumption record — the
149
- figure its issuer needs to settle the allocation (a record stated at or
150
- after `exp` is final; one on a live token is a snapshot and releases
151
- nothing).
172
+ - **Revocation** (-11 §11.12): `make_revocation_endpoint(meter, authenticate_ps=…)`
173
+ takes the signed `POST {"jti", "exp"}`, keys the record by the caller's
174
+ verified identity, answers an empty `200` always (no 404) and problem+json
175
+ `invalid_request` / `unsupported_iss`. A revoked token stops spending at
176
+ once, in-flight requests complete, and a later presentation is answered
177
+ `401` + `Signature-Error: error=revoked_jwt` + `AAuth-Requirement:
178
+ requirement=person-token` — no resource token (§11.12.5). Wire
179
+ `HttpsigVerifier(is_revoked=meter.is_revoked)` so the verifier names it first.
180
+ - An **expired** auth token is answered the same way with `expired_jwt`
181
+ (budgets editor's copy, 6 October): nothing is served or metered, and the
182
+ token's final consumption record reaches its issuer through the usage
183
+ endpoint, not on the challenge.
152
184
  - **Streaming** responses run in the draft's cost-omitted mode: `reserved` in
153
185
  the header, commit when the stream ends (set `request.state.budget_cost`
154
186
  mid-stream if you learn the actual), and the agent recovers the exact cost
@@ -160,11 +192,15 @@ production on [get4agent.com](https://get4agent.com).
160
192
  ```python
161
193
  from regent_httpsig import InMemoryMeter, ResponseSigner, make_usage_endpoint
162
194
 
195
+ async def authenticate_ps(request): # the PS signs as a server, §11.3.2
196
+ return await verifier.verify_server(request.method, str(request.url),
197
+ dict(request.headers), await request.body())
198
+
163
199
  handler = make_usage_endpoint(
164
200
  meter,
165
- authenticate_ps=my_ps_authenticator, # verify the PS's jwks_uri signature
201
+ authenticate_ps=authenticate_ps,
166
202
  unit="USD", decimals=6,
167
- signer=ResponseSigner(seed=SEED, jwks_url="https://api.example/jwks.json"),
203
+ signer=ResponseSigner(seed=SEED, server_id="https://api.example"), # dwk aauth-resource.json
168
204
  )
169
205
 
170
206
  @app.post("/usage")
@@ -208,9 +244,12 @@ from regent_httpsig import HttpsigConfig, HttpsigVerifier
208
244
 
209
245
  verifier = HttpsigVerifier(HttpsigConfig(
210
246
  trusted_agents=frozenset({"https://chatgpt.com", "https://operator.openai.com"}),
211
- max_age_hours=25, # reject signatures created earlier than this
212
- cache_ttl=600, # key-directory cache seconds
213
- ))
247
+ max_age_hours=25, # Web Bot Auth: reject signatures created earlier than this
248
+ signature_window_seconds=60, # AAuth -11: the `created` window (advertise as signature_window)
249
+ resource_url="https://api.example", # accept person / auth tokens for us
250
+ trusted_ps={"https://ps.example": ""}, # pinned issuers ("" = discover the JWKS)
251
+ cache_ttl=600, # key-directory cache seconds
252
+ ), is_revoked=meter.is_revoked) # optional: name revoked tokens as revoked
214
253
  ```
215
254
 
216
255
  Pass your app's shared client to reuse its pool: `HttpsigVerifier(http_client=my_async_client)`.
@@ -219,9 +258,13 @@ Pass your app's shared client to reuse its pool: `HttpsigVerifier(http_client=my
219
258
 
220
259
  - Web Bot Auth and AAuth are **IETF drafts** (RFC 9421 itself is a final standard). We track
221
260
  the drafts; breaking draft changes land as minor releases while we're 0.x.
222
- - **Ed25519 only** for now — it's what the agent ecosystem ships.
223
- - Body coverage (`content-digest`) is verified when covered by the signature, but this
224
- library does not require it; decide per-route whether you need it.
261
+ - Web Bot Auth is **Ed25519 only** — it's what the agent ecosystem ships. AAuth
262
+ possession keys may be Ed25519 or P-256 (ES256); RS256 is accepted for issuer keys.
263
+ - On the AAuth path a request with a body **must** cover `content-digest` and
264
+ `content-type` (-11 §11.3.3.1) and the digest is checked against the body you pass;
265
+ the FastAPI layer reads the body for you. Web Bot Auth keeps its per-route choice.
266
+ - Not in 0.7.0: issuing person/resource tokens, missions, the `202` deferred
267
+ `requirement=auth-token`, federated mode — the PS half of the protocol.
225
268
 
226
269
  ## Related projects
227
270
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "regent-httpsig"
7
- version = "0.5.1"
7
+ version = "0.7.0"
8
8
  description = "Verify and sign AI agent HTTP traffic in Python — RFC 9421 HTTP Message Signatures: Web Bot Auth (what OpenAI ships) and AAuth."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -9,28 +9,45 @@ from regent_httpsig.budget import (
9
9
  InMemoryMeter,
10
10
  InsufficientBudget,
11
11
  InvalidBudgetClaim,
12
+ TokenRevoked,
12
13
  UnitMismatch,
13
14
  )
14
15
  from regent_httpsig.config import HttpsigConfig
15
16
  from regent_httpsig.jwk import b64url, jwk_thumbprint, load_ed25519_jwk
16
17
  from regent_httpsig.netguard import NotPublicURL, assert_public_url
18
+ from regent_httpsig.sfv import (
19
+ build_aauth_budget_header,
20
+ build_aauth_requirement,
21
+ build_person_token_requirement,
22
+ parse_signature_agent,
23
+ parse_signature_input,
24
+ parse_signature_key,
25
+ )
26
+ from regent_httpsig.sign import (
27
+ DIRECTORY_MEDIA_TYPE,
28
+ EgressSigner,
29
+ ServerSigner,
30
+ content_digest,
31
+ generate_seed,
32
+ sign_request,
33
+ )
17
34
  from regent_httpsig.usage import (
18
35
  ResponseSigner,
19
36
  UsageQueryError,
20
37
  build_usage_response,
38
+ make_revocation_endpoint,
21
39
  make_usage_endpoint,
22
40
  parse_usage_request,
23
41
  validate_budget_grant,
24
42
  )
25
- from regent_httpsig.sfv import (
26
- build_aauth_budget_header,
27
- build_aauth_requirement,
28
- parse_signature_agent,
43
+ from regent_httpsig.verify import (
44
+ WBA_TAG,
45
+ HttpsigVerifier,
46
+ VerificationError,
47
+ VerifiedSignature,
29
48
  )
30
- from regent_httpsig.sign import DIRECTORY_MEDIA_TYPE, EgressSigner, generate_seed
31
- from regent_httpsig.verify import WBA_TAG, HttpsigVerifier, VerifiedSignature
32
49
 
33
- __version__ = "0.3.0"
50
+ __version__ = "0.7.0"
34
51
 
35
52
  __all__ = [
36
53
  "DIRECTORY_MEDIA_TYPE",
@@ -40,10 +57,18 @@ __all__ = [
40
57
  "HttpsigVerifier",
41
58
  "InMemoryMeter",
42
59
  "InsufficientBudget",
60
+ "TokenRevoked",
43
61
  "InvalidBudgetClaim",
44
62
  "NotPublicURL",
45
63
  "UnitMismatch",
64
+ "VerificationError",
46
65
  "VerifiedSignature",
66
+ "ServerSigner",
67
+ "content_digest",
68
+ "sign_request",
69
+ "build_person_token_requirement",
70
+ "parse_signature_input",
71
+ "parse_signature_key",
47
72
  "WBA_TAG",
48
73
  "__version__",
49
74
  "assert_public_url",
@@ -51,6 +76,7 @@ __all__ = [
51
76
  "ResponseSigner",
52
77
  "UsageQueryError",
53
78
  "build_usage_response",
79
+ "make_revocation_endpoint",
54
80
  "make_usage_endpoint",
55
81
  "parse_usage_request",
56
82
  "validate_budget_grant",