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.
- pq_http_message_signatures-0.1.0/LICENSE +21 -0
- pq_http_message_signatures-0.1.0/PKG-INFO +315 -0
- pq_http_message_signatures-0.1.0/README.md +267 -0
- pq_http_message_signatures-0.1.0/pyproject.toml +79 -0
- pq_http_message_signatures-0.1.0/setup.cfg +4 -0
- pq_http_message_signatures-0.1.0/src/pq_http/__init__.py +56 -0
- pq_http_message_signatures-0.1.0/src/pq_http/algorithms.py +55 -0
- pq_http_message_signatures-0.1.0/src/pq_http/diagnostics.py +21 -0
- pq_http_message_signatures-0.1.0/src/pq_http/digest.py +43 -0
- pq_http_message_signatures-0.1.0/src/pq_http/exceptions.py +96 -0
- pq_http_message_signatures-0.1.0/src/pq_http/integrations/__init__.py +9 -0
- pq_http_message_signatures-0.1.0/src/pq_http/integrations/fastapi.py +68 -0
- pq_http_message_signatures-0.1.0/src/pq_http/integrations/requests.py +49 -0
- pq_http_message_signatures-0.1.0/src/pq_http/integrations/starlette.py +159 -0
- pq_http_message_signatures-0.1.0/src/pq_http/key_resolver.py +36 -0
- pq_http_message_signatures-0.1.0/src/pq_http/keys.py +310 -0
- pq_http_message_signatures-0.1.0/src/pq_http/message.py +273 -0
- pq_http_message_signatures-0.1.0/src/pq_http/policy.py +69 -0
- pq_http_message_signatures-0.1.0/src/pq_http/py.typed +0 -0
- pq_http_message_signatures-0.1.0/src/pq_http/replay.py +68 -0
- pq_http_message_signatures-0.1.0/src/pq_http/signature_base.py +164 -0
- pq_http_message_signatures-0.1.0/src/pq_http/signer.py +172 -0
- pq_http_message_signatures-0.1.0/src/pq_http/structured_fields.py +351 -0
- pq_http_message_signatures-0.1.0/src/pq_http/verifier.py +289 -0
- pq_http_message_signatures-0.1.0/src/pq_http_message_signatures.egg-info/PKG-INFO +315 -0
- pq_http_message_signatures-0.1.0/src/pq_http_message_signatures.egg-info/SOURCES.txt +27 -0
- pq_http_message_signatures-0.1.0/src/pq_http_message_signatures.egg-info/dependency_links.txt +1 -0
- pq_http_message_signatures-0.1.0/src/pq_http_message_signatures.egg-info/requires.txt +28 -0
- 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.
|