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}