code wiki / (root) / nx_ghash.nx

nx_ghash.nx source

↩ module page · 231 lines · 9283 B

1// nx_ghash.nx -- GF(2^128) multiplication and GHASH accumulator. 2// 3// Per NIST SP 800-38D Algorithm 1. Used as the authentication tag 4// engine inside AES-GCM (RFC 5288 / RFC 8446 §5.3 for TLS 1.3 5// AES-128-GCM cipher suite). 6// 7// Bit convention (SP 800-38D §6.1, "MSB-first" within each byte): 8// - bytes are streamed left to right 9// - bit 0 of byte 0 is the MSB of the GF element 10// - reduction polynomial R = 11100001 || 0^120 (byte[0] = 0xe1) 11// 12// Public API: 13// nx_ghash_mul(x16, h16, z16) z = x * h (16-byte buffers) 14// nx_ghash_update_block(y16, h16, x16) y = (y XOR x) * h 15// nx_ghash_update_buf(y16, h16, buf, len) zero-pad-to-16 + accumulate 16// nx_ghash_finalize(y16, h16, aad_len, ct_len, tag16) 17// append 16-byte length block 18// + final XOR with E_K(J0) is the 19// CALLER's responsibility (it 20// has the AES key); ghash produces 21// the raw S = GHASH(AAD || CT || lenblk). 22// 23// Per the bit-level no-proliferation cardinal: this is the substrate's 24// CANONICAL GF(2^128) implementation. AES-GCM seal/open compose 25// THIS file's primitives -- they MUST NOT re-implement _ghash_mul 26// inline. Future hardware-accelerated variants (CLMUL on x86, 27// PMULL on aarch64) ship as alternative backends behind the same 28// API surface. 29// 30// license_tier: INDEPENDENT_REDERIVE 31// genealogy_id: international-research-sources/nist/sp_800_38d 32// lineage_id: nishi_ghash_q10 33 34// nx_safety_envelope: 35// intended_use: AUTO_APPLIED -- canonical GHASH for AEAD 36// sil_target: SIL3 37// evidence: [bulk_applied_2026-05-20, ghash-bit-level-canonical] 38// verdict: NOT_YET_EVALUATED 39 40import "nx_syscalls.nx" 41import "nx_ghash_clmul.nx" 42 43// z = x * h (over GF(2^128), MSB-first per SP 800-38D §6.3 Alg 1). 44// Caller passes 16-byte buffers x, h, z. z may alias x but NOT h. 45// 46// CANONICAL ENTRY: delegates to the hardware PCLMULQDQ backend (nx_ghash_mul_clmul), 47// proven byte-identical to the bit-by-bit reference below by nx_ghash_clmul_gate 48// (NIST vector + 2000 random vectors). The reference is RETAINED as the oracle 49// (nx_ghash_mul_bitwise) -- the gate re-proves equivalence on every build. Mirrors the 50// P-384 field-mul fast/bitwise split. Future non-x86 targets swap the backend here. 51func nx_ghash_mul(x16: *u8, h16: *u8, z16: *u8) -> i64 { 52 return nx_ghash_mul_clmul(x16, h16, z16) 53} 54 55// Bit-by-bit GF(2^128) reference (SP 800-38D §6.3 Alg 1) -- the trusted oracle. 56func nx_ghash_mul_bitwise(x16: *u8, h16: *u8, z16: *u8) -> i64 { 57 let v16: *u8 = sys_mmap(16) 58 var k: i64 = 0 59 while k < 16 { z16[k] = 0; v16[k] = h16[k]; k = k + 1 } 60 61 var bit: i64 = 0 62 while bit < 128 { 63 let byte_idx: i64 = bit >> 3 64 let bit_in_byte: i64 = 7 - (bit & 7) 65 let xbit: i64 = (x16[byte_idx] >> bit_in_byte) & 1 66 if xbit != 0 { 67 var j: i64 = 0 68 while j < 16 { z16[j] = z16[j] ^ v16[j]; j = j + 1 } 69 } 70 // V = V >> 1, with optional XOR of R if low bit shifted out. 71 let lsb: i64 = v16[15] & 1 72 var i: i64 = 15 73 while i > 0 { 74 v16[i] = ((v16[i] >> 1) | ((v16[i - 1] & 1) << 7)) & 0xff 75 i = i - 1 76 } 77 v16[0] = (v16[0] >> 1) & 0xff 78 if lsb != 0 { v16[0] = v16[0] ^ 0xe1 } 79 bit = bit + 1 80 } 81 return 0 82} 83 84// reusable 16-byte scratch for the per-block XOR (single-threaded GHASH). Replaces a 85// per-call sys_mmap that became the dominant AES-GCM cost once GHASH itself was on CLMUL 86// (~185K syscalls for a 2.97 MB seal). Lazy one-time alloc. 87static GH_UPD_TEMP: i64 88func gh_upd_temp() -> *u8 { 89 if GH_UPD_TEMP == 0 { GH_UPD_TEMP = sys_mmap(16) as i64 } 90 return GH_UPD_TEMP as *u8 91} 92 93// y = (y XOR x) * h. Mutates y in place. 94func nx_ghash_update_block(y16: *u8, h16: *u8, x16: *u8) -> i64 { 95 let temp_y: *u8 = gh_upd_temp() 96 var i: i64 = 0 97 while i < 16 { temp_y[i] = y16[i] ^ x16[i]; i = i + 1 } 98 nx_ghash_mul(temp_y, h16, y16) 99 return 0 100} 101 102// ---- REFLECTED-DOMAIN GHASH (fast AES-GCM path) -------------------------------------- 103// Keeps the running GHASH accumulator in the PCLMULQDQ (reflected) domain so the caller 104// reflects H ONCE (rh = reflect(H)) and reflects the result back ONCE at the end -- one 105// byte-reversal per block instead of three, plus 8-byte word XOR instead of a byte loop. 106// Field arithmetic is identical to the Horner nx_ghash_update_block: both reduce to 107// gh_clmul_core, which nx_ghash_clmul_gate re-proves == the bit-by-bit reference each build. 108 109// reflect a 16-byte block (bytewise bit-reversal, byte order preserved) into dst. 110func nx_ghash_reflect16(dst: *u8, src: *u8) -> i64 { 111 gh_byterev_bits(src, dst) 112 return 0 113} 114 115// reusable 32-byte scratch for the reflected update: rx[0:16] (reflect(x)), comb[16:32]. 116static GH_REV_SCRATCH: i64 117func gh_rev_scratch() -> *u8 { 118 if GH_REV_SCRATCH == 0 { GH_REV_SCRATCH = sys_mmap(32) as i64 } 119 return GH_REV_SCRATCH as *u8 120} 121 122// py = (py XOR reflect(x16)) (x) rh, all in the reflected domain. Mutates py in place. 123// py : reflected accumulator phi(Y) (starts 0; phi(0)=0) 124// rh : reflected H = phi(H), precomputed once by the caller 125// x16 : NORMAL-domain (GCM) input block (AAD / ciphertext / length block) 126func nx_ghash_upd_rev(py: *u8, rh: *u8, x16: *u8) -> i64 { 127 let sc: *u8 = gh_rev_scratch() 128 let rx: *u8 = sc 129 let comb: *u8 = ((sc as i64) + 16) as *u8 130 gh_byterev_bits(x16, rx) 131 let pyi: *i64 = py as *i64 132 let rxi: *i64 = rx as *i64 133 let ci: *i64 = comb as *i64 134 ci[0] = pyi[0] ^ rxi[0] 135 ci[1] = pyi[1] ^ rxi[1] 136 gh_clmul_core(comb, rh, py) 137 return 0 138} 139 140// reflected-domain GF(2^128) multiply out = a (x) b (both reflected). Used to precompute 141// the reflected H-powers rh2=rh(x)rh, rh3=rh2(x)rh, rh4=rh3(x)rh once per seal. 142func nx_ghash_mul_rev(a16: *u8, b16: *u8, out16: *u8) -> i64 { 143 gh_clmul_core(a16, b16, out16) 144 return 0 145} 146 147// reusable scratch for the 4-way aggregate: rcb[0:16] (reflected block), acc256[16:48]. 148static GH_AGG_SCRATCH: i64 149func gh_agg_scratch() -> *u8 { 150 if GH_AGG_SCRATCH == 0 { GH_AGG_SCRATCH = sys_mmap(48) as i64 } 151 return GH_AGG_SCRATCH as *u8 152} 153 154// 4-WAY AGGREGATED GHASH update (one reduction per 4 blocks). `blocks` points at 4 155// contiguous 16-byte NORMAL-domain input blocks C1..C4. Computes, in the reflected domain, 156// py <- (py XOR phi(C1))(x)rh4 XOR phi(C2)(x)rh3 XOR phi(C3)(x)rh2 XOR phi(C4)(x)rh1 157// which equals four sequential Horner steps py<-(py XOR Ci)*H (proven by expansion), but 158// XOR-accumulates the four 256-bit products and reduces ONCE. rh1..rh4 = phi(H^1..H^4). 159func nx_ghash_upd_rev4(py: *u8, rh1: *u8, rh2: *u8, rh3: *u8, rh4: *u8, blocks: *u8) -> i64 { 160 let sc: *u8 = gh_agg_scratch() 161 let rcb: *u8 = sc 162 let acc: *i64 = ((sc as i64) + 16) as *i64 163 let rcbi: *i64 = rcb as *i64 164 let pyi: *i64 = py as *i64 165 acc[0] = 0; acc[1] = 0; acc[2] = 0; acc[3] = 0 166 167 // C1: (py XOR phi(C1)) (x) rh4 168 gh_byterev_bits(blocks, rcb) 169 rcbi[0] = rcbi[0] ^ pyi[0] 170 rcbi[1] = rcbi[1] ^ pyi[1] 171 gh_clmul_mul256_acc(rcb, rh4, acc) 172 // C2: phi(C2) (x) rh3 173 gh_byterev_bits(((blocks as i64) + 16) as *u8, rcb) 174 gh_clmul_mul256_acc(rcb, rh3, acc) 175 // C3: phi(C3) (x) rh2 176 gh_byterev_bits(((blocks as i64) + 32) as *u8, rcb) 177 gh_clmul_mul256_acc(rcb, rh2, acc) 178 // C4: phi(C4) (x) rh1 179 gh_byterev_bits(((blocks as i64) + 48) as *u8, rcb) 180 gh_clmul_mul256_acc(rcb, rh1, acc) 181 182 gh_clmul_reduce256(acc, py) 183 return 0 184} 185 186// Accumulate buf (zero-padded to 16-byte block) into GHASH state y16. 187func nx_ghash_update_buf(y16: *u8, h16: *u8, 188 buf: *u8, buf_len: i64) -> i64 { 189 let block: *u8 = sys_mmap(16) 190 var pos: i64 = 0 191 while pos < buf_len { 192 var b: i64 = 0 193 while b < 16 { 194 if pos + b < buf_len { block[b] = buf[pos + b] } 195 else { block[b] = 0 } 196 b = b + 1 197 } 198 nx_ghash_update_block(y16, h16, block) 199 pos = pos + 16 200 } 201 return 0 202} 203 204// Big-endian 64-bit write into 8 bytes at dst. 205func nx_ghash_be64_put(dst: *u8, v: i64) -> i64 { 206 dst[0] = ((v >> 56) & 0xff) as u8 207 dst[1] = ((v >> 48) & 0xff) as u8 208 dst[2] = ((v >> 40) & 0xff) as u8 209 dst[3] = ((v >> 32) & 0xff) as u8 210 dst[4] = ((v >> 24) & 0xff) as u8 211 dst[5] = ((v >> 16) & 0xff) as u8 212 dst[6] = ((v >> 8) & 0xff) as u8 213 dst[7] = (v & 0xff) as u8 214 return 0 215} 216 217// Append the 16-byte length block [aad_len_bits BE 64][ct_len_bits BE 64] 218// and accumulate. Produces the raw S = GHASH(H, A || C || L). The 219// CALLER (AES-GCM seal/open) then XORs S with E_K(J0) to get the tag. 220func nx_ghash_finalize(y16: *u8, h16: *u8, 221 aad_len_bytes: i64, ct_len_bytes: i64) -> i64 { 222 let lenblk: *u8 = sys_mmap(16) 223 nx_ghash_be64_put(lenblk, aad_len_bytes * 8) 224 nx_ghash_be64_put(lenblk + 8, ct_len_bytes * 8) 225 nx_ghash_update_block(y16, h16, lenblk) 226 return 0 227} 228 229func main() -> i64 { 230 return 0 231}