nx_sha3.nx source
↩ module page · 324 lines · 11534 B
1// sha3.nx -- FIPS 202 SHA3-256 (Keccak-f[1600], rate=1088 bits).
2//
3// license_tier: INDEPENDENT_REDERIVE
4// genealogy_id: international-research-sources/nist/fips_202
5//
6// Quantum-safe companion to sha256.nx. Under Grover's algorithm both
7// SHA-256 and SHA3-256 have ~128-bit collision resistance on a quantum
8// adversary, which remains secure -- but SHA-3 is the primitive used
9// internally by NIST post-quantum schemes (ML-KEM FIPS 203, ML-DSA
10// FIPS 204, SLH-DSA FIPS 205), so shipping it here is the prerequisite
11// for every quantum-safe KEM/signature we'll add later.
12//
13// Design:
14// - State: 25 * u64 = 1600 bits, laid out as a 5x5 lane grid A[x,y]
15// - Permutation: 24 rounds of theta, rho, pi, chi, iota
16// - Padding: SHA-3 domain separation byte 0x06, trailing 0x80
17// - Rate for SHA3-256: r = 1088 bits = 136 bytes; capacity c = 512
18// - Output: 32 bytes (first 256 bits of the state after finalisation)
19//
20// Reference: FIPS PUB 202, sections 3.2-3.3. Known answer:
21// sha3_256("abc") =
22// 3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532
23//
24// This implementation:
25// - Pure NishiLang; no C interop; no libc.
26// - Uses only arithmetic + bit ops (XOR, AND, NOT, rotate-left)
27// defined on i64; 1600-bit state lives in a 25-entry i64 array.
28// - Sequential round loop; no table lookups on secret data.
29//
30// Timing-side-channel stance: Keccak-f is naturally constant-time
31// when compiled straight from the spec. The only operations are
32// XOR, AND, NOT, and rotate-left -- all secret-independent in time.
33// No S-box table, no conditional on state bits. Safe to handle
34// secret data directly.
35//
36// nx_safety_envelope:
37// intended_use: "SHA-3 / Keccak hash + SHAKE XOF -- post-
38// SHA-2-collision migration target, also
39// used as CSPRNG output stream"
40// sil_target: SIL3
41// asil_target: QM
42// dal_target: DAL B
43// evidence: [FIPS_202_canonical_basis, no_FP,
44// constant_time_keccak_f_permutation,
45// no_table_lookup_on_secret_data]
46// hazard_register: [bug-tape-state-not-cleared-after-use,
47// bug-tape-SHAKE-output-truncation]
48// residual_risk: "SHA-3 is the substrate's recommended hash
49// for new code; SHA-256 remains for legacy
50// TLS / RFC interop."
51// verdict: NOT_YET_EVALUATED
52
53import "nx_syscalls.nx"
54import "nx_bits.nx"
55
56// Round constants -- FIPS 202 Appendix A. 24 u64 constants.
57// Stored as a static table; no secret-dependent index -- only the
58// round counter (0..24) is used.
59// Rewritten 2026-05-20: of the 17 RC constants stored as hand-
60// converted negative-decimal i64 representations, multiple had
61// arithmetic errors (RC6, RC14, RC15, RC16 confirmed wrong; others
62// converted just for consistency). Silent SHA-3 output corruption
63// followed for an unknown duration with no external-reference KAT
64// to catch it. Per [[feedback-canonical-constants-kat-against-
65// reference-not-hand-converted]] -- hex literals only.
66const RC0: i64 = 0x0000000000000001
67const RC1: i64 = 0x0000000000008082
68const RC2: i64 = 0x800000000000808A
69const RC3: i64 = 0x8000000080008000
70const RC4: i64 = 0x000000000000808B
71const RC5: i64 = 0x0000000080000001
72const RC6: i64 = 0x8000000080008081
73const RC7: i64 = 0x8000000000008009
74const RC8: i64 = 0x000000000000008A
75const RC9: i64 = 0x0000000000000088
76const RC10: i64 = 0x0000000080008009
77const RC11: i64 = 0x000000008000000A
78const RC12: i64 = 0x000000008000808B
79const RC13: i64 = 0x800000000000008B
80const RC14: i64 = 0x8000000000008089
81const RC15: i64 = 0x8000000000008003
82const RC16: i64 = 0x8000000000008002
83const RC17: i64 = 0x8000000000000080
84const RC18: i64 = 0x000000000000800A
85const RC19: i64 = 0x800000008000000A
86const RC20: i64 = 0x8000000080008081
87const RC21: i64 = 0x8000000000008080
88const RC22: i64 = 0x0000000080000001
89const RC23: i64 = 0x8000000080008008
90
91// Read one round constant by index 0..24.
92func sha3_rc(r: i64) -> i64 {
93 if r == 0 { return RC0 }
94 if r == 1 { return RC1 }
95 if r == 2 { return RC2 }
96 if r == 3 { return RC3 }
97 if r == 4 { return RC4 }
98 if r == 5 { return RC5 }
99 if r == 6 { return RC6 }
100 if r == 7 { return RC7 }
101 if r == 8 { return RC8 }
102 if r == 9 { return RC9 }
103 if r == 10 { return RC10 }
104 if r == 11 { return RC11 }
105 if r == 12 { return RC12 }
106 if r == 13 { return RC13 }
107 if r == 14 { return RC14 }
108 if r == 15 { return RC15 }
109 if r == 16 { return RC16 }
110 if r == 17 { return RC17 }
111 if r == 18 { return RC18 }
112 if r == 19 { return RC19 }
113 if r == 20 { return RC20 }
114 if r == 21 { return RC21 }
115 if r == 22 { return RC22 }
116 return RC23
117}
118
119// Rotation offsets for the ρ step, indexed by (5 * y + x). Derived
120// from FIPS 202 Table 2. Constant; indexed only by public lane-pos.
121func sha3_rho(i: i64) -> i64 {
122 if i == 0 { return 0 }
123 if i == 1 { return 1 }
124 if i == 2 { return 62 }
125 if i == 3 { return 28 }
126 if i == 4 { return 27 }
127 if i == 5 { return 36 }
128 if i == 6 { return 44 }
129 if i == 7 { return 6 }
130 if i == 8 { return 55 }
131 if i == 9 { return 20 }
132 if i == 10 { return 3 }
133 if i == 11 { return 10 }
134 if i == 12 { return 43 }
135 if i == 13 { return 25 }
136 if i == 14 { return 39 }
137 if i == 15 { return 41 }
138 if i == 16 { return 45 }
139 if i == 17 { return 15 }
140 if i == 18 { return 21 }
141 if i == 19 { return 8 }
142 if i == 20 { return 18 }
143 if i == 21 { return 2 }
144 if i == 22 { return 61 }
145 if i == 23 { return 56 }
146 return 14 // i == 24
147}
148
149// Delegated to nx_bits_rotl64 (rolq/rol intrinsic). Keccak does 24
150// rounds * 25 lanes * 1 rotate per lane = 600 rotates per permutation;
151// every speedup compounds.
152func rotl64(x: i64, r: i64) -> i64 {
153 return nx_bits_rotl64(x, r)
154}
155
156// Read/write one lane at (x, y) in a 25-u64 state array.
157func lane_at(state: *i64, x: i64, y: i64) -> i64 {
158 return state[y * 5 + x]
159}
160
161func lane_set(state: *i64, x: i64, y: i64, v: i64) -> i64 {
162 state[y * 5 + x] = v
163 return 0
164}
165
166// One round of Keccak-f[1600]. state is 25 i64. scratch is 25 i64
167// used as working storage so the round's reads don't overlap writes.
168func keccak_round(state: *i64, scratch: *i64, round_idx: i64) -> i64 {
169 // theta: compute column parities C[x] = xor over y of A[x,y],
170 // then D[x] = C[x-1] xor rotl(C[x+1], 1). Apply A[x,y] ^= D[x].
171 var x: i64 = 0
172 while x < 5 {
173 var c: i64 = lane_at(state, x, 0)
174 c = c ^ lane_at(state, x, 1)
175 c = c ^ lane_at(state, x, 2)
176 c = c ^ lane_at(state, x, 3)
177 c = c ^ lane_at(state, x, 4)
178 scratch[x] = c
179 x = x + 1
180 }
181 x = 0
182 while x < 5 {
183 let xm: i64 = (x + 4) % 5
184 let xp: i64 = (x + 1) % 5
185 let d: i64 = scratch[xm] ^ rotl64(scratch[xp], 1)
186 var y: i64 = 0
187 while y < 5 {
188 let v: i64 = lane_at(state, x, y) ^ d
189 lane_set(state, x, y, v)
190 y = y + 1
191 }
192 x = x + 1
193 }
194
195 // rho + pi: each lane gets rotated by its offset and moved to
196 // a permuted position. B[y, 2x+3y mod 5] = rotl(A[x,y], rho).
197 var yy: i64 = 0
198 while yy < 5 {
199 var xx: i64 = 0
200 while xx < 5 {
201 let r: i64 = sha3_rho(yy * 5 + xx)
202 let a: i64 = lane_at(state, xx, yy)
203 let nx: i64 = yy
204 let ny: i64 = (2 * xx + 3 * yy) % 5
205 scratch[ny * 5 + nx] = rotl64(a, r)
206 xx = xx + 1
207 }
208 yy = yy + 1
209 }
210 var k: i64 = 0
211 while k < 25 { state[k] = scratch[k]; k = k + 1 }
212
213 // chi: A[x,y] = A[x,y] xor ((not A[x+1,y]) and A[x+2,y]).
214 var y2: i64 = 0
215 while y2 < 5 {
216 var xxx: i64 = 0
217 while xxx < 5 {
218 scratch[xxx] = lane_at(state, xxx, y2)
219 xxx = xxx + 1
220 }
221 xxx = 0
222 while xxx < 5 {
223 let nx1: i64 = (xxx + 1) % 5
224 let nx2: i64 = (xxx + 2) % 5
225 let v: i64 = scratch[xxx] ^ ((scratch[nx1] ^ -1) & scratch[nx2])
226 lane_set(state, xxx, y2, v)
227 xxx = xxx + 1
228 }
229 y2 = y2 + 1
230 }
231
232 // iota: A[0,0] ^= RC[round_idx]
233 let a00: i64 = lane_at(state, 0, 0)
234 lane_set(state, 0, 0, a00 ^ sha3_rc(round_idx))
235 return 0
236}
237
238// Run all 24 rounds of the Keccak-f[1600] permutation.
239func keccak_permute(state: *i64, scratch: *i64) -> i64 {
240 var r: i64 = 0
241 while r < 24 {
242 keccak_round(state, scratch, r)
243 r = r + 1
244 }
245 return 0
246}
247
248// SHA3-256 absorb. `in_bytes` of length `n`; rate is 136 bytes.
249// Fully-filled blocks are XORed into the state then permuted.
250// Final partial block is padded with SHA-3 domain separator 0x06 at
251// the end of the message + 0x80 at position (rate - 1). Writes the
252// 32-byte digest to `out`.
253func sha3_256(in_bytes: *u8, n: i64, out: *u8) -> i64 {
254 // State: 25 * 8 = 200 bytes, zero-initialised.
255 let state_raw: *u8 = sys_mmap(200)
256 let state: *i64 = state_raw as *i64
257 var i: i64 = 0
258 while i < 25 { state[i] = 0; i = i + 1 }
259
260 let scratch_raw: *u8 = sys_mmap(200)
261 let scratch: *i64 = scratch_raw as *i64
262
263 let rate: i64 = 136 // bytes per block for SHA3-256
264
265 var pos: i64 = 0
266 // Absorb full blocks.
267 while n - pos >= rate {
268 var b: i64 = 0
269 while b < rate {
270 // XOR byte b of this block into state byte b.
271 let byte_idx: i64 = b
272 let lane_idx: i64 = byte_idx / 8
273 let shift: i64 = (byte_idx % 8) * 8
274 let in_b: i64 = in_bytes[pos + b]
275 state[lane_idx] = state[lane_idx] ^ (in_b << shift)
276 b = b + 1
277 }
278 keccak_permute(state, scratch)
279 pos = pos + rate
280 }
281
282 // Final partial block. Build a 136-byte scratch buffer with the
283 // remaining message bytes, 0x06 domain separator, zeros, 0x80.
284 let last_raw: *u8 = sys_mmap(rate)
285 var j: i64 = 0
286 while j < rate { last_raw[j] = 0; j = j + 1 }
287 let remaining: i64 = n - pos
288 j = 0
289 while j < remaining { last_raw[j] = in_bytes[pos + j]; j = j + 1 }
290 last_raw[remaining] = 0x06
291 last_raw[rate - 1] = last_raw[rate - 1] | 0x80
292
293 // Absorb the padded last block.
294 var b2: i64 = 0
295 while b2 < rate {
296 let lane_idx: i64 = b2 / 8
297 let shift: i64 = (b2 % 8) * 8
298 let lb: i64 = last_raw[b2]
299 state[lane_idx] = state[lane_idx] ^ (lb << shift)
300 b2 = b2 + 1
301 }
302 keccak_permute(state, scratch)
303
304 // Squeeze 32 bytes from the start of the state.
305 var k: i64 = 0
306 while k < 32 {
307 let lane_idx: i64 = k / 8
308 let shift: i64 = (k % 8) * 8
309 let v: i64 = (state[lane_idx] >> shift) & 0xFF
310 out[k] = v
311 k = k + 1
312 }
313 return 0
314}
315
316// Compile-only smoke. Real KAT ("abc" -> 3a985d...) requires a
317// execution harness we'll wire into f6_gate.sh once qemu-riscv64 is
318// available on the dev host.
319func main() -> i64 {
320 let msg: *u8 = "abc"
321 let digest: *u8 = sys_mmap(32)
322 sha3_256(msg, 3, digest)
323 return digest[0] as i64
324}