hpke-ng
A clean-slate Rust implementation of HPKE (RFC 9180) with type-driven ciphersuite selection.
Read the announcement: hpke-ng: A Clean-Slate HPKE Implementation for Rust — for the full design rationale, benchmarks, and migration notes.
use *;
use OsRng;
type Suite = ;
let mut os = OsRng;
let mut rng = os.unwrap_mut;
let = generate?;
let = seal_base?;
let pt = open_base?;
assert_eq!;
# Ok::
Why a new HPKE crate?
hpke-ng exists because three friction points in the existing Rust HPKE story kept producing real bugs and real overhead:
- Provider abstraction overhead. A trait-based pluggable backend pushes dispatch costs into hot paths and inflates the
Hpkestruct to hundreds of bytes — for a value the type system already knows. - Struct-owned PRNG hazard. When the
Hpkeinstance owns its RNG, cloning silently aliases randomness state. The fix is structural: don't own it. - Type-system gaps.
Option<&[u8]>for mode-specific parameters turns missing-PSK and wrong-mode into runtime errors that should be compile errors.
The design takes one position on each: no provider abstraction, no owned RNG, type parameters instead of mode enums. The math is a solved problem; the surrounding library is where the engineering still has slack.
Design highlights
- Type-parameterized API.
Hpke<K, F, A>is zero-sized; the ciphersuite lives in the type system. Mismatched primitives are compile errors. - Four explicit methods per mode.
seal_base,seal_psk,seal_auth,seal_auth_psk— noOption<&[u8]>parameters for required-by-mode arguments. - Auth restricted to DHKEMs at the type level.
Hpke::<XWingDraft06, ...>::seal_auth(...)does not compile. - Export-only restricted at the type level.
Hpke::<_, _, ExportOnly>::seal_base(...)does not compile; only*_export*methods are available. - Type-tagged keys. Private keys carry their KEM in their type, so passing a
DhKemP256key into an X25519 suite is rejected by the compiler, not at runtime. - Caller-provided RNG. No PRNG owned by the configuration; cloning cannot alias randomness.
- Structural nonce-reuse prevention.
Contextis non-cloneable and refuses to encrypt atseq == u64::MAX. no_std+allocby default.stdfeature forstd::error::Errorimpl onHpkeError.- One provider stack. All primitives from RustCrypto-org crates.
Compile-time guarantees
| Operation | Elsewhere | hpke-ng |
|---|---|---|
Calling seal_auth on a non-DH KEM |
Runtime error | Compile error |
| Using a wrong-KEM private key | Runtime mismatch | Compile error (type-tagged) |
| Base-mode call with a PSK supplied | Runtime error | Compile error (no PSK param) |
Encrypt with an ExportOnly AEAD |
Runtime error | Compile error |
Supported ciphersuites
| Component | Variants |
|---|---|
| KEMs | DhKemX25519HkdfSha256, DhKemX448HkdfSha512, DhKemP256HkdfSha256, DhKemP384HkdfSha384, DhKemP521HkdfSha512, DhKemK256HkdfSha256 |
KEMs (post-quantum, pq feature) |
XWingDraft06, MlKem768, MlKem1024 |
| KDFs | HkdfSha256, HkdfSha384, HkdfSha512 |
| AEADs | Aes128Gcm, Aes256Gcm, ChaCha20Poly1305, ExportOnly |
| Modes | Base, Psk, Auth, AuthPsk |
Performance
Across 44 head-to-head benchmarks vs. hpke-rs: 16 wins for hpke-ng (notably encap/decap, 21–22% faster), 25 ties where the underlying primitive dominates, 3 losses on isolated key-generation paths.
Memory and binary footprint:
| Quantity | hpke-rs | hpke-ng |
|---|---|---|
Hpke<...> struct |
320 bytes | 0 bytes (PhantomData) |
Context<...> struct |
400 bytes | 80 bytes |
| Minimal release binary | 561 KB | 392 KB (~30% smaller) |
Build with RUSTFLAGS="-C target-cpu=native" for AES-NI / SHA-NI where available. The [profile.bench] in Cargo.toml enables lto = "thin" and codegen-units = 1. For head-to-head numbers, run cargo bench --features comparative --bench comparative locally.
Security posture
The library responds to two classes of issue observed in prior implementations:
- Zero shared-secret check (RFC 9180 §7.1.4). Enforced for X25519 and X448 using
subtle::ConstantTimeEq. - Nonce counter wraparound. Prevented structurally:
Contextuses au64sequence number, refuses to encrypt atu64::MAX, and is non-cloneable so a counter cannot fork.
The post-DH all-zeros check is constant-time. Context cannot be Cloned, so two ciphertexts cannot be produced under the same (key, nonce) from two copies of the same context.
Constant-time considerations
This crate composes RustCrypto primitives. Constant-time properties are inherited from those crates:
| Primitive | CT property |
|---|---|
| X25519, X448 | CT by construction. |
| P-256, P-384, P-521, secp256k1 | CT in arithmetic mode (pinned). |
| HKDF-SHA-{256,384,512} | CT (deterministic; no secret-dependent branches). |
| ChaCha20-Poly1305 | CT by construction. |
| AES-128-GCM, AES-256-GCM | CT only with hardware AES-NI/PCLMULQDQ. Prefer ChaCha20Poly1305 on platforms without these instructions. |
| ML-KEM, X-Wing | CT per upstream documentation; both crates are pre-1.0. |
Testing
Coverage includes 59 macro-generated roundtrip tests across every ciphersuite × mode combination, four cargo-fuzz targets (panics treated as bugs), and differential testing against hpke-rs for wire-format interop. The full suite (without differential) runs in under two seconds.
Migration from hpke-rs
Three mechanical steps, typically under an hour for a real codebase:
- Define a
type Suite = Hpke<K, F, A>;alias for the ciphersuite you use. - Replace
hpke.seal(...)calls with the explicit mode method:Suite::seal_base,seal_psk,seal_auth, orseal_auth_psk. - Thread
&mut rngthrough call sites — the configuration no longer owns one.
See the announcement post for a worked example.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.