regent-httpsig 0.6.0__tar.gz → 0.7.1__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.
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/CHANGELOG.md +106 -0
- regent_httpsig-0.7.1/CONTRIBUTING.md +11 -0
- regent_httpsig-0.7.1/GOVERNANCE.md +35 -0
- regent_httpsig-0.7.1/MAINTAINERS.md +11 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/PKG-INFO +67 -27
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/README.md +66 -26
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/pyproject.toml +1 -1
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/src/regent_httpsig/__init__.py +25 -3
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/src/regent_httpsig/budget.py +53 -9
- regent_httpsig-0.7.1/src/regent_httpsig/config.py +62 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/src/regent_httpsig/fastapi.py +188 -132
- regent_httpsig-0.7.1/src/regent_httpsig/py.typed +0 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/src/regent_httpsig/sfv.py +58 -6
- regent_httpsig-0.7.1/src/regent_httpsig/sign.py +329 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/src/regent_httpsig/usage.py +105 -48
- regent_httpsig-0.7.1/src/regent_httpsig/verify.py +744 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/tests/test_budget.py +115 -67
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/tests/test_signer.py +22 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/tests/test_usage.py +7 -3
- regent_httpsig-0.7.1/tests/test_verifier.py +512 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/vectors/aauth-budgets-vectors.json +1 -1
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/vectors/generate.py +1 -1
- regent_httpsig-0.6.0/src/regent_httpsig/config.py +0 -47
- regent_httpsig-0.6.0/src/regent_httpsig/sign.py +0 -121
- regent_httpsig-0.6.0/src/regent_httpsig/verify.py +0 -490
- regent_httpsig-0.6.0/tests/test_verifier.py +0 -314
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/.github/workflows/ci.yml +0 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/.github/workflows/release.yml +0 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/.gitignore +0 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/LICENSE +0 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/SECURITY.md +0 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/examples/fastapi_verify.py +0 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/examples/httpx_signer.py +0 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/src/regent_httpsig/cli.py +0 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/src/regent_httpsig/jwk.py +0 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/src/regent_httpsig/netguard.py +0 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/tests/test_netguard.py +0 -0
- {regent_httpsig-0.6.0 → regent_httpsig-0.7.1}/tests/test_vectors.py +0 -0
|
@@ -1,5 +1,111 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.1
|
|
4
|
+
|
|
5
|
+
- Ship the `py.typed` marker: the package has been mypy-strict since 0.1, and
|
|
6
|
+
without the marker a strict consumer sees `import-untyped` on every import.
|
|
7
|
+
|
|
8
|
+
## 0.7.0
|
|
9
|
+
|
|
10
|
+
**AAuth -11 cut-over** (draft-hardt-oauth-aauth-protocol-11, published
|
|
11
|
+
25 September 2026; Signature-Key -09; budgets editor's copy of 6 October). One
|
|
12
|
+
dialect, as the editor asked: the -10 forms are gone, not kept behind a flag.
|
|
13
|
+
Breaking for every AAuth signer and resource; Web Bot Auth is untouched.
|
|
14
|
+
|
|
15
|
+
*Resource side (§11.3.4 — a typed answer for every refusal):*
|
|
16
|
+
|
|
17
|
+
- **`HttpsigVerifier.verify_detailed()`** returns a `VerifiedSignature` or a
|
|
18
|
+
**`VerificationError`** carrying the Signature-Error code -11 assigns:
|
|
19
|
+
`invalid_signature`, `invalid_input` (+ `required_input`),
|
|
20
|
+
`unsupported_scheme` (+ `Accept-Signature-Scheme`), `unsupported_algorithm`
|
|
21
|
+
(+ `Accept-Signature-Alg`), `invalid_key`, `unknown_key`, `issuer_missing`,
|
|
22
|
+
`issuer_mismatch`, `invalid_jwt`, `expired_jwt`, `revoked_jwt`, `clock_skew`.
|
|
23
|
+
`error.headers()` is the 401's header set, `error.problem()` the
|
|
24
|
+
`application/problem+json` body (`type: urn:ietf:params:sig-error:<code>`).
|
|
25
|
+
`verify()` keeps its `None`-on-failure contract.
|
|
26
|
+
- **Covered components are enforced** (§11.3.3.1): `@method @authority @path
|
|
27
|
+
signature-key`, plus `content-digest content-type` when the request has a
|
|
28
|
+
body — and the digest is checked against the body you pass. Missing
|
|
29
|
+
components are `invalid_input` with the full `required_input` list.
|
|
30
|
+
- **`created` window** (§11.3.4 step 3): `HttpsigConfig.signature_window_seconds`
|
|
31
|
+
(default 60). Older than the window is `invalid_signature`; further ahead of
|
|
32
|
+
our clock is `clock_skew`; a past `expires` is `invalid_signature`. The
|
|
33
|
+
upstream library's 5-second skew rule no longer pre-empts these.
|
|
34
|
+
- **RS-51 token time**: `exp` has no tolerance; `iat` is not a validity check
|
|
35
|
+
(only an `iat` beyond the window ahead is refused, as `clock_skew`); person
|
|
36
|
+
and auth tokens live at most **3600 s exactly** (the 90 s grace is gone);
|
|
37
|
+
`typ` is read first; an auth token's `dwk` must be `aauth-person.json` or
|
|
38
|
+
`aauth-access.json`.
|
|
39
|
+
- **Fully-specified algorithms by default** (§11.3.1, RFC 9864):
|
|
40
|
+
`require_fully_specified_algs` is now `True`. The polymorphic `EdDSA` and a
|
|
41
|
+
`cnf.jwk` without `alg` are `unsupported_algorithm` with
|
|
42
|
+
`Accept-Signature-Alg: Ed25519, ES256, RS256`. A P-256 possession key
|
|
43
|
+
(ES256) now verifies. The flag stays only for private test rigs.
|
|
44
|
+
- **The `jwks_uri` server scheme** (§11.3.2, Signature-Key §3.6):
|
|
45
|
+
`sig=jwks_uri;id="…";dwk="aauth-person.json";kid="…"` — the metadata
|
|
46
|
+
document at `{id}/.well-known/{dwk}` must name `issuer == id`
|
|
47
|
+
(`issuer_mismatch`), its `jwks_uri` is followed, the `kid` found (one
|
|
48
|
+
refetch on rotation, then `unknown_key`). `verify_server()` is the
|
|
49
|
+
entry point for usage and revocation endpoints; `trusted_ps` may pin a
|
|
50
|
+
JWKS URL or `""` to discover it.
|
|
51
|
+
- **Revocation hook**: `HttpsigVerifier(is_revoked=meter.is_revoked)` — a
|
|
52
|
+
token the application has on record as revoked is answered `revoked_jwt`
|
|
53
|
+
(RS-43, §11.12.5), never "invalid" or "expired".
|
|
54
|
+
- **RS-52**: agent identifiers compare exactly; no case folding (test pinned).
|
|
55
|
+
|
|
56
|
+
*FastAPI layer:*
|
|
57
|
+
|
|
58
|
+
- `RequiredSignatureDep` answers a failed signature with `401` +
|
|
59
|
+
`Signature-Error` (+ `Accept-Signature-*`, + `AAuth-Requirement:
|
|
60
|
+
requirement=person-token` for a revoked/expired person or auth token) and a
|
|
61
|
+
problem+json body; an unsigned request gets `AAuth-Requirement:
|
|
62
|
+
requirement=agent-token` (§6.1). New `SignatureErrorDep`,
|
|
63
|
+
`RequiredServerSignatureDep`, `signature_error_response()`.
|
|
64
|
+
- `BudgetMiddleware`: a **revoked** auth token is `revoked_jwt` +
|
|
65
|
+
`requirement=person-token`, with **no resource token** (the PS would reject
|
|
66
|
+
one naming a revoked token) and no `AAuth-Budget`; an **expired** auth token
|
|
67
|
+
is `expired_jwt` + `requirement=person-token` the same way (budgets editor's
|
|
68
|
+
copy, 6 October). The 0.5.1 final-record challenge and
|
|
69
|
+
`allow_expired_auth_token` are removed — the final figure reaches the
|
|
70
|
+
issuer through the usage endpoint. Any other signed-but-failing request on
|
|
71
|
+
a priced route is refused with `Signature-Error` instead of being treated
|
|
72
|
+
as unsigned.
|
|
73
|
+
|
|
74
|
+
*Revocation endpoint (RS-60, §11.12):*
|
|
75
|
+
|
|
76
|
+
- `make_revocation_endpoint`: the body is **`{"jti", "exp"}`**; the issuer
|
|
77
|
+
comes from the caller's verified server signature, never from the body.
|
|
78
|
+
The pair is recorded whether or not the token was ever seen here; the
|
|
79
|
+
answer is always an empty `200` — **no 404**. Errors are problem+json:
|
|
80
|
+
`invalid_request` 400 (incl. `exp` beyond `max_token_lifetime` + skew),
|
|
81
|
+
`unsupported_iss` 403 (`accepted_issuers=` predicate), a failed signature
|
|
82
|
+
401 + `Signature-Error`.
|
|
83
|
+
- `InMemoryMeter.record_revocation(iss, jti, exp)` and `is_revoked(iss, jti)`;
|
|
84
|
+
entries live until `exp` + 60 s. `revoke()` remains for tokens the meter
|
|
85
|
+
holds.
|
|
86
|
+
- `make_usage_endpoint` / `make_revocation_endpoint` accept an
|
|
87
|
+
`authenticate_ps` that returns a `VerifiedSignature`, an issuer string, a
|
|
88
|
+
`VerificationError` or `None`; refusals are 401 + `Signature-Error`.
|
|
89
|
+
|
|
90
|
+
*Signers:*
|
|
91
|
+
|
|
92
|
+
- **`EgressSigner.sign_aauth(method, url, headers, token=…, body=…)`**: the
|
|
93
|
+
-11 agent signature — `Signature-Key: sig=jwt;jwt="…"`, the required
|
|
94
|
+
components (+ body components with a computed `Content-Digest`), `created`
|
|
95
|
+
only, no `keyid`. `EgressSigner.public_jwk` now carries `alg: Ed25519`
|
|
96
|
+
(RFC 9864) for `cnf.jwk`; the Web Bot Auth directory is unchanged.
|
|
97
|
+
- **`ServerSigner(seed, server_id, dwk)`**: the jwks_uri scheme for a PS/AS
|
|
98
|
+
(or a resource) calling another server; `.jwks()` is what to publish.
|
|
99
|
+
- **`ResponseSigner(seed, server_id, dwk="aauth-resource.json")`** emits the
|
|
100
|
+
-11 `Signature-Key` form; the `jwks_url` form is gone. Golden vectors
|
|
101
|
+
regenerated.
|
|
102
|
+
- `sign_request()`, `content_digest()`, `parse_signature_key()`,
|
|
103
|
+
`parse_signature_input()`, `build_person_token_requirement()` exported.
|
|
104
|
+
|
|
105
|
+
Dropped: `allow_expired_auth_token`, `VerifiedSignature.expired`,
|
|
106
|
+
`EXPIRED_AUTH_TOKEN_GRACE`, `ResponseSigner(jwks_url=)`, the `{iss, jti}`
|
|
107
|
+
revocation body, the 90 s lifetime grace, `EdDSA` by default.
|
|
108
|
+
|
|
3
109
|
## 0.6.0
|
|
4
110
|
|
|
5
111
|
**Budgets — revocation reaches the meter** (base protocol §Token Revocation,
|
|
@@ -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.
|
|
3
|
+
Version: 0.7.1
|
|
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)
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
132
|
-
|
|
133
|
-
`
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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,14 +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
|
-
- **Revocation
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
185
214
|
- **Streaming** responses run in the draft's cost-omitted mode: `reserved` in
|
|
186
215
|
the header, commit when the stream ends (set `request.state.budget_cost`
|
|
187
216
|
mid-stream if you learn the actual), and the agent recovers the exact cost
|
|
@@ -193,11 +222,15 @@ production on [get4agent.com](https://get4agent.com).
|
|
|
193
222
|
```python
|
|
194
223
|
from regent_httpsig import InMemoryMeter, ResponseSigner, make_usage_endpoint
|
|
195
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
|
+
|
|
196
229
|
handler = make_usage_endpoint(
|
|
197
230
|
meter,
|
|
198
|
-
authenticate_ps=
|
|
231
|
+
authenticate_ps=authenticate_ps,
|
|
199
232
|
unit="USD", decimals=6,
|
|
200
|
-
signer=ResponseSigner(seed=SEED,
|
|
233
|
+
signer=ResponseSigner(seed=SEED, server_id="https://api.example"), # dwk aauth-resource.json
|
|
201
234
|
)
|
|
202
235
|
|
|
203
236
|
@app.post("/usage")
|
|
@@ -241,9 +274,12 @@ from regent_httpsig import HttpsigConfig, HttpsigVerifier
|
|
|
241
274
|
|
|
242
275
|
verifier = HttpsigVerifier(HttpsigConfig(
|
|
243
276
|
trusted_agents=frozenset({"https://chatgpt.com", "https://operator.openai.com"}),
|
|
244
|
-
max_age_hours=25,
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
247
283
|
```
|
|
248
284
|
|
|
249
285
|
Pass your app's shared client to reuse its pool: `HttpsigVerifier(http_client=my_async_client)`.
|
|
@@ -252,9 +288,13 @@ Pass your app's shared client to reuse its pool: `HttpsigVerifier(http_client=my
|
|
|
252
288
|
|
|
253
289
|
- Web Bot Auth and AAuth are **IETF drafts** (RFC 9421 itself is a final standard). We track
|
|
254
290
|
the drafts; breaking draft changes land as minor releases while we're 0.x.
|
|
255
|
-
- **Ed25519 only**
|
|
256
|
-
|
|
257
|
-
|
|
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.
|
|
258
298
|
|
|
259
299
|
## Related projects
|
|
260
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)
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
102
|
-
|
|
103
|
-
`
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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,14 +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
|
-
- **Revocation
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
155
184
|
- **Streaming** responses run in the draft's cost-omitted mode: `reserved` in
|
|
156
185
|
the header, commit when the stream ends (set `request.state.budget_cost`
|
|
157
186
|
mid-stream if you learn the actual), and the agent recovers the exact cost
|
|
@@ -163,11 +192,15 @@ production on [get4agent.com](https://get4agent.com).
|
|
|
163
192
|
```python
|
|
164
193
|
from regent_httpsig import InMemoryMeter, ResponseSigner, make_usage_endpoint
|
|
165
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
|
+
|
|
166
199
|
handler = make_usage_endpoint(
|
|
167
200
|
meter,
|
|
168
|
-
authenticate_ps=
|
|
201
|
+
authenticate_ps=authenticate_ps,
|
|
169
202
|
unit="USD", decimals=6,
|
|
170
|
-
signer=ResponseSigner(seed=SEED,
|
|
203
|
+
signer=ResponseSigner(seed=SEED, server_id="https://api.example"), # dwk aauth-resource.json
|
|
171
204
|
)
|
|
172
205
|
|
|
173
206
|
@app.post("/usage")
|
|
@@ -211,9 +244,12 @@ from regent_httpsig import HttpsigConfig, HttpsigVerifier
|
|
|
211
244
|
|
|
212
245
|
verifier = HttpsigVerifier(HttpsigConfig(
|
|
213
246
|
trusted_agents=frozenset({"https://chatgpt.com", "https://operator.openai.com"}),
|
|
214
|
-
max_age_hours=25,
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
217
253
|
```
|
|
218
254
|
|
|
219
255
|
Pass your app's shared client to reuse its pool: `HttpsigVerifier(http_client=my_async_client)`.
|
|
@@ -222,9 +258,13 @@ Pass your app's shared client to reuse its pool: `HttpsigVerifier(http_client=my
|
|
|
222
258
|
|
|
223
259
|
- Web Bot Auth and AAuth are **IETF drafts** (RFC 9421 itself is a final standard). We track
|
|
224
260
|
the drafts; breaking draft changes land as minor releases while we're 0.x.
|
|
225
|
-
- **Ed25519 only**
|
|
226
|
-
|
|
227
|
-
|
|
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.
|
|
228
268
|
|
|
229
269
|
## Related projects
|
|
230
270
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "regent-httpsig"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.1"
|
|
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"
|
|
@@ -18,9 +18,19 @@ from regent_httpsig.netguard import NotPublicURL, assert_public_url
|
|
|
18
18
|
from regent_httpsig.sfv import (
|
|
19
19
|
build_aauth_budget_header,
|
|
20
20
|
build_aauth_requirement,
|
|
21
|
+
build_person_token_requirement,
|
|
21
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,
|
|
22
33
|
)
|
|
23
|
-
from regent_httpsig.sign import DIRECTORY_MEDIA_TYPE, EgressSigner, generate_seed
|
|
24
34
|
from regent_httpsig.usage import (
|
|
25
35
|
ResponseSigner,
|
|
26
36
|
UsageQueryError,
|
|
@@ -30,9 +40,14 @@ from regent_httpsig.usage import (
|
|
|
30
40
|
parse_usage_request,
|
|
31
41
|
validate_budget_grant,
|
|
32
42
|
)
|
|
33
|
-
from regent_httpsig.verify import
|
|
43
|
+
from regent_httpsig.verify import (
|
|
44
|
+
WBA_TAG,
|
|
45
|
+
HttpsigVerifier,
|
|
46
|
+
VerificationError,
|
|
47
|
+
VerifiedSignature,
|
|
48
|
+
)
|
|
34
49
|
|
|
35
|
-
__version__ = "0.
|
|
50
|
+
__version__ = "0.7.1"
|
|
36
51
|
|
|
37
52
|
__all__ = [
|
|
38
53
|
"DIRECTORY_MEDIA_TYPE",
|
|
@@ -46,7 +61,14 @@ __all__ = [
|
|
|
46
61
|
"InvalidBudgetClaim",
|
|
47
62
|
"NotPublicURL",
|
|
48
63
|
"UnitMismatch",
|
|
64
|
+
"VerificationError",
|
|
49
65
|
"VerifiedSignature",
|
|
66
|
+
"ServerSigner",
|
|
67
|
+
"content_digest",
|
|
68
|
+
"sign_request",
|
|
69
|
+
"build_person_token_requirement",
|
|
70
|
+
"parse_signature_input",
|
|
71
|
+
"parse_signature_key",
|
|
50
72
|
"WBA_TAG",
|
|
51
73
|
"__version__",
|
|
52
74
|
"assert_public_url",
|