hpke-http 0.1.2__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.
@@ -0,0 +1,266 @@
1
+ Metadata-Version: 2.4
2
+ Name: hpke_http
3
+ Version: 0.1.2
4
+ Summary: End-to-end encryption for HTTP APIs using RFC 9180 HPKE
5
+ Author: Duale AI
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/dualeai/hpke-http
8
+ Project-URL: Documentation, https://github.com/dualeai/hpke-http#readme
9
+ Project-URL: Repository, https://github.com/dualeai/hpke-http.git
10
+ Project-URL: Changelog, https://github.com/dualeai/hpke-http/releases
11
+ Project-URL: Issues, https://github.com/dualeai/hpke-http/issues
12
+ Keywords: hpke,encryption,rfc9180,cryptography,http,e2e,chacha20,x25519,fastapi,aiohttp,sse,streaming
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: Implementation :: CPython
23
+ Classifier: Topic :: Security :: Cryptography
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+ Requires-Dist: cryptography~=46.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: granian~=2.0; extra == "dev"
31
+ Requires-Dist: hypothesis~=6.148; extra == "dev"
32
+ Requires-Dist: pyright~=1.1; extra == "dev"
33
+ Requires-Dist: pytest-asyncio~=0.24; extra == "dev"
34
+ Requires-Dist: pytest-cov~=7.0; extra == "dev"
35
+ Requires-Dist: pytest-mock~=3.14; extra == "dev"
36
+ Requires-Dist: pytest-xdist[psutil]~=3.8; extra == "dev"
37
+ Requires-Dist: pytest~=8.3; extra == "dev"
38
+ Requires-Dist: ruff~=0.7; extra == "dev"
39
+ Requires-Dist: twine~=6.1; extra == "dev"
40
+ Requires-Dist: typing_extensions~=4.12; extra == "dev"
41
+ Requires-Dist: vulture~=2.14; extra == "dev"
42
+ Provides-Extra: testing
43
+ Requires-Dist: pytest-asyncio~=0.24; extra == "testing"
44
+ Requires-Dist: pytest~=8.3; extra == "testing"
45
+ Provides-Extra: fastapi
46
+ Requires-Dist: starlette~=0.50; extra == "fastapi"
47
+ Provides-Extra: aiohttp
48
+ Requires-Dist: aiohttp~=3.13; extra == "aiohttp"
49
+ Provides-Extra: zstd
50
+ Requires-Dist: backports.zstd>=1.0; python_version < "3.14" and extra == "zstd"
51
+
52
+ # hpke-http
53
+
54
+ End-to-end encryption for HTTP APIs.
55
+
56
+ ```bash
57
+ uv add git+https://github.com/duale-ai/hpke-http
58
+ ```
59
+
60
+ ## Highlights
61
+
62
+ - **Transparent** - Drop-in middleware, no application code changes
63
+ - **E2E encryption** - Protects data even with TLS termination at CDN/LB
64
+ - **PSK binding** - Each request cryptographically bound to API key
65
+ - **Replay protection** - SSE counter prevents replay attacks
66
+ - **RFC 9180 compliant** - Auditable, interoperable standard
67
+
68
+ ## Quick Start
69
+
70
+ ### Server (FastAPI)
71
+
72
+ ```python
73
+ from fastapi import FastAPI, Request
74
+ from fastapi.responses import StreamingResponse
75
+ from hpke_http.middleware.fastapi import HPKEMiddleware
76
+ from hpke_http.constants import KemId
77
+
78
+ app = FastAPI()
79
+
80
+ async def resolve_psk(scope: dict) -> tuple[bytes, bytes]:
81
+ api_key = dict(scope["headers"]).get(b"authorization", b"").decode()
82
+ return (api_key.encode(), (await lookup_tenant(api_key)).encode())
83
+
84
+ app.add_middleware(
85
+ HPKEMiddleware,
86
+ private_keys={KemId.DHKEM_X25519_HKDF_SHA256: private_key},
87
+ psk_resolver=resolve_psk,
88
+ # compress=True, # Optional: Zstd compression for SSE responses
89
+ # max_sse_event_size=128 * 1024 * 1024, # Optional: 128MB for large payloads
90
+ )
91
+
92
+ @app.post("/chat")
93
+ async def chat(request: Request):
94
+ data = await request.json() # Decrypted by middleware
95
+
96
+ async def generate():
97
+ yield b"event: progress\ndata: {\"step\": 1}\n\n"
98
+ yield b"event: complete\ndata: {\"result\": \"done\"}\n\n"
99
+
100
+ # Just use StreamingResponse - encryption is automatic!
101
+ return StreamingResponse(generate(), media_type="text/event-stream")
102
+ ```
103
+
104
+ ### Client (aiohttp)
105
+
106
+ ```python
107
+ from hpke_http.middleware.aiohttp import HPKEClientSession
108
+
109
+ async with HPKEClientSession(
110
+ base_url="https://api.example.com",
111
+ psk=api_key, # >= 32 bytes
112
+ psk_id=tenant_id,
113
+ # compress=True, # Optional: Zstd compression for requests
114
+ ) as session:
115
+ resp = await session.post("/chat", json={"prompt": "Hello"})
116
+ async for chunk in session.iter_sse(resp):
117
+ # bytes - matches native aiohttp response.content iteration
118
+ print(chunk) # b"event: progress\ndata: {...}\n\n"
119
+ ```
120
+
121
+ ## Documentation
122
+
123
+ - [RFC 9180 - HPKE](https://datatracker.ietf.org/doc/rfc9180/)
124
+ - [RFC 7748 - X25519](https://datatracker.ietf.org/doc/rfc7748/)
125
+ - [RFC 5869 - HKDF](https://datatracker.ietf.org/doc/rfc5869/)
126
+ - [RFC 8439 - ChaCha20-Poly1305](https://datatracker.ietf.org/doc/rfc8439/)
127
+ - [RFC 8878 - Zstandard](https://datatracker.ietf.org/doc/rfc8878/) (optional compression)
128
+
129
+ ## Cipher Suite
130
+
131
+ | Component | Algorithm | ID |
132
+ |-----------|-----------|------|
133
+ | KEM | DHKEM(X25519, HKDF-SHA256) | 0x0020 |
134
+ | KDF | HKDF-SHA256 | 0x0001 |
135
+ | AEAD | ChaCha20-Poly1305 | 0x0003 |
136
+ | Mode | PSK | 0x01 |
137
+
138
+ ## Wire Format
139
+
140
+ ### Request
141
+
142
+ ```text
143
+ ┌─────────┬─────────┬─────────┬─────────┬──────┬────────────┐
144
+ │ Ver(1B) │ KEM(2B) │ KDF(2B) │AEAD(2B) │Mode │ Ciphertext │
145
+ │ 0x01 │ 0x0020 │ 0x0001 │ 0x0003 │(1B) │ + 16B tag │
146
+ └─────────┴─────────┴─────────┴─────────┴──────┴────────────┘
147
+ Header: X-HPKE-Enc: <base64url(32B ephemeral key)>
148
+ Overhead: 24 bytes (8B header + 16B tag)
149
+ ```
150
+
151
+ ### SSE Event
152
+
153
+ ```text
154
+ event: enc
155
+ data: <base64url(counter_be32 || ciphertext || tag)>
156
+ Decrypted: raw SSE chunk (e.g., "event: progress\ndata: {...}\n\n")
157
+ ```
158
+
159
+ ## How SSE Auto-Encryption Works
160
+
161
+ The middleware automatically encrypts SSE responses when **both** conditions are met:
162
+
163
+ 1. **Request was encrypted** - `SCOPE_HPKE_CONTEXT` exists in scope (from decrypted request)
164
+ 2. **Response is SSE** - `Content-Type: text/event-stream` header detected
165
+
166
+ ```python
167
+ # Middleware detection logic (simplified)
168
+ from hpke_http.constants import SCOPE_HPKE_CONTEXT
169
+
170
+ if scope.get(SCOPE_HPKE_CONTEXT) and b"text/event-stream" in content_type:
171
+ # Auto-encrypt this streaming response
172
+ ```
173
+
174
+ This is why `media_type="text/event-stream"` is required - it's the WHATWG-standard MIME type that signals "this is an SSE stream" to both browsers and the middleware.
175
+
176
+ ## Compression (Optional)
177
+
178
+ Zstd compression reduces bandwidth by **40-95%** for JSON/text. Events <64B are sent uncompressed automatically.
179
+
180
+ ```python
181
+ HPKEMiddleware(..., compress=True) # Server: compress SSE responses
182
+ HPKEClientSession(..., compress=True) # Client: compress requests
183
+ ```
184
+
185
+ ### Design
186
+
187
+ | Choice | Rationale |
188
+ |--------|-----------|
189
+ | **Compress-then-encrypt** | Encrypted data is incompressible |
190
+ | **Zstd (RFC 8878)** | Best ratio/speed. Python 3.14 native. |
191
+ | **64B threshold** | Smaller payloads skip compression |
192
+ | **Per-chunk** | Each SSE event independent for streaming |
193
+
194
+ ### Expected Savings
195
+
196
+ | Data Type | Savings |
197
+ |-----------|---------|
198
+ | Large JSON (>1KB) | 80-95% |
199
+ | Medium JSON (200B-1KB) | 40-70% |
200
+ | HTML/XML | 70-85% |
201
+ | Logs, code | 40-60% |
202
+ | Small events (64-200B) | 0-20% |
203
+ | Base64, random | 0-25% |
204
+
205
+ ### Wire Format
206
+
207
+ ```text
208
+ Plaintext: encoding_id (1B) || compressed_data
209
+ Encoding: 0x00 = identity, 0x01 = zstd
210
+ ```
211
+
212
+ ## Pitfalls
213
+
214
+ ```python
215
+ # PSK too short
216
+ HPKEClientSession(psk=b"short") # ❌ InvalidPSKError
217
+ HPKEClientSession(psk=secrets.token_bytes(32)) # ✅ >= 32 bytes
218
+
219
+ # SSE without proper content-type (won't auto-encrypt)
220
+ return StreamingResponse(gen()) # ❌ No encryption
221
+ return StreamingResponse(gen(), media_type="text/event-stream") # ✅ Auto-encrypted
222
+
223
+ # Out-of-order decryption (multi-message context)
224
+ recipient.open(aad, ct2) # ❌ Expects seq=0
225
+ recipient.open(aad, ct1) # ✅ Decrypt in order
226
+ ```
227
+
228
+ ## Limits
229
+
230
+ | Resource | Limit |
231
+ |----------|-------|
232
+ | HPKE messages/context | 2^96-1 |
233
+ | SSE events/session | 2^32-1 |
234
+ | SSE event buffer | 64MB (configurable) |
235
+ | PSK minimum | 32 bytes |
236
+ | Overhead | 24 bytes |
237
+
238
+ > **Note:** SSE is text-only (UTF-8). Binary data must be base64-encoded (+33% overhead).
239
+
240
+ ## Security
241
+
242
+ Uses OpenSSL constant-time implementations via `cryptography` library.
243
+
244
+ ## Development
245
+
246
+ ```bash
247
+ # Install with extras
248
+ uv add "hpke-http[fastapi] @ git+https://github.com/duale-ai/hpke-http" # Server
249
+ uv add "hpke-http[aiohttp] @ git+https://github.com/duale-ai/hpke-http" # Client
250
+ uv add "hpke-http[fastapi,zstd] @ git+https://github.com/duale-ai/hpke-http" # Server + compression
251
+
252
+ # Local development
253
+ make install # Setup venv
254
+ make test # Run tests (1273 tests, 93% coverage)
255
+ make test-fuzz # Property-based fuzz tests
256
+ make lint # Format and lint
257
+ ```
258
+
259
+ ### Low-Level API
260
+
261
+ ```python
262
+ from hpke_http.hpke import seal_psk, open_psk
263
+
264
+ enc, ct = seal_psk(pk_r, b"info", psk, psk_id, b"aad", b"plaintext")
265
+ pt = open_psk(enc, sk_r, b"info", psk, psk_id, b"aad", ct)
266
+ ```
@@ -0,0 +1,215 @@
1
+ # hpke-http
2
+
3
+ End-to-end encryption for HTTP APIs.
4
+
5
+ ```bash
6
+ uv add git+https://github.com/duale-ai/hpke-http
7
+ ```
8
+
9
+ ## Highlights
10
+
11
+ - **Transparent** - Drop-in middleware, no application code changes
12
+ - **E2E encryption** - Protects data even with TLS termination at CDN/LB
13
+ - **PSK binding** - Each request cryptographically bound to API key
14
+ - **Replay protection** - SSE counter prevents replay attacks
15
+ - **RFC 9180 compliant** - Auditable, interoperable standard
16
+
17
+ ## Quick Start
18
+
19
+ ### Server (FastAPI)
20
+
21
+ ```python
22
+ from fastapi import FastAPI, Request
23
+ from fastapi.responses import StreamingResponse
24
+ from hpke_http.middleware.fastapi import HPKEMiddleware
25
+ from hpke_http.constants import KemId
26
+
27
+ app = FastAPI()
28
+
29
+ async def resolve_psk(scope: dict) -> tuple[bytes, bytes]:
30
+ api_key = dict(scope["headers"]).get(b"authorization", b"").decode()
31
+ return (api_key.encode(), (await lookup_tenant(api_key)).encode())
32
+
33
+ app.add_middleware(
34
+ HPKEMiddleware,
35
+ private_keys={KemId.DHKEM_X25519_HKDF_SHA256: private_key},
36
+ psk_resolver=resolve_psk,
37
+ # compress=True, # Optional: Zstd compression for SSE responses
38
+ # max_sse_event_size=128 * 1024 * 1024, # Optional: 128MB for large payloads
39
+ )
40
+
41
+ @app.post("/chat")
42
+ async def chat(request: Request):
43
+ data = await request.json() # Decrypted by middleware
44
+
45
+ async def generate():
46
+ yield b"event: progress\ndata: {\"step\": 1}\n\n"
47
+ yield b"event: complete\ndata: {\"result\": \"done\"}\n\n"
48
+
49
+ # Just use StreamingResponse - encryption is automatic!
50
+ return StreamingResponse(generate(), media_type="text/event-stream")
51
+ ```
52
+
53
+ ### Client (aiohttp)
54
+
55
+ ```python
56
+ from hpke_http.middleware.aiohttp import HPKEClientSession
57
+
58
+ async with HPKEClientSession(
59
+ base_url="https://api.example.com",
60
+ psk=api_key, # >= 32 bytes
61
+ psk_id=tenant_id,
62
+ # compress=True, # Optional: Zstd compression for requests
63
+ ) as session:
64
+ resp = await session.post("/chat", json={"prompt": "Hello"})
65
+ async for chunk in session.iter_sse(resp):
66
+ # bytes - matches native aiohttp response.content iteration
67
+ print(chunk) # b"event: progress\ndata: {...}\n\n"
68
+ ```
69
+
70
+ ## Documentation
71
+
72
+ - [RFC 9180 - HPKE](https://datatracker.ietf.org/doc/rfc9180/)
73
+ - [RFC 7748 - X25519](https://datatracker.ietf.org/doc/rfc7748/)
74
+ - [RFC 5869 - HKDF](https://datatracker.ietf.org/doc/rfc5869/)
75
+ - [RFC 8439 - ChaCha20-Poly1305](https://datatracker.ietf.org/doc/rfc8439/)
76
+ - [RFC 8878 - Zstandard](https://datatracker.ietf.org/doc/rfc8878/) (optional compression)
77
+
78
+ ## Cipher Suite
79
+
80
+ | Component | Algorithm | ID |
81
+ |-----------|-----------|------|
82
+ | KEM | DHKEM(X25519, HKDF-SHA256) | 0x0020 |
83
+ | KDF | HKDF-SHA256 | 0x0001 |
84
+ | AEAD | ChaCha20-Poly1305 | 0x0003 |
85
+ | Mode | PSK | 0x01 |
86
+
87
+ ## Wire Format
88
+
89
+ ### Request
90
+
91
+ ```text
92
+ ┌─────────┬─────────┬─────────┬─────────┬──────┬────────────┐
93
+ │ Ver(1B) │ KEM(2B) │ KDF(2B) │AEAD(2B) │Mode │ Ciphertext │
94
+ │ 0x01 │ 0x0020 │ 0x0001 │ 0x0003 │(1B) │ + 16B tag │
95
+ └─────────┴─────────┴─────────┴─────────┴──────┴────────────┘
96
+ Header: X-HPKE-Enc: <base64url(32B ephemeral key)>
97
+ Overhead: 24 bytes (8B header + 16B tag)
98
+ ```
99
+
100
+ ### SSE Event
101
+
102
+ ```text
103
+ event: enc
104
+ data: <base64url(counter_be32 || ciphertext || tag)>
105
+ Decrypted: raw SSE chunk (e.g., "event: progress\ndata: {...}\n\n")
106
+ ```
107
+
108
+ ## How SSE Auto-Encryption Works
109
+
110
+ The middleware automatically encrypts SSE responses when **both** conditions are met:
111
+
112
+ 1. **Request was encrypted** - `SCOPE_HPKE_CONTEXT` exists in scope (from decrypted request)
113
+ 2. **Response is SSE** - `Content-Type: text/event-stream` header detected
114
+
115
+ ```python
116
+ # Middleware detection logic (simplified)
117
+ from hpke_http.constants import SCOPE_HPKE_CONTEXT
118
+
119
+ if scope.get(SCOPE_HPKE_CONTEXT) and b"text/event-stream" in content_type:
120
+ # Auto-encrypt this streaming response
121
+ ```
122
+
123
+ This is why `media_type="text/event-stream"` is required - it's the WHATWG-standard MIME type that signals "this is an SSE stream" to both browsers and the middleware.
124
+
125
+ ## Compression (Optional)
126
+
127
+ Zstd compression reduces bandwidth by **40-95%** for JSON/text. Events <64B are sent uncompressed automatically.
128
+
129
+ ```python
130
+ HPKEMiddleware(..., compress=True) # Server: compress SSE responses
131
+ HPKEClientSession(..., compress=True) # Client: compress requests
132
+ ```
133
+
134
+ ### Design
135
+
136
+ | Choice | Rationale |
137
+ |--------|-----------|
138
+ | **Compress-then-encrypt** | Encrypted data is incompressible |
139
+ | **Zstd (RFC 8878)** | Best ratio/speed. Python 3.14 native. |
140
+ | **64B threshold** | Smaller payloads skip compression |
141
+ | **Per-chunk** | Each SSE event independent for streaming |
142
+
143
+ ### Expected Savings
144
+
145
+ | Data Type | Savings |
146
+ |-----------|---------|
147
+ | Large JSON (>1KB) | 80-95% |
148
+ | Medium JSON (200B-1KB) | 40-70% |
149
+ | HTML/XML | 70-85% |
150
+ | Logs, code | 40-60% |
151
+ | Small events (64-200B) | 0-20% |
152
+ | Base64, random | 0-25% |
153
+
154
+ ### Wire Format
155
+
156
+ ```text
157
+ Plaintext: encoding_id (1B) || compressed_data
158
+ Encoding: 0x00 = identity, 0x01 = zstd
159
+ ```
160
+
161
+ ## Pitfalls
162
+
163
+ ```python
164
+ # PSK too short
165
+ HPKEClientSession(psk=b"short") # ❌ InvalidPSKError
166
+ HPKEClientSession(psk=secrets.token_bytes(32)) # ✅ >= 32 bytes
167
+
168
+ # SSE without proper content-type (won't auto-encrypt)
169
+ return StreamingResponse(gen()) # ❌ No encryption
170
+ return StreamingResponse(gen(), media_type="text/event-stream") # ✅ Auto-encrypted
171
+
172
+ # Out-of-order decryption (multi-message context)
173
+ recipient.open(aad, ct2) # ❌ Expects seq=0
174
+ recipient.open(aad, ct1) # ✅ Decrypt in order
175
+ ```
176
+
177
+ ## Limits
178
+
179
+ | Resource | Limit |
180
+ |----------|-------|
181
+ | HPKE messages/context | 2^96-1 |
182
+ | SSE events/session | 2^32-1 |
183
+ | SSE event buffer | 64MB (configurable) |
184
+ | PSK minimum | 32 bytes |
185
+ | Overhead | 24 bytes |
186
+
187
+ > **Note:** SSE is text-only (UTF-8). Binary data must be base64-encoded (+33% overhead).
188
+
189
+ ## Security
190
+
191
+ Uses OpenSSL constant-time implementations via `cryptography` library.
192
+
193
+ ## Development
194
+
195
+ ```bash
196
+ # Install with extras
197
+ uv add "hpke-http[fastapi] @ git+https://github.com/duale-ai/hpke-http" # Server
198
+ uv add "hpke-http[aiohttp] @ git+https://github.com/duale-ai/hpke-http" # Client
199
+ uv add "hpke-http[fastapi,zstd] @ git+https://github.com/duale-ai/hpke-http" # Server + compression
200
+
201
+ # Local development
202
+ make install # Setup venv
203
+ make test # Run tests (1273 tests, 93% coverage)
204
+ make test-fuzz # Property-based fuzz tests
205
+ make lint # Format and lint
206
+ ```
207
+
208
+ ### Low-Level API
209
+
210
+ ```python
211
+ from hpke_http.hpke import seal_psk, open_psk
212
+
213
+ enc, ct = seal_psk(pk_r, b"info", psk, psk_id, b"aad", b"plaintext")
214
+ pt = open_psk(enc, sk_r, b"info", psk, psk_id, b"aad", ct)
215
+ ```
@@ -0,0 +1,174 @@
1
+ [project]
2
+ name = "hpke_http"
3
+ version = "0.1.2"
4
+ description = "End-to-end encryption for HTTP APIs using RFC 9180 HPKE"
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ requires-python = ">=3.10"
8
+ authors = [
9
+ {name = "Duale AI"},
10
+ ]
11
+ keywords = [
12
+ "hpke",
13
+ "encryption",
14
+ "rfc9180",
15
+ "cryptography",
16
+ "http",
17
+ "e2e",
18
+ "chacha20",
19
+ "x25519",
20
+ "fastapi",
21
+ "aiohttp",
22
+ "sse",
23
+ "streaming",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Intended Audience :: Developers",
28
+ "Operating System :: OS Independent",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Programming Language :: Python :: 3.14",
35
+ "Programming Language :: Python :: Implementation :: CPython",
36
+ "Topic :: Security :: Cryptography",
37
+ "Topic :: Internet :: WWW/HTTP",
38
+ "Typing :: Typed",
39
+ ]
40
+ dependencies = [
41
+ "cryptography~=46.0",
42
+ ]
43
+
44
+ [project.optional-dependencies]
45
+ dev = [
46
+ "granian~=2.0", # Rust ASGI server for E2E tests
47
+ "hypothesis~=6.148", # Property-based fuzz testing
48
+ "pyright~=1.1",
49
+ "pytest-asyncio~=0.24",
50
+ "pytest-cov~=7.0",
51
+ "pytest-mock~=3.14",
52
+ "pytest-xdist[psutil]~=3.8",
53
+ "pytest~=8.3",
54
+ "ruff~=0.7",
55
+ "twine~=6.1", # Package verification before PyPI upload
56
+ "typing_extensions~=4.12", # assert_type for API contract tests (Python 3.10)
57
+ "vulture~=2.14",
58
+ ]
59
+ # For downstream services using testing fixtures
60
+ testing = [
61
+ "pytest-asyncio~=0.24",
62
+ "pytest~=8.3",
63
+ ]
64
+ # FastAPI middleware (optional for server-side)
65
+ fastapi = [
66
+ "starlette~=0.50", # Upgraded from ~=0.41 for security fixes
67
+ ]
68
+ # aiohttp client (optional for client-side)
69
+ aiohttp = [
70
+ "aiohttp~=3.13", # Upgraded from ~=3.11 for security fixes
71
+ ]
72
+ # Zstd compression (optional, RFC 8878)
73
+ zstd = [
74
+ "backports.zstd>=1.0; python_version<'3.14'",
75
+ ]
76
+
77
+ [project.urls]
78
+ Homepage = "https://github.com/dualeai/hpke-http"
79
+ Documentation = "https://github.com/dualeai/hpke-http#readme"
80
+ Repository = "https://github.com/dualeai/hpke-http.git"
81
+ Changelog = "https://github.com/dualeai/hpke-http/releases"
82
+ Issues = "https://github.com/dualeai/hpke-http/issues"
83
+
84
+ [build-system]
85
+ requires = ["setuptools>=78.1.1"] # Pinned for CVE-2025-47273
86
+ build-backend = "setuptools.build_meta"
87
+
88
+ [tool.setuptools.packages.find]
89
+ where = ["src"]
90
+ include = ["hpke_http*"]
91
+
92
+ [tool.setuptools.package-data]
93
+ hpke_http = ["py.typed"]
94
+
95
+ [tool.pytest.ini_options]
96
+ testpaths = ["tests"]
97
+ asyncio_mode = "auto"
98
+ addopts = [
99
+ "--cov=hpke_http", # Package name
100
+ "--cov-report=term-missing", # Show missing lines in terminal
101
+ "--cov-report=html", # Generate HTML report
102
+ "--cov-fail-under=80", # Require 80% minimum coverage
103
+ "--tb=short", # Short traceback format
104
+ "-v", # Verbose output, easier LLM debugging
105
+ "--strict-markers", # Ensure all markers are registered
106
+ "-n", # Run tests in parallel
107
+ "auto", # Use all available CPUs
108
+ ]
109
+ markers = [
110
+ "vectors: RFC 9180 test vector validation (slow, 74MB JSON)",
111
+ "fuzz: Property-based fuzz tests using Hypothesis",
112
+ ]
113
+
114
+ # Subprocess coverage for granian E2E tests (pytest-cov 7.x)
115
+ [tool.coverage.run]
116
+ patch = ["subprocess", "fork", "_exit"]
117
+ parallel = true
118
+ sigterm = true
119
+
120
+ # NOTE: Rules for static testing are standard across all the monorepo, those rules are strict and must be followed, in any exception case please discuss with the team, in the meantime, if it fails, fix the issue, NEVER turn off the linter.
121
+
122
+ [tool.pyright]
123
+ typeCheckingMode = "strict"
124
+ venv = ".venv"
125
+ venvPath = "."
126
+ exclude = [".venv", "vulture_whitelist.py"]
127
+
128
+ [tool.ruff]
129
+ line-length = 120
130
+
131
+ [tool.ruff.lint]
132
+ select = [
133
+ "A", # flake8-builtins
134
+ "ARG", # flake8-unused-arguments
135
+ "ASYNC", # flake8-async
136
+ "B", # flake8-bugbear
137
+ "BLE", # flake8-blind-except
138
+ "DTZ", # flake8-datetimez
139
+ "E", # pycodestyle errors
140
+ "F", # pyflakes
141
+ "FAST", # FastAPI
142
+ "FBT", # flake8-boolean-trap
143
+ "I", # isort
144
+ "N", # pep8-naming
145
+ "PERF", # Perflint
146
+ "PIE", # flake8-pie
147
+ "PL", # Pylint
148
+ "PYI", # flake8-pyi
149
+ "Q", # flake8-quotes
150
+ "RET", # flake8-return
151
+ "RUF", # Ruff-specific
152
+ "S", # flake8-bandit (security)
153
+ "SIM", # flake8-simplify
154
+ "SLF", # flake8-self
155
+ "T20", # flake8-print
156
+ "TID", # flake8-tidy-imports
157
+ "UP", # pyupgrade
158
+ "W", # pycodestyle warnings
159
+ ]
160
+ ignore = ["PLR0913", "RUF012", "S311"] # S311: random for non-crypto (we use secrets)
161
+
162
+ [tool.ruff.lint.per-file-ignores]
163
+ "tests/**/*.py" = ["PLR2004", "S101", "S603", "ASYNC109", "ASYNC220", "SIM117", "F841", "RUF059", "PLC0415"] # Tests: magic values, assert, subprocess, async patterns, nested with, unused vars, local imports
164
+ "tests/**/test_rfc9180*.py" = ["SLF001"] # Vector tests need internal state access
165
+ "tests/**/test_fuzz.py" = ["SLF001"] # Fuzz tests need internal state access for overflow testing
166
+ "tests/**/test_aiohttp_client.py" = ["SLF001"] # Unit tests for private parsing methods
167
+ "tests/**/test_streaming_errors.py" = ["SLF001"] # Error tests need internal cipher access
168
+ "src/**/streaming.py" = ["PLW0603", "PLC0415"] # Optional zstd: module caching requires global, lazy import for optional deps
169
+
170
+ [tool.vulture]
171
+ exclude = [".venv/"] # Exclude deps
172
+ paths = ["src", "tests", "vulture_whitelist.py"] # Include whitelist
173
+ min_confidence = 80 # Ignore crap detections
174
+ sort_by_size = true # Most impactful issues first
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+