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};
20
21use crate::{
22    dsa::eddsa_25519_sha512::{PublicKey, SecretKey},
23    ecdh::KeyAgreementScheme,
24    utils::{
25        ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
26        zeroize::{Zeroize, ZeroizeOnDrop},
27    },
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, SimpleHmac<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 + RngCore>(rng: &mut R) -> Self {
111        // we use a seedable CSPRNG and seed it with `rng`
112        // this is a work around the fact that the version of the `rand` dependency in our crate
113        // is different than the one used in the `x25519_dalek` one. This solution will no longer be
114        // needed once `x25519_dalek` gets a new release with a version of the `rand`
115        // dependency matching ours
116        use k256::elliptic_curve::rand_core::SeedableRng;
117        let mut seed = [0_u8; 32];
118        rand::RngCore::fill_bytes(rng, &mut seed);
119        let rng = rand_hc::Hc128Rng::from_seed(seed);
120
121        let sk = x25519_dalek::EphemeralSecret::random_from_rng(rng);
122        Self { inner: sk }
123    }
124
125    /// Returns the corresponding ephemeral public key.
126    pub fn public_key(&self) -> EphemeralPublicKey {
127        EphemeralPublicKey {
128            inner: x25519_dalek::PublicKey::from(&self.inner),
129        }
130    }
131
132    /// Computes a Diffie-Hellman shared secret from this ephemeral secret key and the other party's
133    /// static public key.
134    pub fn diffie_hellman(self, pk_other: &PublicKey) -> SharedSecret {
135        let shared = self.inner.diffie_hellman(&pk_other.to_x25519());
136        SharedSecret::new(shared)
137    }
138}
139
140// EPHEMERAL PUBLIC KEY
141// ================================================================================================
142
143/// Ephemeral public key for X25519 agreement.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct EphemeralPublicKey {
146    pub(crate) inner: x25519_dalek::PublicKey,
147}
148
149impl Serializable for EphemeralPublicKey {
150    fn write_into<W: ByteWriter>(&self, target: &mut W) {
151        target.write_bytes(self.inner.as_bytes());
152    }
153}
154
155impl Deserializable for EphemeralPublicKey {
156    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
157        let bytes: [u8; 32] = source.read_array()?;
158        Ok(Self {
159            inner: x25519_dalek::PublicKey::from(bytes),
160        })
161    }
162}
163
164// KEY AGREEMENT TRAIT IMPLEMENTATION
165// ================================================================================================
166
167pub struct X25519;
168
169impl KeyAgreementScheme for X25519 {
170    type EphemeralSecretKey = EphemeralSecretKey;
171    type EphemeralPublicKey = EphemeralPublicKey;
172
173    type SecretKey = SecretKey;
174    type PublicKey = PublicKey;
175
176    type SharedSecret = SharedSecret;
177
178    fn generate_ephemeral_keypair<R: CryptoRng + RngCore>(
179        rng: &mut R,
180    ) -> (Self::EphemeralSecretKey, Self::EphemeralPublicKey) {
181        let sk = EphemeralSecretKey::with_rng(rng);
182        let pk = sk.public_key();
183
184        (sk, pk)
185    }
186
187    fn exchange_ephemeral_static(
188        ephemeral_sk: Self::EphemeralSecretKey,
189        static_pk: &Self::PublicKey,
190    ) -> Result<Self::SharedSecret, super::KeyAgreementError> {
191        Ok(ephemeral_sk.diffie_hellman(static_pk))
192    }
193
194    fn exchange_static_ephemeral(
195        static_sk: &Self::SecretKey,
196        ephemeral_pk: &Self::EphemeralPublicKey,
197    ) -> Result<Self::SharedSecret, super::KeyAgreementError> {
198        Ok(static_sk.get_shared_secret(ephemeral_pk.clone()))
199    }
200
201    fn extract_key_material(
202        shared_secret: &Self::SharedSecret,
203        length: usize,
204    ) -> Result<Vec<u8>, super::KeyAgreementError> {
205        let hkdf = shared_secret.extract(None);
206        let mut buf = vec![0_u8; length];
207        hkdf.expand(&[], &mut buf)
208            .map_err(|_| super::KeyAgreementError::HkdfExpansionFailed)?;
209        Ok(buf)
210    }
211}
212
213// TESTS
214// ================================================================================================
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::{dsa::eddsa_25519_sha512::SecretKey, rand::test_utils::seeded_rng};
220
221    #[test]
222    fn key_agreement() {
223        let mut rng = seeded_rng([0u8; 32]);
224
225        // 1. Generate the static key-pair for Alice
226        let sk = SecretKey::with_rng(&mut rng);
227        let pk = sk.public_key();
228
229        // 2. Generate the ephemeral key-pair for Bob
230        let sk_e = EphemeralSecretKey::with_rng(&mut rng);
231        let pk_e = sk_e.public_key();
232
233        // 3. Bob computes the shared secret key (Bob will send pk_e with the encrypted note to
234        //    Alice)
235        let shared_secret_key_1 = sk_e.diffie_hellman(&pk);
236
237        // 4. Alice uses its secret key and the ephemeral public key sent with the encrypted note by
238        //    Bob in order to create the shared secret key. This shared secret key will be used to
239        //    decrypt the encrypted note
240        let shared_secret_key_2 = sk.get_shared_secret(pk_e);
241
242        // Check that the computed shared secret keys are equal
243        assert_eq!(shared_secret_key_1.inner.to_bytes(), shared_secret_key_2.inner.to_bytes());
244    }
245}