Skip to main content

securedrop_protocol_minimal/primitives/
x25519.rs

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