Skip to main content

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}