hpke-ng 0.1.0-rc.2

Clean, fast, RFC 9180 HPKE implementation.
Documentation

hpke-ng

CI License

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 hpke_ng::*;
use rand_core::OsRng;

type Suite = Hpke<DhKemX25519HkdfSha256, HkdfSha256, ChaCha20Poly1305>;

let mut os = OsRng;
let mut rng = os.unwrap_mut();
let (sk_r, pk_r) = DhKemX25519HkdfSha256::generate(&mut rng)?;
let (enc, ct)  = Suite::seal_base(&mut rng, &pk_r, b"info", b"aad", b"hello")?;
let pt         = Suite::open_base(&enc, &sk_r, b"info", b"aad", &ct)?;
assert_eq!(pt, b"hello");
# Ok::<_, hpke_ng::HpkeError>(())

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:

  1. Provider abstraction overhead. A trait-based pluggable backend pushes dispatch costs into hot paths and inflates the Hpke struct to hundreds of bytes — for a value the type system already knows.
  2. Struct-owned PRNG hazard. When the Hpke instance owns its RNG, cloning silently aliases randomness state. The fix is structural: don't own it.
  3. 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 — no Option<&[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 DhKemP256 key 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. Context is non-cloneable and refuses to encrypt at seq == u64::MAX.
  • no_std + alloc by default. std feature for std::error::Error impl on HpkeError.
  • 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: Context uses a u64 sequence number, refuses to encrypt at u64::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

cargo test                                              # library + roundtrip
cargo test --features pq                                # + post-quantum tests
cargo test --features pq,kat-internals                  # + RFC 9180 KAT
cargo test --features pq,differential,kat-internals     # + cross-impl differential vs hpke-rs

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:

  1. Define a type Suite = Hpke<K, F, A>; alias for the ciphersuite you use.
  2. Replace hpke.seal(...) calls with the explicit mode method: Suite::seal_base, seal_psk, seal_auth, or seal_auth_psk.
  3. Thread &mut rng through 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.