symbolicsoft/hpke-ng
GitHub: symbolicsoft/hpke-ng
hpke-ng 是一个用 Rust 全新实现的 HPKE(RFC 9180)库,通过类型驱动的密码套件选择将模式不匹配、密钥错配等常见错误从运行时错误提升为编译期错误,同时在性能和内存占用上显著优于现有实现。
Stars: 18 | Forks: 1
# hpke-ng
[](https://github.com/symbolicsoft/hpke-ng/actions)
[](#license)
一个全新编写的 [HPKE (RFC 9180)](https://www.rfc-editor.org/rfc/rfc9180.html) Rust 实现,采用类型驱动的密码套件选择。
```
use hpke_ng::*;
use rand_core::OsRng;
type Suite = Hpke;
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>(())
```
## 为什么需要新的 HPKE crate?
`hpke-ng` 的出现是因为现有 Rust HPKE 生态中的三个摩擦点不断引发真实的错误和开销:
1. **Provider 抽象开销。** 基于 trait 的可插拔后端将分发成本推入热路径,并将 `Hpke` 结构体膨胀到数百字节——而对于类型系统已经知晓的值来说,这并不必要。
2. **结构体拥有的 PRNG 风险。** 当 `Hpke` 实例拥有其 RNG 时,克隆会隐蔽地使随机状态发生别名共享。结构上的修复方法是:不要拥有它。
3. **类型系统缺口。** 针对特定模式参数的 `Option<&[u8]>` 将缺少 PSK 和模式错误变成了本应是编译期错误的运行时错误。
该设计对每个问题采取了一个立场:**没有 Provider 抽象,没有拥有的 RNG,使用类型参数代替模式枚举。** 数学计算是一个已解决的问题;周围的库才是工程上仍有改进空间的地方。
## 设计亮点
- **类型参数化的 API。** `Hpke` 是零大小的;密码套件存在于类型系统中。不匹配的原语会直接触发编译错误。
- **每种模式四个显式方法。** `seal_base`、`seal_psk`、`seal_auth`、`seal_auth_psk`——对于模式必需的参数,没有使用 `Option<&[u8]>`。
- **在类型级别将 Auth 限制为 DHKEM。** `Hpke::::seal_auth(...)` 无法编译。
- **在类型级别将仅导出(Export-only)进行限制。** `Hpke::<_, _, ExportOnly>::seal_base(...)` 无法编译;只有 `*_export*` 方法可用。
- **类型标记的密钥。** 私钥在其类型中携带了其 KEM,因此将 `DhKemP256` 密钥传递给 X25519 套件会被编译器拒绝,而不是在运行时才发现。
- **调用者提供的 RNG。** 配置不拥有任何 PRNG;克隆不会导致随机状态别名。
- **结构性的 Nonce 重用预防。** `Context` 是不可克隆的,并且当 `seq == u64::MAX` 时拒绝加密。
- **默认使用 `no_std` + `alloc`。** 启用 `std` feature 可为 `HpkeError` 提供 `std::error::Error` 实现。
- **单一的 Provider 栈。** 所有原语均来自 RustCrypto-org 的 crate。
## 编译期保证
| 操作 | 其他实现 | hpke-ng |
|------------------------------------------|-----------------|--------------------------------|
| 在非 DH KEM 上调用 `seal_auth` | 运行时错误 | 编译时错误 |
| 使用了 KEM 不匹配的私钥 | 运行时不匹配| 编译时错误 (类型标记) |
| 提供了 PSK 的 Base 模式调用 | 运行时错误 | 编译时错误 (无 PSK 参数) |
| 使用 `ExportOnly` AEAD 进行加密 | 运行时错误 | 编译时错误 |
## 支持的密码套件
| 组件 | 变体 |
|---|---|
| KEMs | `DhKemX25519HkdfSha256`, `DhKemX448HkdfSha512`, `DhKemP256HkdfSha256`, `DhKemP384HkdfSha384`, `DhKemP521HkdfSha512`, `DhKemK256HkdfSha256` |
| KEMs (后量子,`pq` feature) | `XWingDraft06`, `MlKem768`, `MlKem1024` |
| KDFs | `HkdfSha256`, `HkdfSha384`, `HkdfSha512` |
| AEADs | `Aes128Gcm`, `Aes256Gcm`, `ChaCha20Poly1305`, `ExportOnly` |
| 模式 | Base, Psk, Auth, AuthPsk |
## 性能
在与 `hpke-rs` 进行的 62 项正面基准测试中:`hpke-ng` **赢得了 43 项**,在底层原语占主导地位的情况下有 **14 项平局**,在个别密钥生成路径上 **输了 5 项**。最大的差异体现在后量子解封装路径上——ML-KEM-768 和 ML-KEM-1024 快了 53–55%,X-Wing 解封装快了 38%,这是因为 `hpke-ng` 在 `PrivateKey` 中缓存了扩展的 FIPS 203 解封装密钥,而 `hpke-rs` 在每次 `setup_receiver` 时都从种子重新构建。将相同的思路应用于经典 KEM——将接收者的序列化公钥与密钥一起缓存——消除了每次解封装时冗余的基点标量乘法,将 X25519 解封装速度提升了 41%,X-Wing 封装提升了 14%,ML-KEM 封装提升了 30–37%。跨不同 payload 大小的单次打开(single-shot open)快了 23–35%,小于等于 16 KiB payload 的 AES-128-GCM 单次封装(single-shot seal)快了 6–12%,建立上下文后的 `Context::seal`(64 字节)快了 15%,1 KiB 的端到端往返快了 20%。
内存和二进制文件占用:
| 数量 | hpke-rs | hpke-ng |
|------------------------------------------------|-----------|----------------|
| `Hpke` 结构体 | 320 bytes | **0 bytes** (`PhantomData`) |
| `Context<_, _, ChaCha20Poly1305>` 结构体 | 400 bytes | **88 bytes** |
| `Context<_, _, ExportOnly>` 结构体 | n/a | **56 bytes** |
| `Context<_, _, Aes128Gcm>` 结构体 | 400 bytes | 792 bytes |
| `Context<_, _, Aes256Gcm>` 结构体 | 400 bytes | 1,048 bytes |
| 最小 Release 二进制文件 | 561 KB | **392 KB** (缩小约 30%) |
AES-GCM 的 `Context` 行比 `hpke-rs` 大,因为密码的扩展轮密钥 + GHash 表被内联缓存了——这正是消除 `Context::seal` 中每次调用 AES 密钥调度开销的原因。使用 AES-GCM 的流式应用在此处是用内存换取吞吐量;ChaCha20-Poly1305 不受此影响。
在可用的情况下,使用 `RUSTFLAGS="-C target-cpu=native"` 构建,以启用 AES-NI / SHA-NI。`Cargo.toml` 中的 `[profile.bench]` 启用了 `lto = "thin"` 和 `codegen-units = 1`。要查看正面对比的数字,请在本地运行 `cargo bench --features comparative --bench comparative`;对比基准测试启用了 `hpke-rs-rust-crypto` 的 `experimental` feature,以便在 `hpke-rs` 侧连接后量子 KEM 桩代码。
## 安全态势
该库针对先前实现中观察到的两类问题做出了应对:
- **零共享密钥检查 (RFC 9180 §7.1.4)。** 使用 `subtle::ConstantTimeEq` 对 X25519 和 X448 强制执行。
- **Nonce 计数器回绕。** 从结构上预防:`Context` 使用 `u64` 序列号,当达到 `u64::MAX` 时拒绝加密,并且不可克隆,因此计数器无法产生分叉。
DH 后全零检查是常量时间的。`Context` 不能被 `Clone`,因此不能从同一上下文的两个副本在相同的 `(key, nonce)` 下生成两个密文。
## 常量时间考量
此 crate 组合了 RustCrypto 的原语。常量时间特性继承自这些 crate:
| 原语 | CT 特性 |
|---|---|
| X25519, X448 | 构造上即保证 CT。 |
| P-256, P-384, P-521, secp256k1 | 在 `arithmetic` 模式下保证 CT (已固定)。 |
| HKDF-SHA-{256,384,512} | 保证 CT (确定性的;无依赖于秘密数据的分支)。 |
| ChaCha20-Poly1305 | 构造上即保证 CT。 |
| AES-128-GCM, AES-256-GCM | **仅在有硬件 AES-NI/PCLMULQDQ 支持时保证 CT。** 在没有这些指令的平台上请优先使用 `ChaCha20Poly1305`。 |
| ML-KEM, X-Wing | 根据上游文档保证 CT;这两个 crate 均处于 1.0 版本之前。 |
## 测试
```
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
```
测试覆盖范围包括跨越所有密码套件 × 模式组合的 59 个宏生成的往返测试,四个 `cargo-fuzz` 目标(panic 被视为 bug),以及针对 `hpke-rs` 的有线格式互操作差分测试。完整测试套件(不含差分测试)在两秒内即可运行完毕。
## 从 `hpke-rs` 迁移
三个机械性步骤,对于真实的代码库通常不到一小时即可完成:
1. 定义一个 `type Suite = Hpke;` 别名,用于你正在使用的密码套件。
2. 将 `hpke.seal(...)` 调用替换为显式的模式方法:`Suite::seal_base`、`seal_psk`、`seal_auth` 或 `seal_auth_psk`。
3. 在调用处贯穿传递 `&mut rng`——配置不再拥有它。
有关具体的示例,请参阅[公告博文](https://symbolic.software/blog/2026-05-08-hpke-ng/)。
## 许可证
根据您的选择,许可采用 [Apache License, Version 2.0](LICENSE-APACHE) 或 [MIT license](LICENSE-MIT)。
标签:ChaCha20Poly1305, HKDF, HPKE, RFC 9180, Rust, Web3基础设施, X25519, 加密库, 加密算法, 可视化界面, 安全通信, 密码学, 密钥封装, 手动系统调用, 无依赖注入, 混合公钥加密, 类型驱动开发, 网络安全, 网络安全, 网络流量审计, 通知系统, 隐私保护, 隐私保护, 零大小类型, 零开销抽象