miden_crypto/ecdh/
x25519.rs1use alloc::vec::Vec;
16
17use hkdf::{Hkdf, hmac::SimpleHmac};
18use k256::sha2::Sha256;
19use rand::{CryptoRng, RngCore};
20use subtle::ConstantTimeEq;
21
22use crate::{
23 dsa::eddsa_25519_sha512::{PublicKey, SecretKey},
24 ecdh::KeyAgreementScheme,
25 utils::{
26 ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
27 zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing},
28 },
29};
30
31pub struct SharedSecret {
39 pub(crate) inner: x25519_dalek::SharedSecret,
40}
41impl SharedSecret {
42 pub(crate) fn new(inner: x25519_dalek::SharedSecret) -> SharedSecret {
43 Self { inner }
44 }
45
46 pub fn extract(&self, salt: Option<&[u8]>) -> Hkdf<Sha256, SimpleHmac<Sha256>> {
48 Hkdf::new(salt, self.inner.as_bytes())
49 }
50}
51
52impl Zeroize for SharedSecret {
53 fn zeroize(&mut self) {
66 let bytes = self.inner.as_bytes();
67 for byte in
68 unsafe { core::slice::from_raw_parts_mut(bytes.as_ptr() as *mut u8, bytes.len()) }
69 {
70 unsafe {
71 core::ptr::write_volatile(byte, 0u8);
72 }
73 }
74 core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
75 }
76}
77
78impl ZeroizeOnDrop for SharedSecret {}
80
81impl AsRef<[u8]> for SharedSecret {
82 fn as_ref(&self) -> &[u8] {
83 self.inner.as_bytes()
84 }
85}
86
87pub struct EphemeralSecretKey {
95 inner: x25519_dalek::EphemeralSecret,
96}
97
98impl ZeroizeOnDrop for EphemeralSecretKey {}
99
100impl EphemeralSecretKey {
101 #[cfg(feature = "std")]
103 #[allow(clippy::new_without_default)]
104 pub fn new() -> Self {
105 let mut rng = rand::rng();
106
107 Self::with_rng(&mut rng)
108 }
109
110 pub fn with_rng<R: CryptoRng + RngCore>(rng: &mut R) -> Self {
112 use k256::elliptic_curve::rand_core::SeedableRng;
118 let mut seed = Zeroizing::new([0_u8; 32]);
119 rand::RngCore::fill_bytes(rng, &mut *seed);
120 let rng = rand_hc::Hc128Rng::from_seed(*seed);
121
122 let sk = x25519_dalek::EphemeralSecret::random_from_rng(rng);
123 Self { inner: sk }
124 }
125
126 pub fn public_key(&self) -> EphemeralPublicKey {
128 EphemeralPublicKey {
129 inner: x25519_dalek::PublicKey::from(&self.inner),
130 }
131 }
132
133 pub fn diffie_hellman(self, pk_other: &PublicKey) -> SharedSecret {
136 let shared = self.inner.diffie_hellman(&pk_other.to_x25519());
137 SharedSecret::new(shared)
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct EphemeralPublicKey {
147 pub(crate) inner: x25519_dalek::PublicKey,
148}
149
150impl Serializable for EphemeralPublicKey {
151 fn write_into<W: ByteWriter>(&self, target: &mut W) {
152 target.write_bytes(self.inner.as_bytes());
153 }
154}
155
156impl Deserializable for EphemeralPublicKey {
157 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
158 let bytes: [u8; 32] = source.read_array()?;
159 let mont = curve25519_dalek::montgomery::MontgomeryPoint(bytes);
163 let edwards = mont.to_edwards(0).ok_or_else(|| {
164 DeserializationError::InvalidValue("Invalid X25519 public key".into())
165 })?;
166 if edwards.is_small_order() {
167 return Err(DeserializationError::InvalidValue("Invalid X25519 public key".into()));
168 }
169
170 Ok(Self {
171 inner: x25519_dalek::PublicKey::from(bytes),
172 })
173 }
174}
175
176pub struct X25519;
180
181impl KeyAgreementScheme for X25519 {
182 type EphemeralSecretKey = EphemeralSecretKey;
183 type EphemeralPublicKey = EphemeralPublicKey;
184
185 type SecretKey = SecretKey;
186 type PublicKey = PublicKey;
187
188 type SharedSecret = SharedSecret;
189
190 fn generate_ephemeral_keypair<R: CryptoRng + RngCore>(
191 rng: &mut R,
192 ) -> (Self::EphemeralSecretKey, Self::EphemeralPublicKey) {
193 let sk = EphemeralSecretKey::with_rng(rng);
194 let pk = sk.public_key();
195
196 (sk, pk)
197 }
198
199 fn exchange_ephemeral_static(
200 ephemeral_sk: Self::EphemeralSecretKey,
201 static_pk: &Self::PublicKey,
202 ) -> Result<Self::SharedSecret, super::KeyAgreementError> {
203 let shared = ephemeral_sk.diffie_hellman(static_pk);
204 if is_all_zero(shared.as_ref()) {
205 return Err(super::KeyAgreementError::InvalidSharedSecret);
206 }
207 Ok(shared)
208 }
209
210 fn exchange_static_ephemeral(
211 static_sk: &Self::SecretKey,
212 ephemeral_pk: &Self::EphemeralPublicKey,
213 ) -> Result<Self::SharedSecret, super::KeyAgreementError> {
214 let shared = static_sk.get_shared_secret(ephemeral_pk.clone());
215 if is_all_zero(shared.as_ref()) {
216 return Err(super::KeyAgreementError::InvalidSharedSecret);
217 }
218 Ok(shared)
219 }
220
221 fn extract_key_material(
222 shared_secret: &Self::SharedSecret,
223 length: usize,
224 info: &[u8],
225 ) -> Result<Vec<u8>, super::KeyAgreementError> {
226 let hkdf = shared_secret.extract(None);
227 let mut buf = vec![0_u8; length];
228 hkdf.expand(info, &mut buf)
229 .map_err(|_| super::KeyAgreementError::HkdfExpansionFailed)?;
230 Ok(buf)
231 }
232}
233
234fn is_all_zero(bytes: &[u8]) -> bool {
235 if bytes.is_empty() {
237 return false;
238 }
239 let acc = bytes.iter().fold(0u8, |acc, &byte| acc | byte);
240 acc.ct_eq(&0u8).into()
241}
242
243#[cfg(test)]
247mod tests {
248 use curve25519_dalek::{constants::EIGHT_TORSION, montgomery::MontgomeryPoint};
249
250 use super::*;
251 use crate::{
252 dsa::eddsa_25519_sha512::SecretKey, ecdh::KeyAgreementError, rand::test_utils::seeded_rng,
253 utils::Deserializable,
254 };
255
256 #[test]
257 fn key_agreement() {
258 let mut rng = seeded_rng([0u8; 32]);
259
260 let sk = SecretKey::with_rng(&mut rng);
262 let pk = sk.public_key();
263
264 let sk_e = EphemeralSecretKey::with_rng(&mut rng);
266 let pk_e = sk_e.public_key();
267
268 let shared_secret_key_1 = sk_e.diffie_hellman(&pk);
271
272 let shared_secret_key_2 = sk.get_shared_secret(pk_e);
276
277 assert_eq!(shared_secret_key_1.inner.to_bytes(), shared_secret_key_2.inner.to_bytes());
279 }
280
281 #[test]
282 fn ephemeral_public_key_rejects_small_order() {
283 let bytes = EIGHT_TORSION[1].to_montgomery().to_bytes();
284 let result = EphemeralPublicKey::read_from_bytes(&bytes);
285 assert!(result.is_err());
286 }
287
288 #[test]
289 fn ephemeral_public_key_rejects_twist_point() {
290 let bytes = find_twist_point_bytes();
291 let result = EphemeralPublicKey::read_from_bytes(&bytes);
292 assert!(result.is_err());
293 }
294
295 #[test]
296 fn exchange_static_ephemeral_rejects_zero_shared_secret() {
297 let mut rng = seeded_rng([0u8; 32]);
298 let static_sk = SecretKey::with_rng(&mut rng);
299
300 let low_order_bytes = EIGHT_TORSION[0].to_montgomery().to_bytes();
301 let low_order_pk = EphemeralPublicKey {
302 inner: x25519_dalek::PublicKey::from(low_order_bytes),
303 };
304
305 let result = X25519::exchange_static_ephemeral(&static_sk, &low_order_pk);
306 assert!(matches!(result, Err(KeyAgreementError::InvalidSharedSecret)));
307 }
308
309 #[test]
310 fn is_all_zero_accepts_arbitrary_lengths() {
311 assert!(!is_all_zero(&[]));
312 assert!(is_all_zero(&[0u8; 16]));
313 assert!(!is_all_zero(&[0u8, 1u8, 0u8, 0u8]));
314 }
315
316 fn find_twist_point_bytes() -> [u8; 32] {
317 let mut bytes = [0u8; 32];
318 for i in 0u16..=u16::MAX {
319 bytes[0] = (i & 0xff) as u8;
320 bytes[1] = (i >> 8) as u8;
321 if MontgomeryPoint(bytes).to_edwards(0).is_none() {
322 return bytes;
323 }
324 }
325 panic!("no twist point found in 16-bit search space");
326 }
327}