Skip to main content

End-to-end encryption for HTTP APIs using RFC 9180 HPKE

Project description

hpke-http

End-to-end encryption for HTTP APIs using RFC 9180 HPKE.

CI PyPI Downloads Python License

Highlights

  • Transparent - Drop-in middleware, no application code changes
  • E2E encryption - Protects data even with TLS termination at CDN/LB
  • PSK binding - Each request cryptographically bound to API key
  • Replay protection - Counter-based nonces prevent replay attacks
  • RFC 9180 compliant - Auditable, interoperable standard

Installation

uv add "hpke-http[fastapi]"       # Server
uv add "hpke-http[aiohttp]"       # Client
uv add "hpke-http[fastapi,zstd]"  # + compression

Quick Start

Server (FastAPI)

from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from hpke_http.middleware.fastapi import HPKEMiddleware
from hpke_http.constants import KemId

app = FastAPI()

async def resolve_psk(scope: dict) -> tuple[bytes, bytes]:
    api_key = dict(scope["headers"]).get(b"authorization", b"").decode()
    return (api_key.encode(), (await lookup_tenant(api_key)).encode())

app.add_middleware(
    HPKEMiddleware,
    private_keys={KemId.DHKEM_X25519_HKDF_SHA256: private_key},
    psk_resolver=resolve_psk,
)

@app.post("/chat")
async def chat(request: Request):
    data = await request.json()  # Decrypted automatically

    async def generate():
        yield b"event: progress\ndata: {\"step\": 1}\n\n"
        yield b"event: complete\ndata: {\"result\": \"done\"}\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")

Client (aiohttp)

from hpke_http.middleware.aiohttp import HPKEClientSession

async with HPKEClientSession(
    base_url="https://api.example.com",
    psk=api_key,        # >= 32 bytes
    psk_id=tenant_id,
) as session:
    resp = await session.post("/chat", json={"prompt": "Hello"})
    async for chunk in session.iter_sse(resp):
        print(chunk)  # b"event: progress\ndata: {...}\n\n"

Documentation

Security

Uses OpenSSL constant-time implementations via cryptography library.

Contributing

Contributions welcome! Please open an issue first to discuss changes.

make install      # Setup venv
make test         # Run tests
make lint         # Format and lint

License

Apache-2.0


Technical Details

Cipher Suite

Component Algorithm ID
KEM DHKEM(X25519, HKDF-SHA256) 0x0020
KDF HKDF-SHA256 0x0001
AEAD ChaCha20-Poly1305 0x0003
Mode PSK 0x01

Wire Format

Request/Response (Chunked Binary)

Headers:
  X-HPKE-Enc: <base64url(32B ephemeral key)>
  X-HPKE-Stream: <base64url(4B session salt)>

Body (repeating chunks):
┌───────────┬────────────┬─────────────────────────────────┐
│ Length(4B)│ Counter(4B)│ Ciphertext (N + 16B tag)        │
│ big-end   │ big-end    │ encrypted: encoding_id || data  │
└───────────┴────────────┴─────────────────────────────────┘
Overhead: 24B/chunk (4B length + 4B counter + 16B tag)

SSE Event

event: enc
data: <base64(counter_be32 || ciphertext)>
Decrypted: raw SSE chunk (e.g., "event: progress\ndata: {...}\n\n")

Uses standard base64 (not base64url) - SSE data fields allow +/= characters.

SSE Auto-Encryption

The middleware automatically encrypts SSE responses when both conditions are met:

  1. Request was encrypted - SCOPE_HPKE_CONTEXT exists in scope
  2. Response is SSE - Content-Type: text/event-stream detected

This is why media_type="text/event-stream" is required.

Compression (Optional)

Zstd compression reduces bandwidth by 40-95% for JSON/text.

HPKEMiddleware(..., compress=True)      # Server
HPKEClientSession(..., compress=True)   # Client
Choice Rationale
Compress-then-encrypt Encrypted data is incompressible
Zstd (RFC 8878) Best ratio/speed, Python 3.14 native
64B threshold Smaller payloads skip compression

Pitfalls

# PSK too short
HPKEClientSession(psk=b"short")                 # InvalidPSKError
HPKEClientSession(psk=secrets.token_bytes(32))  # >= 32 bytes

# Missing content-type (won't auto-encrypt)
return StreamingResponse(gen())                                  # No encryption
return StreamingResponse(gen(), media_type="text/event-stream")  # Auto-encrypted

Limits

Resource Limit
HPKE messages/context 2^96-1
SSE events/session 2^32-1
SSE event buffer 64MB (configurable)
PSK minimum 32 bytes
Chunk overhead 24B (length + counter + tag)
Chunk size 64KB

Note: SSE is text-only (UTF-8). Binary data must be base64-encoded (+33% overhead).

Low-Level API

from hpke_http.hpke import seal_psk, open_psk

enc, ct = seal_psk(pk_r, b"info", psk, psk_id, b"aad", b"plaintext")
pt = open_psk(enc, sk_r, b"info", psk, psk_id, b"aad", ct)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hpke_http-1.0.0.tar.gz (58.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hpke_http-1.0.0-py3-none-any.whl (40.1 kB view details)

Uploaded Python 3

File details

Details for the file hpke_http-1.0.0.tar.gz.

File metadata

  • Download URL: hpke_http-1.0.0.tar.gz
  • Upload date:
  • Size: 58.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for hpke_http-1.0.0.tar.gz
Algorithm Hash digest
SHA256 e54b7cfc3efa4c837e53c2bdec8e4e5d3e95d2c489cacb9adbb64626b8d8c491
MD5 140eead15cfdb18547631adb3d83dbbb
BLAKE2b-256 8455e9fa1c88a9646bc29bcb6d1dc630303c27034b3e2fed49636fae62ed4b27

See more details on using hashes here.

Provenance

The following attestation bundles were made for hpke_http-1.0.0.tar.gz:

Publisher: release.yml on dualeai/hpke-http

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file hpke_http-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: hpke_http-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 40.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for hpke_http-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b62b0588f97c3ea8c77598715630e0561dbe14fa85c38a24b2ff45ed636d9e31
MD5 e4b96aa381a3fa73725b3f92c79d2371
BLAKE2b-256 d998c9d9217653a51ea0e49d6f8e2c869e5cbc2ca7ff72555b4324b4ca26df2c

See more details on using hashes here.

Provenance

The following attestation bundles were made for hpke_http-1.0.0-py3-none-any.whl:

Publisher: release.yml on dualeai/hpke-http

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page