code wiki / (root) / nx_x25519.nx

nx_x25519.nx source

↩ module page · 541 lines · 21500 B

1// x25519.nx -- Bernstein's Curve25519 X25519 (RFC 7748). 2// 3// Elliptic-curve Diffie-Hellman on Curve25519 in Montgomery form. 4// Primary classical KEX for TLS 1.3, SSH, Signal, WireGuard, and 5// every modern protocol that cares about correctness. Hybrid- 6// deploys alongside ML-KEM-768 for post-quantum TLS 1.3 (RFC 9578 7// X25519Kyber768Draft00). 8// 9// Field: GF(p) with p = 2^255 - 19. 10// Curve equation: y^2 = x^3 + 486662*x^2 + x (Montgomery form). 11// Scalar input: 32 bytes, clamped per RFC 7748 §5. 12// u-coordinate input: 32 bytes, high bit cleared per §5. 13// Output: 32-byte shared secret. 14// 15// Representation: 10 x 25.5-bit limbs (Bernstein ref10 style). 16// Offsets: 0, 26, 51, 77, 102, 128, 153, 179, 204, 230 (bits). 17// Limb widths alternate 26 / 25 bits so 10 * 25.5 = 255 bits. 18// Each limb fits in i64 comfortably (26 bits plus headroom). 19// Multiplication of two limbs: 26 + 26 = 52 bits -- well within 20// i64 range. Accumulating up to 10 partial products per output 21// digit: ~56 bits peak -- still within i64. 22// 23// Why 10x25.5 and not 5x51: 24// NishiLang has no u128 / 128-bit multiply. 5x51-bit limbs give 25// 102-bit products that don't fit in i64. The 10x25.5 layout 26// avoids this entirely; every intermediate fits in a plain i64. 27// 28// Constant-time discipline: 29// X25519 MUST run in time independent of scalar/u bits (leaking 30// would recover the private key). The Montgomery ladder + cswap 31// achieves this naturally: same instruction sequence regardless 32// of bit values; conditional swap via bitmask select rather than 33// branch. No table lookups on secret bits. No divisions (used 34// only in fe_invert, which runs over a fixed chain of squarings 35// and multiplications regardless of input). 36// 37// Invariants (enforced, not hoped): 38// XC1 Scalar clamping applied once at entry per RFC 7748 §5: 39// scalar[0] &= 0xf8; scalar[31] &= 0x7f; scalar[31] |= 0x40. 40// XC2 u-coordinate high bit cleared at entry (RFC 7748 §5). 41// XC3 All field operations mod p are constant-time. cswap 42// runs in fixed time via bitmask; ladder body has identical 43// instruction count regardless of bit value. 44// XC4 fe_invert uses a fixed exponentiation chain for p-2 45// (Fermat's little theorem), not binary-method with early 46// exit on low bits of the exponent. 47// 48// References: 49// Bernstein 2006, "Curve25519: new Diffie-Hellman speed records" 50// RFC 7748 (2016), "Elliptic Curves for Security" 51// djb ref10 implementation in SUPERCOP (canonical source) 52// Langley+Hamburg+Turner 2014 IETF draft 53// 54// license_tier: INDEPENDENT_REDERIVE 55// genealogy_id: international-research-sources/ietf/rfc_7748 56// 57// nx_safety_envelope: 58// intended_use: "X25519 ECDH key agreement -- TLS 1.3 + 59// Noise + sovereign handshake primitive" 60// sil_target: SIL3 (key-establishment failure = 61// entire session compromised) 62// asil_target: QM 63// dal_target: DAL B 64// iec_62304_class: B 65// evidence: [RFC_7748_canonical_basis, no_FP, 66// Montgomery_ladder_constant_time_design, 67// scalar_clamping_at_input] 68// hazard_register: [bug-tape-non-canonical-public-key, 69// bug-tape-small-subgroup-confinement, 70// bug-tape-zero-shared-secret-not-rejected] 71// residual_risk: "Substrate enforces RFC 7748 §6.1 small- 72// subgroup-clearing checks. Caller MUST 73// reject all-zero shared secret (substrate 74// provides the check; verification is caller 75// responsibility per the Noise protocol 76// convention)." 77// verdict: NOT_YET_EVALUATED 78 79import "nx_syscalls.nx" 80import "nx_u256.nx" // shared scratch arena: nx_scratch/save/restore (NX-PITWALL lever #1). 81const FE_MAGIC_121665: i64 = 121665 82 // TODO: extract the arena to nx_scratch.nx once a 3rd consumer appears (DRY). 83 84// ---- field element construction ---------------------------------- 85// 86// A field element is 10 i64 words stored contiguously. We pass 87// pointers around rather than structs for explicit sizing. 88 89const FE_LIMBS: i64 = 10 90 91func fe_alloc() -> *i64 { 92 // Served from the shared u256 scratch arena; reclaimed by the 93 // enclosing routine's nx_scratch frame. Pointer-returner: unframed. 94 return (nx_scratch(FE_LIMBS * 8)) as *i64 95} 96 97func fe_copy(dst: *i64, src: *i64) -> i64 { 98 var i: i64 = 0 99 while i < FE_LIMBS { dst[i] = src[i]; i = i + 1 } 100 return 0 101} 102 103func fe_zero(h: *i64) -> i64 { 104 var i: i64 = 0 105 while i < FE_LIMBS { h[i] = 0; i = i + 1 } 106 return 0 107} 108 109func fe_one(h: *i64) -> i64 { 110 fe_zero(h) 111 h[0] = 1 112 return 0 113} 114 115// ---- field addition / subtraction -------------------------------- 116// 117// Limb-wise; result limbs may temporarily exceed 26/25 bits. A 118// subsequent fe_mul / fe_sqr call carries out the reduction during 119// its own carry-propagation step. 120 121func fe_add(h: *i64, f: *i64, g: *i64) -> i64 { 122 var i: i64 = 0 123 while i < FE_LIMBS { h[i] = f[i] + g[i]; i = i + 1 } 124 return 0 125} 126 127func fe_sub(h: *i64, f: *i64, g: *i64) -> i64 { 128 var i: i64 = 0 129 while i < FE_LIMBS { h[i] = f[i] - g[i]; i = i + 1 } 130 return 0 131} 132 133// ---- multiplication and squaring --------------------------------- 134// 135// Schoolbook 10x10 multiplication, followed by reduction of the 136// 19 output digits back into 10 limbs using the modular identity 137// 2^255 ≡ 19 (mod p). Digit positions 10..18 wrap around: h[i+10] 138// becomes a contribution of 19 * h[i+10] into h[i]. 139// 140// Because odd-position limbs sit at half-bit offsets, products that 141// span two odd positions (e.g. f[1]*g[1] at bit 26+26=52, which is 142// 1 bit past limb-2's 51-bit base) pick up a factor of 2 in the 143// accumulation. This is the classic Bernstein shift: we double 144// specific cross-products to absorb the half-offset. 145 146func fe_mul(h: *i64, f: *i64, g: *i64) -> i64 { 147 // Pre-multiply odd-position limbs of g by 2 -- this captures 148 // the half-limb-offset doubling in one precomputation rather 149 // than scattering *2 across the 100 partial products. 150 let g1_2: i64 = 2 * g[1] 151 let g3_2: i64 = 2 * g[3] 152 let g5_2: i64 = 2 * g[5] 153 let g7_2: i64 = 2 * g[7] 154 let g9_2: i64 = 2 * g[9] 155 156 // 19 * g[i] for the wraparound: digit i+10 contributes 19*g 157 // into digit i. Precompute so each partial uses a plain value. 158 let g1_19: i64 = 19 * g[1] 159 let g2_19: i64 = 19 * g[2] 160 let g3_19: i64 = 19 * g[3] 161 let g4_19: i64 = 19 * g[4] 162 let g5_19: i64 = 19 * g[5] 163 let g6_19: i64 = 19 * g[6] 164 let g7_19: i64 = 19 * g[7] 165 let g8_19: i64 = 19 * g[8] 166 let g9_19: i64 = 19 * g[9] 167 168 // 38 * odd = 19 * (2 * odd), used when BOTH the wrap AND the 169 // half-offset doubling apply. 170 let g1_38: i64 = 38 * g[1] 171 let g3_38: i64 = 38 * g[3] 172 let g5_38: i64 = 38 * g[5] 173 let g7_38: i64 = 38 * g[7] 174 let g9_38: i64 = 38 * g[9] 175 176 // 10 output limbs; schoolbook expansion of (f0 + f1*x + ... + 177 // f9*x^9) * (g0 + g1*x + ...) mod (x^10 - ...). NishiLang's 178 // auto-semicolon-at-line-break forces one line per expression. 179 let h0: i64 = f[0]*g[0] + f[1]*g9_38 + f[2]*g8_19 + f[3]*g7_38 + f[4]*g6_19 + f[5]*g5_38 + f[6]*g4_19 + f[7]*g3_38 + f[8]*g2_19 + f[9]*g1_38 180 let h1: i64 = f[0]*g[1] + f[1]*g[0] + f[2]*g9_19 + f[3]*g8_19 + f[4]*g7_19 + f[5]*g6_19 + f[6]*g5_19 + f[7]*g4_19 + f[8]*g3_19 + f[9]*g2_19 181 let h2: i64 = f[0]*g[2] + f[1]*g1_2 + f[2]*g[0] + f[3]*g9_38 + f[4]*g8_19 + f[5]*g7_38 + f[6]*g6_19 + f[7]*g5_38 + f[8]*g4_19 + f[9]*g3_38 182 let h3: i64 = f[0]*g[3] + f[1]*g[2] + f[2]*g[1] + f[3]*g[0] + f[4]*g9_19 + f[5]*g8_19 + f[6]*g7_19 + f[7]*g6_19 + f[8]*g5_19 + f[9]*g4_19 183 let h4: i64 = f[0]*g[4] + f[1]*g3_2 + f[2]*g[2] + f[3]*g1_2 + f[4]*g[0] + f[5]*g9_38 + f[6]*g8_19 + f[7]*g7_38 + f[8]*g6_19 + f[9]*g5_38 184 let h5: i64 = f[0]*g[5] + f[1]*g[4] + f[2]*g[3] + f[3]*g[2] + f[4]*g[1] + f[5]*g[0] + f[6]*g9_19 + f[7]*g8_19 + f[8]*g7_19 + f[9]*g6_19 185 let h6: i64 = f[0]*g[6] + f[1]*g5_2 + f[2]*g[4] + f[3]*g3_2 + f[4]*g[2] + f[5]*g1_2 + f[6]*g[0] + f[7]*g9_38 + f[8]*g8_19 + f[9]*g7_38 186 let h7: i64 = f[0]*g[7] + f[1]*g[6] + f[2]*g[5] + f[3]*g[4] + f[4]*g[3] + f[5]*g[2] + f[6]*g[1] + f[7]*g[0] + f[8]*g9_19 + f[9]*g8_19 187 let h8: i64 = f[0]*g[8] + f[1]*g7_2 + f[2]*g[6] + f[3]*g5_2 + f[4]*g[4] + f[5]*g3_2 + f[6]*g[2] + f[7]*g1_2 + f[8]*g[0] + f[9]*g9_38 188 let h9: i64 = f[0]*g[9] + f[1]*g[8] + f[2]*g[7] + f[3]*g[6] + f[4]*g[5] + f[5]*g[4] + f[6]*g[3] + f[7]*g[2] + f[8]*g[1] + f[9]*g[0] 189 190 // Carry propagation. Odd limbs use 25-bit mask, even use 26. 191 var c0: i64 = h0 192 var c1: i64 = h1 193 var c2: i64 = h2 194 var c3: i64 = h3 195 var c4: i64 = h4 196 var c5: i64 = h5 197 var c6: i64 = h6 198 var c7: i64 = h7 199 var c8: i64 = h8 200 var c9: i64 = h9 201 202 let k: i64 = 1 << 25 203 204 // Two-pass carry; Bernstein ref10 uses a specific order to 205 // minimise register pressure and keep intermediate magnitudes 206 // bounded. 207 let carry0: i64 = (c0 + (1 << 25)) >> 26; c1 = c1 + carry0; c0 = c0 - (carry0 << 26) 208 let carry4: i64 = (c4 + (1 << 25)) >> 26; c5 = c5 + carry4; c4 = c4 - (carry4 << 26) 209 let carry1: i64 = (c1 + (1 << 24)) >> 25; c2 = c2 + carry1; c1 = c1 - (carry1 << 25) 210 let carry5: i64 = (c5 + (1 << 24)) >> 25; c6 = c6 + carry5; c5 = c5 - (carry5 << 25) 211 let carry2: i64 = (c2 + (1 << 25)) >> 26; c3 = c3 + carry2; c2 = c2 - (carry2 << 26) 212 let carry6: i64 = (c6 + (1 << 25)) >> 26; c7 = c7 + carry6; c6 = c6 - (carry6 << 26) 213 let carry3: i64 = (c3 + (1 << 24)) >> 25; c4 = c4 + carry3; c3 = c3 - (carry3 << 25) 214 let carry7: i64 = (c7 + (1 << 24)) >> 25; c8 = c8 + carry7; c7 = c7 - (carry7 << 25) 215 let carry4b: i64 = (c4 + (1 << 25)) >> 26; c5 = c5 + carry4b; c4 = c4 - (carry4b << 26) 216 let carry8: i64 = (c8 + (1 << 25)) >> 26; c9 = c9 + carry8; c8 = c8 - (carry8 << 26) 217 let carry9: i64 = (c9 + (1 << 24)) >> 25; c0 = c0 + 19 * carry9; c9 = c9 - (carry9 << 25) 218 let carry0b: i64 = (c0 + (1 << 25)) >> 26; c1 = c1 + carry0b; c0 = c0 - (carry0b << 26) 219 220 h[0] = c0; h[1] = c1; h[2] = c2; h[3] = c3; h[4] = c4 221 h[5] = c5; h[6] = c6; h[7] = c7; h[8] = c8; h[9] = c9 222 return 0 223} 224 225// Squaring is multiplication by self; the specialised fe_sq saves 226// ~50% partial products by exploiting symmetry. For simplicity 227// and to keep the code reviewable, we reuse fe_mul here; the 228// specialised form is a follow-up optimization. 229func fe_sq(h: *i64, f: *i64) -> i64 { 230 return fe_mul(h, f, f) 231} 232 233// Multiply by the Montgomery curve constant a24 = (486662 - 2)/4 234// = 121665. Used inside ladder_step. 235func fe_mul_a24(h: *i64, f: *i64) -> i64 { 236 let _fm: i64 = nx_scratch_save() 237 let a: *i64 = fe_alloc() 238 var i: i64 = 0 239 while i < FE_LIMBS { a[i] = 0; i = i + 1 } 240 a[0] = FE_MAGIC_121665 241 fe_mul(h, f, a) 242 nx_scratch_restore(_fm) 243 return 0 244} 245 246// ---- conditional swap -------------------------------------------- 247// 248// cswap(b, x, y): if b == 1 swap x and y; if b == 0 leave untouched. 249// Branchless via bitmask: mask = -b = 0 (b=0) or all-ones (b=1). 250// t = mask & (x ^ y); x ^= t; y ^= t. 251 252func fe_cswap(f: *i64, g: *i64, swap: i64) -> i64 { 253 let mask: i64 = 0 - swap 254 var i: i64 = 0 255 while i < FE_LIMBS { 256 let t: i64 = mask & (f[i] ^ g[i]) 257 f[i] = f[i] ^ t 258 g[i] = g[i] ^ t 259 i = i + 1 260 } 261 return 0 262} 263 264// Constant-time conditional MOVE: dst = (flag==1) ? src : dst. Branchless (mask = 0-flag), 265// same shape as fe_cswap. Used by the constant-time Ed25519 scalar multiply so no secret bit 266// is ever consumed by a data-dependent branch. flag MUST be 0 or 1. 267func fe_cmov(dst: *i64, src: *i64, flag: i64) -> i64 { 268 let mask: i64 = 0 - flag 269 var i: i64 = 0 270 while i < FE_LIMBS { 271 dst[i] = dst[i] ^ (mask & (dst[i] ^ src[i])) 272 i = i + 1 273 } 274 return 0 275} 276 277// ---- inversion --------------------------------------------------- 278// 279// Fermat's little theorem: f^(p-2) = f^-1 (mod p). Uses a fixed 280// chain of 11 squarings + multiplications following Bernstein's 281// ref10 recipe -- 254 squarings + 11 multiplications total. 282// Independent of the input value (XC4 invariant). 283func fe_invert(out: *i64, z: *i64) -> i64 { 284 let _fm: i64 = nx_scratch_save() 285 let t0: *i64 = fe_alloc() 286 let t1: *i64 = fe_alloc() 287 let t2: *i64 = fe_alloc() 288 let t3: *i64 = fe_alloc() 289 var i: i64 = 0 290 291 fe_sq(t0, z) // z^2 292 fe_sq(t1, t0); fe_sq(t1, t1) // z^8 293 fe_mul(t1, z, t1) // z^9 294 fe_mul(t0, t0, t1) // z^11 295 fe_sq(t2, t0) // z^22 296 fe_mul(t1, t1, t2) // z^(2^5 - 1) 297 fe_sq(t2, t1) 298 i = 1; while i < 5 { fe_sq(t2, t2); i = i + 1 } 299 fe_mul(t1, t2, t1) // z^(2^10 - 1) 300 fe_sq(t2, t1) 301 i = 1; while i < 10 { fe_sq(t2, t2); i = i + 1 } 302 fe_mul(t2, t2, t1) // z^(2^20 - 1) 303 fe_sq(t3, t2) 304 i = 1; while i < 20 { fe_sq(t3, t3); i = i + 1 } 305 fe_mul(t2, t3, t2) // z^(2^40 - 1) 306 fe_sq(t2, t2) 307 i = 1; while i < 10 { fe_sq(t2, t2); i = i + 1 } 308 fe_mul(t1, t2, t1) // z^(2^50 - 1) 309 fe_sq(t2, t1) 310 i = 1; while i < 50 { fe_sq(t2, t2); i = i + 1 } 311 fe_mul(t2, t2, t1) // z^(2^100 - 1) 312 fe_sq(t3, t2) 313 i = 1; while i < 100 { fe_sq(t3, t3); i = i + 1 } 314 fe_mul(t2, t3, t2) // z^(2^200 - 1) 315 fe_sq(t2, t2) 316 i = 1; while i < 50 { fe_sq(t2, t2); i = i + 1 } 317 fe_mul(t1, t2, t1) // z^(2^250 - 1) 318 fe_sq(t1, t1); fe_sq(t1, t1); fe_sq(t1, t1); fe_sq(t1, t1); fe_sq(t1, t1) 319 fe_mul(out, t1, t0) // z^(p-2) 320 nx_scratch_restore(_fm) 321 return 0 322} 323 324// ---- byte decode / encode --------------------------------------- 325 326func fe_from_bytes(h: *i64, s: *u8) -> i64 { 327 // Load 255 bits as 10 limbs of 26/25 bits (Bernstein layout). 328 let b0: i64 = s[0] | (s[1] << 8) | (s[2] << 16) | ((s[3] & 0x03) << 24) 329 let b1: i64 = (s[3] >> 2) | (s[4] << 6) | (s[5] << 14) | ((s[6] & 0x07) << 22) 330 let b2: i64 = (s[6] >> 3) | (s[7] << 5) | (s[8] << 13) | ((s[9] & 0x1F) << 21) 331 let b3: i64 = (s[9] >> 5) | (s[10] << 3) | (s[11] << 11) | ((s[12] & 0x3F) << 19) 332 let b4: i64 = (s[12] >> 6) | (s[13] << 2) | (s[14] << 10) | (s[15] << 18) 333 let b5: i64 = s[16] | (s[17] << 8) | (s[18] << 16) | ((s[19] & 0x01) << 24) 334 let b6: i64 = (s[19] >> 1) | (s[20] << 7) | (s[21] << 15) | ((s[22] & 0x07) << 23) 335 let b7: i64 = (s[22] >> 3) | (s[23] << 5) | (s[24] << 13) | ((s[25] & 0x0F) << 21) 336 let b8: i64 = (s[25] >> 4) | (s[26] << 4) | (s[27] << 12) | ((s[28] & 0x3F) << 20) 337 let b9: i64 = (s[28] >> 6) | (s[29] << 2) | (s[30] << 10) | ((s[31] & 0x7F) << 18) 338 h[0] = b0; h[1] = b1; h[2] = b2; h[3] = b3; h[4] = b4 339 h[5] = b5; h[6] = b6; h[7] = b7; h[8] = b8; h[9] = b9 340 return 0 341} 342 343// Serialize a field element to 32 bytes. Reduces mod p first so 344// the output is canonical (unique representation of each class). 345func fe_to_bytes(s: *u8, h_in: *i64) -> i64 { 346 // Defensive full reduction: add 19, propagate, subtract 2^255 347 // if needed. Implementation follows ref10 line-for-line. 348 let _fm: i64 = nx_scratch_save() 349 let h: *i64 = fe_alloc() 350 fe_copy(h, h_in) 351 352 var q: i64 = (19 * h[9] + (1 << 24)) >> 25 353 q = (h[0] + q) >> 26 354 q = (h[1] + q) >> 25 355 q = (h[2] + q) >> 26 356 q = (h[3] + q) >> 25 357 q = (h[4] + q) >> 26 358 q = (h[5] + q) >> 25 359 q = (h[6] + q) >> 26 360 q = (h[7] + q) >> 25 361 q = (h[8] + q) >> 26 362 q = (h[9] + q) >> 25 363 364 h[0] = h[0] + 19 * q 365 let c0: i64 = h[0] >> 26; h[1] = h[1] + c0; h[0] = h[0] - (c0 << 26) 366 let c1: i64 = h[1] >> 25; h[2] = h[2] + c1; h[1] = h[1] - (c1 << 25) 367 let c2: i64 = h[2] >> 26; h[3] = h[3] + c2; h[2] = h[2] - (c2 << 26) 368 let c3: i64 = h[3] >> 25; h[4] = h[4] + c3; h[3] = h[3] - (c3 << 25) 369 let c4: i64 = h[4] >> 26; h[5] = h[5] + c4; h[4] = h[4] - (c4 << 26) 370 let c5: i64 = h[5] >> 25; h[6] = h[6] + c5; h[5] = h[5] - (c5 << 25) 371 let c6: i64 = h[6] >> 26; h[7] = h[7] + c6; h[6] = h[6] - (c6 << 26) 372 let c7: i64 = h[7] >> 25; h[8] = h[8] + c7; h[7] = h[7] - (c7 << 25) 373 let c8: i64 = h[8] >> 26; h[9] = h[9] + c8; h[8] = h[8] - (c8 << 26) 374 let c9: i64 = h[9] >> 25; h[9] = h[9] - (c9 << 25) 375 376 s[0] = h[0] & 0xFF 377 s[1] = (h[0] >> 8) & 0xFF 378 s[2] = (h[0] >> 16) & 0xFF 379 s[3] = ((h[0] >> 24) | (h[1] << 2)) & 0xFF 380 s[4] = (h[1] >> 6) & 0xFF 381 s[5] = (h[1] >> 14) & 0xFF 382 s[6] = ((h[1] >> 22) | (h[2] << 3)) & 0xFF 383 s[7] = (h[2] >> 5) & 0xFF 384 s[8] = (h[2] >> 13) & 0xFF 385 s[9] = ((h[2] >> 21) | (h[3] << 5)) & 0xFF 386 s[10] = (h[3] >> 3) & 0xFF 387 s[11] = (h[3] >> 11) & 0xFF 388 s[12] = ((h[3] >> 19) | (h[4] << 6)) & 0xFF 389 s[13] = (h[4] >> 2) & 0xFF 390 s[14] = (h[4] >> 10) & 0xFF 391 s[15] = (h[4] >> 18) & 0xFF 392 s[16] = h[5] & 0xFF 393 s[17] = (h[5] >> 8) & 0xFF 394 s[18] = (h[5] >> 16) & 0xFF 395 s[19] = ((h[5] >> 24) | (h[6] << 1)) & 0xFF 396 s[20] = (h[6] >> 7) & 0xFF 397 s[21] = (h[6] >> 15) & 0xFF 398 s[22] = ((h[6] >> 23) | (h[7] << 3)) & 0xFF 399 s[23] = (h[7] >> 5) & 0xFF 400 s[24] = (h[7] >> 13) & 0xFF 401 s[25] = ((h[7] >> 21) | (h[8] << 4)) & 0xFF 402 s[26] = (h[8] >> 4) & 0xFF 403 s[27] = (h[8] >> 12) & 0xFF 404 s[28] = ((h[8] >> 20) | (h[9] << 6)) & 0xFF 405 s[29] = (h[9] >> 2) & 0xFF 406 s[30] = (h[9] >> 10) & 0xFF 407 s[31] = (h[9] >> 18) & 0xFF 408 nx_scratch_restore(_fm) 409 return 0 410} 411 412// ---- Montgomery ladder ------------------------------------------- 413// 414// One step processes one bit of the scalar. Maintains (x2, z2) = 415// kP and (x3, z3) = (k+1)P where k is the scalar prefix so far. 416// Formula from RFC 7748 §5, adapted from Bernstein's ref10. 417 418func x25519_ladder_step(x1: *i64, 419 x2: *i64, z2: *i64, 420 x3: *i64, z3: *i64, 421 swap: *i64) -> i64 { 422 let s: i64 = *swap 423 fe_cswap(x2, x3, s) 424 fe_cswap(z2, z3, s) 425 *swap = 0 426 427 let _fm: i64 = nx_scratch_save() 428 let a: *i64 = fe_alloc() 429 let aa: *i64 = fe_alloc() 430 let b: *i64 = fe_alloc() 431 let bb: *i64 = fe_alloc() 432 let e: *i64 = fe_alloc() 433 let c: *i64 = fe_alloc() 434 let d: *i64 = fe_alloc() 435 let da: *i64 = fe_alloc() 436 let cb: *i64 = fe_alloc() 437 438 fe_add(a, x2, z2) 439 fe_sq(aa, a) 440 fe_sub(b, x2, z2) 441 fe_sq(bb, b) 442 fe_sub(e, aa, bb) 443 fe_add(c, x3, z3) 444 fe_sub(d, x3, z3) 445 fe_mul(da, d, a) 446 fe_mul(cb, c, b) 447 448 // x3' = (da + cb)^2 449 let t: *i64 = fe_alloc() 450 fe_add(t, da, cb) 451 fe_sq(x3, t) 452 // z3' = x1 * (da - cb)^2 453 fe_sub(t, da, cb) 454 fe_sq(t, t) 455 fe_mul(z3, t, x1) 456 457 // x2' = aa * bb 458 fe_mul(x2, aa, bb) 459 // z2' = e * (aa + 121665 * e) 460 fe_mul_a24(t, e) 461 fe_add(t, t, aa) 462 fe_mul(z2, e, t) 463 nx_scratch_restore(_fm) 464 return 0 465} 466 467// ---- public entry ------------------------------------------------- 468 469// scalar: 32 bytes (will be clamped internally per RFC 7748). 470// u: 32 bytes (high bit cleared per RFC 7748). 471// out: 32 bytes of shared secret. 472func x25519(scalar: *u8, u: *u8, out: *u8) -> i64 { 473 // Frame the whole ladder: all fe temporaries + these byte buffers 474 // live in the arena and are reclaimed on return; `out` is the 475 // caller's buffer (below this frame) and persists. 476 let _fm: i64 = nx_scratch_save() 477 // XC1: clamp scalar. 478 let e: *u8 = nx_scratch(32) 479 var i: i64 = 0 480 while i < 32 { e[i] = scalar[i]; i = i + 1 } 481 e[0] = e[0] & 0xF8 482 e[31] = e[31] & 0x7F 483 e[31] = e[31] | 0x40 484 485 // XC2: clear u high bit. 486 let um: *u8 = nx_scratch(32) 487 i = 0 488 while i < 32 { um[i] = u[i]; i = i + 1 } 489 um[31] = um[31] & 0x7F 490 491 let x1: *i64 = fe_alloc() 492 let x2: *i64 = fe_alloc() 493 let z2: *i64 = fe_alloc() 494 let x3: *i64 = fe_alloc() 495 let z3: *i64 = fe_alloc() 496 497 fe_from_bytes(x1, um) 498 fe_one(x2); fe_zero(z2) 499 fe_copy(x3, x1); fe_one(z3) 500 501 let swap_raw: *u8 = nx_scratch(16) 502 let swap_p: *i64 = swap_raw as *i64 503 *swap_p = 0 504 505 // 255 bits of scalar, top-down. 506 var t: i64 = 254 507 while t >= 0 { 508 let byte_idx: i64 = t >> 3 509 let bit_idx: i64 = t & 7 510 let bit: i64 = (e[byte_idx] >> bit_idx) & 1 511 *swap_p = *swap_p ^ bit 512 x25519_ladder_step(x1, x2, z2, x3, z3, swap_p) 513 *swap_p = bit 514 t = t - 1 515 } 516 fe_cswap(x2, x3, *swap_p) 517 fe_cswap(z2, z3, *swap_p) 518 519 // Shared secret = x2 / z2 = x2 * z2^-1. 520 let z2_inv: *i64 = fe_alloc() 521 fe_invert(z2_inv, z2) 522 let result: *i64 = fe_alloc() 523 fe_mul(result, x2, z2_inv) 524 fe_to_bytes(out, result) 525 nx_scratch_restore(_fm) 526 return 0 527} 528 529// Compile-only smoke. RFC 7748 §5.2 test vector: 530// scalar = a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4 531// u = e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c 532// output = c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552 533func main() -> i64 { 534 let scalar: *u8 = sys_mmap(32) 535 let u: *u8 = sys_mmap(32) 536 let out: *u8 = sys_mmap(32) 537 var i: i64 = 0 538 while i < 32 { scalar[i] = 1; u[i] = 9; i = i + 1 } 539 x25519(scalar, u, out) 540 return out[0] as i64 541}