miden_crypto/ecdh/
x25519.rs1use 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};
30pub 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 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
71pub struct EphemeralSecretKey {
79 inner: x25519_dalek::EphemeralSecret,
80}
81
82impl ZeroizeOnDrop for EphemeralSecretKey {}
83
84impl EphemeralSecretKey {
85 #[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 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 pub fn public_key(&self) -> EphemeralPublicKey {
102 EphemeralPublicKey {
103 inner: x25519_dalek::PublicKey::from(&self.inner),
104 }
105 }
106
107 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#[derive(Debug, Clone, PartialEq, Eq)]
120#[cfg_attr(
121 all(feature = "arbitrary", test),
122 miden_test_serialization_macros::serialization_test
123)]
124pub struct EphemeralPublicKey {
125 pub(crate) inner: x25519_dalek::PublicKey,
126}
127
128#[cfg(any(test, feature = "arbitrary"))]
129mod ephemeral_key_arbitrary {
130 use proptest::prelude::*;
131 use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng};
132
133 use super::EphemeralPublicKey;
134 use crate::ecdh::x25519::EphemeralSecretKey;
135
136 impl Arbitrary for EphemeralPublicKey {
137 type Parameters = ();
138 type Strategy = BoxedStrategy<Self>;
139
140 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
141 any::<[u8; 32]>()
144 .prop_map(|seed| {
145 let mut rng = ChaCha20Rng::from_seed(seed);
146 EphemeralSecretKey::with_rng(&mut rng).public_key()
147 })
148 .boxed()
149 }
150 }
151}
152
153impl Serializable for EphemeralPublicKey {
154 fn write_into<W: ByteWriter>(&self, target: &mut W) {
155 target.write_bytes(self.inner.as_bytes());
156 }
157}
158
159impl Deserializable for EphemeralPublicKey {
160 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
161 let bytes: [u8; 32] = source.read_array()?;
162 let mont = curve25519_dalek::montgomery::MontgomeryPoint(bytes);
166 let edwards = mont.to_edwards(0).ok_or_else(|| {
167 DeserializationError::InvalidValue("Invalid X25519 public key".into())
168 })?;
169 if edwards.is_small_order() {
170 return Err(DeserializationError::InvalidValue("Invalid X25519 public key".into()));
171 }
172
173 Ok(Self {
174 inner: x25519_dalek::PublicKey::from(bytes),
175 })
176 }
177}
178
179pub struct X25519;
183
184impl KeyAgreementScheme for X25519 {
185 type EphemeralSecretKey = EphemeralSecretKey;
186 type EphemeralPublicKey = EphemeralPublicKey;
187
188 type SecretKey = KeyExchangeKey;
189 type PublicKey = PublicKey;
190
191 type SharedSecret = SharedSecret;
192
193 fn generate_ephemeral_keypair<R: CryptoRng>(
194 rng: &mut R,
195 ) -> (Self::EphemeralSecretKey, Self::EphemeralPublicKey) {
196 let sk = EphemeralSecretKey::with_rng(rng);
197 let pk = sk.public_key();
198
199 (sk, pk)
200 }
201
202 fn exchange_ephemeral_static(
203 ephemeral_sk: Self::EphemeralSecretKey,
204 static_pk: &Self::PublicKey,
205 ) -> Result<Self::SharedSecret, super::KeyAgreementError> {
206 let shared = ephemeral_sk.diffie_hellman(static_pk);
207 if is_all_zero(shared.as_ref()) {
208 return Err(super::KeyAgreementError::InvalidSharedSecret);
209 }
210 Ok(shared)
211 }
212
213 fn exchange_static_ephemeral(
214 static_sk: &Self::SecretKey,
215 ephemeral_pk: &Self::EphemeralPublicKey,
216 ) -> Result<Self::SharedSecret, super::KeyAgreementError> {
217 let shared = static_sk.get_shared_secret(ephemeral_pk.clone());
218 if is_all_zero(shared.as_ref()) {
219 return Err(super::KeyAgreementError::InvalidSharedSecret);
220 }
221 Ok(shared)
222 }
223
224 fn extract_key_material(
225 shared_secret: &Self::SharedSecret,
226 length: usize,
227 info: &[u8],
228 ) -> Result<Vec<u8>, super::KeyAgreementError> {
229 super::extract_key_material(shared_secret.as_ref(), None, length, info)
230 }
231}
232
233fn is_all_zero(bytes: &[u8]) -> bool {
234 if bytes.is_empty() {
236 return false;
237 }
238 let acc = bytes.iter().fold(0u8, |acc, &byte| acc | byte);
239 acc.ct_eq(&0u8).into()
240}
241
242#[cfg(test)]
246mod tests {
247 use curve25519_dalek::{constants::EIGHT_TORSION, montgomery::MontgomeryPoint};
248
249 use super::*;
250 use crate::{
251 dsa::eddsa_25519_sha512::KeyExchangeKey, ecdh::KeyAgreementError,
252 rand::test_utils::seeded_rng, utils::Deserializable,
253 };
254
255 #[test]
256 fn key_agreement() {
257 let mut rng = seeded_rng([0u8; 32]);
258
259 let sk = KeyExchangeKey::with_rng(&mut rng);
261 let pk = sk.public_key();
262
263 let sk_e = EphemeralSecretKey::with_rng(&mut rng);
265 let pk_e = sk_e.public_key();
266
267 let shared_secret_key_1 = sk_e.diffie_hellman(&pk);
270
271 let shared_secret_key_2 = sk.get_shared_secret(pk_e);
275
276 assert_eq!(shared_secret_key_1.as_ref(), shared_secret_key_2.as_ref());
278 }
279
280 #[test]
281 fn ephemeral_public_key_rejects_small_order() {
282 let bytes = EIGHT_TORSION[1].to_montgomery().to_bytes();
283 let result = EphemeralPublicKey::read_from_bytes(&bytes);
284 assert!(result.is_err());
285 }
286
287 #[test]
288 fn ephemeral_public_key_rejects_twist_point() {
289 let bytes = find_twist_point_bytes();
290 let result = EphemeralPublicKey::read_from_bytes(&bytes);
291 assert!(result.is_err());
292 }
293
294 #[test]
295 fn exchange_static_ephemeral_rejects_zero_shared_secret() {
296 let mut rng = seeded_rng([0u8; 32]);
297 let static_sk = KeyExchangeKey::with_rng(&mut rng);
298
299 let low_order_bytes = EIGHT_TORSION[0].to_montgomery().to_bytes();
300 let low_order_pk = EphemeralPublicKey {
301 inner: x25519_dalek::PublicKey::from(low_order_bytes),
302 };
303
304 let result = X25519::exchange_static_ephemeral(&static_sk, &low_order_pk);
305 assert!(matches!(result, Err(KeyAgreementError::InvalidSharedSecret)));
306 }
307
308 #[test]
309 fn is_all_zero_accepts_arbitrary_lengths() {
310 assert!(!is_all_zero(&[]));
311 assert!(is_all_zero(&[0u8; 16]));
312 assert!(!is_all_zero(&[0u8, 1u8, 0u8, 0u8]));
313 }
314
315 fn find_twist_point_bytes() -> [u8; 32] {
316 let mut bytes = [0u8; 32];
317 for i in 0u16..=u16::MAX {
318 bytes[0] = (i & 0xff) as u8;
319 bytes[1] = (i >> 8) as u8;
320 if MontgomeryPoint(bytes).to_edwards(0).is_none() {
321 return bytes;
322 }
323 }
324 panic!("no twist point found in 16-bit search space");
325 }
326}