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::{PublicKey, SecretKey},
23    ecdh::KeyAgreementScheme,
24    utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
25    zeroize::{Zeroize, ZeroizeOnDrop},
26};
27
28// SHARED SECRETE
29// ================================================================================================
30
31/// A shared secret computed using the X25519 (Elliptic Curve Diffie-Hellman) key agreement.
32///
33/// This type implements `ZeroizeOnDrop` because the inner `x25519_dalek::SharedSecret`
34/// implements it, ensuring the shared secret is securely wiped from memory when dropped.
35pub struct SharedSecret {
36    pub(crate) inner: x25519_dalek::SharedSecret,
37}
38impl SharedSecret {
39    pub(crate) fn new(inner: x25519_dalek::SharedSecret) -> SharedSecret {
40        Self { inner }
41    }
42
43    /// Returns a HKDF that can be used to derive uniform keys from the shared secret.
44    pub fn extract(&self, salt: Option<&[u8]>) -> Hkdf<Sha256, SimpleHmac<Sha256>> {
45        Hkdf::new(salt, self.inner.as_bytes())
46    }
47}
48
49impl Zeroize for SharedSecret {
50    /// Securely clears the shared secret from memory.
51    ///
52    /// # Security
53    ///
54    /// This implementation follows the same security methodology as the `zeroize` crate to ensure
55    /// that sensitive cryptographic material is reliably cleared from memory:
56    ///
57    /// - **Volatile writes**: Uses `ptr::write_volatile` to prevent dead store elimination and
58    ///   other compiler optimizations that might remove the zeroing operation.
59    /// - **Memory ordering**: Includes a sequentially consistent compiler fence (`SeqCst`) to
60    ///   prevent instruction reordering that could expose the secret data after this function
61    ///   returns.
62    fn zeroize(&mut self) {
63        let bytes = self.inner.as_bytes();
64        for byte in
65            unsafe { core::slice::from_raw_parts_mut(bytes.as_ptr() as *mut u8, bytes.len()) }
66        {
67            unsafe {
68                core::ptr::write_volatile(byte, 0u8);
69            }
70        }
71        core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
72    }
73}
74
75// Safe to derive ZeroizeOnDrop because we implement Zeroize above
76impl ZeroizeOnDrop for SharedSecret {}
77
78impl AsRef<[u8]> for SharedSecret {
79    fn as_ref(&self) -> &[u8] {
80        self.inner.as_bytes()
81    }
82}
83
84// EPHEMERAL SECRET KEY
85// ================================================================================================
86
87/// Ephemeral secret key for X25519 key agreement.
88///
89/// This type implements `ZeroizeOnDrop` because the inner `x25519_dalek::EphemeralSecret`
90/// implements it, ensuring the secret key material is securely wiped from memory when dropped.
91pub struct EphemeralSecretKey {
92    inner: x25519_dalek::EphemeralSecret,
93}
94
95impl ZeroizeOnDrop for EphemeralSecretKey {}
96
97impl EphemeralSecretKey {
98    /// Generates a new random ephemeral secret key using the OS random number generator.
99    #[cfg(feature = "std")]
100    #[allow(clippy::new_without_default)]
101    pub fn new() -> Self {
102        let mut rng = rand::rng();
103
104        Self::with_rng(&mut rng)
105    }
106
107    /// Generates a new random ephemeral secret key using the provided RNG.
108    pub fn with_rng<R: CryptoRng + RngCore>(rng: &mut R) -> Self {
109        // we use a seedable CSPRNG and seed it with `rng`
110        // this is a work around the fact that the version of the `rand` dependency in our crate
111        // is different than the one used in the `x25519_dalek` one. This solution will no longer be
112        // needed once `x25519_dalek` gets a new release with a version of the `rand`
113        // dependency matching ours
114        use k256::elliptic_curve::rand_core::SeedableRng;
115        let mut seed = [0_u8; 32];
116        rand::RngCore::fill_bytes(rng, &mut seed);
117        let rng = rand_hc::Hc128Rng::from_seed(seed);
118
119        let sk = x25519_dalek::EphemeralSecret::random_from_rng(rng);
120        Self { inner: sk }
121    }
122
123    /// Returns the corresponding ephemeral public key.
124    pub fn public_key(&self) -> EphemeralPublicKey {
125        EphemeralPublicKey {
126            inner: x25519_dalek::PublicKey::from(&self.inner),
127        }
128    }
129
130    /// Computes a Diffie-Hellman shared secret from this ephemeral secret key and the other party's
131    /// static public key.
132    pub fn diffie_hellman(self, pk_other: &PublicKey) -> SharedSecret {
133        let shared = self.inner.diffie_hellman(&pk_other.to_x25519());
134        SharedSecret::new(shared)
135    }
136}
137
138// EPHEMERAL PUBLIC KEY
139// ================================================================================================
140
141/// Ephemeral public key for X25519 agreement.
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct EphemeralPublicKey {
144    pub(crate) inner: x25519_dalek::PublicKey,
145}
146
147impl Serializable for EphemeralPublicKey {
148    fn write_into<W: ByteWriter>(&self, target: &mut W) {
149        target.write_bytes(self.inner.as_bytes());
150    }
151}
152
153impl Deserializable for EphemeralPublicKey {
154    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
155        let bytes: [u8; 32] = source.read_array()?;
156        Ok(Self {
157            inner: x25519_dalek::PublicKey::from(bytes),
158        })
159    }
160}
161
162// KEY AGREEMENT TRAIT IMPLEMENTATION
163// ================================================================================================
164
165pub struct X25519;
166
167impl KeyAgreementScheme for X25519 {
168    type EphemeralSecretKey = EphemeralSecretKey;
169    type EphemeralPublicKey = EphemeralPublicKey;
170
171    type SecretKey = SecretKey;
172    type PublicKey = PublicKey;
173
174    type SharedSecret = SharedSecret;
175
176    fn generate_ephemeral_keypair<R: CryptoRng + RngCore>(
177        rng: &mut R,
178    ) -> (Self::EphemeralSecretKey, Self::EphemeralPublicKey) {
179        let sk = EphemeralSecretKey::with_rng(rng);
180        let pk = sk.public_key();
181
182        (sk, pk)
183    }
184
185    fn exchange_ephemeral_static(
186        ephemeral_sk: Self::EphemeralSecretKey,
187        static_pk: &Self::PublicKey,
188    ) -> Result<Self::SharedSecret, super::KeyAgreementError> {
189        Ok(ephemeral_sk.diffie_hellman(static_pk))
190    }
191
192    fn exchange_static_ephemeral(
193        static_sk: &Self::SecretKey,
194        ephemeral_pk: &Self::EphemeralPublicKey,
195    ) -> Result<Self::SharedSecret, super::KeyAgreementError> {
196        Ok(static_sk.get_shared_secret(ephemeral_pk.clone()))
197    }
198
199    fn extract_key_material(
200        shared_secret: &Self::SharedSecret,
201        length: usize,
202    ) -> Result<Vec<u8>, super::KeyAgreementError> {
203        let hkdf = shared_secret.extract(None);
204        let mut buf = vec![0_u8; length];
205        hkdf.expand(&[], &mut buf)
206            .map_err(|_| super::KeyAgreementError::HkdfExpansionFailed)?;
207        Ok(buf)
208    }
209}
210
211// TESTS
212// ================================================================================================
213
214#[cfg(test)]
215mod tests {
216    use rand::rng;
217
218    use super::*;
219    use crate::dsa::eddsa_25519::SecretKey;
220
221    #[test]
222    fn key_agreement() {
223        let mut rng = rng();
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}