Skip to main content

securedrop_protocol_minimal/primitives/
dh_akem.rs

1use crate::primitives::provider::hpke_rs::{HpkePrivateKey, HpkePublicKey};
2use crate::primitives::provider::kem::{PrivateKey, PublicKey};
3use rand_core::{CryptoRng, RngCore};
4
5pub const DH_AKEM_PUBLIC_KEY_LEN: usize = crate::primitives::provider::curve25519::PK_LEN;
6pub(crate) const DH_AKEM_PRIVATE_KEY_LEN: usize = crate::primitives::provider::curve25519::SK_LEN;
7pub(crate) const DH_AKEM_SECRET_LEN: usize = crate::primitives::provider::curve25519::LEN_DH_SHARE;
8pub(crate) const DH_AKEM_ENCAPS_SECRET_LEN: usize =
9    crate::primitives::provider::curve25519::LEN_DH_SHARE;
10
11/// An DH-AKEM public key.
12#[derive(Debug, Clone)]
13pub(crate) struct DhAkemPublicKey([u8; DH_AKEM_PUBLIC_KEY_LEN]);
14
15/// An DH-AKEM private key.
16#[derive(Debug, Clone)]
17pub(crate) struct DhAkemPrivateKey([u8; DH_AKEM_PRIVATE_KEY_LEN]);
18
19/// An DH-AKEM shared secret.
20#[derive(Debug, Clone)]
21pub(crate) struct DhAkemSecret([u8; DH_AKEM_SECRET_LEN]);
22
23impl DhAkemPublicKey {
24    pub(crate) fn as_bytes(&self) -> &[u8; DH_AKEM_PUBLIC_KEY_LEN] {
25        &self.0
26    }
27
28    pub(crate) fn from_bytes(bytes: [u8; DH_AKEM_PUBLIC_KEY_LEN]) -> Self {
29        Self(bytes)
30    }
31}
32
33impl DhAkemPrivateKey {
34    pub(crate) fn as_bytes(&self) -> &[u8; DH_AKEM_PRIVATE_KEY_LEN] {
35        &self.0
36    }
37
38    pub(crate) fn from_bytes(bytes: [u8; DH_AKEM_PRIVATE_KEY_LEN]) -> Self {
39        Self(bytes)
40    }
41}
42
43impl DhAkemSecret {
44    pub(crate) fn as_bytes(&self) -> &[u8; DH_AKEM_SECRET_LEN] {
45        &self.0
46    }
47
48    pub(crate) fn from_bytes(bytes: [u8; DH_AKEM_SECRET_LEN]) -> Self {
49        Self(bytes)
50    }
51}
52
53/// Clamp a scalar to ensure it's a valid X25519 scalar.
54fn clamp(scalar: &mut [u8; 32]) {
55    // Clear the 3 least significant bits of the first byte
56    scalar[0] &= 248u8;
57    // Clear the most significant bit of the last byte
58    scalar[31] &= 127u8;
59    // Set the second most significant bit of the last byte
60    scalar[31] |= 64u8;
61}
62
63/// Generate DH-AKEM keypair from external randomness
64/// FOR TEST PURPOSES ONLY
65pub(crate) fn deterministic_keygen(
66    randomness: [u8; 32],
67) -> Result<(DhAkemPrivateKey, DhAkemPublicKey), anyhow::Error> {
68    use crate::primitives::provider::kem::{Algorithm, key_gen_derand};
69
70    // Note that the key_gen_derand function expects the seed to be a valid scalar for X25519
71    let mut clamped_randomness = randomness.clone();
72    clamp(&mut clamped_randomness);
73
74    let (sk, pk) = key_gen_derand(Algorithm::X25519, &clamped_randomness)
75        .map_err(|e| anyhow::anyhow!("DH-AKEM deterministic key generation failed: {:?}", e))?;
76
77    typed(sk, pk)
78}
79
80impl From<DhAkemPrivateKey> for HpkePrivateKey {
81    fn from(sk: DhAkemPrivateKey) -> Self {
82        HpkePrivateKey::from(sk.0.to_vec())
83    }
84}
85
86impl From<DhAkemPublicKey> for HpkePublicKey {
87    fn from(pk: DhAkemPublicKey) -> Self {
88        HpkePublicKey::from(pk.0.to_vec())
89    }
90}
91
92#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
93/// Helper, convert libcrux type to our key types
94fn typed(
95    sk: PrivateKey,
96    pk: PublicKey,
97) -> Result<(DhAkemPrivateKey, DhAkemPublicKey), anyhow::Error> {
98    // Convert to our types
99    let private_key_bytes = sk.encode();
100    let public_key_bytes = pk.encode();
101
102    // Validate key sizes (X25519 should have consistent sizes)
103    if private_key_bytes.len() != DH_AKEM_PRIVATE_KEY_LEN
104        || public_key_bytes.len() != DH_AKEM_PUBLIC_KEY_LEN
105    {
106        // 1. This branch is skipped if the conditions are met dynamically.
107        //
108        // 2. Implication: this branch is *unreachable* based on the
109        //    postconditions of `encode()` assumed (in the stub) or subsequently
110        //    proven (by the crate itself).
111        //
112        // 3. Implication: this conditional and the error-handling below are
113        //    unnecessary if the postconditions of `encode()` are proven (by the
114        //    crate itself), and this function can return `(private_key,
115        //    public_key)` directly, without wrapping it in a `Result`.
116        return Err(anyhow::anyhow!(
117            "Unexpected DH-AKEM key sizes: private={}, public={}",
118            private_key_bytes.len(),
119            public_key_bytes.len()
120        ));
121    }
122
123    let private_key = DhAkemPrivateKey::from_bytes(
124        private_key_bytes
125            .try_into()
126            .map_err(|_| anyhow::anyhow!("Failed to convert private key bytes"))?,
127    );
128    let public_key = DhAkemPublicKey::from_bytes(
129        public_key_bytes
130            .try_into()
131            .map_err(|_| anyhow::anyhow!("Failed to convert public key bytes"))?,
132    );
133
134    Ok((private_key, public_key))
135}
136
137/// Generate a new DH-AKEM key pair using libcrux_kem
138pub(crate) fn generate_dh_akem_keypair<R: RngCore + CryptoRng>(
139    rng: &mut R,
140) -> Result<(DhAkemPrivateKey, DhAkemPublicKey), anyhow::Error> {
141    use crate::primitives::provider::kem::{Algorithm, key_gen};
142
143    // Generate DH-AKEM keypair using libcrux_kem with X25519
144    let (sk, pk) = key_gen(Algorithm::X25519, rng)
145        .map_err(|e| anyhow::anyhow!("DH-AKEM key generation failed: {:?}", e))?;
146    typed(sk, pk)
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use proptest::prelude::*;
153    use rand_chacha::ChaCha20Rng;
154    use rand_core::SeedableRng;
155
156    #[test]
157    fn test_dh_akem_key_generation() {
158        let mut rng = ChaCha20Rng::seed_from_u64(42);
159
160        let (private_key, public_key) =
161            generate_dh_akem_keypair(&mut rng).expect("Should generate DH-AKEM keypair");
162
163        // Verify key sizes
164        assert_eq!(private_key.as_bytes().len(), DH_AKEM_PRIVATE_KEY_LEN);
165        assert_eq!(public_key.as_bytes().len(), DH_AKEM_PUBLIC_KEY_LEN);
166
167        // Verify keys are different
168        assert_ne!(private_key.as_bytes(), public_key.as_bytes());
169    }
170
171    #[test]
172    fn test_dh_akem_key_serialization() {
173        let mut rng = ChaCha20Rng::seed_from_u64(42);
174
175        let (private_key, public_key) =
176            generate_dh_akem_keypair(&mut rng).expect("Should generate DH-AKEM keypair");
177
178        // Test round-trip serialization
179        let private_bytes = *private_key.as_bytes();
180        let public_bytes = *public_key.as_bytes();
181
182        let reconstructed_private = DhAkemPrivateKey::from_bytes(private_bytes);
183        let reconstructed_public = DhAkemPublicKey::from_bytes(public_bytes);
184
185        assert_eq!(private_key.as_bytes(), reconstructed_private.as_bytes());
186        assert_eq!(public_key.as_bytes(), reconstructed_public.as_bytes());
187    }
188
189    #[test]
190    fn test_deterministic_keygen() {
191        proptest!(|(randomness in proptest::array::uniform32(any::<u8>()).prop_filter("exclude zero", |arr| arr != &[0u8; 32]))| {
192            let (private_key, public_key) = deterministic_keygen(randomness.try_into().unwrap()).unwrap();
193        });
194    }
195}