Skip to main content

Crate hpke_http

Crate hpke_http 

Source
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_nonce derivation 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.

FieldSizeValue
magic4ASCII HHRQ
version10x01
flags10x00
recipient key-ID length11..=255
PSK-ID length11..=255
KEM ID20x0020
KDF ID20x0001
AEAD ID20x0003
issued-at Unix seconds8unsigned network-order client time
recipient key IDvariableopaque public bytes
PSK IDvariableopaque public bytes, never the PSK
enc32encoded X25519 encapsulated key
ciphertextvariablerequest 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§

AuthenticatedRequest
Result of authentication, paused before replay admission and plaintext release.
BuildInfo
Immutable engine identity returned before a binding accepts secrets.
Client
A client configured for one recipient key and one explicit PSK identity.
CredentialRequest
Public information needed by a host credential resolver.
HeaderField
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.
OpenedRequest
A fully authenticated request and its one-use response capability.
PreparsedRequest
Result of the bounded public preparse stage.
ProtectedRequest
A request envelope and the one-use capability needed to open its response.
ReplayDecision
One atomic replay-provider result bound to its request ID.
ReplayRequest
Stable request identifier passed to an atomic replay-admission provider.
ReplayToken
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.
ResponseCapability
One-use server capability for protecting the response to one opened request.
ResponseToken
One-use client capability for opening the response to one request.
Server
A server configured for one static recipient key.
StartToken
Opaque one-use continuation after public preparse.
SystemEntropy
The operating system or Web Crypto entropy source selected by getrandom.

Enums§

CompressionCoding
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§

EntropySource
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.