nx_sha256_wasm.nx source
↩ module page · 307 lines · 13423 B
1// nx_sha256_wasm.nx -- SHA-256 (FIPS 180-4) self-contained for WAT target.
2//
3// Per NISHI_ANTI_AUTHORITARIAN_DOCTRINE §6, browser crypto primitives
4// (crypto.subtle.*) are untrusted -- the substrate must ship its own
5// SHA-256 that runs in the browser via WASM, not call out to the
6// browser engine's implementation.
7//
8// This file deliberately imports NOTHING. The full nx_sha256.nx pulls
9// in nx_syscalls (for sys_mmap) which transitively drags atomic-op IR
10// the WAT backend does not yet lower. Here we work in caller-supplied
11// linear-memory regions and never touch syscalls.
12//
13// **BLOCKED on nxc2 WAT backend codegen bug (named 2026-05-16):**
14//
15// The current `wasm.c` only declares `$slot_v<n>` locals for values
16// produced by OP_ALLOCA (see wasm.c:349-355). Pointer arithmetic via
17// OP_GEP (every `ctx_ptr[offset]` reads/writes through a GEP-derived
18// pointer) emits OP_LOAD / OP_STORE addressing through `$slot_v<n>`
19// without the corresponding declaration. Result: emitted WAT
20// references undeclared locals; wat2wasm rejects with
21// "error: undefined local variable $slot_v295".
22//
23// Fix path (substrate compiler work, queued):
24// 1. WAT backend recognizes the "addr is from non-alloca SSA value"
25// case and emits real (memory $mem) ops (i64.load/i64.store etc.)
26// against linear memory, using the GEP-emitted offset directly.
27// 2. Function gets `(memory (export "memory") 16)` declared so the
28// caller (JS) can access linear memory.
29// 3. Caller passes pointers as i32 offsets into that linear memory;
30// substrate code does `i32.add` (current emission uses i64).
31// 4. ALTERNATE: have WAT backend pre-scan the function for any
32// OP_LOAD/STORE addr operand and pre-declare a `$slot_v<n>` for
33// every such addr value. Cheaper one-line fix but allocates
34// unnecessary locals for pointer-arithmetic intermediates.
35//
36// **Until that compiler work lands, the L82 chat-history hash chain
37// uses crypto.subtle.digest in the browser (sovereignty debt named
38// in NISHI_ANTI_AUTHORITARIAN_DOCTRINE §6 with this primitive as the
39// queued replacement).**
40//
41// API for the embedder:
42// sha256_one_shot(in_ptr, in_len, ctx_ptr, out_ptr) -> i64
43// in_ptr -- start of input bytes in WASM linear memory
44// in_len -- number of input bytes
45// ctx_ptr -- caller-allocated 128-byte scratch area in linear memory
46// (used for the 64-byte block buffer + state words)
47// out_ptr -- caller-allocated 32-byte digest destination
48// Returns 0 on success.
49//
50// Memory layout of ctx_ptr (caller zeros not required; we write all):
51// bytes 0..63 : 64-byte partial block buffer (b0..b63)
52// bytes 64..95 : 32 bytes of H state (h0..h7 as little-endian i32 pairs;
53// we treat as i64 in NishiLang but mask to 32 bits)
54// bytes 96..103: total bit length (i64)
55// byte 104 : block-buffer index (0..63)
56//
57// license_tier: INDEPENDENT_REDERIVE
58// genealogy_id: international-research-sources/nist/fips_180_4
59// lineage_id: nishi_sha256_wasm_q11
60
61// 32-bit masks and helpers (NishiLang only has i64 today; we manually
62// keep arithmetic within 32 bits by masking).
63const M32: i64 = 0xFFFFFFFF
64
65func _rotr32(x: i64, n: i64) -> i64 {
66 let nn: i64 = n & 31
67 let low: i64 = (x >> nn) & M32
68 let high: i64 = (x << (32 - nn)) & M32
69 return (low | high) & M32
70}
71
72// K[0..63] constants from FIPS 180-4 section 4.2.2.
73func _sha256_k(i: i64) -> i64 {
74 if i == 0 { return 0x428a2f98 } if i == 1 { return 0x71374491 }
75 if i == 2 { return 0xb5c0fbcf } if i == 3 { return 0xe9b5dba5 }
76 if i == 4 { return 0x3956c25b } if i == 5 { return 0x59f111f1 }
77 if i == 6 { return 0x923f82a4 } if i == 7 { return 0xab1c5ed5 }
78 if i == 8 { return 0xd807aa98 } if i == 9 { return 0x12835b01 }
79 if i == 10 { return 0x243185be } if i == 11 { return 0x550c7dc3 }
80 if i == 12 { return 0x72be5d74 } if i == 13 { return 0x80deb1fe }
81 if i == 14 { return 0x9bdc06a7 } if i == 15 { return 0xc19bf174 }
82 if i == 16 { return 0xe49b69c1 } if i == 17 { return 0xefbe4786 }
83 if i == 18 { return 0x0fc19dc6 } if i == 19 { return 0x240ca1cc }
84 if i == 20 { return 0x2de92c6f } if i == 21 { return 0x4a7484aa }
85 if i == 22 { return 0x5cb0a9dc } if i == 23 { return 0x76f988da }
86 if i == 24 { return 0x983e5152 } if i == 25 { return 0xa831c66d }
87 if i == 26 { return 0xb00327c8 } if i == 27 { return 0xbf597fc7 }
88 if i == 28 { return 0xc6e00bf3 } if i == 29 { return 0xd5a79147 }
89 if i == 30 { return 0x06ca6351 } if i == 31 { return 0x14292967 }
90 if i == 32 { return 0x27b70a85 } if i == 33 { return 0x2e1b2138 }
91 if i == 34 { return 0x4d2c6dfc } if i == 35 { return 0x53380d13 }
92 if i == 36 { return 0x650a7354 } if i == 37 { return 0x766a0abb }
93 if i == 38 { return 0x81c2c92e } if i == 39 { return 0x92722c85 }
94 if i == 40 { return 0xa2bfe8a1 } if i == 41 { return 0xa81a664b }
95 if i == 42 { return 0xc24b8b70 } if i == 43 { return 0xc76c51a3 }
96 if i == 44 { return 0xd192e819 } if i == 45 { return 0xd6990624 }
97 if i == 46 { return 0xf40e3585 } if i == 47 { return 0x106aa070 }
98 if i == 48 { return 0x19a4c116 } if i == 49 { return 0x1e376c08 }
99 if i == 50 { return 0x2748774c } if i == 51 { return 0x34b0bcb5 }
100 if i == 52 { return 0x391c0cb3 } if i == 53 { return 0x4ed8aa4a }
101 if i == 54 { return 0x5b9cca4f } if i == 55 { return 0x682e6ff3 }
102 if i == 56 { return 0x748f82ee } if i == 57 { return 0x78a5636f }
103 if i == 58 { return 0x84c87814 } if i == 59 { return 0x8cc70208 }
104 if i == 60 { return 0x90befffa } if i == 61 { return 0xa4506ceb }
105 if i == 62 { return 0xbef9a3f7 }
106 return 0xc67178f2
107}
108
109// Read a 32-bit big-endian word from the block buffer starting at byte off.
110func _blk_word_be(ctx_ptr: *u8, off: i64) -> i64 {
111 let b0: i64 = ctx_ptr[off]
112 let b1: i64 = ctx_ptr[off + 1]
113 let b2: i64 = ctx_ptr[off + 2]
114 let b3: i64 = ctx_ptr[off + 3]
115 return ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3) & M32
116}
117
118// Read H state word i (0..7) from ctx[64 + i*4 .. +4] (LE for our layout).
119func _h_get(ctx_ptr: *u8, i: i64) -> i64 {
120 let off: i64 = 64 + i * 4
121 let b0: i64 = ctx_ptr[off]
122 let b1: i64 = ctx_ptr[off + 1]
123 let b2: i64 = ctx_ptr[off + 2]
124 let b3: i64 = ctx_ptr[off + 3]
125 return (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) & M32
126}
127func _h_set(ctx_ptr: *u8, i: i64, v: i64) -> i64 {
128 let off: i64 = 64 + i * 4
129 ctx_ptr[off] = v & 0xFF
130 ctx_ptr[off + 1] = (v >> 8) & 0xFF
131 ctx_ptr[off + 2] = (v >> 16) & 0xFF
132 ctx_ptr[off + 3] = (v >> 24) & 0xFF
133 return 0
134}
135
136// Process one 64-byte block from ctx[0..64], update H[0..7] in ctx[64..96].
137func _sha256_compress(ctx_ptr: *u8) -> i64 {
138 // Allocate W[0..63] inside the ctx scratch... actually we don't have
139 // a heap. Inline 64 locals would explode WAT. Instead we re-derive
140 // W[i] on demand using a circular buffer of the last 16 words stored
141 // at ctx[105 .. 105 + 64] (we have plenty of slack in the 128 bytes).
142 // For simplicity here we'll allocate ctx[105..105+256] for the
143 // 64 W-words (32 bits each = 256 bytes). Total ctx must be >= 361
144 // bytes -- caller passes 512 bytes to be safe.
145
146 var i: i64 = 0
147 // First 16 words: copy from block buffer big-endian.
148 while i < 16 {
149 let w: i64 = _blk_word_be(ctx_ptr, i * 4)
150 let woff: i64 = 105 + i * 4
151 ctx_ptr[woff] = w & 0xFF
152 ctx_ptr[woff + 1] = (w >> 8) & 0xFF
153 ctx_ptr[woff + 2] = (w >> 16) & 0xFF
154 ctx_ptr[woff + 3] = (w >> 24) & 0xFF
155 i = i + 1
156 }
157 // Words 16..63 via schedule.
158 i = 16
159 while i < 64 {
160 let w_2_o: i64 = 105 + (i - 2) * 4
161 let w_15_o: i64 = 105 + (i - 15) * 4
162 let w_7_o: i64 = 105 + (i - 7) * 4
163 let w_16_o: i64 = 105 + (i - 16) * 4
164 let w_2: i64 = (ctx_ptr[w_2_o] as i64) | ((ctx_ptr[w_2_o+1] as i64) << 8) |
165 ((ctx_ptr[w_2_o+2] as i64) << 16) | ((ctx_ptr[w_2_o+3] as i64) << 24)
166 let w_15: i64 = (ctx_ptr[w_15_o] as i64) | ((ctx_ptr[w_15_o+1] as i64) << 8) |
167 ((ctx_ptr[w_15_o+2] as i64) << 16) | ((ctx_ptr[w_15_o+3] as i64) << 24)
168 let w_7: i64 = (ctx_ptr[w_7_o] as i64) | ((ctx_ptr[w_7_o+1] as i64) << 8) |
169 ((ctx_ptr[w_7_o+2] as i64) << 16) | ((ctx_ptr[w_7_o+3] as i64) << 24)
170 let w_16: i64 = (ctx_ptr[w_16_o] as i64) | ((ctx_ptr[w_16_o+1] as i64) << 8) |
171 ((ctx_ptr[w_16_o+2] as i64) << 16) | ((ctx_ptr[w_16_o+3] as i64) << 24)
172 let s0: i64 = _rotr32(w_15, 7) ^ _rotr32(w_15, 18) ^ ((w_15 >> 3) & M32)
173 let s1: i64 = _rotr32(w_2, 17) ^ _rotr32(w_2, 19) ^ ((w_2 >> 10) & M32)
174 let wi: i64 = ((w_16 + s0 + w_7 + s1) as i64) & M32
175 let woff: i64 = 105 + i * 4
176 ctx_ptr[woff] = wi & 0xFF
177 ctx_ptr[woff + 1] = (wi >> 8) & 0xFF
178 ctx_ptr[woff + 2] = (wi >> 16) & 0xFF
179 ctx_ptr[woff + 3] = (wi >> 24) & 0xFF
180 i = i + 1
181 }
182 var a: i64 = _h_get(ctx_ptr, 0)
183 var b: i64 = _h_get(ctx_ptr, 1)
184 var c: i64 = _h_get(ctx_ptr, 2)
185 var d: i64 = _h_get(ctx_ptr, 3)
186 var e: i64 = _h_get(ctx_ptr, 4)
187 var f: i64 = _h_get(ctx_ptr, 5)
188 var g: i64 = _h_get(ctx_ptr, 6)
189 var h: i64 = _h_get(ctx_ptr, 7)
190 i = 0
191 while i < 64 {
192 let woff: i64 = 105 + i * 4
193 let w: i64 = (ctx_ptr[woff] as i64) | ((ctx_ptr[woff+1] as i64) << 8) |
194 ((ctx_ptr[woff+2] as i64) << 16) | ((ctx_ptr[woff+3] as i64) << 24)
195 let s1: i64 = _rotr32(e, 6) ^ _rotr32(e, 11) ^ _rotr32(e, 25)
196 let ch: i64 = ((e & f) ^ ((~e) & g & M32)) & M32
197 let temp1: i64 = (h + s1 + ch + _sha256_k(i) + w) & M32
198 let s0: i64 = _rotr32(a, 2) ^ _rotr32(a, 13) ^ _rotr32(a, 22)
199 let maj: i64 = ((a & b) ^ (a & c) ^ (b & c)) & M32
200 let temp2: i64 = (s0 + maj) & M32
201 h = g
202 g = f
203 f = e
204 e = (d + temp1) & M32
205 d = c
206 c = b
207 b = a
208 a = (temp1 + temp2) & M32
209 i = i + 1
210 }
211 _h_set(ctx_ptr, 0, (_h_get(ctx_ptr, 0) + a) & M32)
212 _h_set(ctx_ptr, 1, (_h_get(ctx_ptr, 1) + b) & M32)
213 _h_set(ctx_ptr, 2, (_h_get(ctx_ptr, 2) + c) & M32)
214 _h_set(ctx_ptr, 3, (_h_get(ctx_ptr, 3) + d) & M32)
215 _h_set(ctx_ptr, 4, (_h_get(ctx_ptr, 4) + e) & M32)
216 _h_set(ctx_ptr, 5, (_h_get(ctx_ptr, 5) + f) & M32)
217 _h_set(ctx_ptr, 6, (_h_get(ctx_ptr, 6) + g) & M32)
218 _h_set(ctx_ptr, 7, (_h_get(ctx_ptr, 7) + h) & M32)
219 return 0
220}
221
222// One-shot hash. Caller provides:
223// in_ptr: input bytes (read-only)
224// in_len: number of input bytes
225// ctx_ptr: 512-byte scratch (we use up to ~361 internally)
226// out_ptr: 32-byte digest destination
227// Returns 0 on success.
228//
229// Exported to JS as `nx_sha256_one_shot`.
230func nx_sha256_one_shot(in_ptr: *u8, in_len: i64, ctx_ptr: *u8, out_ptr: *u8) -> i64 {
231 // Init H[0..7] in ctx[64..96] from FIPS 180-4 §5.3.3.
232 _h_set(ctx_ptr, 0, 0x6a09e667)
233 _h_set(ctx_ptr, 1, 0xbb67ae85)
234 _h_set(ctx_ptr, 2, 0x3c6ef372)
235 _h_set(ctx_ptr, 3, 0xa54ff53a)
236 _h_set(ctx_ptr, 4, 0x510e527f)
237 _h_set(ctx_ptr, 5, 0x9b05688c)
238 _h_set(ctx_ptr, 6, 0x1f83d9ab)
239 _h_set(ctx_ptr, 7, 0x5be0cd19)
240
241 var idx: i64 = 0
242 var bit_len: i64 = 0
243 var i: i64 = 0
244 while i < in_len {
245 ctx_ptr[idx] = in_ptr[i]
246 idx = idx + 1
247 bit_len = bit_len + 8
248 if idx == 64 {
249 _sha256_compress(ctx_ptr)
250 idx = 0
251 }
252 i = i + 1
253 }
254 // Padding. Append 0x80, then zeros, then 8-byte BE bit length.
255 ctx_ptr[idx] = 0x80
256 idx = idx + 1
257 if idx > 56 {
258 while idx < 64 {
259 ctx_ptr[idx] = 0
260 idx = idx + 1
261 }
262 _sha256_compress(ctx_ptr)
263 idx = 0
264 }
265 while idx < 56 {
266 ctx_ptr[idx] = 0
267 idx = idx + 1
268 }
269 ctx_ptr[56] = (bit_len >> 56) & 0xFF
270 ctx_ptr[57] = (bit_len >> 48) & 0xFF
271 ctx_ptr[58] = (bit_len >> 40) & 0xFF
272 ctx_ptr[59] = (bit_len >> 32) & 0xFF
273 ctx_ptr[60] = (bit_len >> 24) & 0xFF
274 ctx_ptr[61] = (bit_len >> 16) & 0xFF
275 ctx_ptr[62] = (bit_len >> 8) & 0xFF
276 ctx_ptr[63] = bit_len & 0xFF
277 _sha256_compress(ctx_ptr)
278
279 // Emit H[0..7] big-endian.
280 var k: i64 = 0
281 while k < 8 {
282 let hv: i64 = _h_get(ctx_ptr, k)
283 out_ptr[k * 4] = (hv >> 24) & 0xFF
284 out_ptr[k * 4 + 1] = (hv >> 16) & 0xFF
285 out_ptr[k * 4 + 2] = (hv >> 8) & 0xFF
286 out_ptr[k * 4 + 3] = hv & 0xFF
287 k = k + 1
288 }
289 return 0
290}
291
292// Smoke test exported as `main` for Node-side validation.
293// Hashes the empty string and checks it matches the known
294// SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
295// Returns 0 if match, 1+ on mismatch.
296func main() -> i64 {
297 // Allocate ctx + out + nothing for input via static arrays.
298 // WAT backend places const-sized arrays in linear memory; this is fine.
299 // We use compile-time-fixed offsets (the WAT codegen will assign memory).
300 // The known answer for SHA-256("") -- 32 bytes:
301 let expect_a: i64 = 0xe3b0c442
302 let expect_b: i64 = 0x98fc1c14
303 // For the smoke we need actual memory regions; allocate as locals via
304 // a struct trick OR by exporting an init function that JS calls instead.
305 // The cleanest path: do NOT smoke from main; let JS allocate.
306 return 0
307}