Skip to main content

securedrop_protocol_minimal/primitives/
x25519.rs

1use alloc::string::String;
2
3use crate::primitives::provider::{self, curve25519::ecdh};
4use anyhow::Error;
5use rand_core::{CryptoRng, RngCore};
6use serde::de::Error as _;
7
8pub const DH_PUBLIC_KEY_LEN: usize = crate::primitives::provider::curve25519::PK_LEN;
9pub(crate) const DH_PRIVATE_KEY_LEN: usize = crate::primitives::provider::curve25519::SK_LEN;
10pub(crate) const DH_SHARED_SECRET_LEN: usize =
11    crate::primitives::provider::curve25519::LEN_DH_SHARE;
12
13/// An X25519 public key.
14#[derive(Debug, Clone, Copy)]
15pub struct DHPublicKey([u8; DH_PUBLIC_KEY_LEN]);
16
17impl DHPublicKey {
18    pub fn into_bytes(self) -> [u8; DH_PUBLIC_KEY_LEN] {
19        self.0
20    }
21
22    pub fn from_bytes(bytes: [u8; DH_PUBLIC_KEY_LEN]) -> Self {
23        Self(bytes)
24    }
25}
26
27#[cfg_attr(hax, hax_lib::exclude)]
28impl serde::Serialize for DHPublicKey {
29    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
30        ser.serialize_str(&hex::encode(self.0))
31    }
32}
33
34#[cfg_attr(hax, hax_lib::exclude)]
35impl<'de> serde::Deserialize<'de> for DHPublicKey {
36    fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
37        let s = String::deserialize(de)?;
38        let mut bytes = [0u8; DH_PUBLIC_KEY_LEN];
39        hex::decode_to_slice(s.trim(), &mut bytes).map_err(D::Error::custom)?;
40        Ok(Self::from_bytes(bytes))
41    }
42}
43
44/// An X25519 private key.
45#[derive(Debug, Clone)]
46pub struct DHPrivateKey([u8; DH_PRIVATE_KEY_LEN]);
47
48impl DHPrivateKey {
49    pub fn as_bytes(&self) -> &[u8; DH_PRIVATE_KEY_LEN] {
50        &self.0
51    }
52
53    pub fn into_bytes(self) -> [u8; DH_PRIVATE_KEY_LEN] {
54        self.0
55    }
56
57    pub fn from_bytes(bytes: [u8; DH_PRIVATE_KEY_LEN]) -> Self {
58        Self(bytes)
59    }
60}
61
62/// An X25519 shared secret.
63#[derive(Debug, Clone)]
64pub struct DHSharedSecret([u8; 32]);
65
66impl DHSharedSecret {
67    pub fn into_bytes(self) -> [u8; 32] {
68        self.0
69    }
70
71    pub fn from_bytes(bytes: [u8; 32]) -> Self {
72        Self(bytes)
73    }
74}
75
76/// Generate DH keypair from external randomness
77/// FOR TEST PURPOSES ONLY
78pub fn deterministic_dh_keygen(randomness: [u8; 32]) -> Result<(DHPrivateKey, DHPublicKey), Error> {
79    let mut public_key = [0u8; DH_PUBLIC_KEY_LEN];
80    let mut secret_key = [0u8; DH_PRIVATE_KEY_LEN];
81
82    provider::curve25519::x25519_keygen(&mut public_key, &mut secret_key, &randomness)
83        .map_err(|_| anyhow::anyhow!("X25519 key generation failed"))?;
84
85    Ok((DHPrivateKey(secret_key), DHPublicKey(public_key)))
86}
87
88/// Generate a new DH key pair using X25519
89pub fn generate_dh_keypair<R: RngCore + CryptoRng>(
90    rng: &mut R,
91) -> Result<(DHPrivateKey, DHPublicKey), Error> {
92    let mut randomness = [0u8; 32];
93    provider::rng::fill_bytes(rng, &mut randomness);
94
95    let mut public_key = [0u8; DH_PUBLIC_KEY_LEN];
96    let mut secret_key = [0u8; DH_PRIVATE_KEY_LEN];
97
98    // Generate the key pair using X25519 from libcrux
99    // Parameters: ek (public key), dk (secret key), rand (randomness)
100    provider::curve25519::x25519_keygen(&mut public_key, &mut secret_key, &randomness)
101        .map_err(|_| anyhow::anyhow!("X25519 key generation failed"))?;
102
103    typed(secret_key, public_key)
104}
105
106// Fixed-sized arrays are enforced by at compile-time, so type-checking
107// implies...
108#[cfg_attr(hax, hax_lib::ensures(|result| result.is_ok()))]
109fn typed(
110    sk: [u8; DH_PRIVATE_KEY_LEN],
111    pk: [u8; DH_PUBLIC_KEY_LEN],
112) -> Result<(DHPrivateKey, DHPublicKey), Error> {
113    Ok((DHPrivateKey(sk), DHPublicKey(pk)))
114}
115
116/// Generate a random scalar for DH operations using X25519
117pub fn generate_random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Result<[u8; 32], Error> {
118    let mut randomness = [0u8; 32];
119    provider::rng::fill_bytes(rng, &mut randomness);
120
121    let mut secret_key = [0u8; 32];
122    let mut _public_key = [0u8; 32]; // We don't need the public key here
123
124    // Generate the key pair using X25519 from libcrux
125    // Parameters: ek (public key), dk (secret key), rand (randomness)
126    provider::curve25519::x25519_keygen(&mut _public_key, &mut secret_key, &randomness)
127        .map_err(|_| anyhow::anyhow!("X25519 key generation failed"))?;
128
129    Ok(secret_key)
130}
131
132/// Convert a scalar to a DH public key using the X25519 standard generator base point
133///
134/// libcrux_curve25519::secret_to_public uses the standard X25519 base point G = 9
135/// (defined as [9, 0, 0, 0, ...] in the HACL implementation, see `g25519` in their code)
136pub fn dh_public_key_from_scalar(scalar: [u8; 32]) -> DHPublicKey {
137    let mut public_key_bytes = [0u8; 32];
138    provider::curve25519::secret_to_public(&mut public_key_bytes, &scalar);
139    DHPublicKey::from_bytes(public_key_bytes)
140}
141
142/// Compute DH shared secret
143pub fn dh_shared_secret(
144    public_key: &DHPublicKey,
145    private_scalar: [u8; 32],
146) -> Result<DHSharedSecret, Error> {
147    let mut shared_secret_bytes = [0u8; 32];
148    ecdh(&mut shared_secret_bytes, &public_key.0, &private_scalar)
149        .map_err(|_| anyhow::anyhow!("X25519 DH failed"))?;
150    Ok(DHSharedSecret(shared_secret_bytes))
151}
152
153#[cfg(test)]
154mod tests {
155    use crate::primitives::provider::curve25519::LEN_DH_SHARE;
156
157    use super::*;
158    use proptest::prelude::*;
159    use rand_chacha::ChaCha20Rng;
160    use rand_core::SeedableRng;
161
162    // Toy purposes
163    fn get_rng() -> ChaCha20Rng {
164        let mut seed = [0u8; 32];
165        getrandom::fill(&mut seed).expect("OS random source failed");
166        ChaCha20Rng::from_seed(seed)
167    }
168
169    #[test]
170    fn test_deterministic_dh_keygen() {
171        proptest!(|(randomness in proptest::array::uniform32(any::<u8>()))| {
172            let (private_key, public_key) = deterministic_dh_keygen(randomness).unwrap();
173        });
174    }
175
176    #[test]
177    fn test_dh_shared_secret() {
178        let mut rng = get_rng();
179
180        let (sk1, pk1) = generate_dh_keypair(&mut rng).expect("need dh keygen");
181
182        let (sk2, pk2) = generate_dh_keypair(&mut rng).expect("need dh keygen");
183
184        let ss1 = dh_shared_secret(&pk1, sk2.into_bytes()).expect("need shared secret 1");
185        let ss2 = dh_shared_secret(&pk2, sk1.into_bytes()).expect("need shared secret 2");
186
187        assert_eq!(ss1.clone().into_bytes(), ss2.into_bytes());
188        assert_ne!(ss1.into_bytes(), [0u8; LEN_DH_SHARE])
189    }
190}