arcana/ecc/x25519.rs
1//! X25519 Diffie-Hellman key agreement on Curve25519 (RFC 7748).
2//!
3//! X25519 is the "EDDH" (Edwards/Montgomery Diffie-Hellman) pairing
4//! around Curve25519 — a Montgomery curve over `p = 2^255 - 19`. It
5//! is the ECDH variant used by TLS 1.3, Noise, Signal, WireGuard,
6//! and most modern protocols that want a fixed, fast, audit-friendly
7//! ECDH.
8//!
9//! Unlike the short-Weierstrass curves exposed through the
10//! [`super::curves::Curve`] trait, X25519 works entirely on the
11//! **u-coordinate** (the x-coordinate of the Montgomery form). There
12//! is no point addition, no compressed/uncompressed distinction, and
13//! no branch on the y-coordinate — the Montgomery ladder operates on
14//! projective (X:Z) pairs and returns a single 32-byte little-endian
15//! u-coordinate.
16//!
17//! # Side-channel posture
18//!
19//! Per `arcana/doc/sca/countermeasures/x25519_x448.rst`:
20//!
21//! | Threat | Status | Roadmap item |
22//! |---------------------------------------------------|------------|-----------------------------------------------------------|
23//! | Cache-timing on Montgomery ladder | partial | `T1-G` — audit pass mirroring Weierstrass commit `76191c1`|
24//! | SPA on Cortex-M0 (Weissbart-Picek-Batina 2021) | vulnerable | `T1-G` + `T2-A` (Z-rerand defeats their template attack) |
25//! | DPA on field operations | vulnerable | `T2-A` — Z-rerandomization on `(X : Z)` |
26//! | Template attacks | vulnerable | `T2-A` (alignment break) + `T2-B` (scalar blinding) |
27//! | Invalid-curve attack on peer pubkey | covered | RFC 7748 twist security |
28//! | Small-subgroup contributory check | partial | `T2-K` — confirm CT all-zero rejection |
29//!
30//! Curve25519 is CT **by construction** (single u-coordinate, no
31//! special cases for the neutral element), but the concrete
32//! Rust implementation can still leak through the `cswap` mask
33//! pattern (see `super::field` `black_box` shielding) and
34//! through unmodelled cache-line accesses. The
35//! `weissbart2021_curve25519_ml_sca` paper demonstrated
36//! deep-learning template attacks on Cortex-M0 even against
37//! random-delay defences; Z-rerandomization is the standard
38//! answer.
39//!
40//! # API
41//!
42//! ```rust,ignore
43//! use arcana::ecc::x25519::{x25519_derive_public, x25519_ecdh};
44//!
45//! // Alice and Bob each draw 32 random bytes as their secret key.
46//! let alice_sk: [u8; 32] = /* rng */;
47//! let bob_sk: [u8; 32] = /* rng */;
48//!
49//! // Derive public keys.
50//! let alice_pk = x25519_derive_public(&alice_sk);
51//! let bob_pk = x25519_derive_public(&bob_sk);
52//!
53//! // Exchange public keys, then each derives the shared secret.
54//! let s_ab = x25519_ecdh(&alice_sk, &bob_pk);
55//! let s_ba = x25519_ecdh(&bob_sk, &alice_pk);
56//! assert_eq!(s_ab, s_ba);
57//! ```
58//!
59//! # Test vectors
60//!
61//! The tests at the bottom of this file pin the two §5.2 primitive
62//! vectors and the §6.1 full Diffie-Hellman vector directly from
63//! RFC 7748. Any future regression in the ladder, the clamping, or
64//! the LE byte encoding fails against those bytes immediately.
65
66use super::field::*;
67
68// ============================================================================
69// Curve constants (RFC 7748 §4.1)
70// ============================================================================
71
72/// `a24 = (486662 - 2) / 4 = 121665` — the constant used inside the
73/// Montgomery ladder's doubling step.
74///
75/// Declared as a `FieldElement<4>` so it can be fed directly into
76/// `field_mul` without re-encoding on every call.
77fn a24() -> FieldElement<4> {
78 let mut fe = FieldElement::<4>::ZERO;
79 fe.limbs[0] = 121_665;
80 fe
81}
82
83/// X25519 base point u-coordinate = 9 (RFC 7748 §4.1).
84const BASE_U: [u8; 32] = {
85 let mut b = [0u8; 32];
86 b[0] = 9;
87 b
88};
89
90// ============================================================================
91// Scalar clamping + u-coordinate decoding (RFC 7748 §5)
92// ============================================================================
93
94/// RFC 7748 §5 `decodeScalar25519`: clamps the 32-byte secret scalar
95/// so that the resulting integer always lies in [2^254, 2^255) and
96/// is a multiple of 8. This is the property that lets the Montgomery
97/// ladder always iterate exactly 255 times with the top bit at
98/// position 254 guaranteed to be 1.
99///
100/// Concretely:
101/// - Clear the 3 low bits of byte 0 (= make the scalar a multiple of 8)
102/// - Clear the high bit of byte 31 (= force bit 255 = 0)
103/// - Set bit 6 of byte 31 (= force bit 254 = 1)
104fn decode_scalar(scalar: &[u8; 32]) -> [u8; 32] {
105 let mut k = *scalar;
106 k[0] &= 248; // 0b11111000
107 k[31] &= 127; // 0b01111111
108 k[31] |= 64; // 0b01000000
109 k
110}
111
112/// RFC 7748 §5 `decodeUCoordinate` (Curve25519 variant):
113/// clear the most significant bit of the last byte, then interpret
114/// as a little-endian integer in `Fp`.
115///
116/// Clearing the top bit matters because some peers (historically TLS)
117/// don't mask it themselves; the spec requires the receiver to be
118/// permissive.
119fn decode_u(u: &[u8; 32]) -> FieldElement<4> {
120 let mut buf = *u;
121 buf[31] &= 0x7f;
122 FieldElement::<4>::from_bytes_le(&buf)
123}
124
125/// Encode a field element as 32 little-endian bytes.
126fn encode_u(fe: &FieldElement<4>) -> [u8; 32] {
127 let v = fe.to_bytes_le();
128 let mut out = [0u8; 32];
129 out.copy_from_slice(&v);
130 out
131}
132
133// ============================================================================
134// Constant-time conditional swap of two field elements
135// ============================================================================
136
137/// Constant-time swap of `a` and `b` iff `swap == 1`.
138///
139/// `swap` must be 0 or 1. Uses a word-wide mask and XOR trick so the
140/// data dependency is the same for both branches (essential: the
141/// `swap` bit is derived from the secret scalar).
142fn ct_swap_fe(a: &mut FieldElement<4>, b: &mut FieldElement<4>, swap: u64) {
143 let mask = 0u64.wrapping_sub(swap);
144 for i in 0..4 {
145 let t = mask & (a.limbs[i] ^ b.limbs[i]);
146 a.limbs[i] ^= t;
147 b.limbs[i] ^= t;
148 }
149}
150
151// ============================================================================
152// The X25519 primitive — RFC 7748 §5, Montgomery ladder on (X:Z)
153// ============================================================================
154
155/// RFC 7748 §5 `X25519(scalar, u)`.
156///
157/// Takes a 32-byte little-endian scalar and a 32-byte little-endian
158/// u-coordinate, and returns the 32-byte little-endian u-coordinate
159/// of `scalar * (u, v)` on Curve25519 (where `v` is uniquely
160/// determined by `u` up to sign — we never need it).
161///
162/// The ladder operates on projective `(X, Z)` pairs where the affine
163/// u-coordinate is `X/Z`. 255 ladder steps are performed (bits 254..0
164/// of the clamped scalar); the high bit 254 is always 1 after
165/// clamping, so the first iteration deterministically initialises
166/// (x_2, x_3) = (u, 1), (z_2, z_3) = (1, u) via the cswap.
167pub fn x25519(scalar: &[u8; 32], u: &[u8; 32]) -> [u8; 32] {
168 let k = decode_scalar(scalar);
169 let x1 = decode_u(u);
170 let a24 = a24();
171 let p = &CURVE25519_P;
172
173 // Ladder state: x_2 = 1, z_2 = 0, x_3 = x1, z_3 = 1.
174 let mut x_2 = FieldElement::<4>::one();
175 let mut z_2 = FieldElement::<4>::ZERO;
176 let mut x_3 = x1;
177 let mut z_3 = FieldElement::<4>::one();
178
179 // `swap` tracks the conditional swap state between iterations.
180 // See RFC 7748 §5 for the reference Python pseudocode.
181 let mut swap: u64 = 0;
182
183 for t in (0..=254).rev() {
184 let k_t = ((k[t >> 3] >> (t & 7)) & 1) as u64;
185 swap ^= k_t;
186 ct_swap_fe(&mut x_2, &mut x_3, swap);
187 ct_swap_fe(&mut z_2, &mut z_3, swap);
188 swap = k_t;
189
190 // A = x_2 + z_2
191 // AA = A^2
192 // B = x_2 - z_2
193 // BB = B^2
194 // E = AA - BB
195 // C = x_3 + z_3
196 // D = x_3 - z_3
197 // DA = D * A
198 // CB = C * B
199 // x_3 = (DA + CB)^2
200 // z_3 = x_1 * (DA - CB)^2
201 // x_2 = AA * BB
202 // z_2 = E * (AA + a24 * E)
203 let a = field_add(&x_2, &z_2, p);
204 let aa = field_sqr(&a, p);
205 let b = field_sub(&x_2, &z_2, p);
206 let bb = field_sqr(&b, p);
207 let e = field_sub(&aa, &bb, p);
208 let c = field_add(&x_3, &z_3, p);
209 let d = field_sub(&x_3, &z_3, p);
210 let da = field_mul(&d, &a, p);
211 let cb = field_mul(&c, &b, p);
212
213 let da_plus_cb = field_add(&da, &cb, p);
214 x_3 = field_sqr(&da_plus_cb, p);
215
216 let da_minus_cb = field_sub(&da, &cb, p);
217 let da_minus_cb_sq = field_sqr(&da_minus_cb, p);
218 z_3 = field_mul(&x1, &da_minus_cb_sq, p);
219
220 x_2 = field_mul(&aa, &bb, p);
221
222 let a24_e = field_mul(&a24, &e, p);
223 let aa_plus_a24e = field_add(&aa, &a24_e, p);
224 z_2 = field_mul(&e, &aa_plus_a24e, p);
225 }
226
227 // Final swap based on the residual `swap` state.
228 ct_swap_fe(&mut x_2, &mut x_3, swap);
229 ct_swap_fe(&mut z_2, &mut z_3, swap);
230
231 // Return x_2 / z_2 = x_2 * z_2^{p-2} mod p as 32 LE bytes.
232 let z_inv = field_inv(&z_2, p);
233 let result = field_mul(&x_2, &z_inv, p);
234 encode_u(&result)
235}
236
237// ============================================================================
238// Public API — keygen + ECDH convenience wrappers
239// ============================================================================
240
241/// Derive the X25519 **public key** from a 32-byte secret key.
242///
243/// The secret key is any 32 random bytes; the caller is responsible
244/// for drawing them from a suitable CSPRNG. The returned public key
245/// is the 32-byte u-coordinate of `sk * base` on Curve25519.
246///
247/// This is `X25519(sk, 9)` per RFC 7748 §5.
248pub fn x25519_derive_public(sk: &[u8; 32]) -> [u8; 32] {
249 x25519(sk, &BASE_U)
250}
251
252/// X25519 Diffie-Hellman: derive a shared secret from our secret key
253/// and the peer's public key.
254///
255/// Returns the 32-byte u-coordinate of `sk * peer_pk`. This is the
256/// raw shared secret (NIST SP 800-56A "Z"); pass it to an HKDF or
257/// similar KDF before using it for symmetric keying.
258///
259/// # Small-subgroup attack note
260///
261/// RFC 7748 §6.1 warns that X25519 accepts certain "contributory"
262/// public keys (points in small subgroups) that collapse the shared
263/// secret to a fixed value. A defensive implementation may want to
264/// reject the result if it is all-zero, which signals the peer sent
265/// a low-order point. We deliberately do **not** reject here because
266/// (a) the spec allows it, and (b) a downstream KDF with context
267/// binding (TLS 1.3 `transcript_hash`, Noise `HKDF`, ...) is the
268/// standard mitigation. Callers who want the low-order check can
269/// run it themselves:
270///
271/// ```rust,ignore
272/// let shared = x25519_ecdh(&sk, &peer_pk);
273/// if shared.iter().all(|&b| b == 0) {
274/// return Err("contributory shared secret");
275/// }
276/// ```
277pub fn x25519_ecdh(sk: &[u8; 32], peer_pk: &[u8; 32]) -> [u8; 32] {
278 x25519(sk, peer_pk)
279}
280
281// ============================================================================
282// Tests (RFC 7748 pinned vectors)
283// ============================================================================
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 fn hex32(h: &str) -> [u8; 32] {
290 assert_eq!(h.len(), 64);
291 let mut out = [0u8; 32];
292 for i in 0..32 {
293 out[i] = u8::from_str_radix(&h[2 * i..2 * i + 2], 16).unwrap();
294 }
295 out
296 }
297
298 // ----- RFC 7748 §5.2 primitive test vector #1 -----
299 //
300 // Scalar: a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4
301 // u: e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c
302 // X25519(k,u): c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552
303 #[test]
304 fn rfc7748_section_5_2_vector_1() {
305 let scalar = hex32("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4");
306 let u = hex32("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c");
307 let expected = hex32("c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552");
308 let got = x25519(&scalar, &u);
309 assert_eq!(got, expected);
310 }
311
312 // ----- RFC 7748 §5.2 primitive test vector #2 -----
313 //
314 // Scalar: 4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d
315 // u: e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493
316 // X25519(k,u): 95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957
317 #[test]
318 fn rfc7748_section_5_2_vector_2() {
319 let scalar = hex32("4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d");
320 let u = hex32("e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493");
321 let expected = hex32("95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957");
322 let got = x25519(&scalar, &u);
323 assert_eq!(got, expected);
324 }
325
326 // ----- RFC 7748 §6.1 full Diffie-Hellman test vector -----
327 //
328 // Alice's private key: 77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a
329 // Alice's public key: 8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a
330 // Bob's private key: 5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb
331 // Bob's public key: de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f
332 // Shared secret (K): 4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742
333 #[test]
334 fn rfc7748_section_6_1_alice_pk() {
335 let alice_sk = hex32("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a");
336 let expected = hex32("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a");
337 assert_eq!(x25519_derive_public(&alice_sk), expected);
338 }
339
340 #[test]
341 fn rfc7748_section_6_1_bob_pk() {
342 let bob_sk = hex32("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb");
343 let expected = hex32("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f");
344 assert_eq!(x25519_derive_public(&bob_sk), expected);
345 }
346
347 #[test]
348 fn rfc7748_section_6_1_shared_secret() {
349 let alice_sk = hex32("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a");
350 let bob_sk = hex32("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb");
351 let alice_pk = x25519_derive_public(&alice_sk);
352 let bob_pk = x25519_derive_public(&bob_sk);
353 let expected = hex32("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742");
354 assert_eq!(x25519_ecdh(&alice_sk, &bob_pk), expected);
355 assert_eq!(x25519_ecdh(&bob_sk, &alice_pk), expected);
356 }
357
358 // ----- Roundtrip on an arbitrary key pair -----
359 #[test]
360 fn x25519_roundtrip_custom_keys() {
361 let alice_sk = hex32("0101010101010101010101010101010101010101010101010101010101010101");
362 let bob_sk = hex32("0202020202020202020202020202020202020202020202020202020202020202");
363 let alice_pk = x25519_derive_public(&alice_sk);
364 let bob_pk = x25519_derive_public(&bob_sk);
365 let s_ab = x25519_ecdh(&alice_sk, &bob_pk);
366 let s_ba = x25519_ecdh(&bob_sk, &alice_pk);
367 assert_eq!(s_ab, s_ba);
368 // Sanity: the shared secret is non-zero (an all-zero output
369 // would signal a low-order input point; our TestRng keys are
370 // not in a small subgroup).
371 assert!(s_ab.iter().any(|&b| b != 0));
372 }
373
374 // ----- Clamping test -----
375 //
376 // Changing any of the bits that `decode_scalar` forces (low 3
377 // bits of byte 0, top 2 bits of byte 31) must not change the
378 // output. This exercises the clamping path in isolation.
379 #[test]
380 fn clamping_is_idempotent() {
381 let base = hex32("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4");
382 let u = hex32("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c");
383 let ref_out = x25519(&base, &u);
384
385 // Force-dirty the bits that clamping will clear anyway.
386 let mut dirty = base;
387 dirty[0] |= 0b0000_0111; // set low 3 bits (will be cleared)
388 dirty[31] |= 0b1000_0000; // set high bit (will be cleared)
389 dirty[31] &= !0b0100_0000; // clear bit 6 (will be set)
390 let dirty_out = x25519(&dirty, &u);
391 assert_eq!(ref_out, dirty_out);
392 }
393
394 // ----- High-bit-of-u decoding test -----
395 //
396 // decode_u clears the top bit of byte 31 per RFC 7748, so toggling
397 // that bit on the input u must not change the output.
398 #[test]
399 fn u_high_bit_is_ignored() {
400 let scalar = hex32("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4");
401 let u = hex32("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c");
402 let ref_out = x25519(&scalar, &u);
403
404 let mut u_dirty = u;
405 u_dirty[31] |= 0x80;
406 let dirty_out = x25519(&scalar, &u_dirty);
407 assert_eq!(ref_out, dirty_out);
408 }
409}