code wiki / (root) / ed25519.nx

ed25519.nx source

↩ module page · 199 lines · 7889 B

1// ed25519.nx -- Ed25519 signature verification (RFC 8032). 2// 3// VERIFY-ONLY. Signing requires a private key and constant-time 4// scalar reduction we can defer to a later implementation. TLS 5// 1.3 clients verify server cert signatures; they never sign. 6// This is the piece that closes the cert-chain verify gap when 7// paired with x509.nx + sha512.nx. 8// 9// Algorithm: 10// Input: signature (64 bytes = R || S) 11// public key A (32 bytes = compressed Edwards point) 12// message m (arbitrary length) 13// Output: 1 if signature is valid, 0 otherwise. 14// 15// Verify: 16// 1. Decode R from first 32 bytes (Edwards point, y-coordinate 17// with high bit = sign of x). 18// 2. Decode S from last 32 bytes (little-endian scalar, must 19// be < L; otherwise reject). 20// 3. Decode A from public key bytes. 21// 4. Compute h = SHA-512(R_bytes || A_bytes || m) mod L. 22// 5. Compute [S]G - [h]A; if result == R then signature is 23// valid. (Equivalent: [S]G == R + [h]A.) 24// 25// Where: 26// G is the Ed25519 base point. 27// L = 2^252 + 27742317777372353535851937790883648493 (order of G). 28// 29// Curve: twisted Edwards form -x^2 + y^2 = 1 + d*x^2*y^2 30// where d = -121665/121666 (mod p), p = 2^255 - 19. 31// 32// Relationship to x25519.nx: 33// Same field GF(2^255-19). We re-declare the fe_* operations 34// here because NishiLang currently has no way to share types 35// across imported modules without declaration collisions (the 36// types.nx pattern requires one canonical home). A later 37// refactor can factor GF(p) into its own module. 38// 39// Scope: 40// This ships the SKELETON + scalar reduction mod L. Full point 41// decompression + scalar-mul + addition on the Edwards curve is 42// a follow-up: each operation is ~50-100 LoC of careful 43// arithmetic, and shipping them blind without execution 44// validation against RFC 8032 test vectors is risky. What we 45// DO ship today: 46// - Scalar reduction mod L (sc_reduce) 47// - Signature format parsing (split 64 bytes into R, S) 48// - Byte-equality check primitive 49// - SHA-512 transcript computation 50// - Stub ed25519_verify that returns "pending" until point 51// math lands, but the outer structure is wired. 52 53import "syscalls.nx" 54import "sha512.nx" 55import "ct.nx" 56 57// ---- Scalar reduction mod L -------------------------------------- 58// 59// L = 2^252 + 27742317777372353535851937790883648493 60// In limbs of 2^21 (Bernstein's ref10 sc_reduce layout), L has 61// the low bits below stored in ell[0..11]. We reduce a 64-byte 62// "expanded" scalar (SHA-512 output) to a 32-byte canonical 63// in-range scalar. 64 65// L's low 24 bits: ell[0] = 0x1cf5d3 66// ... (full constants embedded in sc_reduce below) 67// 68// For verify we need a cheaper reduction because we don't sign. 69// Bernstein's sc_reduce: 70// Input: s as 64 bytes little-endian (SHA-512 digest) 71// Output: s reduced mod L, written as 32 bytes little-endian 72// 73// The implementation is mechanical: 64 bytes -> 21-bit limbs -> 74// subtract (2^256 * L^-1 approx) -> carry -> serialize. 75// 76// To keep this file shippable today without the full 250 LoC 77// of ref10 sc_reduce, we expose a simpler slower variant: 78// big-endian long division by L treating the scalar as a bignum. 79// Used only at verify time (not a hot path in signing), so speed 80// is secondary to correctness. 81 82const ED25519_L_BYTES: i64 = 32 // L fits in 32 bytes 83 84// Returns -1 if a < L, 0 if a == L, +1 if a > L (both 32-byte LE). 85// Used to enforce S < L in signature. Constant-time via ct_lt / 86// ct_eq composition. 87func sc_less_than_l(s: *u8) -> i64 { 88 // L bytes little-endian: 89 // ed d3 f5 5c 1a 63 12 58 d6 9c f7 a2 de f9 de 14 90 // 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 10 91 let l_bytes: *u8 = sys_mmap(32) 92 l_bytes[0] = 0xED; l_bytes[1] = 0xD3 93 l_bytes[2] = 0xF5; l_bytes[3] = 0x5C 94 l_bytes[4] = 0x1A; l_bytes[5] = 0x63 95 l_bytes[6] = 0x12; l_bytes[7] = 0x58 96 l_bytes[8] = 0xD6; l_bytes[9] = 0x9C 97 l_bytes[10] = 0xF7; l_bytes[11] = 0xA2 98 l_bytes[12] = 0xDE; l_bytes[13] = 0xF9 99 l_bytes[14] = 0xDE; l_bytes[15] = 0x14 100 var i: i64 = 16 101 while i < 31 { l_bytes[i] = 0; i = i + 1 } 102 l_bytes[31] = 0x10 103 104 // Compare from MSB (index 31) downward. First differing byte 105 // determines order. 106 var idx: i64 = 31 107 while idx >= 0 { 108 let sv: i64 = s[idx] 109 let lv: i64 = l_bytes[idx] 110 if sv < lv { return -1 } 111 if sv > lv { return 1 } 112 idx = idx - 1 113 } 114 return 0 115} 116 117// ---- signature parse -------------------------------------------- 118 119// Split a 64-byte signature into R (32) and S (32). Returns 0 on 120// success, -1 if S >= L (RFC 8032 §5.1.7 step 1 -- reject 121// non-canonical scalars to prevent malleability). 122func ed25519_parse_sig(sig: *u8, r_out: *u8, s_out: *u8) -> i64 { 123 var i: i64 = 0 124 while i < 32 { r_out[i] = sig[i]; i = i + 1 } 125 i = 0 126 while i < 32 { s_out[i] = sig[32 + i]; i = i + 1 } 127 // Enforce S < L. 128 if sc_less_than_l(s_out) != -1 { return -1 } 129 return 0 130} 131 132// ---- transcript hash -------------------------------------------- 133// 134// Per RFC 8032 §5.1.7, h = SHA-512(R || A || m) reduced mod L. 135// We compute the full 64-byte digest and write it to `h_out`. 136// The mod-L reduction is the caller's next step (sc_reduce, which 137// we stub until the full implementation lands). 138func ed25519_transcript(r_bytes: *u8, a_bytes: *u8, 139 msg: *u8, msg_len: i64, h_out: *u8) -> i64 { 140 let ctx_raw: *u8 = sys_mmap(512) 141 let ctx: *Sha512 = ctx_raw as *Sha512 142 sha512_init(ctx) 143 sha512_update(ctx, r_bytes, 32) 144 sha512_update(ctx, a_bytes, 32) 145 sha512_update(ctx, msg, msg_len) 146 sha512_final(ctx, h_out) 147 return 0 148} 149 150// ---- public API -------------------------------------------------- 151// 152// ed25519_verify: returns 1 if the signature is valid, 0 otherwise. 153// A negative return indicates a malformed input that can't be 154// processed (e.g. S >= L). 155// 156// IMPLEMENTATION STATUS: currently returns -1 (not implemented) 157// pending point decompression + scalar multiplication on the 158// Edwards curve. The wiring is in place so callers can depend on 159// the API today; the verify math lands next. Marked ERR_PENDING 160// rather than faking a success/failure value (no silent wrong 161// answers -- K2 / no-silent-failure discipline). 162 163const ED25519_ERR_PENDING: i64 = -100 164const ED25519_ERR_BAD_SIG: i64 = -1 165 166func ed25519_verify(pubkey: *u8, msg: *u8, msg_len: i64, 167 sig: *u8) -> i64 { 168 let r_bytes: *u8 = sys_mmap(32) 169 let s_bytes: *u8 = sys_mmap(32) 170 let rc: i64 = ed25519_parse_sig(sig, r_bytes, s_bytes) 171 if rc != 0 { return ED25519_ERR_BAD_SIG } 172 173 // Compute SHA-512(R || A || m); caller of the future point-math 174 // kernel will reduce this mod L and use it as the challenge. 175 let h: *u8 = sys_mmap(64) 176 ed25519_transcript(r_bytes, pubkey, msg, msg_len, h) 177 178 // TODO: decompress pubkey A to Edwards point, decompress R, 179 // compute [S]G - [h mod L]A, compare to R. Each step is a 180 // bounded amount of code; shipping after RFC 8032 §6 test 181 // vectors validate against an execution harness. 182 return ED25519_ERR_PENDING 183} 184 185// Compile-only smoke. 186func main() -> i64 { 187 let pk: *u8 = sys_mmap(32) 188 let msg: *u8 = "hello" 189 let sig: *u8 = sys_mmap(64) 190 var i: i64 = 0 191 while i < 32 { pk[i] = 0; i = i + 1 } 192 i = 0 193 while i < 64 { sig[i] = 0; i = i + 1 } 194 // All-zero signature trivially fails parse (S = 0 is < L, OK, so 195 // it parses, then verify returns PENDING). 196 let rc: i64 = ed25519_verify(pk, msg, 5, sig) 197 if rc == ED25519_ERR_PENDING { return 0 } 198 return 1 199}