hpke/hpke.rs
1//! HPKE: Hybrid Public Key Encryption, as specified in
2//! `draft-ietf-hpke-hpke-05` (which obsoletes RFC 9180).
3//!
4//! HPKE provides public-key encryption of arbitrary-sized plaintexts for a
5//! recipient public key. It also includes an authenticated variant that
6//! authenticates possession of a pre-shared key (PSK). The two asymmetric
7//! authenticated variants of RFC 9180 (`mode_auth` and `mode_auth_psk`) were
8//! removed from the specification and are not implemented.
9//!
10//! A ciphersuite is a triple `(KEM, KDF, AEAD)` selected at compile time via
11//! type parameters. The crate ships the registered algorithms, the
12//! MLKEM768-X25519 hybrid post-quantum KEM (KEM id `0x647a`) and the SHAKE256
13//! single-stage KDF (KDF id `0x0011`) from `draft-ietf-hpke-pq-05`:
14//!
15//! - KEMs: [`kem::MLKEM768X25519`], [`kem::X25519HkdfSha256`],
16//! [`kem::P256HkdfSha256`], [`kem::P521HkdfSha512`]
17//! - KDFs: [`kdf::HkdfSha256`], [`kdf::HkdfSha512`], [`kdf::Shake256`],
18//! [`kdf::Blake3`] (unofficial KDF id `0xFF01`, see the type docs)
19//! - AEADs: [`aead::Aes256Gcm`], [`aead::ChaCha20Poly1305`], [`aead::ExportOnly`]
20//!
21//! Any of the three roles can be replaced by a custom implementation: define a
22//! type and implement [`kem::Kem`], [`kdf::Kdf`] or [`aead::Aead`] for it.
23//!
24//! # Example
25//!
26//! One-shot encryption with the hybrid post-quantum MLKEM768-X25519 +
27//! SHAKE256 + ChaCha20-Poly1305 suite:
28//!
29//! ```
30//! # #[cfg(all(feature = "alloc", feature = "random"))] {
31//! use hpke::{
32//! self, Mode,
33//! aead::ChaCha20Poly1305,
34//! kdf::Shake256,
35//! kem::Kem,
36//! kem::MLKEM768X25519,
37//! };
38//!
39//! let (bob_sk, bob_pk) = MLKEM768X25519::generate_keypair().unwrap();
40//!
41//! let info = b"Alice and Bob's weekly chat";
42//! let (encapped_key, mut sender) = hpke::new_sender::<MLKEM768X25519, Shake256, ChaCha20Poly1305>(
43//! &Mode::Base, &bob_pk, info,
44//! ).unwrap();
45//! let ciphertext = sender.seal(b"fronthand or backhand?", b"a gentleman's game").unwrap();
46//!
47//! let mut recipient = hpke::new_recipient::<MLKEM768X25519, Shake256, ChaCha20Poly1305>(
48//! &Mode::Base, &bob_sk, &encapped_key, info,
49//! ).unwrap();
50//! let plaintext = recipient.open(&ciphertext, b"a gentleman's game").unwrap();
51//! assert_eq!(plaintext, b"fronthand or backhand?");
52//! # }
53//! ```
54//!
55//! # Pre-shared key mode
56//!
57//! In pre-shared key mode both parties are provisioned with a `pre_shared_key`
58//! (at least 32 bytes of entropy) and a `pre_shared_key_id` that identifies it.
59//! The pre-shared key strengthens confidentiality in certain adversary models
60//! and proves possession of the key, but it does not by itself say *which*
61//! sender produced a message. Use the sender-authentication construction below
62//! when the recipient must attribute a message to a specific sender.
63//!
64//! ```
65//! # #[cfg(all(feature = "alloc", feature = "random"))] {
66//! use hpke::{
67//! self, Mode,
68//! aead::Aes256Gcm,
69//! kdf::HkdfSha256,
70//! kem::Kem,
71//! kem::X25519HkdfSha256,
72//! };
73//!
74//! let (bob_sk, bob_pk) = X25519HkdfSha256::generate_keypair().unwrap();
75//! let pre_shared_key = [0x42u8; 32];
76//! let pre_shared_key_id = b"weekly-chat";
77//!
78//! let info = b"Alice and Bob's weekly chat";
79//! let (encapped_key, mut sender) = hpke::new_sender::<X25519HkdfSha256, HkdfSha256, Aes256Gcm>(
80//! &Mode::PreSharedKey { pre_shared_key: &pre_shared_key, pre_shared_key_id },
81//! &bob_pk, info,
82//! ).unwrap();
83//! let ciphertext = sender.seal(b"fronthand or backhand?", b"a gentleman's game").unwrap();
84//!
85//! let mut recipient = hpke::new_recipient::<X25519HkdfSha256, HkdfSha256, Aes256Gcm>(
86//! &Mode::PreSharedKey { pre_shared_key: &pre_shared_key, pre_shared_key_id },
87//! &bob_sk, &encapped_key, info,
88//! ).unwrap();
89//! let plaintext = recipient.open(&ciphertext, b"a gentleman's game").unwrap();
90//! assert_eq!(plaintext, b"fronthand or backhand?");
91//! # }
92//! ```
93//!
94//! # Sender authentication
95//!
96//! `draft-ietf-hpke-hpke-05` removed the asymmetric authenticated modes of
97//! RFC 9180 (`mode_auth` and `mode_auth_psk`), so an HPKE ciphertext does not
98//! carry a sender signature. To let the recipient attribute a message to a
99//! specific sender, combine Base-mode HPKE with a signature scheme over the
100//! transmitted `(encapped_key, ciphertext)` tuple. This example uses ML-DSA-65
101//! (FIPS 204), which is post-quantum; the same construction works with any
102//! signature scheme.
103//!
104//! ```
105//! # #[cfg(all(feature = "alloc", feature = "random"))] {
106//! use crypto::mldsa::MlDsa65SecretKey;
107//! use hpke::{
108//! self, Mode,
109//! aead::Aes256Gcm,
110//! kdf::HkdfSha256,
111//! kem::{Kem, X25519HkdfSha256},
112//! };
113//!
114//! // Alice's long-term ML-DSA-65 signing key pair; Bob holds the HPKE key pair.
115//! let alice_signing = MlDsa65SecretKey::generate().unwrap();
116//! let alice_signing_public = alice_signing.public_key();
117//! let (bob_sk, bob_pk) = X25519HkdfSha256::generate_keypair().unwrap();
118//!
119//! let info = b"Alice and Bob's weekly chat";
120//! let (encapped_key, mut sender) = hpke::new_sender::<X25519HkdfSha256, HkdfSha256, Aes256Gcm>(
121//! &Mode::Base, &bob_pk, info,
122//! ).unwrap();
123//! let ciphertext = sender.seal(b"fronthand or backhand?", b"a gentleman's game").unwrap();
124//!
125//! // Alice signs the message `encapped_key || ciphertext` under a
126//! // domain-separation context.
127//! let mut encapped_key_bytes = [0u8; X25519HkdfSha256::ENCAPPED_KEY_SIZE];
128//! X25519HkdfSha256::encapped_key_to_bytes(&mut encapped_key_bytes, &encapped_key).unwrap();
129//! let mut transcript = encapped_key_bytes.to_vec();
130//! transcript.extend_from_slice(&ciphertext);
131//! let signature = alice_signing.sign(&transcript, b"HPKE sender auth").unwrap();
132//!
133//! // Bob verifies the signature before decrypting and rejects on failure.
134//! assert!(
135//! alice_signing_public
136//! .verify(&transcript, &signature, b"HPKE sender auth")
137//! .is_ok()
138//! );
139//!
140//! let mut recipient = hpke::new_recipient::<X25519HkdfSha256, HkdfSha256, Aes256Gcm>(
141//! &Mode::Base, &bob_sk, &encapped_key, info,
142//! ).unwrap();
143//! let plaintext = recipient.open(&ciphertext, b"a gentleman's game").unwrap();
144//! assert_eq!(plaintext, b"fronthand or backhand?");
145//! # }
146//! ```
147//!
148//! The signature covers the encapsulated key as well as the ciphertext, so a
149//! replayed or re-encapsulated message does not verify against the same
150//! signature. The recipient must verify (and reject on failure) *before*
151//! treating the plaintext as authenticated.
152//!
153//! # Security
154//!
155//! - Messages **must** be sealed and opened in the same order. There is no
156//! tolerance for reordering or loss; per-message context (e.g. a counter)
157//! can be authenticated by passing it as `associated_data`.
158//! - Replaying an `encapped_key` reproduces the same context, so exported
159//! secrets are not replay protected. Do not derive an AEAD `(key, nonce)`
160//! pair from an exported secret; mix in fresh recipient-provided randomness
161//! (`draft-ietf-hpke-hpke-05` Section 9.8).
162//! - Plaintexts larger than the AEAD's `MAX_PLAINTEXT_SIZE` (or ciphertexts larger than
163//! `MAX_CIPHERTEXT_SIZE`) are rejected rather than processed, as `draft-ietf-hpke-hpke-05`
164//! Section 5.2 requires. Sealing and opening also take `&mut self`, so nonce
165//! derivation, encryption, and sequence-number advancement cannot be
166//! interleaved on a shared context.
167//! - Pre-shared keys must contain at least 32 bytes of entropy. Low-entropy
168//! pre-shared keys are vulnerable to dictionary attacks.
169//! - There is no forward secrecy against recipient static key compromise:
170//! anyone with the recipient's secret key can decrypt all past captures.
171
172#![cfg_attr(not(test), no_std)]
173
174#[cfg(feature = "alloc")]
175extern crate alloc;
176
177pub mod aead;
178mod context;
179pub mod kdf;
180pub mod kem;
181
182#[cfg(all(test, feature = "alloc", feature = "random"))]
183mod tests;
184
185pub use aead::Aead;
186#[cfg(feature = "random")]
187pub use context::new_sender;
188pub use context::{Mode, RecipientContext, SenderContext, new_recipient};
189pub use kdf::Kdf;
190pub use kem::Kem;
191
192/// Best-effort wipe of transient secret buffers.
193///
194/// This is a no-op unless the `zeroize` feature is enabled.
195#[inline(always)]
196pub(crate) fn wipe(_bytes: &mut [u8]) {
197 #[cfg(feature = "zeroize")]
198 {
199 use zeroize::Zeroize;
200 _bytes.zeroize();
201 }
202}
203
204////////////////////////////////////////////////////////////////////////////////////////////////////
205// Errors
206
207/// Errors that can occur during HPKE setup, key encapsulation, key derivation
208/// or message encryption/decryption.
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub enum HpkeError {
211 /// A KEM input or output failed validation (e.g. an invalid or low-order
212 /// public key was provided), or a pre-shared key mode input is missing (an
213 /// empty pre-shared key or pre-shared key identifier).
214 ValidationError,
215 /// A key, encapsulated key or ciphertext could not be deserialized
216 /// (e.g. wrong length or malformed encoding).
217 DeserializeError,
218 /// The encapsulation step failed (e.g. the recipient's public key is
219 /// invalid).
220 EncapError,
221 /// The decapsulation step failed (e.g. the encapsulated key is invalid,
222 /// or the Diffie-Hellman output was rejected).
223 DecapError,
224 /// The AEAD failed to decrypt the ciphertext (wrong key, tampered
225 /// ciphertext or wrong associated data).
226 OpenError,
227 /// Deriving a key pair from input keying material failed after
228 /// exhausting all rejection-sampling candidates.
229 DeriveKeyPairError,
230 /// A message length or sequence-number limit was reached: the plaintext
231 /// exceeds the AEAD's `P_MAX`, the ciphertext exceeds `C_MAX`, or the
232 /// context has exhausted its sequence numbers, so no further messages can
233 /// be sealed or opened with this context (`draft-ietf-hpke-hpke-05`
234 /// Sections 4.3 and 5.2).
235 MessageLimitReached,
236 /// The operation is not supported by this KEM, KDF or AEAD (e.g. an
237 /// authenticated mode with a KEM that does not support it, or sealing a
238 /// message with the Export-Only pseudo-AEAD).
239 NotSupported,
240 /// The pseudorandom key given to the KDF expand step does not have the
241 /// expected length.
242 InvalidPseudorandomKey,
243 /// The key given to the AEAD does not have the expected length.
244 InvalidKey,
245 /// The KDF was asked for more output than it can produce
246 /// (`255 * OUTPUT_SIZE` for two-stage KDFs, `2^16 - 1` bytes for
247 /// single-stage KDFs).
248 KdfOutputTooLong,
249 /// An underlying AEAD error.
250 Aead(crypto::AeadError),
251 /// An underlying elliptic-curve error.
252 EllipticCurve(crypto::EllipticCurveError),
253 /// An underlying ML-KEM error.
254 MlKem(crypto::mlkem::MlKemError),
255 /// The operating system's random number generator failed.
256 Random(crypto::RandomError),
257 /// Something went wrong, without further details.
258 Unspecified,
259}
260
261impl core::fmt::Display for HpkeError {
262 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
263 match self {
264 HpkeError::ValidationError => write!(f, "key validation failed"),
265 HpkeError::DeserializeError => write!(f, "key or ciphertext deserialization failed"),
266 HpkeError::EncapError => write!(f, "encapsulation failed"),
267 HpkeError::DecapError => write!(f, "decapsulation failed"),
268 HpkeError::OpenError => write!(f, "decryption failed"),
269 HpkeError::DeriveKeyPairError => write!(f, "key pair derivation failed"),
270 HpkeError::MessageLimitReached => write!(f, "message limit reached"),
271 HpkeError::NotSupported => write!(f, "operation is not supported"),
272 HpkeError::InvalidPseudorandomKey => write!(f, "pseudorandom key has an invalid length"),
273 HpkeError::InvalidKey => write!(f, "key has an invalid length"),
274 HpkeError::KdfOutputTooLong => write!(f, "KDF output length exceeds the limit"),
275 HpkeError::Aead(err) => write!(f, "{err}"),
276 HpkeError::EllipticCurve(err) => write!(f, "{err}"),
277 HpkeError::MlKem(err) => write!(f, "{err}"),
278 HpkeError::Random(err) => write!(f, "{err}"),
279 HpkeError::Unspecified => write!(f, "unknown error"),
280 }
281 }
282}
283
284impl core::error::Error for HpkeError {}
285
286impl From<crypto::AeadError> for HpkeError {
287 fn from(err: crypto::AeadError) -> Self {
288 HpkeError::Aead(err)
289 }
290}
291
292impl From<crypto::HkdfError> for HpkeError {
293 fn from(err: crypto::HkdfError) -> Self {
294 match err {
295 crypto::HkdfError::PrkIsTooShort(_) => HpkeError::InvalidPseudorandomKey,
296 crypto::HkdfError::OutputIsTooLong => HpkeError::KdfOutputTooLong,
297 }
298 }
299}
300
301impl From<crypto::EllipticCurveError> for HpkeError {
302 fn from(err: crypto::EllipticCurveError) -> Self {
303 HpkeError::EllipticCurve(err)
304 }
305}
306
307impl From<crypto::mlkem::MlKemError> for HpkeError {
308 fn from(err: crypto::mlkem::MlKemError) -> Self {
309 HpkeError::MlKem(err)
310 }
311}
312
313impl From<crypto::RandomError> for HpkeError {
314 fn from(err: crypto::RandomError) -> Self {
315 HpkeError::Random(err)
316 }
317}
318
319impl From<crypto::xwing::XWingError> for HpkeError {
320 fn from(err: crypto::xwing::XWingError) -> Self {
321 match err {
322 crypto::xwing::XWingError::MlKem(err) => HpkeError::MlKem(err),
323 crypto::xwing::XWingError::Random(err) => HpkeError::Random(err),
324 }
325 }
326}