Expand description
Safe, synchronous, bounded whole-message HPKE transactions for HTTP.
This crate is the self-documenting source contract for stable protocol
PROTOCOL_ID. It performs no networking and exposes no transport or
framework objects. Protocol version 1 composes:
- RFC 9180 HPKE in PSK mode with DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, and ChaCha20-Poly1305;
- canonical RFC 9292 known-length Binary HTTP Messages as plaintext, with an optional authenticated body-coding extension; and
- the response-secret, recipient-nonce, and
enc || response_noncederivation pattern from RFC 9458 section 4.4.
The protocol is not wire-compatible with RFC 9458 Oblivious HTTP. Its narrow PSK request envelope is project-specific. HTTPS remains required because the protocol does not hide its endpoint, public identifiers, sizes, or timing.
§Request bytes
Multi-byte integers use network byte order. The authenticated HPKE info
is "message/hpke-http request" || 0x00 || "v1" || 0x00 || header, where header
ends after the PSK ID. HPKE seals the request plaintext with empty AAD.
| Field | Size | Value |
|---|---|---|
| magic | 4 | ASCII HHRQ |
| version | 1 | 0x01 |
| flags | 1 | 0x00 |
| recipient key-ID length | 1 | 1..=255 |
| PSK-ID length | 1 | 1..=255 |
| KEM ID | 2 | 0x0020 |
| KDF ID | 2 | 0x0001 |
| AEAD ID | 2 | 0x0003 |
| issued-at Unix seconds | 8 | unsigned network-order client time |
| recipient key ID | variable | opaque public bytes |
| PSK ID | variable | opaque public bytes, never the PSK |
enc | 32 | encoded X25519 encapsulated key |
| ciphertext | variable | request plaintext plus 16-byte tag |
The PSK is at least 32 bytes. Both IDs are non-empty, at most 255 bytes, and the PSK ID must not equal the PSK.
§Optional authenticated body coding
Identity requests seal the original canonical BHTTP bytes, unchanged. A
client that explicitly selects CompressionCoding instead seals
0x04 || request_coding || response_coding || BHTTP_request, where coding
IDs are 0 for identity, 1 for gzip, and 2 for zstd. response_coding is
the client’s advertised preference. 0x04 is not a BHTTP request framing
indicator, so a peer without this extension rejects the request. The
server must explicitly enable the extension; it never compresses a response
for an identity-only client. When compression saves bytes, the server seals
0x04 || response_coding || BHTTP_response; otherwise it seals the original
canonical BHTTP response. Unadvertised response coding is rejected.
Only the BHTTP body vector is coded. Its fields remain logical HTTP fields;
in particular, content-length names the decoded logical body and is
checked after bounded decompression. This private, authenticated transform
is independent of HTTP Content-Encoding, which remains ordinary opaque
representation metadata at the low-level API. Gzip accepts concatenated
members; zstd accepts up to 16 concatenated or skippable frames without
dictionaries, with history windows capped at 8 MiB. Both coded and decoded
bodies have the configured body limit. The request is decompressed only after replay
admission. Compression is off by default because ciphertext length can
reveal information about a body containing both secrets and attacker input.
§Authenticated HTTP subset
Requests use the fixed https scheme and one of Method’s seven values.
The authority and path are bounded ASCII. Authorities follow the RFC 3986
host plus optional decimal-port grammar, without userinfo or normalization;
IPv6 and IPvFuture literals require brackets. Request targets reject
malformed percent escapes, backslashes, fragments, and dot segments; * is
limited to OPTIONS. Headers are ordered fields with lower-case HTTP token names and
canonical ASCII values without leading or trailing optional whitespace;
repeated names remain ordered. Connection-specific framing, unsupported
proxy semantics, and Expect are rejected: connection, expect, host,
keep-alive, proxy-authenticate, proxy-authentication-info,
proxy-authorization, proxy-connection, te, trailer,
transfer-encoding, and upgrade. A single decimal content-length is
optional. It must equal the authenticated body length on requests and normal
responses, is metadata on HEAD and 304 responses, and is forbidden on 204.
Responses contain one status from 200 through 599. HEAD responses and
status 204, 205, or 304 responses cannot contain a body. Empty bodies remain
part of an authenticated canonical message.
§Replay pause and one-shot response
After complete request authentication and canonical parsing, Server
checks the authenticated issued-at time against REQUEST_LIFETIME_SECS
and CLOCK_SKEW_SECS. It then returns
SHA-256("hpke-http/replay" || 0x00 || "v1" || 0x00 || header || enc) and an
exclusive retention deadline through ReplayRequest. The verified
plaintext stays in an opaque ReplayToken until the host reports one
matching atomic replay-admission decision. A provider must reserve the ID
until that deadline and fail closed on an uncertain result. This crate does
not provide or claim a distributed replay store. ReplayToken::admit
reads the trusted clock again and refuses to release plaintext at or after
the authenticated deadline, including when a provider call was delayed.
Both HPKE contexts export 32 bytes with context
"message/hpke-http response" || 0x00 || "v1". A server samples a fresh 32-byte
response_nonce, uses enc || response_nonce as HKDF-SHA256 salt, and
expands the exported secret with raw info "key" (32 bytes) and
"nonce" (12 bytes). The response bytes are:
response_nonce[32] || ChaCha20Poly1305(response_plaintext, empty_aad)ResponseToken and ResponseCapability consume ownership, so each
request can open and create at most one response.
§Complete transaction
use hpke_http::{
Client, Limits, Method, Request, Response, Server, generate_key_pair,
};
let key_pair = generate_key_pair()?;
let (recipient_private_key, recipient_public_key) = key_pair.into_parts();
let recipient_key_id = b"primary-2026-09".to_vec();
let psk = vec![0x42; 32];
let psk_id = b"tenant-42".to_vec();
let limits = Limits::default();
let client = Client::new(
&recipient_public_key,
recipient_key_id.clone(),
psk.clone(),
psk_id.clone(),
limits,
)?;
let server = Server::new(&recipient_private_key, recipient_key_id, limits)?;
let request = Request {
method: Method::Post,
authority: b"api.example.test".to_vec(),
path: b"/items".to_vec(),
headers: Vec::new(),
body: b"payload".to_vec(),
};
let protected = client.protect(&request)?;
let (request_envelope, response_token) = protected.into_parts();
let preparsed = server.preparse(&request_envelope)?;
assert_eq!(preparsed.credential.psk_id, psk_id);
let authenticated = server.authenticate(preparsed.token, &psk)?;
let replay_decision = authenticated.replay.decision(true);
let opened = authenticated.token.admit(replay_decision)?;
assert_eq!(opened.request, request);
let expected = Response {
status: 200,
headers: Vec::new(),
body: b"ok".to_vec(),
};
let response_envelope = opened.response.protect(&expected)?;
assert_eq!(response_token.open(&response_envelope)?, expected);§Limits and errors
Limits defaults to an 8 MiB body, 16 KiB of header names/values, 64
fields, and an 8 KiB authority-plus-path. Its documented hard ceilings are
enforced before attacker-controlled lengths drive allocations. Error
provides stable coarse categories and contains no credentials, plaintext,
ciphertext, or parser offsets.
The crate version, PROTOCOL_ID, and BINDING_ABI_VERSION are distinct
identities. First-party bindings call build_info before accepting
credentials. Incompatible wire changes require a new protocol identifier and
envelope version rather than reinterpretation of request version 0x01.
Structs§
- Authenticated
Request - Result of authentication, paused before replay admission and plaintext release.
- Build
Info - Immutable engine identity returned before a binding accepts secrets.
- Client
- A client configured for one recipient key and one explicit PSK identity.
- Credential
Request - Public information needed by a host credential resolver.
- Header
Field - One canonical end-to-end HTTP field in an ordered field section.
- KeyPair
- A generated X25519 recipient key pair.
- Limits
- Per-engine limits. Values can become stricter but never exceed hard limits.
- Opened
Request - A fully authenticated request and its one-use response capability.
- Preparsed
Request - Result of the bounded public preparse stage.
- Protected
Request - A request envelope and the one-use capability needed to open its response.
- Replay
Decision - One atomic replay-provider result bound to its request ID.
- Replay
Request - Stable request identifier passed to an atomic replay-admission provider.
- Replay
Token - Opaque one-use continuation after request authentication.
- Request
- A complete plaintext request supplied to or returned by the engine.
- Response
- A complete plaintext response supplied to or returned by the engine.
- Response
Capability - One-use server capability for protecting the response to one opened request.
- Response
Token - One-use client capability for opening the response to one request.
- Server
- A server configured for one static recipient key.
- Start
Token - Opaque one-use continuation after public preparse.
- System
Entropy - The operating system or Web Crypto entropy source selected by
getrandom.
Enums§
- Compression
Coding - Opt-in, authenticated protocol body coding. This is not HTTP
Content-Encoding. - Error
- Errors returned by the protocol engine.
- Method
- HTTP request methods supported by protocol version 1.
Constants§
- BINDING_
ABI_ VERSION - Boundary ABI version used by the first-party bindings.
- CLOCK_
SKEW_ SECS - Maximum accepted client/server clock difference (30 seconds).
- HARD_
MAX_ BODY_ LEN - Absolute body limit accepted by the implementation (64 MiB).
- HARD_
MAX_ HEADER_ BYTES - Absolute combined header-name and header-value limit (64 KiB).
- HARD_
MAX_ HEADER_ COUNT - Absolute number of header fields accepted in one message.
- HARD_
MAX_ ID_ LEN - Absolute public identifier limit accepted by the implementation.
- HARD_
MAX_ TARGET_ LEN - Absolute combined authority and path limit (8 KiB).
- PROTOCOL_
ID - Language-neutral protocol identifier.
- REQUEST_
LIFETIME_ SECS - Time for which a newly created request can be accepted (five minutes).
Traits§
- Entropy
Source - A source of cryptographically secure random bytes.
Functions§
- build_
info - Return the engine identity used for binding skew checks.
- generate_
key_ pair - Generate a recipient key pair with the platform entropy source.
- generate_
key_ pair_ with_ entropy - Generate a recipient key pair with an injected fallible entropy source.