hpke-ng 0.1.0-rc.2

Clean, fast, RFC 9180 HPKE implementation.
Documentation
# hpke-ng

[![CI](https://github.com/symbolicsoft/hpke-ng/actions/workflows/ci.yml/badge.svg)](https://github.com/symbolicsoft/hpke-ng/actions)
[![License](https://img.shields.io/badge/license-Apache--2.0%20OR%20MIT-blue)](#license)

A clean-slate Rust implementation of [HPKE (RFC 9180)](https://www.rfc-editor.org/rfc/rfc9180.html) with type-driven ciphersuite selection.

> Read the announcement: **[hpke-ng: A Clean-Slate HPKE Implementation for Rust]https://symbolic.software/blog/2026-05-08-hpke-ng/** — for the full design rationale, benchmarks, and migration notes.

```rust
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 `Clone`d, 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

```bash
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](https://symbolic.software/blog/2026-05-08-hpke-ng/) for a worked example.

## License

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT license](LICENSE-MIT) at your option.