x25519.nx
buildroot/runtime/x25519.nx
about
x25519.nx -- Bernstein's Curve25519 X25519 (RFC 7748).
Elliptic-curve Diffie-Hellman on Curve25519 in Montgomery form.
Primary classical KEX for TLS 1.3, SSH, Signal, WireGuard, and
every modern protocol that cares about correctness. Hybrid-
deploys alongside ML-KEM-768 for post-quantum TLS 1.3 (RFC 9578
X25519Kyber768Draft00).
Field: GF(p) with p = 2^255 - 19.
Curve equation: y^2 = x^3 + 486662*x^2 + x (Montgomery form).
Scalar input: 32 bytes, clamped per RFC 7748 §5.
u-coordinate input: 32 bytes, high bit cleared per §5.
Output: 32-byte shared secret.
Representation: 10 x 25.5-bit limbs (Bernstein ref10 style).
Offsets: 0, 26, 51, 77, 102, 128, 153, 179, 204, 230 (bits).
Limb widths alternate 26 / 25 bits so 10 * 25.5 = 255 bits.
Each limb fits in i64 comfortably (26 bits plus headroom).
Multiplication of two limbs: 26 + 26 = 52 bits -- well within
i64 range. Accumulating up to 10 partial products per output
digit: ~56 bits peak -- still within i64.
Why 10x25.5 and not 5x51:
NishiLang has no u128 / 128-bit multiply. 5x51-bit limbs give
102-bit products that don't fit in i64. The 10x25.5 layout
avoids this entirely; every intermediate fits in a plain i64.
Constant-time discipline:
X25519 MUST run in time independent of scalar/u bits (leaking
would recover the private key). The Montgomery ladder + cswap
achieves this naturally: same instruction sequence regardless
of bit values; conditional swap via bitmask select rather than
branch. No table lookups on secret bits. No divisions (used
only in fe_invert, which runs over a fixed chain of squarings
and multiplications regardless of input).
Invariants (enforced, not hoped):
XC1 Scalar clamping applied once at entry per RFC 7748 §5:
scalar[0] &= 0xf8; scalar[31] &= 0x7f; scalar[31] |= 0x40.
XC2 u-coordinate high bit cleared at entry (RFC 7748 §5).
dependencies 1 imports · 1 importers
imports: syscalls.nx
imported by: x25519_ephemeral.nx
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| 61 | const FE_LIMBS: i64 = 10 |
functions
| 63 | func fe_alloc() -> *i64 { |
| 68 | func fe_copy(dst: *i64, src: *i64) -> i64 { |
| 74 | func fe_zero(h: *i64) -> i64 { |
| 80 | func fe_one(h: *i64) -> i64 { |
| 92 | func fe_add(h: *i64, f: *i64, g: *i64) -> i64 {
called by 1: x25519_ladder_step |
| 98 | func fe_sub(h: *i64, f: *i64, g: *i64) -> i64 {
called by 1: x25519_ladder_step |
| 117 | func fe_mul(h: *i64, f: *i64, g: *i64) -> i64 { |
| 200 | func fe_sq(h: *i64, f: *i64) -> i64 { |
| 206 | func fe_mul_a24(h: *i64, f: *i64) -> i64 { |
| 220 | func fe_cswap(f: *i64, g: *i64, swap: i64) -> i64 { |
| 238 | func fe_invert(out: *i64, z: *i64) -> i64 { |
| 279 | func fe_from_bytes(h: *i64, s: *u8) -> i64 {
called by 1: x25519 |
| 298 | func fe_to_bytes(s: *u8, h_in: *i64) -> i64 { |
| 369 | func x25519_ladder_step(x1: *i64, |
| 421 | func x25519(scalar: *u8, u: *u8, out: *u8) -> i64 {
called by 3: mainx25519_keypair_publicx25519_shared_secret calls 10: fe_allocfe_from_bytesfe_onefe_zerofe_copyx25519_ladder_step+4 |
| 477 | func main() -> i64 {
calls 1: x25519 |