Skip to main content

x_wing/
lib.rs

1#![no_std]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![doc = include_str!("../README.md")]
4#![doc(
5    html_logo_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg",
6    html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg"
7)]
8
9//! # Usage
10//!
11//! This crate implements the X-Wing Key Encapsulation Method (X-Wing-KEM) algorithm.
12//! X-Wing-KEM is a KEM in the sense that it creates an (decapsulation key, encapsulation key) pair,
13//! such that anyone can use the encapsulation key to establish a shared key with the holder of the
14//! decapsulation key. X-Wing-KEM is a general-purpose hybrid post-quantum KEM, combining x25519 and ML-KEM-768.
15#![cfg_attr(feature = "getrandom", doc = "```")]
16#![cfg_attr(not(feature = "getrandom"), doc = "```ignore")]
17//! // NOTE: requires the `getrandom` feature is enabled
18//! use x_wing::{
19//!     XWingKem,
20//!     kem::{Decapsulate, Encapsulate, Kem}
21//! };
22//!
23//! let (sk, pk) = XWingKem::generate_keypair();
24//! let (ct, sk_sender) = pk.encapsulate();
25//! let sk_receiver = sk.decapsulate(&ct);
26//! assert_eq!(sk_sender, sk_receiver);
27//! ```
28
29mod error;
30
31pub use crate::error::DecapsulationError;
32pub use kem::{
33    self, Decapsulate, Decapsulator, Encapsulate, Generate, InvalidKey, Kem, Key, KeyExport,
34    KeyInit, KeySizeUser, TryDecapsulate, TryKeyInit,
35    common::rand_core::{CryptoRng, TryCryptoRng},
36};
37
38use core::fmt::{self, Debug};
39use ml_kem::{
40    FromSeed, MlKem768,
41    array::{
42        Array, ArrayN, AsArrayRef,
43        sizes::{U32, U1120, U1184, U1216},
44    },
45    ml_kem_768,
46};
47use sha3::Sha3_256;
48use shake::{ExtendableOutput, Shake256, Shake256Reader, XofReader};
49use x25519_dalek::{PublicKey, StaticSecret};
50
51#[cfg(feature = "zeroize")]
52use zeroize::{Zeroize, ZeroizeOnDrop};
53
54type MlKem768DecapsulationKey = ml_kem_768::DecapsulationKey;
55type MlKem768EncapsulationKey = ml_kem_768::EncapsulationKey;
56
57const X_WING_LABEL: &[u8; 6] = br"\.//^\";
58
59/// Size in bytes of the `EncapsulationKey`.
60pub const ENCAPSULATION_KEY_SIZE: usize = 1216;
61/// Size in bytes of the `DecapsulationKey`.
62pub const DECAPSULATION_KEY_SIZE: usize = 32;
63/// Size in bytes of the `Ciphertext`.
64pub const CIPHERTEXT_SIZE: usize = 1120;
65/// Number of bytes necessary to encapsulate a key
66pub const ENCAPSULATION_RANDOMNESS_SIZE: usize = 64;
67
68/// Serialized ciphertext.
69pub type Ciphertext = kem::Ciphertext<XWingKem>;
70/// Shared secret key.
71pub type SharedKey = Array<u8, U32>;
72
73/// X-Wing Key Encapsulation Method (X-Wing-KEM).
74#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd, Ord)]
75pub struct XWingKem;
76
77impl Kem for XWingKem {
78    type DecapsulationKey = DecapsulationKey;
79    type EncapsulationKey = EncapsulationKey;
80    type CiphertextSize = U1120;
81    type SharedKeySize = U32;
82}
83
84// The naming convention of variables matches the RFC.
85// ss -> Shared Secret
86// ct -> Cipher Text
87// ek -> Ephemeral Key
88// pk -> Public Key
89// sk -> Secret Key
90// Postfixes:
91// _m -> ML-Kem related key
92// _x -> x25519 related key
93
94/// X-Wing encapsulation or public key.
95#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct EncapsulationKey {
97    pk_m: MlKem768EncapsulationKey,
98    pk_x: PublicKey,
99}
100
101impl EncapsulationKey {
102    /// Encapsulates with the given randomness. Uses the first 32 bytes for ML-KEM and the last 32
103    /// bytes for x25519. This is useful for testing against known vectors.
104    ///
105    /// # Warning
106    /// Do NOT use this function unless you know what you're doing. If you fail to use all uniform
107    /// random bytes even once, you can have catastrophic security failure.
108    #[doc(hidden)]
109    #[cfg_attr(not(feature = "hazmat"), doc(hidden))]
110    #[expect(clippy::must_use_candidate)]
111    pub fn encapsulate_deterministic(
112        &self,
113        randomness: &ArrayN<u8, ENCAPSULATION_RANDOMNESS_SIZE>,
114    ) -> (Ciphertext, SharedKey) {
115        // Split randomness into two 32-byte arrays
116        let (rand_m, rand_x) = randomness.split::<U32>();
117
118        // Encapsulate with ML-KEM first. This is infallible
119        let (ct_m, ss_m) = self.pk_m.encapsulate_deterministic(&rand_m);
120
121        let ek_x = StaticSecret::from(rand_x.0);
122        // Equal to ct_x = x25519(ek_x, BASE_POINT)
123        let ct_x = PublicKey::from(&ek_x);
124        // Equal to ss_x = x25519(ek_x, pk_x)
125        let ss_x = ek_x.diffie_hellman(&self.pk_x);
126
127        let ss = combiner(&ss_m, &ss_x, &ct_x, &self.pk_x);
128        let ct = CiphertextMessage { ct_m, ct_x };
129
130        (ct.into(), ss)
131    }
132}
133
134impl Encapsulate for EncapsulationKey {
135    type Kem = XWingKem;
136
137    fn encapsulate_with_rng<R>(&self, rng: &mut R) -> (Ciphertext, SharedKey)
138    where
139        R: CryptoRng + ?Sized,
140    {
141        #[allow(unused_mut)]
142        let mut randomness = Array::generate_from_rng(rng);
143        let res = self.encapsulate_deterministic(&randomness);
144
145        #[cfg(feature = "zeroize")]
146        randomness.zeroize();
147
148        res
149    }
150}
151
152impl KeySizeUser for EncapsulationKey {
153    type KeySize = U1216;
154}
155
156impl KeyExport for EncapsulationKey {
157    fn to_bytes(&self) -> Key<Self> {
158        let mut key_bytes = Key::<Self>::default();
159        let (m, x) = key_bytes.split_at_mut(1184);
160        m.copy_from_slice(&self.pk_m.to_bytes());
161        x.copy_from_slice(self.pk_x.as_bytes());
162        key_bytes
163    }
164}
165
166impl TryKeyInit for EncapsulationKey {
167    fn new(key_bytes: &Key<Self>) -> Result<Self, InvalidKey> {
168        let (m_bytes, x_bytes) = key_bytes.split_ref::<U1184>();
169
170        let pk_m = MlKem768EncapsulationKey::new(m_bytes)?;
171        let pk_x = PublicKey::from(x_bytes.0);
172
173        Ok(EncapsulationKey { pk_m, pk_x })
174    }
175}
176
177impl TryFrom<&[u8]> for EncapsulationKey {
178    type Error = InvalidKey;
179
180    fn try_from(key_bytes: &[u8]) -> Result<Self, InvalidKey> {
181        Self::new_from_slice(key_bytes)
182    }
183}
184
185/// X-Wing decapsulation key or private key.
186///
187/// This decapsulation key accepts "non-contributory behaviour" in the X25519 component of
188/// X-Wing. To reject instead, use [`DecapsulationKeyRejectNonContrib`].
189#[derive(Clone)]
190pub struct DecapsulationKey {
191    sk: [u8; DECAPSULATION_KEY_SIZE],
192    ek: EncapsulationKey,
193}
194
195impl DecapsulationKey {
196    /// Private key as bytes.
197    #[must_use]
198    pub fn as_bytes(&self) -> &[u8; DECAPSULATION_KEY_SIZE] {
199        &self.sk
200    }
201}
202
203impl Debug for DecapsulationKey {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        f.debug_struct("DecapsulationKey")
206            .field("ek", &self.ek)
207            .finish_non_exhaustive()
208    }
209}
210
211impl DecapsulationKey {
212    fn decapsulate_inner(
213        &self,
214        ct: &CiphertextMessage,
215    ) -> (SharedKey, x25519_dalek::SharedSecret, PublicKey) {
216        let (sk_m, sk_x, _pk_m, pk_x) = expand_key(&self.sk);
217
218        let ss_m = sk_m.decapsulate(&ct.ct_m);
219
220        // equal to ss_x = x25519(sk_x, ct_x)
221        let ss_x = sk_x.diffie_hellman(&ct.ct_x);
222
223        (ss_m, ss_x, pk_x)
224    }
225}
226
227impl Decapsulate for DecapsulationKey {
228    #[allow(clippy::similar_names)] // So we can use the names as in the RFC
229    fn decapsulate(&self, ct: &Ciphertext) -> SharedKey {
230        let ct = CiphertextMessage::from(ct);
231        let (ss_m, ss_x, pk_x) = self.decapsulate_inner(&ct);
232
233        combiner(&ss_m, &ss_x, &ct.ct_x, &pk_x)
234    }
235}
236
237impl Decapsulator for DecapsulationKey {
238    type Kem = XWingKem;
239
240    fn encapsulation_key(&self) -> &EncapsulationKey {
241        &self.ek
242    }
243}
244
245impl Drop for DecapsulationKey {
246    fn drop(&mut self) {
247        #[cfg(feature = "zeroize")]
248        self.sk.zeroize();
249    }
250}
251
252impl From<[u8; DECAPSULATION_KEY_SIZE]> for DecapsulationKey {
253    fn from(sk: [u8; DECAPSULATION_KEY_SIZE]) -> Self {
254        DecapsulationKey::new(sk.as_array_ref())
255    }
256}
257
258impl Generate for DecapsulationKey {
259    fn try_generate_from_rng<R>(rng: &mut R) -> Result<Self, R::Error>
260    where
261        R: TryCryptoRng + ?Sized,
262    {
263        <[u8; DECAPSULATION_KEY_SIZE]>::try_generate_from_rng(rng).map(Into::into)
264    }
265}
266
267impl KeySizeUser for DecapsulationKey {
268    type KeySize = U32;
269}
270
271impl KeyInit for DecapsulationKey {
272    fn new(key: &Key<Self>) -> Self {
273        let (_sk_m, _sk_x, pk_m, pk_x) = expand_key(key.as_ref());
274        let ek = EncapsulationKey { pk_m, pk_x };
275        Self { sk: key.0, ek }
276    }
277}
278
279impl KeyExport for DecapsulationKey {
280    fn to_bytes(&self) -> Key<Self> {
281        self.sk.into()
282    }
283}
284
285#[cfg(feature = "zeroize")]
286impl ZeroizeOnDrop for DecapsulationKey {}
287
288/// X-Wing decapsulation key or private key.
289///
290/// This decapsulation key rejects "non-contributory behaviour" in the X25519 component of
291/// X-Wing. To accept instead, use [`DecapsulationKey`].
292///
293/// # Backstory
294///
295/// [RFC 7748] defines the `X25519` function, and [specifies] that when used for ECDH
296/// (as it is inside X-Wing), the implementation **MAY** abort if the all-zero value
297/// is produced as a shared secret. X-Wing, as initially specified, used the X25519
298/// function without making any mention of this **MAY**, meaning that implementations
299/// inherited whatever behaviour their underlying X25519 ECDH implementation provided.
300///
301/// This crate initially did not check for non-contributory behaviour, which meant it
302/// was incompatible with other implementations that did (in that it would accept
303/// ciphertexts that other implementations reject).
304///
305/// [CFRG have decided] that they will pick a single behaviour for the IETF X-Wing
306/// standard. Until the corresponding RFC is published, this crate supports both
307/// behaviours: constructing a [`DecapsulationKey`] accepts non-contributory behaviour for
308/// backwards-compatibility with existing usages, and this struct can be used to instead
309/// reject non-contributory behaviour. Once the RFC is published, `DecapsulationKey` will
310/// be altered to match it.
311///
312/// [RFC 7748]: https://www.rfc-editor.org/info/rfc7748/#section-5
313/// [specifies]: https://www.rfc-editor.org/info/rfc7748/#section-6.1
314/// [CFRG have decided]: https://mailarchive.ietf.org/arch/msg/cfrg/v9fEHQj3QTUpdu72AzjyyrY4j2g/
315#[derive(Clone, Debug)]
316pub struct DecapsulationKeyRejectNonContrib(DecapsulationKey);
317
318impl TryDecapsulate for DecapsulationKeyRejectNonContrib {
319    type Error = DecapsulationError;
320
321    #[allow(clippy::similar_names)] // So we can use the names as in the RFC
322    fn try_decapsulate(&self, ct: &Ciphertext) -> Result<SharedKey, Self::Error> {
323        let ct = CiphertextMessage::from(ct);
324        let (ss_m, ss_x, pk_x) = self.0.decapsulate_inner(&ct);
325
326        if !ss_x.was_contributory() {
327            return Err(DecapsulationError);
328        }
329
330        Ok(combiner(&ss_m, &ss_x, &ct.ct_x, &pk_x))
331    }
332}
333
334impl Decapsulator for DecapsulationKeyRejectNonContrib {
335    type Kem = XWingKem;
336
337    fn encapsulation_key(&self) -> &EncapsulationKey {
338        self.0.encapsulation_key()
339    }
340}
341
342impl From<[u8; DECAPSULATION_KEY_SIZE]> for DecapsulationKeyRejectNonContrib {
343    fn from(sk: [u8; DECAPSULATION_KEY_SIZE]) -> Self {
344        Self(sk.into())
345    }
346}
347
348impl Generate for DecapsulationKeyRejectNonContrib {
349    fn try_generate_from_rng<R>(rng: &mut R) -> Result<Self, R::Error>
350    where
351        R: TryCryptoRng + ?Sized,
352    {
353        DecapsulationKey::try_generate_from_rng(rng).map(Self)
354    }
355}
356
357impl KeySizeUser for DecapsulationKeyRejectNonContrib {
358    type KeySize = U32;
359}
360
361impl KeyInit for DecapsulationKeyRejectNonContrib {
362    fn new(key: &Key<Self>) -> Self {
363        Self(DecapsulationKey::new(key))
364    }
365}
366
367impl KeyExport for DecapsulationKeyRejectNonContrib {
368    fn to_bytes(&self) -> Key<Self> {
369        self.0.to_bytes()
370    }
371}
372
373fn expand_key(
374    sk: &[u8; DECAPSULATION_KEY_SIZE],
375) -> (
376    MlKem768DecapsulationKey,
377    StaticSecret,
378    MlKem768EncapsulationKey,
379    PublicKey,
380) {
381    use sha3::digest::Update;
382    let mut shaker = Shake256::default();
383    shaker.update(sk);
384    let mut expanded: Shake256Reader = shaker.finalize_xof();
385
386    let seed = read_from(&mut expanded).into();
387    let (sk_m, pk_m) = MlKem768::from_seed(&seed);
388
389    let sk_x = read_from(&mut expanded);
390    let sk_x = StaticSecret::from(sk_x);
391    let pk_x = PublicKey::from(&sk_x);
392
393    (sk_m, sk_x, pk_m, pk_x)
394}
395
396/// X-Wing ciphertext.
397#[derive(Clone, Debug, PartialEq, Eq)]
398pub struct CiphertextMessage {
399    ct_m: ArrayN<u8, 1088>,
400    ct_x: PublicKey,
401}
402
403impl CiphertextMessage {
404    /// Convert the ciphertext to the following format:
405    /// ML-KEM-768 ciphertext(1088 bytes) || X25519 ciphertext(32 bytes).
406    #[must_use]
407    pub fn to_bytes(&self) -> Ciphertext {
408        let mut buffer = Ciphertext::default();
409        buffer[0..1088].copy_from_slice(&self.ct_m);
410        buffer[1088..].copy_from_slice(self.ct_x.as_bytes());
411        buffer
412    }
413}
414
415impl From<&Ciphertext> for CiphertextMessage {
416    fn from(value: &Ciphertext) -> Self {
417        let mut ct_m = [0; 1088];
418        ct_m.copy_from_slice(&value[0..1088]);
419        let mut ct_x = [0; 32];
420        ct_x.copy_from_slice(&value[1088..]);
421
422        CiphertextMessage {
423            ct_m: ct_m.into(),
424            ct_x: ct_x.into(),
425        }
426    }
427}
428
429impl From<&CiphertextMessage> for Ciphertext {
430    #[inline]
431    fn from(msg: &CiphertextMessage) -> Self {
432        msg.to_bytes()
433    }
434}
435
436impl From<CiphertextMessage> for Ciphertext {
437    #[inline]
438    fn from(msg: CiphertextMessage) -> Self {
439        Self::from(&msg)
440    }
441}
442
443fn combiner(
444    ss_m: &ArrayN<u8, 32>,
445    ss_x: &x25519_dalek::SharedSecret,
446    ct_x: &PublicKey,
447    pk_x: &PublicKey,
448) -> SharedKey {
449    use sha3::Digest;
450
451    let mut hasher = Sha3_256::new();
452    hasher.update(ss_m);
453    hasher.update(ss_x);
454    hasher.update(ct_x);
455    hasher.update(pk_x.as_bytes());
456    hasher.update(X_WING_LABEL);
457    hasher.finalize()
458}
459
460fn read_from<const N: usize>(reader: &mut Shake256Reader) -> [u8; N] {
461    let mut data = [0; N];
462    reader.read(&mut data);
463    data
464}
465
466#[cfg(test)]
467mod tests {
468    #[cfg(feature = "getrandom")]
469    use super::*;
470
471    #[test]
472    #[cfg(feature = "getrandom")]
473    fn key_serialize() {
474        let (sk, pk) = XWingKem::generate_keypair();
475
476        let sk_bytes = sk.as_bytes();
477        let pk_bytes = pk.to_bytes();
478
479        let sk_b = DecapsulationKey::from(*sk_bytes);
480        let pk_b = EncapsulationKey::new(&pk_bytes).unwrap();
481
482        assert_eq!(sk.sk, sk_b.sk);
483        assert!(pk == pk_b);
484    }
485
486    #[test]
487    #[cfg(feature = "getrandom")]
488    fn non_contributory() {
489        let (sk, pk) = XWingKem::generate_keypair();
490
491        // Construct a ciphertext with non-contributory behaviour.
492        let ct = CiphertextMessage {
493            ct_m: pk.pk_m.encapsulate().0,
494            ct_x: PublicKey::from([0; 32]),
495        }
496        .into();
497
498        // By default, sk accepts.
499        assert!(sk.try_decapsulate(&ct).is_ok());
500
501        // If rejecting non-contributory behaviour, sk errors.
502        let sk = DecapsulationKeyRejectNonContrib(sk);
503        assert!(matches!(sk.try_decapsulate(&ct), Err(DecapsulationError)));
504    }
505}