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