{"id":50535476,"url":"https://github.com/symbolicsoft/hpke-ng","last_synced_at":"2026-06-03T16:01:41.715Z","repository":{"id":356530185,"uuid":"1232368621","full_name":"symbolicsoft/hpke-ng","owner":"symbolicsoft","description":"Faster, Smaller, Harder HPKE for Rust","archived":false,"fork":false,"pushed_at":"2026-05-27T07:54:43.000Z","size":2037,"stargazers_count":26,"open_issues_count":2,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-05-27T09:25:03.779Z","etag":null,"topics":["crypto","cryptography","hpke","rust"],"latest_commit_sha":null,"homepage":"https://symbolic.software/blog/2026-05-08-hpke-ng/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/symbolicsoft.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-07T21:31:55.000Z","updated_at":"2026-05-27T07:54:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/symbolicsoft/hpke-ng","commit_stats":null,"previous_names":["symbolicsoft/hpke-ng"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/symbolicsoft/hpke-ng","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/symbolicsoft%2Fhpke-ng","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/symbolicsoft%2Fhpke-ng/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/symbolicsoft%2Fhpke-ng/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/symbolicsoft%2Fhpke-ng/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/symbolicsoft","download_url":"https://codeload.github.com/symbolicsoft/hpke-ng/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/symbolicsoft%2Fhpke-ng/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33872298,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crypto","cryptography","hpke","rust"],"created_at":"2026-06-03T16:01:38.391Z","updated_at":"2026-06-03T16:01:41.709Z","avatar_url":"https://github.com/symbolicsoft.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# hpke-ng\n\n[![CI](https://github.com/symbolicsoft/hpke-ng/actions/workflows/ci.yml/badge.svg)](https://github.com/symbolicsoft/hpke-ng/actions)\n[![License](https://img.shields.io/badge/license-Apache--2.0%20OR%20MIT-blue)](#license)\n\nA clean-slate Rust implementation of [HPKE (RFC 9180)](https://www.rfc-editor.org/rfc/rfc9180.html) with type-driven ciphersuite selection.\n\n\u003e Read the announcement: **[hpke-ng: Faster, Smaller, Harder HPKE for Rust](https://symbolic.software/blog/2026-05-08-hpke-ng/)** — for the full design rationale, benchmarks, and migration notes.\n\n```rust\nuse hpke_ng::*;\nuse rand_core::OsRng;\n\ntype Suite = Hpke\u003cDhKemX25519HkdfSha256, HkdfSha256, ChaCha20Poly1305\u003e;\n\nlet mut os = OsRng;\nlet mut rng = os.unwrap_mut();\nlet (sk_r, pk_r) = DhKemX25519HkdfSha256::generate(\u0026mut rng)?;\nlet (enc, ct)  = Suite::seal_base(\u0026mut rng, \u0026pk_r, b\"info\", b\"aad\", b\"hello\")?;\nlet pt         = Suite::open_base(\u0026enc, \u0026sk_r, b\"info\", b\"aad\", \u0026ct)?;\nassert_eq!(pt, b\"hello\");\n# Ok::\u003c_, hpke_ng::HpkeError\u003e(())\n```\n\n## Why a new HPKE crate?\n\n`hpke-ng` exists because three friction points in the existing Rust HPKE story kept producing real bugs and real overhead:\n\n1. **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.\n2. **Struct-owned PRNG hazard.** When the `Hpke` instance owns its RNG, cloning silently aliases randomness state. The fix is structural: don't own it.\n3. **Type-system gaps.** `Option\u003c\u0026[u8]\u003e` for mode-specific parameters turns missing-PSK and wrong-mode into runtime errors that should be compile errors.\n\nThe 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.\n\n## Design highlights\n\n- **Type-parameterized API.** `Hpke\u003cK, F, A\u003e` is zero-sized; the ciphersuite lives in the type system. Mismatched primitives are compile errors.\n- **Four explicit methods per mode.** `seal_base`, `seal_psk`, `seal_auth`, `seal_auth_psk` — no `Option\u003c\u0026[u8]\u003e` parameters for required-by-mode arguments.\n- **Auth restricted to DHKEMs at the type level.** `Hpke::\u003cXWingDraft06, ...\u003e::seal_auth(...)` does not compile.\n- **Export-only restricted at the type level.** `Hpke::\u003c_, _, ExportOnly\u003e::seal_base(...)` does not compile; only `*_export*` methods are available.\n- **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.\n- **Caller-provided RNG.** No PRNG owned by the configuration; cloning cannot alias randomness.\n- **Structural nonce-reuse prevention.** `Context` is non-cloneable and refuses to encrypt at `seq == u64::MAX`.\n- **`no_std` + `alloc`** by default. `std` feature for `std::error::Error` impl on `HpkeError`.\n- **One provider stack.** All primitives from RustCrypto-org crates.\n\n## Compile-time guarantees\n\n| Operation                                | Elsewhere       | hpke-ng                        |\n|------------------------------------------|-----------------|--------------------------------|\n| Calling `seal_auth` on a non-DH KEM      | Runtime error   | Compile error                  |\n| Using a wrong-KEM private key            | Runtime mismatch| Compile error (type-tagged)    |\n| Base-mode call with a PSK supplied       | Runtime error   | Compile error (no PSK param)   |\n| Encrypt with an `ExportOnly` AEAD        | Runtime error   | Compile error                  |\n\n## Supported ciphersuites\n\n| Component | Variants |\n|-----------|----------|\n| KEMs      | `DhKemX25519HkdfSha256`, `DhKemX448HkdfSha512`, `DhKemP256HkdfSha256`, `DhKemP384HkdfSha384`, `DhKemP521HkdfSha512`, `DhKemK256HkdfSha256` |\n| KEMs (post-quantum, `pq` feature) | `XWingDraft06`, `MlKem768`, `MlKem1024` |\n| KDFs      | `HkdfSha256`, `HkdfSha384`, `HkdfSha512` |\n| AEADs     | `Aes128Gcm`, `Aes256Gcm`, `ChaCha20Poly1305`, `ExportOnly` |\n| Modes     | Base, Psk, Auth, AuthPsk |\n\n## Performance\n\n`hpke-ng` is benchmarked head-to-head against the two major Rust HPKE libraries, `hpke-rs` and `rust-hpke`, across **137 benchmark cells** (76 against `hpke-rs`, 61 against `rust-hpke`) spanning every supported ciphersuite. A cell counts as a *tie* when the two medians fall within ±2% of each other.\n\n| Comparison       | Cells  | Wins   | Ties   | Losses |\n|------------------|-------:|-------:|-------:|-------:|\n| vs `hpke-rs`     |     76 |     61 |     13 |      2 |\n| vs `rust-hpke`   |     61 |     38 |      7 |     16 |\n| **Combined**     | **137**| **99** | **20** | **18** |\n\n\u003e `rust-hpke` has no standalone ML-KEM-768 / ML-KEM-1024 and no secp256k1 support, so those ciphersuites are scored only against `hpke-rs`.\n\n### Where the KEM wins come from\n\nMost of the speedup traces to two pieces of caching. On the **decapsulation** path, `hpke-ng` stores the expanded FIPS 203 decapsulation key directly in the `PrivateKey`, whereas `hpke-rs` rebuilds it from the seed on every `setup_receiver`. For **classical KEMs**, caching the recipient's serialized public key alongside the secret removes a redundant base-point scalar multiplication on every decap.\n\n| Operation                 | vs `hpke-rs`        | vs `rust-hpke`     |\n|---------------------------|---------------------|--------------------|\n| ML-KEM-768 / 1024 decap   | **54–56% faster**   | n/a                |\n| X25519 decap              | **44% faster**      | **51% faster**     |\n| X-Wing decap              | **38% faster**      | ≈ parity ¹         |\n| ML-KEM encap              | 33–41% faster       | n/a                |\n| X-Wing encap              | 15% faster          | ≈ parity           |\n\n¹ `rust-hpke` wraps raw decap inside a full HPKE setup, so the closest comparison is `hpke-ng`'s `setup_receiver`, which lands at roughly parity.\n\n### AEAD and single-shot throughput\n\n| Operation                          | vs `hpke-rs`             | vs `rust-hpke`                           |\n|------------------------------------|--------------------------|------------------------------------------|\n| Export (all 5 output lengths)      | **71–76% faster**        | —                                        |\n| Single-shot open (all payloads)    | 13–41% faster            | 8–47% faster                             |\n| AES-128-GCM single-shot seal       | 9–22% faster (≤ 16 KiB)  | 29–51% faster (≤ 4 KiB); slower ≥ 16 KiB |\n| Post-setup `Context::seal` (64 B)  | 13% faster               | 44% faster                               |\n| End-to-end roundtrip (1 KiB)       | **30% faster**           | **49% faster**                           |\n\nExport is the largest sustained advantage over `hpke-rs`. For bulk AEAD the per-byte rates converge as payloads grow: `rust-hpke` pulls ahead on AES-GCM at ≥ 16 KiB, and on post-setup `Context::seal` at ≥ 1 KiB, once framing overhead stops dominating.\n\n*(n/a = unsupported by that library; — = no separate head-to-head figure reported.)*\n\n### Memory and binary footprint\n\n| Quantity                                   | hpke-rs   | hpke-ng                                | rust-hpke     |\n|--------------------------------------------|-----------|----------------------------------------|---------------|\n| `Hpke\u003cK, F, A\u003e` struct                     | 344 bytes | **0 bytes** (`PhantomData`)            | n/a           |\n| `Context\u003c_, _, ChaCha20Poly1305\u003e` struct   | 424 bytes | **88 bytes**                           | 96 bytes      |\n| `Context\u003c_, _, ExportOnly\u003e` struct         | n/a       | **56 bytes**                           | 184 bytes     |\n| `Context\u003c_, _, Aes128Gcm\u003e` struct          | 424 bytes | 792 bytes                              | 912 bytes     |\n| `Context\u003c_, _, Aes256Gcm\u003e` struct          | 424 bytes | 1,048 bytes                            | 1,168 bytes   |\n| Minimal release binary                     | 586 KB    | **370 KB** (~37% smaller than hpke-rs) | 385 KB        |\n\n**Notes on the table above:**\n\n- `rust-hpke` has no typed configuration handle — it uses free `setup_sender` / `setup_receiver` functions rather than a struct like `Hpke\u003cK, F, A\u003e`, so that row is n/a. Its context types are `AeadCtxS\u003cA, Kdf, Kem\u003e` (sender) and `AeadCtxR\u003cA, Kdf, Kem\u003e` (receiver), measured here as `AeadCtxS` with `X25519HkdfSha256` + `HkdfSha256`.\n- Context size grows by `Nh` bytes with a larger KDF — e.g. +32 bytes for `HkdfSha512`.\n- `ExportOnly` maps to `rust-hpke`'s `ExportOnlyAead`. It is larger there (184 B vs 56 B) because `rust-hpke`'s `AeadCtx` always reserves space for a full nonce buffer regardless of the AEAD variant.\n- The AES-GCM `Context` rows are larger in `hpke-ng` than in `hpke-rs` because the expanded round keys + GHash table are cached inline — which is exactly what eliminates the per-call AES key-schedule cost in `Context::seal`. AES-GCM streaming trades memory for throughput; `ChaCha20-Poly1305` is unaffected.\n\n### Reproducing the benchmarks\n\nBuild with `RUSTFLAGS=\"-C target-cpu=native\"` to pick up AES-NI / SHA-NI where available; `[profile.bench]` in `Cargo.toml` sets `lto = \"thin\"` and `codegen-units = 1`. For the head-to-head numbers:\n\n```bash\ncargo bench --features comparative --bench comparative\n```\n\nThis loads both `hpke-rs` (with its `experimental` feature, so the post-quantum KEM stubs are wired up) and `rust-hpke` (pinned to a v0.14 pre-release commit for X-Wing support) as dev-dependencies, and emits side-by-side criterion results for every supported ciphersuite. KEM-op rows for `hpke-rs` and `rust-hpke` carry a `_via_setup_*` suffix: neither library exposes raw `encap` / `decap` separable from setup, so those rows are explicitly *not* apples-to-apples with `hpke-ng`'s bare-operation rows.\n\n## Security posture\n\nThe library responds to two classes of issue observed in prior implementations:\n\n- **Zero shared-secret check (RFC 9180 §7.1.4).** Enforced for X25519 and X448 using `subtle::ConstantTimeEq`.\n- **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.\n\nThe 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.\n\n## Constant-time considerations\n\nThis crate composes RustCrypto primitives. Constant-time properties are inherited from those crates:\n\n| Primitive | CT property |\n|-----------|-------------|\n| X25519, X448 | CT by construction. |\n| P-256, P-384, P-521, secp256k1 | CT in `arithmetic` mode (pinned). |\n| HKDF-SHA-{256,384,512} | CT (deterministic; no secret-dependent branches). |\n| ChaCha20-Poly1305 | CT by construction. |\n| AES-128-GCM, AES-256-GCM | **CT only with hardware AES-NI/PCLMULQDQ.** Prefer `ChaCha20Poly1305` on platforms without these instructions. |\n| ML-KEM, X-Wing | CT per upstream documentation; both crates are pre-1.0. |\n\n## Testing\n\n```bash\ncargo test                                             # library + roundtrip\ncargo test --features pq                               # + post-quantum tests\ncargo test --features pq --test compile_fail           # + compile-time invariant tests\ncargo test --features pq,kat-internals                 # + RFC 9180 KAT\ncargo test --features pq,differential,kat-internals    # + cross-impl differential vs hpke-rs\n```\n\nTo regenerate the compile-fail `.stderr` fixtures after an intentional change (e.g. a toolchain bump), run:\n```bash\nTRYBUILD=overwrite cargo test --features pq --test compile_fail\n```\nThis rewrites the fixtures unconditionally and should not be used as the normal test invocation.\n\nCoverage includes 59 macro-generated roundtrip tests across every ciphersuite × mode combination, four `cargo-fuzz` targets (panics treated as bugs), differential testing against `hpke-rs` for wire-format interop, compile-fail tests that lock in type-system invariants (`Context` is non-cloneable, `ExportOnly` cannot seal, PQ KEMs cannot authenticate), and unit tests that directly verify the RFC 9180 §5.2 nonce derivation formula (`nonce = base_nonce XOR I2OSP(seq, Nn)`) across specific sequence number boundary values. The full suite (without differential) runs in under two seconds.\n\n## Migration from `hpke-rs`\n\nThree mechanical steps, typically under an hour for a real codebase:\n\n1. Define a `type Suite = Hpke\u003cK, F, A\u003e;` alias for the ciphersuite you use.\n2. Replace `hpke.seal(...)` calls with the explicit mode method: `Suite::seal_base`, `seal_psk`, `seal_auth`, or `seal_auth_psk`.\n3. Thread `\u0026mut rng` through call sites — the configuration no longer owns one.\n\nSee the [announcement post](https://symbolic.software/blog/2026-05-08-hpke-ng/) for a worked example.\n\n## Authors\n\nhpke-ng is a joint project between [Nadim Kobeissi](https://nadim.computer) and [Daniel Dia](https://danieldia.me).\n\n## License\n\nLicensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT license](LICENSE-MIT) at your option.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsymbolicsoft%2Fhpke-ng","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsymbolicsoft%2Fhpke-ng","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsymbolicsoft%2Fhpke-ng/lists"}