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)]
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 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
150pub 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 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#[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 let sk = KeyExchangeKey::with_rng(&mut rng);
232 let pk = sk.public_key();
233
234 let sk_e = EphemeralSecretKey::with_rng(&mut rng);
236 let pk_e = sk_e.public_key();
237
238 let shared_secret_key_1 = sk_e.diffie_hellman(&pk);
241
242 let shared_secret_key_2 = sk.get_shared_secret(pk_e);
246
247 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}