pq-http-message-signatures 0.1.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 (29) hide show
  1. pq_http_message_signatures-0.1.0/LICENSE +21 -0
  2. pq_http_message_signatures-0.1.0/PKG-INFO +315 -0
  3. pq_http_message_signatures-0.1.0/README.md +267 -0
  4. pq_http_message_signatures-0.1.0/pyproject.toml +79 -0
  5. pq_http_message_signatures-0.1.0/setup.cfg +4 -0
  6. pq_http_message_signatures-0.1.0/src/pq_http/__init__.py +56 -0
  7. pq_http_message_signatures-0.1.0/src/pq_http/algorithms.py +55 -0
  8. pq_http_message_signatures-0.1.0/src/pq_http/diagnostics.py +21 -0
  9. pq_http_message_signatures-0.1.0/src/pq_http/digest.py +43 -0
  10. pq_http_message_signatures-0.1.0/src/pq_http/exceptions.py +96 -0
  11. pq_http_message_signatures-0.1.0/src/pq_http/integrations/__init__.py +9 -0
  12. pq_http_message_signatures-0.1.0/src/pq_http/integrations/fastapi.py +68 -0
  13. pq_http_message_signatures-0.1.0/src/pq_http/integrations/requests.py +49 -0
  14. pq_http_message_signatures-0.1.0/src/pq_http/integrations/starlette.py +159 -0
  15. pq_http_message_signatures-0.1.0/src/pq_http/key_resolver.py +36 -0
  16. pq_http_message_signatures-0.1.0/src/pq_http/keys.py +310 -0
  17. pq_http_message_signatures-0.1.0/src/pq_http/message.py +273 -0
  18. pq_http_message_signatures-0.1.0/src/pq_http/policy.py +69 -0
  19. pq_http_message_signatures-0.1.0/src/pq_http/py.typed +0 -0
  20. pq_http_message_signatures-0.1.0/src/pq_http/replay.py +68 -0
  21. pq_http_message_signatures-0.1.0/src/pq_http/signature_base.py +164 -0
  22. pq_http_message_signatures-0.1.0/src/pq_http/signer.py +172 -0
  23. pq_http_message_signatures-0.1.0/src/pq_http/structured_fields.py +351 -0
  24. pq_http_message_signatures-0.1.0/src/pq_http/verifier.py +289 -0
  25. pq_http_message_signatures-0.1.0/src/pq_http_message_signatures.egg-info/PKG-INFO +315 -0
  26. pq_http_message_signatures-0.1.0/src/pq_http_message_signatures.egg-info/SOURCES.txt +27 -0
  27. pq_http_message_signatures-0.1.0/src/pq_http_message_signatures.egg-info/dependency_links.txt +1 -0
  28. pq_http_message_signatures-0.1.0/src/pq_http_message_signatures.egg-info/requires.txt +28 -0
  29. pq_http_message_signatures-0.1.0/src/pq_http_message_signatures.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PQ-HTTP contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,315 @@
1
+ Metadata-Version: 2.4
2
+ Name: pq-http-message-signatures
3
+ Version: 0.1.0
4
+ Summary: Post-quantum HTTP Message Signatures (RFC 9421 + C2SP httpsig-pq, ML-DSA / FIPS 204) for Python
5
+ Author: PQ-HTTP contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/OWNER/pq-http-message-signatures
8
+ Project-URL: Issues, https://github.com/OWNER/pq-http-message-signatures/issues
9
+ Keywords: post-quantum,ml-dsa,fips-204,http-signatures,rfc9421,httpsig-pq,fastapi,api-security
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: cryptography>=47.0.0
24
+ Provides-Extra: starlette
25
+ Requires-Dist: starlette>=0.37; extra == "starlette"
26
+ Provides-Extra: fastapi
27
+ Requires-Dist: fastapi>=0.110; extra == "fastapi"
28
+ Requires-Dist: starlette>=0.37; extra == "fastapi"
29
+ Provides-Extra: requests
30
+ Requires-Dist: requests>=2.31; extra == "requests"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=8; extra == "dev"
33
+ Requires-Dist: pytest-cov>=5; extra == "dev"
34
+ Requires-Dist: hypothesis>=6.100; extra == "dev"
35
+ Requires-Dist: httpx>=0.27; extra == "dev"
36
+ Requires-Dist: fastapi>=0.110; extra == "dev"
37
+ Requires-Dist: starlette>=0.37; extra == "dev"
38
+ Requires-Dist: requests>=2.31; extra == "dev"
39
+ Requires-Dist: ruff>=0.5; extra == "dev"
40
+ Requires-Dist: mypy>=1.10; extra == "dev"
41
+ Requires-Dist: types-requests; extra == "dev"
42
+ Requires-Dist: build>=1.2; extra == "dev"
43
+ Requires-Dist: twine>=5; extra == "dev"
44
+ Requires-Dist: pip-audit>=2.7; extra == "dev"
45
+ Requires-Dist: bandit>=1.7; extra == "dev"
46
+ Requires-Dist: http-sfv>=0.9; extra == "dev"
47
+ Dynamic: license-file
48
+
49
+ # PQ-HTTP: post-quantum HTTP Message Signatures for Python
50
+
51
+ [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421.html) HTTP Message Signatures with the
52
+ [C2SP `httpsig-pq`](https://c2sp.org/httpsig-pq) ML-DSA algorithms (NIST [FIPS 204](https://csrc.nist.gov/pubs/fips/204/final)),
53
+ with a secure-by-default verifier and FastAPI / Starlette / `requests` integrations.
54
+
55
+ > **Status: alpha, unaudited.** See [Verification status](#verification-status) for exactly what has and has not been run.
56
+ > This project does not invent cryptography: ML-DSA is provided by the [`cryptography`](https://cryptography.io) package.
57
+ > It is not affiliated with or endorsed by NIST, the IETF, C2SP or any vendor.
58
+
59
+ ## Features
60
+
61
+ - C2SP `httpsig-pq` v1.0.1 identifiers `ml-dsa-44`, `ml-dsa-65`, `ml-dsa-87`; signature base passed to ML-DSA unchanged, empty context, 32-octet seed keys only.
62
+ - Signing and verification of **requests and responses**, `Content-Digest` (RFC 9530, sha-256/sha-512), `created` / `expires` / `nonce` / `tag` / `keyid` / `alg`.
63
+ - Verifier policy: algorithm allow-list, required covered components, body must be covered, clock skew and max age, optional nonce replay protection, signature/header/component/body limits.
64
+ - Strict RFC 8941 parser (bounded work on untrusted input), no extra runtime dependencies besides `cryptography`.
65
+ - Starlette middleware, FastAPI dependency, `requests` auth plugin.
66
+
67
+ ## Security scope
68
+
69
+ Provides: authenticity and integrity of the *covered* message components, bound to a trusted key and algorithm.
70
+ Does **not** provide: confidentiality, replay protection unless you configure it, key distribution, authorization,
71
+ or protection of components you do not cover. Read [docs/security.md](docs/security.md) before deploying.
72
+
73
+ ## Standards
74
+
75
+ RFC 9421 (subset, see [Known limitations](#known-limitations)), C2SP httpsig-pq v1.0.1, FIPS 204, RFC 8941, RFC 9530.
76
+
77
+ ## Supported algorithms
78
+
79
+ | `alg` | Public key | Signature | Base64 `Signature` value |
80
+ |---|---|---|---|
81
+ | `ml-dsa-44` | 1,312 B | 2,420 B | 3,228 chars |
82
+ | `ml-dsa-65` | 1,952 B | 3,309 B | 4,412 chars |
83
+ | `ml-dsa-87` | 2,592 B | 4,627 B | 6,172 chars |
84
+
85
+ Signing keys are the 32-octet FIPS 204 seed. Expanded private keys are rejected, as C2SP requires.
86
+
87
+ ## Installation
88
+
89
+ Requires Python 3.10+ and `cryptography >= 47` built against an OpenSSL with ML-DSA.
90
+
91
+ ```bash
92
+ pip install pq-http-message-signatures # core (not yet published; use the source install below)
93
+ pip install "pq-http-message-signatures[fastapi,requests]" # with integrations
94
+ # from source:
95
+ git clone https://github.com/OWNER/pq-http-message-signatures.git && cd pq-http-message-signatures
96
+ pip install -e ".[dev]"
97
+ python -m pq_http.diagnostics # prints which ML-DSA parameter sets your backend supports
98
+ ```
99
+
100
+ ## Quick start
101
+
102
+ ```python
103
+ from pq_http import Signer, StaticKeyResolver, Verifier
104
+
105
+ signer = Signer.generate(algorithm="ml-dsa-65")
106
+ signed = signer.sign_request(
107
+ method="POST",
108
+ url="https://api.example.com/data",
109
+ headers={"content-type": "application/json"},
110
+ body=b'{"message": "hello"}',
111
+ )
112
+
113
+ verifier = Verifier(
114
+ key_resolver=StaticKeyResolver({signer.keyid: signer.public_key()}),
115
+ allowed_algorithms=["ml-dsa-65"],
116
+ )
117
+ result = verifier.verify_request(signed)
118
+ print(result.valid) # True
119
+ print(result.covered_components) # trust only these verified values
120
+ ```
121
+
122
+ `verify_*` never raises for a bad message: it returns a `VerificationResult` with `valid=False` and a
123
+ `FailureReason`. Call `result.raise_for_failure()` if you prefer exceptions.
124
+
125
+ ## Key generation
126
+
127
+ ```python
128
+ from pq_http import SigningKey, VerifyingKey
129
+
130
+ key = SigningKey.generate("ml-dsa-87") # 32 random bytes from the OS CSPRNG
131
+ key.save("signing-key.json") # new file, mode 0600, never overwrites
132
+ key.public_key().save("public-key.json") # share this one
133
+ key = SigningKey.load("signing-key.json") # rejects group/world-readable files on POSIX
134
+ ```
135
+
136
+ The key file format is this project's own (C2SP defines no container); the file contains the algorithm and seed.
137
+
138
+ ## Signing examples
139
+
140
+ Responses can be bound to their request with the `req` parameter:
141
+
142
+ ```python
143
+ response = signer.sign_response(status=200, headers={"content-type": "text/plain"}, body=b"ok", request=original_request)
144
+ ```
145
+
146
+ `Signer` options: `request_components`, `response_components`, `include_alg`, `expires_in`, `include_nonce`, `tag`,
147
+ `add_content_digest`, `label`, `keyid`. A missing covered header raises `SigningError`; nothing is silently dropped.
148
+
149
+ ## Verification examples
150
+
151
+ ```python
152
+ from pq_http import VerificationPolicy, Limits
153
+
154
+ verifier = Verifier(
155
+ resolver, ["ml-dsa-65"],
156
+ policy=VerificationPolicy(
157
+ required_request_components=("@method", "@target-uri", "content-type"),
158
+ max_age=120, max_clock_skew=10, required_tag="my-app",
159
+ limits=Limits(max_body_bytes=256 * 1024),
160
+ ),
161
+ )
162
+ ```
163
+
164
+ Verification order: size limits, structured-field parse, label selection, `keyid` and algorithm checks (allow-list and
165
+ key binding; `alg` can only narrow, never widen), time checks, required components, body coverage, signature,
166
+ `Content-Digest`, then replay storage (only after the signature is valid).
167
+
168
+ ## FastAPI integration
169
+
170
+ ```python
171
+ from fastapi import Depends, FastAPI
172
+ from pq_http import VerificationResult
173
+ from pq_http.integrations.fastapi import SignatureVerification
174
+
175
+ app = FastAPI()
176
+ require_signature = SignatureVerification(verifier, trusted_base_url="https://api.example.com")
177
+
178
+ @app.post("/data")
179
+ async def data(payload: dict, result: VerificationResult = Depends(require_signature)):
180
+ return {"signed_by": result.keyid}
181
+ ```
182
+
183
+ For whole-app protection use `pq_http.integrations.starlette.PQHTTPSignatureMiddleware(app, verifier, exclude_paths=["/health"])`.
184
+ Behind a proxy always set `trusted_base_url`, otherwise `@target-uri` is derived from the client-controlled `Host` header.
185
+ Runnable: `python examples/fastapi_server.py --self-test`.
186
+
187
+ ## Client integration
188
+
189
+ ```python
190
+ import requests
191
+ from pq_http.integrations.requests import PQHTTPSignatureAuth
192
+
193
+ requests.post("https://api.example.com/data", json={"message": "hello"}, auth=PQHTTPSignatureAuth(signer))
194
+ ```
195
+
196
+ Streaming request bodies are rejected (the digest cannot be computed without consuming them).
197
+
198
+ ## Replay protection
199
+
200
+ Signatures alone can be replayed. Protection exists only when **all three** are true: signers include a `nonce`,
201
+ the policy has `require_nonce=True`, and the verifier has a replay store.
202
+
203
+ ```python
204
+ from pq_http import InMemoryReplayStore, VerificationPolicy
205
+
206
+ signer = Signer.generate("ml-dsa-65", include_nonce=True)
207
+ verifier = Verifier(resolver, ["ml-dsa-65"], policy=VerificationPolicy(require_nonce=True), replay_store=InMemoryReplayStore())
208
+ ```
209
+
210
+ `InMemoryReplayStore` is per process: it is **not shared** across workers or machines and is lost on restart. In
211
+ production implement `ReplayStore.check_and_store(key, expires_at)` atomically on shared storage (e.g. Redis
212
+ `SET key 1 NX EXAT ...`). The verifier refuses to run a replay store without `require_nonce`, `require_created` and a
213
+ finite `max_age`, and `VerificationResult.replay_checked` tells you whether a nonce was actually recorded.
214
+
215
+ ## Key management
216
+
217
+ Never commit keys; generate at deploy time and keep seeds in a secrets manager, KMS-wrapped storage or an HSM-backed
218
+ vault; give each signer its own key; keep one key per algorithm; rotate by publishing a new `keyid` in the resolver
219
+ before the signer switches, then retire the old one after `max_age`. `SigningKey` redacts itself in `repr`, cannot be
220
+ pickled, and `save()` refuses to overwrite. Test keys must be generated in tests, never reused in production.
221
+
222
+ ## Configuration
223
+
224
+ | Setting | Default |
225
+ |---|---|
226
+ | `VerificationPolicy.max_age` / `max_clock_skew` | 300 s / 30 s |
227
+ | `require_created`, `require_content_digest_when_body`, `verify_content_digest` | `True` |
228
+ | `Limits.max_header_bytes` / `max_signatures` / `max_components` / `max_nodes` / `max_body_bytes` | 16 KiB / 4 / 32 / 512 / 1 MiB |
229
+
230
+ Size limits matter: one ML-DSA-87 signature uses ~6.2 KB of the typical 8 KB header budget of proxies and servers.
231
+
232
+ ## Testing
233
+
234
+ ```bash
235
+ make install && make vectors && make test # lint/type/fuzz/examples: make all
236
+ ```
237
+
238
+ ## Benchmarks
239
+
240
+ ```bash
241
+ python benchmarks/bench.py --output benchmarks/results.json
242
+ ```
243
+
244
+ Reports key generation, raw sign/verify, full HTTP sign/verify, sizes, header overhead and an Ed25519 reference
245
+ (same backend, same process). **No benchmark numbers are published here**: the author's environment had no ML-DSA
246
+ backend, so nothing was measured. Run it on your hardware and report the JSON (it records OS, CPU, Python and
247
+ dependency versions).
248
+
249
+ ## Compatibility
250
+
251
+ Python 3.10-3.13 targeted; Linux/macOS/Windows in the CI matrix (not yet run). Wire-compatible by construction with
252
+ other C2SP httpsig-pq implementations such as Linzer (Ruby); no cross-implementation exchange has been performed yet.
253
+ See [docs/interoperability.md](docs/interoperability.md).
254
+
255
+ ## Known limitations
256
+
257
+ - Unsupported (rejected, never approximated): `@query-param`; component parameters `sf`, `bs`, `key`, `tr`; covering `signature`/`signature-input`.
258
+ - The signer adds exactly one signature per message; the verifier checks one label. No hybrid/classical+PQ policy engine.
259
+ - `@target-uri` is normalised (lower-case scheme/host, default port dropped, empty path -> `/`). ASGI cannot distinguish `/p` from `/p?`.
260
+ - Signing uses the backend's default mode; hedged behaviour is asserted by a test but depends on the backend. No deterministic mode is exposed.
261
+ - Seeds come from `secrets.token_bytes` (OS CSPRNG); no claim is made that this meets FIPS 204's approved-RBG requirement.
262
+ - RFC 9421 Appendix B vectors are not cross-checked. `Accept-Signature` is not implemented. Response signing is not offered by the ASGI middleware.
263
+
264
+ ## Security warning
265
+
266
+ Alpha software, never independently audited, no side-channel or FIPS-validation claims. See [SECURITY.md](SECURITY.md).
267
+
268
+ ## Roadmap
269
+
270
+ Run the official vectors and cross-implementation tests in CI; RFC 9421 Appendix B vectors; `@query-param`; Redis
271
+ replay store example; multiple-signature signing; hybrid policy helpers; PyPI release.
272
+
273
+ ## Contributing
274
+
275
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
276
+
277
+ ## License
278
+
279
+ MIT, see [LICENSE](LICENSE). Runtime dependency `cryptography` is Apache-2.0 OR BSD-3-Clause (compatible). Check the
280
+ licenses of the exact versions you install (`pip-licenses`).
281
+
282
+ ## Architecture
283
+
284
+ ```mermaid
285
+ flowchart LR
286
+ App[Application] --> Signer
287
+ Signer --> Base[signature_base: RFC 9421 base]
288
+ Base --> Keys[keys: SigningKey / VerifyingKey]
289
+ Keys --> Crypto[(cryptography ML-DSA)]
290
+ Req[HTTP request] --> MW[Starlette middleware / FastAPI dependency]
291
+ MW --> Verifier
292
+ Verifier --> SF[structured_fields parser + limits]
293
+ Verifier --> Resolver[KeyResolver]
294
+ Verifier --> Base
295
+ Verifier --> Replay[ReplayStore]
296
+ ```
297
+
298
+ ## Verification status
299
+
300
+ Honest record of what was actually executed when this repository was produced (sandbox without network, with
301
+ `cryptography 46.0.6`, which has **no** ML-DSA API):
302
+
303
+ | Item | Status |
304
+ |---|---|
305
+ | Structured-field parser/serializer, message model, signature base | **Executed, passed** (offline). Signature bases for all three C2SP vectors match the published SHA-256 values. |
306
+ | Signer/verifier/replay/limits/key-file logic (~150 test cases) | **Executed, passed**, but first against an *insecure stand-in* and, for key/signer tests, against the reference oracle above (both kept outside this repository). |
307
+ | Parser fuzz harness | **Executed**: 30,000 inputs, no unexpected exception. |
308
+ | `requests` plugin; ASGI middleware (driven by hand-written ASGI calls: 200/401/413 paths); benchmark and example scripts | Executed against the stand-in only (timings discarded). The package also builds with setuptools (`pip install --no-build-isolation .`). |
309
+ | Official C2SP vector **ML-DSA-44**, end to end through this library (seed -> public key, signature base, `Signature-Input` text, official signature verified directly and via the full `Verifier`, all negative cases of the spec, hedged control) | **Executed, all passed.** Run against an independent pure-Python FIPS 204 reference that I wrote as a test oracle (kept outside this repo, not shipped). It reproduced the official deterministic signature byte-for-byte, which validates both the oracle and the vector transcription. Key and signer tests (48 cases) also passed on it. |
310
+ | Official vectors **ML-DSA-65 / ML-DSA-87**; the same checks against the real `cryptography` backend; hedged-signing test on the real backend | **Written, NOT executed** (vectors were not transcribed offline; needs `cryptography>=47` and `make vectors`). |
311
+ | Starlette/FastAPI `TestClient` tests, FastAPI dependency, Hypothesis tests, `http-sfv` cross-check | **Written, NOT executed** (packages unavailable offline). |
312
+ | ruff, mypy, build, twine, pip-audit, bandit, CI workflows | **NOT executed**. Run `make format` before the first `make lint`. |
313
+ | Real benchmarks | **NOT run**. |
314
+
315
+ Run `make vectors && make all` on a normal machine and fix anything that fails before relying on this code.
@@ -0,0 +1,267 @@
1
+ # PQ-HTTP: post-quantum HTTP Message Signatures for Python
2
+
3
+ [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421.html) HTTP Message Signatures with the
4
+ [C2SP `httpsig-pq`](https://c2sp.org/httpsig-pq) ML-DSA algorithms (NIST [FIPS 204](https://csrc.nist.gov/pubs/fips/204/final)),
5
+ with a secure-by-default verifier and FastAPI / Starlette / `requests` integrations.
6
+
7
+ > **Status: alpha, unaudited.** See [Verification status](#verification-status) for exactly what has and has not been run.
8
+ > This project does not invent cryptography: ML-DSA is provided by the [`cryptography`](https://cryptography.io) package.
9
+ > It is not affiliated with or endorsed by NIST, the IETF, C2SP or any vendor.
10
+
11
+ ## Features
12
+
13
+ - C2SP `httpsig-pq` v1.0.1 identifiers `ml-dsa-44`, `ml-dsa-65`, `ml-dsa-87`; signature base passed to ML-DSA unchanged, empty context, 32-octet seed keys only.
14
+ - Signing and verification of **requests and responses**, `Content-Digest` (RFC 9530, sha-256/sha-512), `created` / `expires` / `nonce` / `tag` / `keyid` / `alg`.
15
+ - Verifier policy: algorithm allow-list, required covered components, body must be covered, clock skew and max age, optional nonce replay protection, signature/header/component/body limits.
16
+ - Strict RFC 8941 parser (bounded work on untrusted input), no extra runtime dependencies besides `cryptography`.
17
+ - Starlette middleware, FastAPI dependency, `requests` auth plugin.
18
+
19
+ ## Security scope
20
+
21
+ Provides: authenticity and integrity of the *covered* message components, bound to a trusted key and algorithm.
22
+ Does **not** provide: confidentiality, replay protection unless you configure it, key distribution, authorization,
23
+ or protection of components you do not cover. Read [docs/security.md](docs/security.md) before deploying.
24
+
25
+ ## Standards
26
+
27
+ RFC 9421 (subset, see [Known limitations](#known-limitations)), C2SP httpsig-pq v1.0.1, FIPS 204, RFC 8941, RFC 9530.
28
+
29
+ ## Supported algorithms
30
+
31
+ | `alg` | Public key | Signature | Base64 `Signature` value |
32
+ |---|---|---|---|
33
+ | `ml-dsa-44` | 1,312 B | 2,420 B | 3,228 chars |
34
+ | `ml-dsa-65` | 1,952 B | 3,309 B | 4,412 chars |
35
+ | `ml-dsa-87` | 2,592 B | 4,627 B | 6,172 chars |
36
+
37
+ Signing keys are the 32-octet FIPS 204 seed. Expanded private keys are rejected, as C2SP requires.
38
+
39
+ ## Installation
40
+
41
+ Requires Python 3.10+ and `cryptography >= 47` built against an OpenSSL with ML-DSA.
42
+
43
+ ```bash
44
+ pip install pq-http-message-signatures # core (not yet published; use the source install below)
45
+ pip install "pq-http-message-signatures[fastapi,requests]" # with integrations
46
+ # from source:
47
+ git clone https://github.com/OWNER/pq-http-message-signatures.git && cd pq-http-message-signatures
48
+ pip install -e ".[dev]"
49
+ python -m pq_http.diagnostics # prints which ML-DSA parameter sets your backend supports
50
+ ```
51
+
52
+ ## Quick start
53
+
54
+ ```python
55
+ from pq_http import Signer, StaticKeyResolver, Verifier
56
+
57
+ signer = Signer.generate(algorithm="ml-dsa-65")
58
+ signed = signer.sign_request(
59
+ method="POST",
60
+ url="https://api.example.com/data",
61
+ headers={"content-type": "application/json"},
62
+ body=b'{"message": "hello"}',
63
+ )
64
+
65
+ verifier = Verifier(
66
+ key_resolver=StaticKeyResolver({signer.keyid: signer.public_key()}),
67
+ allowed_algorithms=["ml-dsa-65"],
68
+ )
69
+ result = verifier.verify_request(signed)
70
+ print(result.valid) # True
71
+ print(result.covered_components) # trust only these verified values
72
+ ```
73
+
74
+ `verify_*` never raises for a bad message: it returns a `VerificationResult` with `valid=False` and a
75
+ `FailureReason`. Call `result.raise_for_failure()` if you prefer exceptions.
76
+
77
+ ## Key generation
78
+
79
+ ```python
80
+ from pq_http import SigningKey, VerifyingKey
81
+
82
+ key = SigningKey.generate("ml-dsa-87") # 32 random bytes from the OS CSPRNG
83
+ key.save("signing-key.json") # new file, mode 0600, never overwrites
84
+ key.public_key().save("public-key.json") # share this one
85
+ key = SigningKey.load("signing-key.json") # rejects group/world-readable files on POSIX
86
+ ```
87
+
88
+ The key file format is this project's own (C2SP defines no container); the file contains the algorithm and seed.
89
+
90
+ ## Signing examples
91
+
92
+ Responses can be bound to their request with the `req` parameter:
93
+
94
+ ```python
95
+ response = signer.sign_response(status=200, headers={"content-type": "text/plain"}, body=b"ok", request=original_request)
96
+ ```
97
+
98
+ `Signer` options: `request_components`, `response_components`, `include_alg`, `expires_in`, `include_nonce`, `tag`,
99
+ `add_content_digest`, `label`, `keyid`. A missing covered header raises `SigningError`; nothing is silently dropped.
100
+
101
+ ## Verification examples
102
+
103
+ ```python
104
+ from pq_http import VerificationPolicy, Limits
105
+
106
+ verifier = Verifier(
107
+ resolver, ["ml-dsa-65"],
108
+ policy=VerificationPolicy(
109
+ required_request_components=("@method", "@target-uri", "content-type"),
110
+ max_age=120, max_clock_skew=10, required_tag="my-app",
111
+ limits=Limits(max_body_bytes=256 * 1024),
112
+ ),
113
+ )
114
+ ```
115
+
116
+ Verification order: size limits, structured-field parse, label selection, `keyid` and algorithm checks (allow-list and
117
+ key binding; `alg` can only narrow, never widen), time checks, required components, body coverage, signature,
118
+ `Content-Digest`, then replay storage (only after the signature is valid).
119
+
120
+ ## FastAPI integration
121
+
122
+ ```python
123
+ from fastapi import Depends, FastAPI
124
+ from pq_http import VerificationResult
125
+ from pq_http.integrations.fastapi import SignatureVerification
126
+
127
+ app = FastAPI()
128
+ require_signature = SignatureVerification(verifier, trusted_base_url="https://api.example.com")
129
+
130
+ @app.post("/data")
131
+ async def data(payload: dict, result: VerificationResult = Depends(require_signature)):
132
+ return {"signed_by": result.keyid}
133
+ ```
134
+
135
+ For whole-app protection use `pq_http.integrations.starlette.PQHTTPSignatureMiddleware(app, verifier, exclude_paths=["/health"])`.
136
+ Behind a proxy always set `trusted_base_url`, otherwise `@target-uri` is derived from the client-controlled `Host` header.
137
+ Runnable: `python examples/fastapi_server.py --self-test`.
138
+
139
+ ## Client integration
140
+
141
+ ```python
142
+ import requests
143
+ from pq_http.integrations.requests import PQHTTPSignatureAuth
144
+
145
+ requests.post("https://api.example.com/data", json={"message": "hello"}, auth=PQHTTPSignatureAuth(signer))
146
+ ```
147
+
148
+ Streaming request bodies are rejected (the digest cannot be computed without consuming them).
149
+
150
+ ## Replay protection
151
+
152
+ Signatures alone can be replayed. Protection exists only when **all three** are true: signers include a `nonce`,
153
+ the policy has `require_nonce=True`, and the verifier has a replay store.
154
+
155
+ ```python
156
+ from pq_http import InMemoryReplayStore, VerificationPolicy
157
+
158
+ signer = Signer.generate("ml-dsa-65", include_nonce=True)
159
+ verifier = Verifier(resolver, ["ml-dsa-65"], policy=VerificationPolicy(require_nonce=True), replay_store=InMemoryReplayStore())
160
+ ```
161
+
162
+ `InMemoryReplayStore` is per process: it is **not shared** across workers or machines and is lost on restart. In
163
+ production implement `ReplayStore.check_and_store(key, expires_at)` atomically on shared storage (e.g. Redis
164
+ `SET key 1 NX EXAT ...`). The verifier refuses to run a replay store without `require_nonce`, `require_created` and a
165
+ finite `max_age`, and `VerificationResult.replay_checked` tells you whether a nonce was actually recorded.
166
+
167
+ ## Key management
168
+
169
+ Never commit keys; generate at deploy time and keep seeds in a secrets manager, KMS-wrapped storage or an HSM-backed
170
+ vault; give each signer its own key; keep one key per algorithm; rotate by publishing a new `keyid` in the resolver
171
+ before the signer switches, then retire the old one after `max_age`. `SigningKey` redacts itself in `repr`, cannot be
172
+ pickled, and `save()` refuses to overwrite. Test keys must be generated in tests, never reused in production.
173
+
174
+ ## Configuration
175
+
176
+ | Setting | Default |
177
+ |---|---|
178
+ | `VerificationPolicy.max_age` / `max_clock_skew` | 300 s / 30 s |
179
+ | `require_created`, `require_content_digest_when_body`, `verify_content_digest` | `True` |
180
+ | `Limits.max_header_bytes` / `max_signatures` / `max_components` / `max_nodes` / `max_body_bytes` | 16 KiB / 4 / 32 / 512 / 1 MiB |
181
+
182
+ Size limits matter: one ML-DSA-87 signature uses ~6.2 KB of the typical 8 KB header budget of proxies and servers.
183
+
184
+ ## Testing
185
+
186
+ ```bash
187
+ make install && make vectors && make test # lint/type/fuzz/examples: make all
188
+ ```
189
+
190
+ ## Benchmarks
191
+
192
+ ```bash
193
+ python benchmarks/bench.py --output benchmarks/results.json
194
+ ```
195
+
196
+ Reports key generation, raw sign/verify, full HTTP sign/verify, sizes, header overhead and an Ed25519 reference
197
+ (same backend, same process). **No benchmark numbers are published here**: the author's environment had no ML-DSA
198
+ backend, so nothing was measured. Run it on your hardware and report the JSON (it records OS, CPU, Python and
199
+ dependency versions).
200
+
201
+ ## Compatibility
202
+
203
+ Python 3.10-3.13 targeted; Linux/macOS/Windows in the CI matrix (not yet run). Wire-compatible by construction with
204
+ other C2SP httpsig-pq implementations such as Linzer (Ruby); no cross-implementation exchange has been performed yet.
205
+ See [docs/interoperability.md](docs/interoperability.md).
206
+
207
+ ## Known limitations
208
+
209
+ - Unsupported (rejected, never approximated): `@query-param`; component parameters `sf`, `bs`, `key`, `tr`; covering `signature`/`signature-input`.
210
+ - The signer adds exactly one signature per message; the verifier checks one label. No hybrid/classical+PQ policy engine.
211
+ - `@target-uri` is normalised (lower-case scheme/host, default port dropped, empty path -> `/`). ASGI cannot distinguish `/p` from `/p?`.
212
+ - Signing uses the backend's default mode; hedged behaviour is asserted by a test but depends on the backend. No deterministic mode is exposed.
213
+ - Seeds come from `secrets.token_bytes` (OS CSPRNG); no claim is made that this meets FIPS 204's approved-RBG requirement.
214
+ - RFC 9421 Appendix B vectors are not cross-checked. `Accept-Signature` is not implemented. Response signing is not offered by the ASGI middleware.
215
+
216
+ ## Security warning
217
+
218
+ Alpha software, never independently audited, no side-channel or FIPS-validation claims. See [SECURITY.md](SECURITY.md).
219
+
220
+ ## Roadmap
221
+
222
+ Run the official vectors and cross-implementation tests in CI; RFC 9421 Appendix B vectors; `@query-param`; Redis
223
+ replay store example; multiple-signature signing; hybrid policy helpers; PyPI release.
224
+
225
+ ## Contributing
226
+
227
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
228
+
229
+ ## License
230
+
231
+ MIT, see [LICENSE](LICENSE). Runtime dependency `cryptography` is Apache-2.0 OR BSD-3-Clause (compatible). Check the
232
+ licenses of the exact versions you install (`pip-licenses`).
233
+
234
+ ## Architecture
235
+
236
+ ```mermaid
237
+ flowchart LR
238
+ App[Application] --> Signer
239
+ Signer --> Base[signature_base: RFC 9421 base]
240
+ Base --> Keys[keys: SigningKey / VerifyingKey]
241
+ Keys --> Crypto[(cryptography ML-DSA)]
242
+ Req[HTTP request] --> MW[Starlette middleware / FastAPI dependency]
243
+ MW --> Verifier
244
+ Verifier --> SF[structured_fields parser + limits]
245
+ Verifier --> Resolver[KeyResolver]
246
+ Verifier --> Base
247
+ Verifier --> Replay[ReplayStore]
248
+ ```
249
+
250
+ ## Verification status
251
+
252
+ Honest record of what was actually executed when this repository was produced (sandbox without network, with
253
+ `cryptography 46.0.6`, which has **no** ML-DSA API):
254
+
255
+ | Item | Status |
256
+ |---|---|
257
+ | Structured-field parser/serializer, message model, signature base | **Executed, passed** (offline). Signature bases for all three C2SP vectors match the published SHA-256 values. |
258
+ | Signer/verifier/replay/limits/key-file logic (~150 test cases) | **Executed, passed**, but first against an *insecure stand-in* and, for key/signer tests, against the reference oracle above (both kept outside this repository). |
259
+ | Parser fuzz harness | **Executed**: 30,000 inputs, no unexpected exception. |
260
+ | `requests` plugin; ASGI middleware (driven by hand-written ASGI calls: 200/401/413 paths); benchmark and example scripts | Executed against the stand-in only (timings discarded). The package also builds with setuptools (`pip install --no-build-isolation .`). |
261
+ | Official C2SP vector **ML-DSA-44**, end to end through this library (seed -> public key, signature base, `Signature-Input` text, official signature verified directly and via the full `Verifier`, all negative cases of the spec, hedged control) | **Executed, all passed.** Run against an independent pure-Python FIPS 204 reference that I wrote as a test oracle (kept outside this repo, not shipped). It reproduced the official deterministic signature byte-for-byte, which validates both the oracle and the vector transcription. Key and signer tests (48 cases) also passed on it. |
262
+ | Official vectors **ML-DSA-65 / ML-DSA-87**; the same checks against the real `cryptography` backend; hedged-signing test on the real backend | **Written, NOT executed** (vectors were not transcribed offline; needs `cryptography>=47` and `make vectors`). |
263
+ | Starlette/FastAPI `TestClient` tests, FastAPI dependency, Hypothesis tests, `http-sfv` cross-check | **Written, NOT executed** (packages unavailable offline). |
264
+ | ruff, mypy, build, twine, pip-audit, bandit, CI workflows | **NOT executed**. Run `make format` before the first `make lint`. |
265
+ | Real benchmarks | **NOT run**. |
266
+
267
+ Run `make vectors && make all` on a normal machine and fix anything that fails before relying on this code.