Skip to main content

hpke_http/
lib.rs

1//! HPKE requests and checked response records for HTTP.
2//!
3//! This crate defines one wire contract, [`PROTOCOL_ID`] `hpke-http/3`.
4//! It does no network I/O. Hosts must use HTTPS. The protocol does not hide
5//! endpoints, public IDs, payload size, record counts, or timing, and it does
6//! not add padding. It uses RFC 9180 HPKE PSK mode with X25519,
7//! HKDF-SHA256, and ChaCha20-Poly1305. It is not Oblivious HTTP.
8//!
9//! # Request bytes
10//!
11//! All fixed-width integers use big-endian order. Every request, including
12//! an empty one, uses this one START/DATA/END form:
13//!
14//! ```text
15//! "HHRQ" || 0x03 || key_id_len:u8 || psk_id_len:u8
16//! || 0x0020 || 0x0001 || 0x0003 || issued_at_unix_s:u64be
17//! || key_id || psk_id || enc[32] || records...
18//! record = ciphertext_len:u32be || ciphertext[ciphertext_len]
19//! ```
20//!
21//! Both IDs have 1 to 255 bytes. The public header ends after the PSK ID.
22//! Each ciphertext includes a 16-byte HPKE tag. The first clear record is
23//! `0x01 || method:vector || authority:vector || path:vector || fields:vector`.
24//! These four fields use shortest RFC 9292 QUIC variable-length integers.
25//! `fields` contains ordered name/value vector pairs. The scheme is HTTPS.
26//! Each DATA is `0x02 || coding:u8 || coded_body`, with a nonempty clear
27//! body of at most 64 KiB. Coding 0 means raw; 1 means one zstd frame. Rust
28//! picks zstd only when it makes that DATA record shorter. It compresses
29//! clear body bytes before HPKE encryption. A reader accepts a valid zstd
30//! frame within record limits even if it is longer than its clear bytes.
31//! END is exactly `0x03`.
32//! There is one START, zero or more DATA records, one END, and then true
33//! outer HTTP body EOF. Extra bytes, missing END, and partial frames fail.
34//!
35//! One HPKE PSK context seals all request records. Its `info` is
36//! `"message/hpke-http request\0v3\0" || public_header`. Record number
37//! `n` starts at zero. Its AAD is
38//! `"hpke-http/3 request record\0" || public_header || n:u64be ||
39//! ciphertext_len:u32be`. HPKE also advances its own nonce sequence per
40//! record. A host must check END and true outer EOF before app dispatch.
41//! A logical `Content-Length`, if present, names the sum of clear DATA bytes.
42//! [`Limits::max_request_bytes`] defaults to 1 GiB and has a 4 GiB hard cap.
43//! There can be at most 1,048,576 DATA records.
44//! Complete in-memory request helpers also use [`Limits::max_body_len`],
45//! which defaults to 8 MiB. Incremental requests use the total request cap.
46//!
47//! # Public key discovery
48//!
49//! The crate does no HTTP I/O. [`Server::public_key`] returns the server's
50//! X25519 public key. The host key GET record is `"HHKD" || 0x01 ||
51//! id_len:u8 || id || public_key[32]`. Its format version is separate from
52//! [`PROTOCOL_ID`]. The host must keep the same key across its workers.
53//!
54//! # Replay check
55//!
56//! The replay ID is SHA-256 of
57//! `"hpke-http/replay\0v3\0" || public_header || enc`. The server checks
58//! the request time against [`REQUEST_LIFETIME_SECS`] and [`CLOCK_SKEW_SECS`].
59//! The host must make one atomic replay decision and keep it through the
60//! authenticated deadline. The app must not see clear bytes before the host
61//! checks the full request, including END and EOF.
62//!
63//! # Response bytes
64//!
65//! The HPKE context exports 32 bytes with context
66//! `"message/hpke-http response\0v3"`. The server samples a fresh 32-byte
67//! nonce. HKDF-SHA256 uses `enc || server_nonce` as salt and that exported
68//! secret as input. It expands the key with `"hpke-http/3 response key"`
69//! and nonce with `"hpke-http/3 response nonce"`. The response is:
70//!
71//! ```text
72//! "HHRP" || 0x03 || server_nonce[32] || records...
73//! record = ciphertext_len:u32be || ciphertext[ciphertext_len]
74//! ```
75//!
76//! START is `0x01 || status:u16be || fields`, with ordered name/value
77//! vectors. DATA is `0x02 || coding:u8 || coded_body`; coding 0 is raw and
78//! 1 is one zstd frame. Rust picks zstd only when it saves bytes. Each SSE
79//! DATA holds one complete SSE block after decoding.
80//! END is exactly `0x03`. The response reader needs true outer EOF after
81//! END. The record AAD is `"hpke-http/3 response record\0" ||
82//! prefix[37] || n:u64be || ciphertext_len:u32be`. The record nonce is the
83//! derived base nonce with its last eight bytes combined with `n:u64be` by XOR.
84//!
85//! [`Limits`] also bounds each finite reply, SSE block, head, and target.
86//! Errors do not include clear or secret bytes. The crate version,
87//! [`PROTOCOL_ID`], and [`BINDING_ABI_VERSION`] are separate values.
88//!
89//! # Complete transaction
90//!
91//! The host must make a shared atomic replay decision before it admits a
92//! request. This example accepts one request to show the API sequence.
93//!
94//! ```
95//! use hpke_http::{Client, Limits, Method, Request, Response, Server, generate_key_pair};
96//!
97//! fn main() -> Result<(), hpke_http::Error> {
98//!     let keys = generate_key_pair()?;
99//!     let (private_key, public_key) = keys.into_parts();
100//!     let psk = b"example pre-shared key with 32 bytes!!";
101//!     let client = Client::new(
102//!         &public_key, b"key-1".to_vec(), psk.to_vec(), b"tenant-1".to_vec(),
103//!         Limits::default(),
104//!     )?;
105//!     let server = Server::new(&private_key, b"key-1".to_vec(), Limits::default())?;
106//!     let request = Request {
107//!         method: Method::Post,
108//!         authority: b"api.example.test".to_vec(),
109//!         path: b"/items".to_vec(),
110//!         headers: Vec::new(),
111//!         body: b"hello".to_vec(),
112//!     };
113//!     let (request_bytes, response_right) = client.protect(&request)?.into_parts();
114//!     let parsed = server.preparse(&request_bytes)?;
115//!     let authenticated = server.authenticate(parsed.token, psk)?;
116//!     let opened = authenticated.token.admit(authenticated.replay.decision(true))?;
117//!     assert_eq!(opened.request, request);
118//!     let reply = Response { status: 200, headers: Vec::new(), body: b"ok".to_vec() };
119//!     let reply_bytes = opened.response.protect_finite(&reply)?;
120//!     assert_eq!(response_right.open_finite(&reply_bytes)?, reply);
121//!     Ok(())
122//! }
123//! ```
124//!
125//! # Streamed upload
126//!
127//! Send the `begin_stream` bytes first, then each record from `push`, then the
128//! bytes from `finish`. End the outer HTTP body after `finish`. On the server,
129//! hold checked DATA until `finish_eof` confirms END and real outer EOF. This
130//! example accepts one replay decision and passes bytes directly between the
131//! client and server in place of an HTTP transport. A host must use a shared
132//! atomic replay store:
133//!
134//! ```
135//! use hpke_http::{
136//!     Client, Error, Limits, Method, RequestHead, Server, StreamRequestOpener,
137//!     StreamRequestRecord, generate_key_pair,
138//! };
139//!
140//! fn feed(reader: &mut StreamRequestOpener, bytes: &[u8], body: &mut Vec<u8>) -> Result<(), Error> {
141//!     let mut remaining = bytes;
142//!     while !remaining.is_empty() {
143//!         let (used, record) = reader.feed(remaining)?;
144//!         remaining = &remaining[used..];
145//!         if let Some(StreamRequestRecord::Data(part)) = record {
146//!             body.extend_from_slice(&part);
147//!         }
148//!     }
149//!     Ok(())
150//! }
151//!
152//! fn main() -> Result<(), Error> {
153//!     let keys = generate_key_pair()?;
154//!     let (private_key, public_key) = keys.into_parts();
155//!     let psk = b"example pre-shared key with 32 bytes!!";
156//!     let client = Client::new(&public_key, b"key-1".to_vec(), psk.to_vec(), b"tenant-1".to_vec(), Limits::default())?;
157//!     let server = Server::new(&private_key, b"key-1".to_vec(), Limits::default())?;
158//!     let head = RequestHead {
159//!         method: Method::Post,
160//!         authority: b"api.example.test".to_vec(),
161//!         path: b"/upload".to_vec(),
162//!         headers: Vec::new(),
163//!     };
164//!     let (mut writer, first) = client.begin_stream(&head)?;
165//!     let parsed = server.preparse_stream(&first)?;
166//!     let authenticated = server.authenticate_stream(parsed.token, psk)?;
167//!     let opened = authenticated.token.admit(authenticated.replay.decision(true))?;
168//!     let mut reader = opened.reader;
169//!     let mut body = Vec::new();
170//!     for part in [b"hello".as_slice(), b" world".as_slice()] {
171//!         let mut remaining = part;
172//!         while !remaining.is_empty() {
173//!             let (used, record) = writer.push(remaining)?;
174//!             remaining = &remaining[used..];
175//!             if let Some(record) = record {
176//!                 feed(&mut reader, &record, &mut body)?;
177//!             }
178//!         }
179//!     }
180//!     let (end, _response_right) = writer.finish()?;
181//!     feed(&mut reader, &end, &mut body)?;
182//!     let _server_response_right = reader.finish_eof()?;
183//!     assert_eq!(body.as_slice(), b"hello world");
184//!     Ok(())
185//! }
186//! ```
187//!
188#![forbid(unsafe_code)]
189
190mod codec;
191mod compression;
192mod engine;
193mod entropy;
194mod error;
195mod limits;
196mod message;
197mod method;
198mod response;
199mod sse;
200
201pub use engine::{
202    AuthenticatedRequest, AuthenticatedStreamRequest, Client, CredentialRequest, KeyPair,
203    OpenedRequest, OpenedStreamRequest, PreparsedRequest, PreparsedStreamRequest, ProtectedRequest,
204    ReplayDecision, ReplayRequest, ReplayToken, ResponseCapability, ResponseToken, STREAM_DATA_LEN,
205    Server, StartToken, StreamReplayToken, StreamRequestOpener, StreamRequestRecord,
206    StreamRequestSealer, StreamStartToken, generate_key_pair, generate_key_pair_with_entropy,
207};
208pub use entropy::{EntropySource, SystemEntropy};
209pub use error::Error;
210pub use limits::{
211    HARD_MAX_BODY_LEN, HARD_MAX_HEADER_BYTES, HARD_MAX_HEADER_COUNT, HARD_MAX_ID_LEN,
212    HARD_MAX_REQUEST_BYTES, HARD_MAX_TARGET_LEN, Limits,
213};
214pub use message::{HeaderField, Request, RequestHead, Response};
215pub use method::Method;
216pub use response::{ResponseHead, ResponseMode, ResponseOpener, ResponseRecord, ResponseSealer};
217pub use sse::SseSplitter;
218
219/// Language-neutral protocol identifier.
220pub const PROTOCOL_ID: &str = "hpke-http/3";
221/// Boundary ABI version used by the first-party bindings.
222pub const BINDING_ABI_VERSION: u32 = 7;
223/// Time for which a newly created request can be accepted (five minutes).
224pub const REQUEST_LIFETIME_SECS: u64 = 300;
225/// Maximum accepted client/server clock difference (30 seconds).
226pub const CLOCK_SKEW_SECS: u64 = 30;
227
228/// Immutable engine identity returned before a binding accepts secrets.
229#[derive(Clone, Copy, Debug, Eq, PartialEq)]
230pub struct BuildInfo {
231    /// Rust crate version.
232    pub version: &'static str,
233    /// Language-neutral protocol identifier.
234    pub protocol: &'static str,
235    /// Boundary ABI version.
236    pub binding_abi: u32,
237}
238
239/// Return the engine identity used for binding skew checks.
240#[must_use]
241pub const fn build_info() -> BuildInfo {
242    BuildInfo {
243        version: env!("CARGO_PKG_VERSION"),
244        protocol: PROTOCOL_ID,
245        binding_abi: BINDING_ABI_VERSION,
246    }
247}