web engine - experimental web browser

Implement X25519 Diffie-Hellman key exchange (RFC 7748)

Field arithmetic in GF(2^255 - 19) using 5x51-bit limb representation.
Montgomery ladder scalar multiplication on Curve25519. Constant-time
implementation with no secret-dependent branches or memory accesses.

Passes RFC 7748 §6.1 DH test vectors and §5.2 iterated test vector
(1,000 iterations).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

authored by pierrelf.com

Claude Opus 4.6 and committed by tangled.org b455eb13 4be3d255

+669
+1
crates/crypto/src/lib.rs
··· 5 5 pub mod hkdf; 6 6 pub mod hmac; 7 7 pub mod sha2; 8 + pub mod x25519;
+668
crates/crypto/src/x25519.rs
··· 1 + //! X25519 Diffie-Hellman key exchange (RFC 7748). 2 + //! 3 + //! Field arithmetic in GF(2^255 - 19) using 5×51-bit limb representation. 4 + //! Montgomery ladder scalar multiplication on Curve25519. 5 + //! Constant-time: no secret-dependent branches or memory accesses. 6 + 7 + // --------------------------------------------------------------------------- 8 + // GF(2^255 - 19) field element 9 + // --------------------------------------------------------------------------- 10 + 11 + const MASK51: u64 = (1u64 << 51) - 1; 12 + 13 + /// Basepoint u-coordinate = 9. 14 + const BASEPOINT: [u8; 32] = [ 15 + 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 + ]; 17 + 18 + #[derive(Clone, Copy)] 19 + struct Fe([u64; 5]); 20 + 21 + impl Fe { 22 + const ZERO: Fe = Fe([0; 5]); 23 + const ONE: Fe = Fe([1, 0, 0, 0, 0]); 24 + } 25 + 26 + // --------------------------------------------------------------------------- 27 + // Field encoding / decoding 28 + // --------------------------------------------------------------------------- 29 + 30 + fn fe_frombytes(bytes: &[u8; 32]) -> Fe { 31 + let load8 = |src: &[u8]| -> u64 { 32 + let mut dst = [0u8; 8]; 33 + let n = src.len().min(8); 34 + dst[..n].copy_from_slice(&src[..n]); 35 + u64::from_le_bytes(dst) 36 + }; 37 + let mut h = Fe::ZERO; 38 + h.0[0] = load8(&bytes[0..]) & MASK51; 39 + h.0[1] = (load8(&bytes[6..]) >> 3) & MASK51; 40 + h.0[2] = (load8(&bytes[12..]) >> 6) & MASK51; 41 + h.0[3] = (load8(&bytes[19..]) >> 1) & MASK51; 42 + h.0[4] = (load8(&bytes[24..]) >> 12) & MASK51; 43 + h 44 + } 45 + 46 + fn fe_tobytes(h: &Fe) -> [u8; 32] { 47 + let h = fe_reduce(h); 48 + let mut out = [0u8; 32]; 49 + // Pack 5×51-bit limbs into 256 bits little-endian 50 + let combine = |lo: u64, hi: u64, shift: u32| -> u64 { lo | (hi << shift) }; 51 + let w0 = combine(h.0[0], h.0[1], 51); 52 + let w1 = combine(h.0[1] >> 13, h.0[2], 38); 53 + let w2 = combine(h.0[2] >> 26, h.0[3], 25); 54 + let w3 = combine(h.0[3] >> 39, h.0[4], 12); 55 + out[0..8].copy_from_slice(&w0.to_le_bytes()); 56 + out[8..16].copy_from_slice(&w1.to_le_bytes()); 57 + out[16..24].copy_from_slice(&w2.to_le_bytes()); 58 + out[24..32].copy_from_slice(&w3.to_le_bytes()); 59 + out 60 + } 61 + 62 + // --------------------------------------------------------------------------- 63 + // Field addition and subtraction 64 + // --------------------------------------------------------------------------- 65 + 66 + fn fe_add(a: &Fe, b: &Fe) -> Fe { 67 + Fe([ 68 + a.0[0] + b.0[0], 69 + a.0[1] + b.0[1], 70 + a.0[2] + b.0[2], 71 + a.0[3] + b.0[3], 72 + a.0[4] + b.0[4], 73 + ]) 74 + } 75 + 76 + fn fe_sub(a: &Fe, b: &Fe) -> Fe { 77 + // Add 2*p before subtracting to avoid underflow. 78 + // 2*p = 2*(2^255 - 19) in 5×51-bit limbs: 79 + // limb 0: 2*(2^51 - 19) = 0xFFFFFFFFFFFDA 80 + // limbs 1-4: 2*(2^51 - 1) = 0xFFFFFFFFFFFFE 81 + Fe([ 82 + (a.0[0] + 0xFFFFFFFFFFFDA) - b.0[0], 83 + (a.0[1] + 0xFFFFFFFFFFFFE) - b.0[1], 84 + (a.0[2] + 0xFFFFFFFFFFFFE) - b.0[2], 85 + (a.0[3] + 0xFFFFFFFFFFFFE) - b.0[3], 86 + (a.0[4] + 0xFFFFFFFFFFFFE) - b.0[4], 87 + ]) 88 + } 89 + 90 + // --------------------------------------------------------------------------- 91 + // Field multiplication 92 + // --------------------------------------------------------------------------- 93 + 94 + fn fe_mul(a: &Fe, b: &Fe) -> Fe { 95 + let a0 = a.0[0] as u128; 96 + let a1 = a.0[1] as u128; 97 + let a2 = a.0[2] as u128; 98 + let a3 = a.0[3] as u128; 99 + let a4 = a.0[4] as u128; 100 + 101 + let b0 = b.0[0] as u128; 102 + let b1 = b.0[1] as u128; 103 + let b2 = b.0[2] as u128; 104 + let b3 = b.0[3] as u128; 105 + let b4 = b.0[4] as u128; 106 + 107 + // Precompute 19*b[j] for reduction (2^255 ≡ 19 mod p) 108 + let b1_19 = 19 * b1; 109 + let b2_19 = 19 * b2; 110 + let b3_19 = 19 * b3; 111 + let b4_19 = 19 * b4; 112 + 113 + // Schoolbook multiply with modular reduction 114 + let mut t0 = a0 * b0 + a1 * b4_19 + a2 * b3_19 + a3 * b2_19 + a4 * b1_19; 115 + let mut t1 = a0 * b1 + a1 * b0 + a2 * b4_19 + a3 * b3_19 + a4 * b2_19; 116 + let mut t2 = a0 * b2 + a1 * b1 + a2 * b0 + a3 * b4_19 + a4 * b3_19; 117 + let mut t3 = a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0 + a4 * b4_19; 118 + let mut t4 = a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; 119 + 120 + // Carry propagation 121 + let c = t0 >> 51; 122 + t0 &= MASK51 as u128; 123 + t1 += c; 124 + 125 + let c = t1 >> 51; 126 + t1 &= MASK51 as u128; 127 + t2 += c; 128 + 129 + let c = t2 >> 51; 130 + t2 &= MASK51 as u128; 131 + t3 += c; 132 + 133 + let c = t3 >> 51; 134 + t3 &= MASK51 as u128; 135 + t4 += c; 136 + 137 + let c = t4 >> 51; 138 + t4 &= MASK51 as u128; 139 + t0 += c * 19; 140 + 141 + let c = t0 >> 51; 142 + t0 &= MASK51 as u128; 143 + t1 += c; 144 + 145 + Fe([t0 as u64, t1 as u64, t2 as u64, t3 as u64, t4 as u64]) 146 + } 147 + 148 + fn fe_sq(a: &Fe) -> Fe { 149 + let a0 = a.0[0] as u128; 150 + let a1 = a.0[1] as u128; 151 + let a2 = a.0[2] as u128; 152 + let a3 = a.0[3] as u128; 153 + let a4 = a.0[4] as u128; 154 + 155 + let d0 = 2 * a0; 156 + let d1 = 2 * a1; 157 + let d2 = 2 * a2; 158 + let d3 = 2 * a3; 159 + 160 + let a4_19 = 19 * a4; 161 + let a3_19 = 19 * a3; 162 + 163 + let mut t0 = a0 * a0 + d1 * a4_19 + d2 * a3_19; 164 + let mut t1 = d0 * a1 + d2 * a4_19 + a3 * a3_19; 165 + let mut t2 = d0 * a2 + a1 * a1 + d3 * a4_19; 166 + let mut t3 = d0 * a3 + d1 * a2 + a4 * a4_19; 167 + let mut t4 = d0 * a4 + d1 * a3 + a2 * a2; 168 + 169 + // Carry propagation 170 + let c = t0 >> 51; 171 + t0 &= MASK51 as u128; 172 + t1 += c; 173 + 174 + let c = t1 >> 51; 175 + t1 &= MASK51 as u128; 176 + t2 += c; 177 + 178 + let c = t2 >> 51; 179 + t2 &= MASK51 as u128; 180 + t3 += c; 181 + 182 + let c = t3 >> 51; 183 + t3 &= MASK51 as u128; 184 + t4 += c; 185 + 186 + let c = t4 >> 51; 187 + t4 &= MASK51 as u128; 188 + t0 += c * 19; 189 + 190 + let c = t0 >> 51; 191 + t0 &= MASK51 as u128; 192 + t1 += c; 193 + 194 + Fe([t0 as u64, t1 as u64, t2 as u64, t3 as u64, t4 as u64]) 195 + } 196 + 197 + /// Multiply by the curve constant a24 = (A - 2) / 4 = 121665 for Curve25519. 198 + /// This is the correct constant when the ladder uses AA (not BB) in the z_2 update. 199 + fn fe_mul_a24(a: &Fe) -> Fe { 200 + let mut t0 = a.0[0] as u128 * 121665; 201 + let mut t1 = a.0[1] as u128 * 121665; 202 + let mut t2 = a.0[2] as u128 * 121665; 203 + let mut t3 = a.0[3] as u128 * 121665; 204 + let mut t4 = a.0[4] as u128 * 121665; 205 + 206 + let c = t0 >> 51; 207 + t0 &= MASK51 as u128; 208 + t1 += c; 209 + 210 + let c = t1 >> 51; 211 + t1 &= MASK51 as u128; 212 + t2 += c; 213 + 214 + let c = t2 >> 51; 215 + t2 &= MASK51 as u128; 216 + t3 += c; 217 + 218 + let c = t3 >> 51; 219 + t3 &= MASK51 as u128; 220 + t4 += c; 221 + 222 + let c = t4 >> 51; 223 + t4 &= MASK51 as u128; 224 + t0 += c * 19; 225 + 226 + let c = t0 >> 51; 227 + t0 &= MASK51 as u128; 228 + t1 += c; 229 + 230 + Fe([t0 as u64, t1 as u64, t2 as u64, t3 as u64, t4 as u64]) 231 + } 232 + 233 + // --------------------------------------------------------------------------- 234 + // Field reduction and canonical form 235 + // --------------------------------------------------------------------------- 236 + 237 + /// Fully reduce to canonical form in [0, p). 238 + fn fe_reduce(a: &Fe) -> Fe { 239 + let mut h = *a; 240 + 241 + // First, carry-propagate 242 + let mut c = h.0[0] >> 51; 243 + h.0[0] &= MASK51; 244 + h.0[1] += c; 245 + 246 + c = h.0[1] >> 51; 247 + h.0[1] &= MASK51; 248 + h.0[2] += c; 249 + 250 + c = h.0[2] >> 51; 251 + h.0[2] &= MASK51; 252 + h.0[3] += c; 253 + 254 + c = h.0[3] >> 51; 255 + h.0[3] &= MASK51; 256 + h.0[4] += c; 257 + 258 + c = h.0[4] >> 51; 259 + h.0[4] &= MASK51; 260 + h.0[0] += c * 19; 261 + 262 + c = h.0[0] >> 51; 263 + h.0[0] &= MASK51; 264 + h.0[1] += c; 265 + 266 + // Now test if h >= p by computing h + 19 and checking overflow 267 + let mut q = (h.0[0] + 19) >> 51; 268 + q = (h.0[1] + q) >> 51; 269 + q = (h.0[2] + q) >> 51; 270 + q = (h.0[3] + q) >> 51; 271 + q = (h.0[4] + q) >> 51; 272 + 273 + // q is 1 if h >= p, 0 otherwise 274 + h.0[0] += 19 * q; 275 + 276 + // Carry-propagate again 277 + c = h.0[0] >> 51; 278 + h.0[0] &= MASK51; 279 + h.0[1] += c; 280 + 281 + c = h.0[1] >> 51; 282 + h.0[1] &= MASK51; 283 + h.0[2] += c; 284 + 285 + c = h.0[2] >> 51; 286 + h.0[2] &= MASK51; 287 + h.0[3] += c; 288 + 289 + c = h.0[3] >> 51; 290 + h.0[3] &= MASK51; 291 + h.0[4] += c; 292 + 293 + h.0[4] &= MASK51; 294 + 295 + h 296 + } 297 + 298 + // --------------------------------------------------------------------------- 299 + // Field inversion via Fermat's little theorem: a^(p-2) 300 + // --------------------------------------------------------------------------- 301 + 302 + /// Compute a^(2^n) by repeated squaring. 303 + fn fe_sq_n(a: &Fe, n: usize) -> Fe { 304 + let mut r = fe_sq(a); 305 + for _ in 1..n { 306 + r = fe_sq(&r); 307 + } 308 + r 309 + } 310 + 311 + /// Compute a^(p-2) = a^(2^255 - 21) using an addition chain. 312 + fn fe_invert(a: &Fe) -> Fe { 313 + let z1 = *a; 314 + 315 + // t0 = a^2 316 + let t0 = fe_sq(&z1); 317 + 318 + // t1 = a^4 319 + let t1 = fe_sq(&t0); 320 + 321 + // t1 = a^8 322 + let t1 = fe_sq(&t1); 323 + 324 + // t1 = a^9 = a^(8+1) 325 + let t1 = fe_mul(&t1, &z1); 326 + 327 + // t0 = a^11 = a^(9+2) 328 + let t0 = fe_mul(&t0, &t1); 329 + 330 + // t2 = a^22 331 + let t2 = fe_sq(&t0); 332 + 333 + // t1 = a^31 = a^(22+9) = a^(2^5 - 1) 334 + let t1 = fe_mul(&t1, &t2); 335 + 336 + // t2 = a^(2^10 - 2^5) 337 + let t2 = fe_sq_n(&t1, 5); 338 + 339 + // t1 = a^(2^10 - 1) 340 + let t1 = fe_mul(&t2, &t1); 341 + 342 + // t2 = a^(2^20 - 2^10) 343 + let t2 = fe_sq_n(&t1, 10); 344 + 345 + // t2 = a^(2^20 - 1) 346 + let t2 = fe_mul(&t2, &t1); 347 + 348 + // t3 = a^(2^40 - 2^20) 349 + let t3 = fe_sq_n(&t2, 20); 350 + 351 + // t2 = a^(2^40 - 1) 352 + let t2 = fe_mul(&t3, &t2); 353 + 354 + // t2 = a^(2^50 - 2^10) 355 + let t2 = fe_sq_n(&t2, 10); 356 + 357 + // t1 = a^(2^50 - 1) 358 + let t1 = fe_mul(&t2, &t1); 359 + 360 + // t2 = a^(2^100 - 2^50) 361 + let t2 = fe_sq_n(&t1, 50); 362 + 363 + // t2 = a^(2^100 - 1) 364 + let t2 = fe_mul(&t2, &t1); 365 + 366 + // t3 = a^(2^200 - 2^100) 367 + let t3 = fe_sq_n(&t2, 100); 368 + 369 + // t2 = a^(2^200 - 1) 370 + let t2 = fe_mul(&t3, &t2); 371 + 372 + // t2 = a^(2^250 - 2^50) 373 + let t2 = fe_sq_n(&t2, 50); 374 + 375 + // t1 = a^(2^250 - 1) 376 + let t1 = fe_mul(&t2, &t1); 377 + 378 + // t1 = a^(2^255 - 2^5) 379 + let t1 = fe_sq_n(&t1, 5); 380 + 381 + // a^(2^255 - 21) = a^(p-2) 382 + fe_mul(&t1, &t0) 383 + } 384 + 385 + // --------------------------------------------------------------------------- 386 + // Constant-time utilities 387 + // --------------------------------------------------------------------------- 388 + 389 + /// Constant-time conditional swap: swap a and b if swap == 1, no-op if swap == 0. 390 + fn fe_cswap(a: &mut Fe, b: &mut Fe, swap: u64) { 391 + let mask = 0u64.wrapping_sub(swap); 392 + for i in 0..5 { 393 + let t = mask & (a.0[i] ^ b.0[i]); 394 + a.0[i] ^= t; 395 + b.0[i] ^= t; 396 + } 397 + } 398 + 399 + // --------------------------------------------------------------------------- 400 + // Scalar clamping 401 + // --------------------------------------------------------------------------- 402 + 403 + fn clamp_scalar(s: &[u8; 32]) -> [u8; 32] { 404 + let mut k = *s; 405 + k[0] &= 248; 406 + k[31] &= 127; 407 + k[31] |= 64; 408 + k 409 + } 410 + 411 + // --------------------------------------------------------------------------- 412 + // Montgomery ladder scalar multiplication 413 + // --------------------------------------------------------------------------- 414 + 415 + fn x25519_scalar_mult(scalar: &[u8; 32], u_point: &[u8; 32]) -> [u8; 32] { 416 + let mut u_bytes = *u_point; 417 + u_bytes[31] &= 127; // Mask bit 255 per RFC 7748 418 + let u = fe_frombytes(&u_bytes); 419 + 420 + let mut x_2 = Fe::ONE; 421 + let mut z_2 = Fe::ZERO; 422 + let mut x_3 = u; 423 + let mut z_3 = Fe::ONE; 424 + let mut swap: u64 = 0; 425 + 426 + // Montgomery ladder: iterate from bit 254 down to 0 427 + for t in (0..=254).rev() { 428 + let k_t = ((scalar[t >> 3] >> (t & 7)) & 1) as u64; 429 + swap ^= k_t; 430 + fe_cswap(&mut x_2, &mut x_3, swap); 431 + fe_cswap(&mut z_2, &mut z_3, swap); 432 + swap = k_t; 433 + 434 + let a = fe_add(&x_2, &z_2); 435 + let aa = fe_sq(&a); 436 + let b = fe_sub(&x_2, &z_2); 437 + let bb = fe_sq(&b); 438 + let e = fe_sub(&aa, &bb); 439 + let c = fe_add(&x_3, &z_3); 440 + let d = fe_sub(&x_3, &z_3); 441 + let da = fe_mul(&d, &a); 442 + let cb = fe_mul(&c, &b); 443 + 444 + x_3 = fe_sq(&fe_add(&da, &cb)); 445 + z_3 = fe_mul(&u, &fe_sq(&fe_sub(&da, &cb))); 446 + x_2 = fe_mul(&aa, &bb); 447 + z_2 = fe_mul(&e, &fe_add(&aa, &fe_mul_a24(&e))); 448 + } 449 + 450 + fe_cswap(&mut x_2, &mut x_3, swap); 451 + fe_cswap(&mut z_2, &mut z_3, swap); 452 + 453 + let result = fe_mul(&x_2, &fe_invert(&z_2)); 454 + fe_tobytes(&result) 455 + } 456 + 457 + // --------------------------------------------------------------------------- 458 + // Public API 459 + // --------------------------------------------------------------------------- 460 + 461 + /// Compute X25519 Diffie-Hellman: scalar multiplication of `u_point` by `scalar`. 462 + /// 463 + /// The scalar is clamped internally per RFC 7748. 464 + pub fn x25519(scalar: &[u8; 32], u_point: &[u8; 32]) -> [u8; 32] { 465 + let k = clamp_scalar(scalar); 466 + x25519_scalar_mult(&k, u_point) 467 + } 468 + 469 + /// Compute the X25519 public key from a private key (basepoint multiplication). 470 + pub fn x25519_base(scalar: &[u8; 32]) -> [u8; 32] { 471 + x25519(scalar, &BASEPOINT) 472 + } 473 + 474 + // --------------------------------------------------------------------------- 475 + // Tests 476 + // --------------------------------------------------------------------------- 477 + 478 + #[cfg(test)] 479 + mod tests { 480 + use super::*; 481 + 482 + fn hex(bytes: &[u8]) -> String { 483 + bytes.iter().map(|b| format!("{:02x}", b)).collect() 484 + } 485 + 486 + fn from_hex(s: &str) -> Vec<u8> { 487 + (0..s.len()) 488 + .step_by(2) 489 + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) 490 + .collect() 491 + } 492 + 493 + fn hex32(s: &str) -> [u8; 32] { 494 + let v = from_hex(s); 495 + let mut out = [0u8; 32]; 496 + out.copy_from_slice(&v); 497 + out 498 + } 499 + 500 + // --- Field arithmetic tests --- 501 + 502 + #[test] 503 + fn fe_encode_decode_roundtrip() { 504 + let bytes: [u8; 32] = [ 505 + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 506 + 25, 26, 27, 28, 29, 30, 31, 0, 507 + ]; 508 + let fe = fe_frombytes(&bytes); 509 + let out = fe_tobytes(&fe); 510 + assert_eq!(bytes, out); 511 + } 512 + 513 + #[test] 514 + fn fe_mul_identity() { 515 + let bytes = hex32("0900000000000000000000000000000000000000000000000000000000000000"); 516 + let a = fe_frombytes(&bytes); 517 + let result = fe_mul(&a, &Fe::ONE); 518 + assert_eq!(fe_tobytes(&result), bytes); 519 + } 520 + 521 + #[test] 522 + fn fe_mul_commutative() { 523 + let a = fe_frombytes(&hex32( 524 + "0900000000000000000000000000000000000000000000000000000000000000", 525 + )); 526 + let b = fe_frombytes(&hex32( 527 + "0500000000000000000000000000000000000000000000000000000000000000", 528 + )); 529 + let ab = fe_tobytes(&fe_mul(&a, &b)); 530 + let ba = fe_tobytes(&fe_mul(&b, &a)); 531 + assert_eq!(ab, ba); 532 + } 533 + 534 + #[test] 535 + fn fe_sq_matches_mul() { 536 + let a = fe_frombytes(&hex32( 537 + "0900000000000000000000000000000000000000000000000000000000000000", 538 + )); 539 + let sq = fe_tobytes(&fe_sq(&a)); 540 + let mul = fe_tobytes(&fe_mul(&a, &a)); 541 + assert_eq!(sq, mul); 542 + } 543 + 544 + #[test] 545 + fn fe_invert_roundtrip() { 546 + let a = fe_frombytes(&hex32( 547 + "0900000000000000000000000000000000000000000000000000000000000000", 548 + )); 549 + let inv = fe_invert(&a); 550 + let product = fe_mul(&a, &inv); 551 + assert_eq!( 552 + fe_tobytes(&product), 553 + hex32("0100000000000000000000000000000000000000000000000000000000000000") 554 + ); 555 + } 556 + 557 + #[test] 558 + fn fe_add_sub_roundtrip() { 559 + let a = fe_frombytes(&hex32( 560 + "0900000000000000000000000000000000000000000000000000000000000000", 561 + )); 562 + let b = fe_frombytes(&hex32( 563 + "0500000000000000000000000000000000000000000000000000000000000000", 564 + )); 565 + let sum = fe_add(&a, &b); 566 + let diff = fe_sub(&sum, &b); 567 + assert_eq!(fe_tobytes(&diff), fe_tobytes(&a)); 568 + } 569 + 570 + // --- RFC 7748 §6.1 test vectors --- 571 + 572 + #[test] 573 + fn rfc7748_section_6_1_alice_public_key() { 574 + let alice_private = 575 + hex32("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"); 576 + let expected = hex32("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"); 577 + assert_eq!(x25519_base(&alice_private), expected); 578 + } 579 + 580 + #[test] 581 + fn rfc7748_section_6_1_bob_public_key() { 582 + let bob_private = hex32("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"); 583 + let expected = hex32("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"); 584 + assert_eq!(x25519_base(&bob_private), expected); 585 + } 586 + 587 + #[test] 588 + fn rfc7748_section_6_1_shared_secret() { 589 + let alice_private = 590 + hex32("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"); 591 + let bob_public = hex32("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"); 592 + let expected = hex32("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"); 593 + assert_eq!(x25519(&alice_private, &bob_public), expected); 594 + } 595 + 596 + #[test] 597 + fn rfc7748_section_6_1_shared_secret_both_sides() { 598 + let alice_private = 599 + hex32("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"); 600 + let bob_private = hex32("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"); 601 + let alice_public = x25519_base(&alice_private); 602 + let bob_public = x25519_base(&bob_private); 603 + let shared_ab = x25519(&alice_private, &bob_public); 604 + let shared_ba = x25519(&bob_private, &alice_public); 605 + assert_eq!(shared_ab, shared_ba); 606 + assert_eq!( 607 + hex(&shared_ab), 608 + "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742" 609 + ); 610 + } 611 + 612 + // --- RFC 7748 §5.2 iterated test vectors --- 613 + 614 + #[test] 615 + fn rfc7748_section_5_2_one_iteration() { 616 + let mut k = hex32("0900000000000000000000000000000000000000000000000000000000000000"); 617 + let mut u = k; 618 + let output = x25519(&k, &u); 619 + u = k; 620 + k = output; 621 + assert_eq!( 622 + hex(&k), 623 + "422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079" 624 + ); 625 + let _ = u; // suppress unused warning 626 + } 627 + 628 + #[test] 629 + fn rfc7748_section_5_2_1000_iterations() { 630 + let mut k = hex32("0900000000000000000000000000000000000000000000000000000000000000"); 631 + let mut u = k; 632 + for _ in 0..1000 { 633 + let output = x25519(&k, &u); 634 + u = k; 635 + k = output; 636 + } 637 + assert_eq!( 638 + hex(&k), 639 + "684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51" 640 + ); 641 + } 642 + 643 + #[test] 644 + #[ignore] // Takes too long for regular test runs 645 + fn rfc7748_section_5_2_1000000_iterations() { 646 + let mut k = hex32("0900000000000000000000000000000000000000000000000000000000000000"); 647 + let mut u = k; 648 + for _ in 0..1_000_000 { 649 + let output = x25519(&k, &u); 650 + u = k; 651 + k = output; 652 + } 653 + assert_eq!( 654 + hex(&k), 655 + "7c3911e0ab2586fd864497297e575e6f3bc601c0883c30df5f4dd2d24f665424" 656 + ); 657 + } 658 + 659 + // --- Low-order point test --- 660 + 661 + #[test] 662 + fn x25519_zero_point_gives_zero() { 663 + let scalar = hex32("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"); 664 + let zero_point = [0u8; 32]; 665 + let result = x25519(&scalar, &zero_point); 666 + assert_eq!(result, [0u8; 32]); 667 + } 668 + }