Skip to main content

miden_crypto/ecdh/
x25519.rs

1//! X25519 (Elliptic Curve Diffie-Hellman) key agreement implementation using
2//! Curve25519.
3//!
4//! Note that the intended use is in the context of a one-way, sender initiated key agreement
5//! scenario. Namely, when the sender knows the (static) public key of the receiver and it
6//! uses that, together with an ephemeral secret key that it generates, to derive a shared
7//! secret.
8//!
9//! This shared secret will then be used to encrypt some message (using for example a key
10//! derivation function).
11//!
12//! The public key associated with the ephemeral secret key will be sent alongside the encrypted
13//! message.
14
15use alloc::vec::Vec;
16
17use hkdf::{Hkdf, hmac::SimpleHmac};
18use k256::sha2::Sha256;
19use rand::{CryptoRng, RngCore};
20use subtle::ConstantTimeEq;
21
22use crate::{
23    dsa::eddsa_25519_sha512::{PublicKey, SecretKey},
24    ecdh::KeyAgreementScheme,
25    utils::{
26        ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
27        zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing},
28    },
29};
30
31// SHARED SECRETE
32// ================================================================================================
33
34/// A shared secret computed using the X25519 (Elliptic Curve Diffie-Hellman) key agreement.
35///
36/// This type implements `ZeroizeOnDrop` because the inner `x25519_dalek::SharedSecret`
37/// implements it, ensuring the shared secret is securely wiped from memory when dropped.
38pub struct SharedSecret {
39    pub(crate) inner: x25519_dalek::SharedSecret,
40}
41impl SharedSecret {
42    pub(crate) fn new(inner: x25519_dalek::SharedSecret) -> SharedSecret {
43        Self { inner }
44    }
45
46    /// Returns a HKDF that can be used to derive uniform keys from the shared secret.
47    pub fn extract(&self, salt: Option<&[u8]>) -> Hkdf<Sha256, SimpleHmac<Sha256>> {
48        Hkdf::new(salt, self.inner.as_bytes())
49    }
50}
51
52impl Zeroize for SharedSecret {
53    /// Securely clears the shared secret from memory.
54    ///
55    /// # Security
56    ///
57    /// This implementation follows the same security methodology as the `zeroize` crate to ensure
58    /// that sensitive cryptographic material is reliably cleared from memory:
59    ///
60    /// - **Volatile writes**: Uses `ptr::write_volatile` to prevent dead store elimination and
61    ///   other compiler optimizations that might remove the zeroing operation.
62    /// - **Memory ordering**: Includes a sequentially consistent compiler fence (`SeqCst`) to
63    ///   prevent instruction reordering that could expose the secret data after this function
64    ///   returns.
65    fn zeroize(&mut self) {
66        let bytes = self.inner.as_bytes();
67        for byte in
68            unsafe { core::slice::from_raw_parts_mut(bytes.as_ptr() as *mut u8, bytes.len()) }
69        {
70            unsafe {
71                core::ptr::write_volatile(byte, 0u8);
72            }
73        }
74        core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
75    }
76}
77
78// Safe to derive ZeroizeOnDrop because we implement Zeroize above
79impl ZeroizeOnDrop for SharedSecret {}
80
81impl AsRef<[u8]> for SharedSecret {
82    fn as_ref(&self) -> &[u8] {
83        self.inner.as_bytes()
84    }
85}
86
87// EPHEMERAL SECRET KEY
88// ================================================================================================
89
90/// Ephemeral secret key for X25519 key agreement.
91///
92/// This type implements `ZeroizeOnDrop` because the inner `x25519_dalek::EphemeralSecret`
93/// implements it, ensuring the secret key material is securely wiped from memory when dropped.
94pub struct EphemeralSecretKey {
95    inner: x25519_dalek::EphemeralSecret,
96}
97
98impl ZeroizeOnDrop for EphemeralSecretKey {}
99
100impl EphemeralSecretKey {
101    /// Generates a new random ephemeral secret key using the OS random number generator.
102    #[cfg(feature = "std")]
103    #[allow(clippy::new_without_default)]
104    pub fn new() -> Self {
105        let mut rng = rand::rng();
106
107        Self::with_rng(&mut rng)
108    }
109
110    /// Generates a new random ephemeral secret key using the provided RNG.
111    pub fn with_rng<R: CryptoRng + RngCore>(rng: &mut R) -> Self {
112        // we use a seedable CSPRNG and seed it with `rng`
113        // this is a work around the fact that the version of the `rand` dependency in our crate
114        // is different than the one used in the `x25519_dalek` one. This solution will no longer be
115        // needed once `x25519_dalek` gets a new release with a version of the `rand`
116        // dependency matching ours
117        use k256::elliptic_curve::rand_core::SeedableRng;
118        let mut seed = Zeroizing::new([0_u8; 32]);
119        rand::RngCore::fill_bytes(rng, &mut *seed);
120        let rng = rand_hc::Hc128Rng::from_seed(*seed);
121
122        let sk = x25519_dalek::EphemeralSecret::random_from_rng(rng);
123        Self { inner: sk }
124    }
125
126    /// Returns the corresponding ephemeral public key.
127    pub fn public_key(&self) -> EphemeralPublicKey {
128        EphemeralPublicKey {
129            inner: x25519_dalek::PublicKey::from(&self.inner),
130        }
131    }
132
133    /// Computes a Diffie-Hellman shared secret from this ephemeral secret key and the other party's
134    /// static public key.
135    pub fn diffie_hellman(self, pk_other: &PublicKey) -> SharedSecret {
136        let shared = self.inner.diffie_hellman(&pk_other.to_x25519());
137        SharedSecret::new(shared)
138    }
139}
140
141// EPHEMERAL PUBLIC KEY
142// ================================================================================================
143
144/// Ephemeral public key for X25519 agreement.
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct EphemeralPublicKey {
147    pub(crate) inner: x25519_dalek::PublicKey,
148}
149
150impl Serializable for EphemeralPublicKey {
151    fn write_into<W: ByteWriter>(&self, target: &mut W) {
152        target.write_bytes(self.inner.as_bytes());
153    }
154}
155
156impl Deserializable for EphemeralPublicKey {
157    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
158        let bytes: [u8; 32] = source.read_array()?;
159        // Reject twist points and low-order points. We intentionally avoid the more expensive
160        // torsion-free check; small-order rejection mitigates the most dangerous malleability
161        // issues, even though it does not guarantee torsion-freeness.
162        let mont = curve25519_dalek::montgomery::MontgomeryPoint(bytes);
163        let edwards = mont.to_edwards(0).ok_or_else(|| {
164            DeserializationError::InvalidValue("Invalid X25519 public key".into())
165        })?;
166        if edwards.is_small_order() {
167            return Err(DeserializationError::InvalidValue("Invalid X25519 public key".into()));
168        }
169
170        Ok(Self {
171            inner: x25519_dalek::PublicKey::from(bytes),
172        })
173    }
174}
175
176// KEY AGREEMENT TRAIT IMPLEMENTATION
177// ================================================================================================
178
179pub struct X25519;
180
181impl KeyAgreementScheme for X25519 {
182    type EphemeralSecretKey = EphemeralSecretKey;
183    type EphemeralPublicKey = EphemeralPublicKey;
184
185    type SecretKey = SecretKey;
186    type PublicKey = PublicKey;
187
188    type SharedSecret = SharedSecret;
189
190    fn generate_ephemeral_keypair<R: CryptoRng + RngCore>(
191        rng: &mut R,
192    ) -> (Self::EphemeralSecretKey, Self::EphemeralPublicKey) {
193        let sk = EphemeralSecretKey::with_rng(rng);
194        let pk = sk.public_key();
195
196        (sk, pk)
197    }
198
199    fn exchange_ephemeral_static(
200        ephemeral_sk: Self::EphemeralSecretKey,
201        static_pk: &Self::PublicKey,
202    ) -> Result<Self::SharedSecret, super::KeyAgreementError> {
203        let shared = ephemeral_sk.diffie_hellman(static_pk);
204        if is_all_zero(shared.as_ref()) {
205            return Err(super::KeyAgreementError::InvalidSharedSecret);
206        }
207        Ok(shared)
208    }
209
210    fn exchange_static_ephemeral(
211        static_sk: &Self::SecretKey,
212        ephemeral_pk: &Self::EphemeralPublicKey,
213    ) -> Result<Self::SharedSecret, super::KeyAgreementError> {
214        let shared = static_sk.get_shared_secret(ephemeral_pk.clone());
215        if is_all_zero(shared.as_ref()) {
216            return Err(super::KeyAgreementError::InvalidSharedSecret);
217        }
218        Ok(shared)
219    }
220
221    fn extract_key_material(
222        shared_secret: &Self::SharedSecret,
223        length: usize,
224        info: &[u8],
225    ) -> Result<Vec<u8>, super::KeyAgreementError> {
226        let hkdf = shared_secret.extract(None);
227        let mut buf = vec![0_u8; length];
228        hkdf.expand(info, &mut buf)
229            .map_err(|_| super::KeyAgreementError::HkdfExpansionFailed)?;
230        Ok(buf)
231    }
232}
233
234fn is_all_zero(bytes: &[u8]) -> bool {
235    // Empty input is treated as invalid caller input rather than "all zero".
236    if bytes.is_empty() {
237        return false;
238    }
239    let acc = bytes.iter().fold(0u8, |acc, &byte| acc | byte);
240    acc.ct_eq(&0u8).into()
241}
242
243// TESTS
244// ================================================================================================
245
246#[cfg(test)]
247mod tests {
248    use curve25519_dalek::{constants::EIGHT_TORSION, montgomery::MontgomeryPoint};
249
250    use super::*;
251    use crate::{
252        dsa::eddsa_25519_sha512::SecretKey, ecdh::KeyAgreementError, rand::test_utils::seeded_rng,
253        utils::Deserializable,
254    };
255
256    #[test]
257    fn key_agreement() {
258        let mut rng = seeded_rng([0u8; 32]);
259
260        // 1. Generate the static key-pair for Alice
261        let sk = SecretKey::with_rng(&mut rng);
262        let pk = sk.public_key();
263
264        // 2. Generate the ephemeral key-pair for Bob
265        let sk_e = EphemeralSecretKey::with_rng(&mut rng);
266        let pk_e = sk_e.public_key();
267
268        // 3. Bob computes the shared secret key (Bob will send pk_e with the encrypted note to
269        //    Alice)
270        let shared_secret_key_1 = sk_e.diffie_hellman(&pk);
271
272        // 4. Alice uses its secret key and the ephemeral public key sent with the encrypted note by
273        //    Bob in order to create the shared secret key. This shared secret key will be used to
274        //    decrypt the encrypted note
275        let shared_secret_key_2 = sk.get_shared_secret(pk_e);
276
277        // Check that the computed shared secret keys are equal
278        assert_eq!(shared_secret_key_1.inner.to_bytes(), shared_secret_key_2.inner.to_bytes());
279    }
280
281    #[test]
282    fn ephemeral_public_key_rejects_small_order() {
283        let bytes = EIGHT_TORSION[1].to_montgomery().to_bytes();
284        let result = EphemeralPublicKey::read_from_bytes(&bytes);
285        assert!(result.is_err());
286    }
287
288    #[test]
289    fn ephemeral_public_key_rejects_twist_point() {
290        let bytes = find_twist_point_bytes();
291        let result = EphemeralPublicKey::read_from_bytes(&bytes);
292        assert!(result.is_err());
293    }
294
295    #[test]
296    fn exchange_static_ephemeral_rejects_zero_shared_secret() {
297        let mut rng = seeded_rng([0u8; 32]);
298        let static_sk = SecretKey::with_rng(&mut rng);
299
300        let low_order_bytes = EIGHT_TORSION[0].to_montgomery().to_bytes();
301        let low_order_pk = EphemeralPublicKey {
302            inner: x25519_dalek::PublicKey::from(low_order_bytes),
303        };
304
305        let result = X25519::exchange_static_ephemeral(&static_sk, &low_order_pk);
306        assert!(matches!(result, Err(KeyAgreementError::InvalidSharedSecret)));
307    }
308
309    #[test]
310    fn is_all_zero_accepts_arbitrary_lengths() {
311        assert!(!is_all_zero(&[]));
312        assert!(is_all_zero(&[0u8; 16]));
313        assert!(!is_all_zero(&[0u8, 1u8, 0u8, 0u8]));
314    }
315
316    fn find_twist_point_bytes() -> [u8; 32] {
317        let mut bytes = [0u8; 32];
318        for i in 0u16..=u16::MAX {
319            bytes[0] = (i & 0xff) as u8;
320            bytes[1] = (i >> 8) as u8;
321            if MontgomeryPoint(bytes).to_edwards(0).is_none() {
322                return bytes;
323            }
324        }
325        panic!("no twist point found in 16-bit search space");
326    }
327}