Skip to main content

securedrop_protocol_minimal/primitives/
xwing.rs

1use crate::primitives::provider::hpke_rs::{HpkePrivateKey, HpkePublicKey};
2use crate::primitives::provider::kem::{PrivateKey, PublicKey};
3use rand_core::{CryptoRng, RngCore};
4
5// From: https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/#name-encoding-and-sizes
6pub const XWING_PUBLIC_KEY_LEN: usize = 1216;
7pub(crate) const XWING_PRIVATE_KEY_LEN: usize = 32;
8pub(crate) const LEN_XWING_SHAREDSECRET_ENCAPS: usize = 1120;
9
10/// XWING public key.
11#[derive(Debug, Clone)]
12pub(crate) struct XWingPublicKey([u8; XWING_PUBLIC_KEY_LEN]);
13
14/// XWING private key.
15#[derive(Debug, Clone)]
16pub(crate) struct XWingPrivateKey([u8; XWING_PRIVATE_KEY_LEN]);
17
18impl XWingPublicKey {
19    /// Get the public key as bytes
20    pub(crate) fn as_bytes(&self) -> &[u8; XWING_PUBLIC_KEY_LEN] {
21        &self.0
22    }
23
24    /// Create from bytes
25    pub(crate) fn from_bytes(bytes: [u8; XWING_PUBLIC_KEY_LEN]) -> Self {
26        Self(bytes)
27    }
28}
29
30impl XWingPrivateKey {
31    /// Get the private key as bytes
32    pub(crate) fn as_bytes(&self) -> &[u8; XWING_PRIVATE_KEY_LEN] {
33        &self.0
34    }
35
36    /// Create from bytes
37    pub(crate) fn from_bytes(bytes: [u8; XWING_PRIVATE_KEY_LEN]) -> Self {
38        Self(bytes)
39    }
40}
41
42impl From<XWingPrivateKey> for HpkePrivateKey {
43    fn from(sk: XWingPrivateKey) -> Self {
44        HpkePrivateKey::from(sk.0.to_vec())
45    }
46}
47
48impl From<XWingPublicKey> for HpkePublicKey {
49    fn from(pk: XWingPublicKey) -> Self {
50        HpkePublicKey::from(pk.0.to_vec())
51    }
52}
53
54/// Generate XWING keypair from external randomness
55/// FOR TEST PURPOSES ONLY
56pub(crate) fn deterministic_keygen(
57    randomness: [u8; 32],
58) -> Result<(XWingPrivateKey, XWingPublicKey), anyhow::Error> {
59    use crate::primitives::provider::kem::{Algorithm, key_gen_derand};
60
61    // Generate XWING keypair using libcrux_kem with deterministic randomness
62    let (sk, pk) = key_gen_derand(Algorithm::XWingKemDraft06, &randomness)
63        .map_err(|e| anyhow::anyhow!("XWING deterministic key generation failed: {:?}", e))?;
64
65    typed(sk, pk)
66}
67
68#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
69/// Helper, convert libcrux type to our key types
70fn typed(
71    sk: PrivateKey,
72    pk: PublicKey,
73) -> Result<(XWingPrivateKey, XWingPublicKey), anyhow::Error> {
74    // Convert to our types
75    let private_key_bytes = sk.encode();
76    let public_key_bytes = pk.encode();
77
78    // Validate key sizes (XWING should have consistent sizes)
79    if private_key_bytes.len() != XWING_PRIVATE_KEY_LEN
80        || public_key_bytes.len() != XWING_PUBLIC_KEY_LEN
81    {
82        // 1. This branch is skipped if the conditions are met dynamically.
83        //
84        // 2. Implication: this branch is *unreachable* based on the
85        //    postconditions of `encode()` assumed (in the stub) or subsequently
86        //    proven (by the crate itself).
87        //
88        // 3. Implication: this conditional and the error-handling below are
89        //    unnecessary if the postconditions of `encode()` are proven (by the
90        //    crate itself), and this function can return `(private_key,
91        //    public_key)` directly, without wrapping it in a `Result`.
92        return Err(anyhow::anyhow!(
93            "Unexpected XWING key sizes: private={}, public={}",
94            private_key_bytes.len(),
95            public_key_bytes.len()
96        ));
97    }
98
99    let private_key = XWingPrivateKey::from_bytes(
100        private_key_bytes
101            .as_slice()
102            .try_into()
103            .map_err(|_| anyhow::anyhow!("Failed to convert private key bytes"))?,
104    );
105    let public_key = XWingPublicKey::from_bytes(
106        public_key_bytes
107            .as_slice()
108            .try_into()
109            .map_err(|_| anyhow::anyhow!("Failed to convert public key bytes"))?,
110    );
111
112    Ok((private_key, public_key))
113}
114
115/// Generate a new XWING keypair using libcrux_kem
116pub(crate) fn generate_xwing_keypair<R: RngCore + CryptoRng>(
117    rng: &mut R,
118) -> Result<(XWingPrivateKey, XWingPublicKey), anyhow::Error> {
119    use crate::primitives::provider::kem::{Algorithm, key_gen};
120
121    // Generate XWING keypair using libcrux_kem
122    let (sk, pk) = key_gen(Algorithm::XWingKemDraft06, rng)
123        .map_err(|e| anyhow::anyhow!("XWING key generation failed: {:?}", e))?;
124
125    typed(sk, pk)
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use proptest::prelude::*;
132    use rand_chacha::ChaCha20Rng;
133    use rand_core::SeedableRng;
134
135    #[test]
136    fn test_xwing_key_generation() {
137        let mut rng = ChaCha20Rng::seed_from_u64(42);
138
139        let (private_key, public_key) =
140            generate_xwing_keypair(&mut rng).expect("Should generate XWING keypair");
141    }
142
143    #[test]
144    fn test_xwing_key_serialization() {
145        let mut rng = ChaCha20Rng::seed_from_u64(42);
146
147        let (private_key, public_key) =
148            generate_xwing_keypair(&mut rng).expect("Should generate XWING keypair");
149
150        // Test round-trip serialization
151        let private_bytes = *private_key.as_bytes();
152        let public_bytes = *public_key.as_bytes();
153
154        let reconstructed_private = XWingPrivateKey::from_bytes(private_bytes);
155        let reconstructed_public = XWingPublicKey::from_bytes(public_bytes);
156
157        assert_eq!(private_key.as_bytes(), reconstructed_private.as_bytes());
158        assert_eq!(public_key.as_bytes(), reconstructed_public.as_bytes());
159    }
160
161    #[test]
162    fn test_deterministic_keygen() {
163        proptest!(|(randomness in proptest::array::uniform32(any::<u8>()).prop_filter("exclude zero", |arr| arr != &[0u8; 32]))| {
164            let (private_key, public_key) = deterministic_keygen(randomness.try_into().unwrap()).unwrap();
165        });
166    }
167}