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            .try_into()
102            .map_err(|_| anyhow::anyhow!("Failed to convert private key bytes"))?,
103    );
104    let public_key = XWingPublicKey::from_bytes(
105        public_key_bytes
106            .try_into()
107            .map_err(|_| anyhow::anyhow!("Failed to convert public key bytes"))?,
108    );
109
110    Ok((private_key, public_key))
111}
112
113/// Generate a new XWING keypair using libcrux_kem
114pub(crate) fn generate_xwing_keypair<R: RngCore + CryptoRng>(
115    rng: &mut R,
116) -> Result<(XWingPrivateKey, XWingPublicKey), anyhow::Error> {
117    use crate::primitives::provider::kem::{Algorithm, key_gen};
118
119    // Generate XWING keypair using libcrux_kem
120    let (sk, pk) = key_gen(Algorithm::XWingKemDraft06, rng)
121        .map_err(|e| anyhow::anyhow!("XWING key generation failed: {:?}", e))?;
122
123    typed(sk, pk)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use proptest::prelude::*;
130    use rand_chacha::ChaCha20Rng;
131    use rand_core::SeedableRng;
132
133    #[test]
134    fn test_xwing_key_generation() {
135        let mut rng = ChaCha20Rng::seed_from_u64(42);
136
137        let (private_key, public_key) =
138            generate_xwing_keypair(&mut rng).expect("Should generate XWING keypair");
139    }
140
141    #[test]
142    fn test_xwing_key_serialization() {
143        let mut rng = ChaCha20Rng::seed_from_u64(42);
144
145        let (private_key, public_key) =
146            generate_xwing_keypair(&mut rng).expect("Should generate XWING keypair");
147
148        // Test round-trip serialization
149        let private_bytes = *private_key.as_bytes();
150        let public_bytes = *public_key.as_bytes();
151
152        let reconstructed_private = XWingPrivateKey::from_bytes(private_bytes);
153        let reconstructed_public = XWingPublicKey::from_bytes(public_bytes);
154
155        assert_eq!(private_key.as_bytes(), reconstructed_private.as_bytes());
156        assert_eq!(public_key.as_bytes(), reconstructed_public.as_bytes());
157    }
158
159    #[test]
160    fn test_deterministic_keygen() {
161        proptest!(|(randomness in proptest::array::uniform32(any::<u8>()).prop_filter("exclude zero", |arr| arr != &[0u8; 32]))| {
162            let (private_key, public_key) = deterministic_keygen(randomness.try_into().unwrap()).unwrap();
163        });
164    }
165}