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