Skip to main content

dryoc/
dryocsealedbox.rs

1//! # Post-quantum sealed boxes
2//!
3//! [`DryocSealedBox`] encrypts a message to a recipient's [`kem`](crate::kem)
4//! public key so that only the holder of the matching secret key can read
5//! it. Like [`DryocBox::seal`](crate::dryocbox::DryocBox::seal), it is
6//! anonymous: the sender needs no key pair, and the box does not identify the
7//! sender. Unlike it, the key agreement uses X-Wing, the hybrid of ML-KEM-768
8//! and X25519, so recorded boxes stay confidential even if a quantum computer
9//! later breaks X25519.
10//!
11//! Moving from [`DryocBox::seal`](crate::dryocbox::DryocBox::seal) takes two
12//! changes: generate the recipient's key pair with [`KeyPair`] from this
13//! module (a [`kem`](crate::kem) key pair), and use [`DryocSealedBox`] in
14//! place of [`DryocBox`](crate::dryocbox::DryocBox). The method names are the
15//! same. Boxes are larger: 1136 bytes of overhead instead of 48.
16//!
17//! ## Format
18//!
19//! The format is dryoc's application profile of [HPKE (RFC 9180)][rfc9180],
20//! which leaves the wire encoding to applications (section 10). The profile
21//! is base mode, single-shot, with an empty `info`, empty associated data and
22//! one fixed ciphersuite:
23//!
24//! * KEM `0x647A`, X-Wing, as defined by
25//!   [draft-connolly-cfrg-xwing-kem-11][xwing]. The IANA registration cites
26//!   -06; the draft's test vectors are identical from -05 through -11.
27//! * KDF `0x0001`, HKDF-SHA256.
28//! * AEAD `0x0003`, ChaCha20-Poly1305.
29//!
30//! A box is HPKE's `(enc, ct)` output concatenated: `enc` (the 1120-byte
31//! X-Wing ciphertext), then the AEAD ciphertext, then its 16-byte tag. The box
32//! carries no suite identifier. Any HPKE implementation that supports this
33//! ciphersuite can open it. The tests check the implementation against the
34//! known-answer vector in [draft-ietf-hpke-pq-05][hpke-pq] Appendix A.5.
35//! [draft-ietf-hpke-hpke-04][hpke-hpke], the RFC 9180 revision in IESG review,
36//! is backwards-compatible with RFC 9180 for this ciphersuite.
37//!
38//! This byte format, written by [`DryocSealedBox::to_bytes`] (and `to_vec`)
39//! and read by [`DryocSealedBox::from_bytes`], is stable for the 2.x series. A
40//! different ciphersuite or profile would be a new type, so existing boxes stay
41//! readable.
42//!
43//! With the `serde` feature,
44//! [`serde::Deserialize`](https://docs.rs/serde/latest/serde/trait.Deserialize.html) and
45//! [`serde::Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) are implemented
46//! for [`DryocSealedBox`] as a struct with the fields `enc`, `tag` and `data`,
47//! in that order. That representation is separate from the byte format above;
48//! use the byte format to exchange boxes with other HPKE implementations.
49//!
50//! [rfc9180]: https://www.rfc-editor.org/rfc/rfc9180.html
51//! [xwing]: https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/11/
52//! [hpke-pq]: https://datatracker.ietf.org/doc/draft-ietf-hpke-pq/05/
53//! [hpke-hpke]: https://datatracker.ietf.org/doc/draft-ietf-hpke-hpke/04/
54//!
55//! ## Example
56//!
57//! ```
58//! # #[cfg(feature = "alloc")]
59//! # {
60//! use dryoc::dryocsealedbox::*;
61//!
62//! let recipient_keypair = StackKeyPair::generate();
63//! let message = b"Now is the winter of our discontent.";
64//!
65//! let sealed = DryocSealedBox::seal_to_vecbox(message, &recipient_keypair.public_key)
66//!     .expect("unable to seal");
67//!
68//! // Serialize, send, and read the box back.
69//! let bytes = sealed.to_vec();
70//! let sealed = VecBox::from_bytes(&bytes).expect("unable to read box");
71//!
72//! let decrypted = sealed
73//!     .open_to_vec(&recipient_keypair)
74//!     .expect("unable to open");
75//! assert_eq!(message, decrypted.as_slice());
76//! # }
77//! ```
78
79#[cfg(feature = "alloc")]
80use alloc::vec::Vec;
81
82#[cfg(feature = "serde")]
83use serde::{Deserialize, Serialize};
84use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
85
86use crate::classic::crypto_aead_chacha20poly1305_ietf::{
87    crypto_aead_chacha20poly1305_ietf_decrypt_detached,
88    crypto_aead_chacha20poly1305_ietf_encrypt_detached,
89};
90use crate::classic::crypto_kdf::{
91    crypto_kdf_hkdf_sha256_expand, crypto_kdf_hkdf_sha256_extract_final,
92    crypto_kdf_hkdf_sha256_extract_init, crypto_kdf_hkdf_sha256_extract_update,
93};
94use crate::classic::crypto_kem_xwing;
95use crate::constants::{
96    CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES, CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES,
97    CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES, CRYPTO_KDF_HKDF_SHA256_KEYBYTES,
98    CRYPTO_KEM_XWING_CIPHERTEXTBYTES, CRYPTO_KEM_XWING_ENCSEEDBYTES,
99    CRYPTO_KEM_XWING_PUBLICKEYBYTES, CRYPTO_KEM_XWING_SECRETKEYBYTES,
100    CRYPTO_KEM_XWING_SHAREDSECRETBYTES,
101};
102use crate::error::{Error, ErrorContext};
103pub use crate::kem::xwing::{KeyPair, PublicKey, SecretKey, StackKeyPair};
104use crate::mlkem::Arith;
105use crate::rng::copy_randombytes;
106use crate::types::*;
107
108/// Stack-allocated X-Wing ciphertext that carries the box's key (HPKE's
109/// `enc`).
110pub type EncapsulatedKey = StackByteArray<CRYPTO_KEM_XWING_CIPHERTEXTBYTES>;
111/// Stack-allocated authentication tag.
112pub type Mac = StackByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES>;
113
114/// Bytes a sealed box adds to its message.
115pub const SEALBYTES: usize =
116    CRYPTO_KEM_XWING_CIPHERTEXTBYTES + CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES;
117
118#[cfg(any(
119    all(feature = "protected", any(unix, windows)),
120    all(doc, not(doctest), feature = "std")
121))]
122#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "protected")))]
123pub mod protected {
124    //! # Protected memory type aliases for [`DryocSealedBox`]
125    //!
126    //! ```
127    //! use dryoc::dryocsealedbox::DryocSealedBox;
128    //! use dryoc::dryocsealedbox::protected::*;
129    //!
130    //! let recipient_keypair = LockedROKeyPair::generate_readonly_locked_keypair().expect("keypair");
131    //! let message = HeapBytes::from_slice_into_readonly_locked(b"Secret message").expect("message");
132    //!
133    //! let sealed: LockedBox =
134    //!     DryocSealedBox::seal(&message, &recipient_keypair.public_key).expect("seal");
135    //! let decrypted: LockedBytes = sealed.open(&recipient_keypair).expect("open");
136    //! assert_eq!(message.as_slice(), decrypted.as_slice());
137    //! ```
138    use super::DryocSealedBox;
139    use crate::constants::{
140        CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES, CRYPTO_KEM_XWING_CIPHERTEXTBYTES,
141    };
142    pub use crate::kem::xwing::protected::*;
143
144    /// Heap-allocated X-Wing ciphertext, for use with protected memory.
145    pub type EncapsulatedKey = HeapByteArray<CRYPTO_KEM_XWING_CIPHERTEXTBYTES>;
146    /// Heap-allocated authentication tag, for use with protected memory.
147    pub type Mac = HeapByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES>;
148    /// Locked [`DryocSealedBox`].
149    pub type LockedBox = DryocSealedBox<Locked<EncapsulatedKey>, Locked<Mac>, LockedBytes>;
150}
151
152#[derive(Zeroize, Clone, Debug)]
153#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
154/// A post-quantum sealed box: an HPKE-encrypted message for one recipient.
155///
156/// Refer to [crate::dryocsealedbox] for the byte format and sample usage.
157pub struct DryocSealedBox<
158    EncapsulatedKey: ByteArray<CRYPTO_KEM_XWING_CIPHERTEXTBYTES> + Zeroize,
159    Mac: ByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES> + Zeroize,
160    Data: Bytes + Zeroize,
161> {
162    enc: EncapsulatedKey,
163    tag: Mac,
164    data: Data,
165}
166
167/// [Vec]-based sealed box.
168#[cfg(feature = "alloc")]
169pub type VecBox = DryocSealedBox<EncapsulatedKey, Mac, Vec<u8>>;
170
171/// HPKE's `suite_id` for X-Wing, HKDF-SHA256 and ChaCha20-Poly1305.
172const SUITE_ID: &[u8; 10] = b"HPKE\x64\x7a\x00\x01\x00\x03";
173
174type AeadKey = [u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES];
175type AeadNonce = [u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES];
176
177/// HPKE `LabeledExtract(salt, label, ikm)`, written to `prk`.
178fn labeled_extract(prk: &mut [u8; 32], salt: &[u8], label: &[u8], ikm: &[u8]) {
179    let mut state = crypto_kdf_hkdf_sha256_extract_init(Some(salt));
180    for part in [b"HPKE-v1".as_slice(), SUITE_ID, label, ikm] {
181        crypto_kdf_hkdf_sha256_extract_update(&mut state, part);
182    }
183    crypto_kdf_hkdf_sha256_extract_final(state, prk);
184}
185
186/// HPKE's key-schedule context: the mode byte, `psk_id_hash` and `info_hash`.
187const KEY_SCHEDULE_CONTEXT_BYTES: usize = 1 + 2 * CRYPTO_KDF_HKDF_SHA256_KEYBYTES;
188
189/// Longest `LabeledExpand` info built here: the two-byte length, `HPKE-v1`,
190/// the suite id, the longest label (`base_nonce`) and the key-schedule
191/// context.
192const LABELED_INFO_MAX_BYTES: usize = 2
193    + <[u8]>::len(b"HPKE-v1")
194    + <[u8]>::len(SUITE_ID)
195    + <[u8]>::len(b"base_nonce")
196    + KEY_SCHEDULE_CONTEXT_BYTES;
197
198/// HPKE `LabeledExpand(prk, label, info, output.len())`.
199fn labeled_expand(output: &mut [u8], prk: &[u8; 32], label: &[u8], info: &[u8]) {
200    let length = u16::try_from(output.len()).expect("short HPKE output");
201    let mut labeled_info = [0u8; LABELED_INFO_MAX_BYTES];
202    let mut labeled_info_len = 0;
203    for part in [&length.to_be_bytes()[..], b"HPKE-v1", SUITE_ID, label, info] {
204        labeled_info[labeled_info_len..labeled_info_len + part.len()].copy_from_slice(part);
205        labeled_info_len += part.len();
206    }
207    crypto_kdf_hkdf_sha256_expand(output, &labeled_info[..labeled_info_len], prk)
208        .expect("HPKE output lengths are within HKDF's limit");
209}
210
211/// An HPKE encryption context at sequence number 0: the AEAD key and nonce.
212/// Filled in place and wiped on drop.
213#[derive(Zeroize, ZeroizeOnDrop)]
214struct Context {
215    key: AeadKey,
216    nonce: AeadNonce,
217}
218
219impl Context {
220    fn new() -> Self {
221        Self {
222            key: [0u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES],
223            nonce: [0u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES],
224        }
225    }
226
227    /// HPKE's base-mode key schedule for `shared_secret` and `info`.
228    fn schedule(&mut self, shared_secret: &[u8], info: &[u8]) {
229        let mut psk_id_hash = [0u8; CRYPTO_KDF_HKDF_SHA256_KEYBYTES];
230        labeled_extract(&mut psk_id_hash, b"", b"psk_id_hash", b"");
231        let mut info_hash = [0u8; CRYPTO_KDF_HKDF_SHA256_KEYBYTES];
232        labeled_extract(&mut info_hash, b"", b"info_hash", info);
233        // Mode byte 0 (base mode), then the two hashes.
234        let mut context = [0u8; KEY_SCHEDULE_CONTEXT_BYTES];
235        let (psk_id_part, info_part) = context[1..].split_at_mut(CRYPTO_KDF_HKDF_SHA256_KEYBYTES);
236        psk_id_part.copy_from_slice(&psk_id_hash);
237        info_part.copy_from_slice(&info_hash);
238        let mut secret = Zeroizing::new([0u8; CRYPTO_KDF_HKDF_SHA256_KEYBYTES]);
239        labeled_extract(&mut secret, shared_secret, b"secret", b"");
240        labeled_expand(&mut self.key, &secret, b"key", &context);
241        labeled_expand(&mut self.nonce, &secret, b"base_nonce", &context);
242    }
243
244    /// HPKE `SetupBaseS`: encapsulates to `public_key` with the randomness
245    /// `seed`, writing `enc`. ML-KEM arithmetic comes from `arith`.
246    fn setup_sender(
247        &mut self,
248        arith: Arith,
249        enc: &mut [u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES],
250        public_key: &[u8; CRYPTO_KEM_XWING_PUBLICKEYBYTES],
251        info: &[u8],
252        seed: &[u8; CRYPTO_KEM_XWING_ENCSEEDBYTES],
253    ) -> Result<(), Error> {
254        let mut shared_secret = Zeroizing::new([0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES]);
255        crypto_kem_xwing::enc_deterministic(arith, enc, &mut shared_secret, public_key, seed)?;
256        self.schedule(&*shared_secret, info);
257        Ok(())
258    }
259
260    /// HPKE `SetupBaseR`: decapsulates `enc`. ML-KEM arithmetic comes from
261    /// `arith`.
262    fn setup_receiver(
263        &mut self,
264        arith: Arith,
265        enc: &[u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES],
266        secret_key: &[u8; CRYPTO_KEM_XWING_SECRETKEYBYTES],
267        info: &[u8],
268    ) -> Result<(), Error> {
269        let mut shared_secret = Zeroizing::new([0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES]);
270        crypto_kem_xwing::dec(arith, &mut shared_secret, enc, secret_key)?;
271        self.schedule(&*shared_secret, info);
272        Ok(())
273    }
274}
275
276impl<
277    EncapsulatedKey: NewByteArray<CRYPTO_KEM_XWING_CIPHERTEXTBYTES> + Zeroize,
278    Mac: NewByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES> + Zeroize,
279    Data: NewBytes + ResizableBytes + Zeroize,
280> DryocSealedBox<EncapsulatedKey, Mac, Data>
281{
282    /// Encrypts `message` for `recipient_public_key`, returning a new
283    /// [`DryocSealedBox`].
284    ///
285    /// # Errors
286    ///
287    /// Returns an error if the message is too long or `recipient_public_key`
288    /// is not a valid X-Wing public key.
289    ///
290    /// # Panics
291    ///
292    /// Panics if the operating system's random number generator fails.
293    pub fn seal<
294        Message: Bytes + ?Sized,
295        RecipientPublicKey: ByteArray<CRYPTO_KEM_XWING_PUBLICKEYBYTES>,
296    >(
297        message: &Message,
298        recipient_public_key: &RecipientPublicKey,
299    ) -> Result<Self, Error> {
300        let mut seed = Zeroizing::new([0u8; CRYPTO_KEM_XWING_ENCSEEDBYTES]);
301        copy_randombytes(seed.as_mut_slice());
302        let mut sealed = Self {
303            enc: EncapsulatedKey::new_byte_array(),
304            tag: Mac::new_byte_array(),
305            data: Data::new_bytes(),
306        };
307        sealed.data.resize(message.as_slice().len(), 0);
308        let mut context = Context::new();
309        context.setup_sender(
310            Arith::detect(),
311            sealed.enc.as_mut_array(),
312            recipient_public_key.as_array(),
313            b"",
314            &seed,
315        )?;
316        crypto_aead_chacha20poly1305_ietf_encrypt_detached(
317            sealed.data.as_mut_slice(),
318            sealed.tag.as_mut_array(),
319            message.as_slice(),
320            None,
321            &context.nonce,
322            &context.key,
323        )?;
324        Ok(sealed)
325    }
326}
327
328impl<
329    'a,
330    EncapsulatedKey: ByteArray<CRYPTO_KEM_XWING_CIPHERTEXTBYTES> + TryFrom<&'a [u8]> + Zeroize,
331    Mac: ByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES> + TryFrom<&'a [u8]> + Zeroize,
332    Data: Bytes + From<&'a [u8]> + Zeroize,
333> DryocSealedBox<EncapsulatedKey, Mac, Data>
334{
335    /// Reads a sealed box from its wire format: the X-Wing ciphertext, the
336    /// encrypted message, then the tag.
337    ///
338    /// # Errors
339    ///
340    /// Returns an error if `bytes` is shorter than [`SEALBYTES`] or a field
341    /// cannot be converted to its target type.
342    pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, Error> {
343        validate_length!(min SEALBYTES, bytes.len(), ErrorContext::SealedBox);
344        let (enc, rest) = bytes.split_at(CRYPTO_KEM_XWING_CIPHERTEXTBYTES);
345        let (data, tag) = rest.split_at(rest.len() - CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES);
346        Ok(Self {
347            enc: EncapsulatedKey::try_from(enc)
348                .map_err(|_| Error::invalid_encoding(ErrorContext::Ciphertext))?,
349            tag: Mac::try_from(tag)
350                .map_err(|_| Error::invalid_encoding(ErrorContext::AuthenticationTag))?,
351            data: Data::from(data),
352        })
353    }
354}
355
356impl<
357    EncapsulatedKey: ByteArray<CRYPTO_KEM_XWING_CIPHERTEXTBYTES> + Zeroize,
358    Mac: ByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES> + Zeroize,
359    Data: Bytes + Zeroize,
360> DryocSealedBox<EncapsulatedKey, Mac, Data>
361{
362    /// Returns a new box from its parts, consuming each.
363    #[must_use]
364    pub fn from_parts(enc: EncapsulatedKey, tag: Mac, data: Data) -> Self {
365        Self { enc, tag, data }
366    }
367
368    /// Returns the X-Wing ciphertext (HPKE `enc`).
369    pub fn enc(&self) -> &EncapsulatedKey {
370        &self.enc
371    }
372
373    /// Returns the authentication tag.
374    pub fn tag(&self) -> &Mac {
375        &self.tag
376    }
377
378    /// Returns the encrypted message.
379    pub fn data(&self) -> &Data {
380        &self.data
381    }
382
383    /// Moves the X-Wing ciphertext, tag and encrypted message out of this
384    /// box.
385    #[must_use]
386    pub fn into_parts(self) -> (EncapsulatedKey, Mac, Data) {
387        (self.enc, self.tag, self.data)
388    }
389
390    /// Copies the box's wire format into a new [`Vec`].
391    #[cfg(feature = "alloc")]
392    #[must_use]
393    pub fn to_vec(&self) -> Vec<u8> {
394        self.to_bytes()
395    }
396
397    /// Copies the box's wire format into new `Bytes`.
398    #[must_use]
399    pub fn to_bytes<Bytes: NewBytes + ResizableBytes>(&self) -> Bytes {
400        let mut bytes = Bytes::new_bytes();
401        bytes.resize(SEALBYTES + self.data.len(), 0);
402        let (enc, rest) = bytes
403            .as_mut_slice()
404            .split_at_mut(CRYPTO_KEM_XWING_CIPHERTEXTBYTES);
405        let (data, tag) = rest.split_at_mut(self.data.len());
406        enc.copy_from_slice(self.enc.as_array());
407        data.copy_from_slice(self.data.as_slice());
408        tag.copy_from_slice(self.tag.as_array());
409        bytes
410    }
411
412    /// Decrypts this box with `recipient_keypair`, returning the message.
413    ///
414    /// # Errors
415    ///
416    /// Returns [`Error::AuthenticationFailed`] if the box was not sealed for
417    /// this key pair or was modified, or an error if the X-Wing ciphertext
418    /// carries a low-order X25519 point.
419    pub fn open<
420        Output: ResizableBytes + NewBytes + Zeroize,
421        RecipientPublicKey: ByteArray<CRYPTO_KEM_XWING_PUBLICKEYBYTES> + Zeroize,
422        RecipientSecretKey: ByteArray<CRYPTO_KEM_XWING_SECRETKEYBYTES> + Zeroize,
423    >(
424        &self,
425        recipient_keypair: &KeyPair<RecipientPublicKey, RecipientSecretKey>,
426    ) -> Result<Output, Error> {
427        let mut message = Output::new_bytes();
428        message.resize(self.data.len(), 0);
429        let mut context = Context::new();
430        context.setup_receiver(
431            Arith::detect(),
432            self.enc.as_array(),
433            recipient_keypair.secret_key.as_array(),
434            b"",
435        )?;
436        crypto_aead_chacha20poly1305_ietf_decrypt_detached(
437            message.as_mut_slice(),
438            self.data.as_slice(),
439            self.tag.as_array(),
440            None,
441            &context.nonce,
442            &context.key,
443        )?;
444        Ok(message)
445    }
446}
447
448#[cfg(feature = "alloc")]
449impl DryocSealedBox<EncapsulatedKey, Mac, Vec<u8>> {
450    /// Encrypts `message` for `recipient_public_key` into a [`VecBox`].
451    /// Provided for convenience.
452    ///
453    /// # Errors
454    ///
455    /// Returns the same errors as [`DryocSealedBox::seal`].
456    pub fn seal_to_vecbox<
457        Message: Bytes + ?Sized,
458        RecipientPublicKey: ByteArray<CRYPTO_KEM_XWING_PUBLICKEYBYTES>,
459    >(
460        message: &Message,
461        recipient_public_key: &RecipientPublicKey,
462    ) -> Result<Self, Error> {
463        Self::seal(message, recipient_public_key)
464    }
465
466    /// Decrypts this box with `recipient_keypair` into a [`Vec`]. Provided
467    /// for convenience.
468    ///
469    /// # Errors
470    ///
471    /// Returns the same errors as [`DryocSealedBox::open`].
472    pub fn open_to_vec<
473        RecipientPublicKey: ByteArray<CRYPTO_KEM_XWING_PUBLICKEYBYTES> + Zeroize,
474        RecipientSecretKey: ByteArray<CRYPTO_KEM_XWING_SECRETKEYBYTES> + Zeroize,
475    >(
476        &self,
477        recipient_keypair: &KeyPair<RecipientPublicKey, RecipientSecretKey>,
478    ) -> Result<Vec<u8>, Error> {
479        self.open(recipient_keypair)
480    }
481}
482
483#[cfg(all(test, feature = "alloc"))]
484mod tests {
485    use super::*;
486    use crate::mlkem::tests::{field, records};
487
488    /// draft-ietf-hpke-pq Appendix A.5: the X-Wing encapsulation and
489    /// decapsulation, the key schedule and the first encryption, through the
490    /// sender and receiver setups, on every ML-KEM backend the CPU supports.
491    #[test]
492    fn test_hpke_known_answer() {
493        use crate::classic::crypto_aead_chacha20poly1305_ietf::{
494            crypto_aead_chacha20poly1305_ietf_decrypt, crypto_aead_chacha20poly1305_ietf_encrypt,
495        };
496
497        let record = &records(include_str!(
498            "mlkem/test-vectors/hpke_xwing_hkdfsha256_chacha20poly1305.txt"
499        ))[0];
500        let bytes = |key| hex::decode(record[key]).expect("hex");
501        let (info, aad, message) = (bytes("info"), bytes("aad"), bytes("pt"));
502        let (public_key, secret_key, seed) = (
503            field(record, "pkRm"),
504            field(record, "skRm"),
505            field(record, "ikmE"),
506        );
507        let expected_enc: [u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES] = field(record, "enc");
508        let expected_secret: [u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES] =
509            field(record, "shared_secret");
510
511        for arith in Arith::all() {
512            let mut enc = [0u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
513            let mut shared_secret = [0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES];
514            crypto_kem_xwing::enc_deterministic(
515                arith,
516                &mut enc,
517                &mut shared_secret,
518                &public_key,
519                &seed,
520            )
521            .expect("enc");
522            assert_eq!(enc, expected_enc, "{arith:?}");
523            assert_eq!(shared_secret, expected_secret, "{arith:?}");
524            let mut shared_secret = [0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES];
525            crypto_kem_xwing::dec(arith, &mut shared_secret, &expected_enc, &secret_key)
526                .expect("dec");
527            assert_eq!(shared_secret, expected_secret, "{arith:?}");
528
529            let mut enc = [0u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
530            let mut sender = Context::new();
531            sender
532                .setup_sender(arith, &mut enc, &public_key, &info, &seed)
533                .expect("sender");
534            assert_eq!(enc, expected_enc, "{arith:?}");
535            assert_eq!(sender.key, field::<32>(record, "key"), "{arith:?}");
536            assert_eq!(sender.nonce, field::<12>(record, "base_nonce"), "{arith:?}");
537            let mut ciphertext =
538                vec![0u8; message.len() + CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES];
539            crypto_aead_chacha20poly1305_ietf_encrypt(
540                &mut ciphertext,
541                &message,
542                Some(&aad),
543                &sender.nonce,
544                &sender.key,
545            )
546            .expect("encrypt");
547            assert_eq!(ciphertext, bytes("ct"), "{arith:?}");
548
549            let mut receiver = Context::new();
550            receiver
551                .setup_receiver(arith, &expected_enc, &secret_key, &info)
552                .expect("receiver");
553            assert_eq!(receiver.key, sender.key, "{arith:?}");
554            assert_eq!(receiver.nonce, sender.nonce, "{arith:?}");
555            let mut opened = vec![0u8; message.len()];
556            crypto_aead_chacha20poly1305_ietf_decrypt(
557                &mut opened,
558                &ciphertext,
559                Some(&aad),
560                &receiver.nonce,
561                &receiver.key,
562            )
563            .expect("decrypt");
564            assert_eq!(opened, message, "{arith:?}");
565        }
566    }
567
568    /// The wire format is `enc || ciphertext || tag`, a box for another key
569    /// or with any byte changed fails to open, and short inputs are rejected.
570    #[test]
571    fn test_wire_format_and_tampering() {
572        let keypair = StackKeyPair::generate();
573        let message = b"All the world's a stage";
574        let sealed = VecBox::seal_to_vecbox(message, &keypair.public_key).expect("seal");
575        let bytes = sealed.to_vec();
576        assert_eq!(bytes.len(), SEALBYTES + message.len());
577        let (enc, tag, data) = sealed.clone().into_parts();
578        assert_eq!(bytes, [enc.as_slice(), &data, tag.as_slice()].concat());
579
580        for index in [
581            0,
582            CRYPTO_KEM_XWING_CIPHERTEXTBYTES - 1,
583            bytes.len() - 20,
584            bytes.len() - 1,
585        ] {
586            let mut tampered = bytes.clone();
587            tampered[index] ^= 0x01;
588            let tampered = VecBox::from_bytes(&tampered).expect("parse");
589            assert!(tampered.open_to_vec(&keypair).is_err(), "byte {index}");
590        }
591        let other = StackKeyPair::generate();
592        assert!(matches!(
593            sealed.open_to_vec(&other),
594            Err(Error::AuthenticationFailed)
595        ));
596        for short in [0, CRYPTO_KEM_XWING_CIPHERTEXTBYTES, SEALBYTES - 1] {
597            assert!(matches!(
598                VecBox::from_bytes(&bytes[..short]),
599                Err(Error::InvalidLength {
600                    context: ErrorContext::SealedBox,
601                    actual,
602                    constraint: crate::error::LengthConstraint::AtLeast(SEALBYTES),
603                }) if actual == short
604            ));
605        }
606        let empty = VecBox::seal_to_vecbox(b"", &keypair.public_key).expect("seal");
607        let empty = VecBox::from_bytes(&empty.to_vec()).expect("parse");
608        assert!(empty.open_to_vec(&keypair).expect("open").is_empty());
609    }
610}