nx_sha1.nx source
↩ module page · 238 lines · 7683 B
1// sha1.nx -- SHA-1 (FIPS 180-4 §6.1), 160-bit hash.
2//
3// NOT cryptographically safe for new protocols -- SHAttered (2017)
4// produced chosen-prefix collisions; any signature scheme using
5// SHA-1 is considered broken. But SHA-1 remains on the wire:
6// - TLS 1.0/1.1 MAC (legacy handshake interop)
7// - WebSocket handshake (RFC 6455 uses it in Sec-WebSocket-Accept;
8// collision resistance is not relied on here -- just the
9// mixing property, so it's safe in this specific use)
10// - git object IDs (migrating to SHA-256 slowly)
11// - HMAC-SHA1 (still fine; HMAC is collision-agnostic)
12// - OAuth 1.0, PBKDF2, old certificates
13//
14// So we ship it for protocol interop, never for new signatures.
15//
16// Algorithm (FIPS 180-4 §6.1):
17// - Pad: append 1 bit, zero-pad, append 64-bit big-endian
18// length so total length is a multiple of 512 bits.
19// - Process in 512-bit blocks using 80 rounds.
20// - State is 5 words (H0..H4) initialised to the FIPS constants.
21//
22// Invariants:
23// S1 Matches FIPS 180-4 test vectors.
24// S2 Empty input hashes to da39a3ee5e6b4b0d3255bfef95601890afd80709.
25// S3 State mmap'd per call -- no globals.
26//
27// license_tier: INDEPENDENT_REDERIVE
28// genealogy_id: international-research-sources/nist/fips_180_4
29//
30
31// nx_safety_envelope:
32// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
33// sil_target: SIL1
34// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
35// verdict: NOT_YET_EVALUATED
36
37import "nx_syscalls.nx"
38
39const SHA1_H0: i64 = 0x67452301
40const SHA1_H1: i64 = 0xEFCDAB89
41const SHA1_H2: i64 = 0x98BADCFE
42const SHA1_H3: i64 = 0x10325476
43const SHA1_H4: i64 = 0xC3D2E1F0
44
45const SHA1_K0: i64 = 0x5A827999
46const SHA1_K1: i64 = 0x6ED9EBA1
47const SHA1_K2: i64 = 0x8F1BBCDC
48const SHA1_K3: i64 = 0xCA62C1D6
49
50const SHA1_MASK32: i64 = 0xFFFFFFFF
51
52// 32-bit left rotate.
53//
54// BUG FIX (2026-05-15): the original `hi` mask `(1 << (32-k)) - 1` was
55// inverted -- for ROTL by k it kept only the LOW (32-k) bits of the
56// shifted-down top, dropping the actual rotated-in bits. This made
57// every SHA-1 digest wrong for inputs that hit `_wsx_rotl(b, 30)` in
58// the round-function -- which is every input. Just mask to MASK32
59// (the shifted-down value is at most 32 bits anyway). Verified
60// against the RFC 6455 example vector + sha1sum(1) on the same input.
61func sha1_rotl(x: i64, k: i64) -> i64 {
62 let x32: i64 = x & SHA1_MASK32
63 let lo: i64 = (x32 << k) & SHA1_MASK32
64 let hi: i64 = (x32 >> (32 - k)) & SHA1_MASK32
65 return lo | hi
66}
67
68// Process one 64-byte block at buf[off..off+64] into state[0..5]. The 80-word schedule `w`
69// is caller-owned scratch reused across blocks (perf 2026-06-10: was an mmap syscall PER block).
70func sha1_process_block(buf: *u8, off: i64, state: *i64, w: *i64) -> i64 {
71 var t: i64 = 0
72 while t < 16 {
73 let b0: i64 = buf[off + t * 4]
74 let b1: i64 = buf[off + t * 4 + 1]
75 let b2: i64 = buf[off + t * 4 + 2]
76 let b3: i64 = buf[off + t * 4 + 3]
77 w[t] = ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3) & SHA1_MASK32
78 t = t + 1
79 }
80 t = 16
81 while t < 80 {
82 let v: i64 = w[t-3] ^ w[t-8] ^ w[t-14] ^ w[t-16]
83 w[t] = ((v << 1) | (v >> 31)) & SHA1_MASK32 // inline rotl(v,1)
84 t = t + 1
85 }
86
87 var a: i64 = state[0]
88 var b: i64 = state[1]
89 var c: i64 = state[2]
90 var d: i64 = state[3]
91 var e: i64 = state[4]
92
93 t = 0
94 while t < 80 {
95 var f: i64 = 0
96 var k: i64 = 0
97 if t < 20 {
98 f = (b & c) | ((b ^ SHA1_MASK32) & d)
99 k = SHA1_K0
100 }
101 if t >= 20 {
102 if t < 40 {
103 f = b ^ c ^ d
104 k = SHA1_K1
105 }
106 }
107 if t >= 40 {
108 if t < 60 {
109 f = (b & c) | (b & d) | (c & d)
110 k = SHA1_K2
111 }
112 }
113 if t >= 60 {
114 f = b ^ c ^ d
115 k = SHA1_K3
116 }
117
118 let ar5: i64 = ((a << 5) | (a >> 27)) & SHA1_MASK32 // inline rotl(a,5)
119 let temp: i64 = (ar5 + f + e + k + w[t]) & SHA1_MASK32
120 e = d
121 d = c
122 c = ((b << 30) | (b >> 2)) & SHA1_MASK32 // inline rotl(b,30)
123 b = a
124 a = temp
125 t = t + 1
126 }
127
128 state[0] = (state[0] + a) & SHA1_MASK32
129 state[1] = (state[1] + b) & SHA1_MASK32
130 state[2] = (state[2] + c) & SHA1_MASK32
131 state[3] = (state[3] + d) & SHA1_MASK32
132 state[4] = (state[4] + e) & SHA1_MASK32
133 return 0
134}
135
136// Forward decl -- used inside sha1().
137func if_ge(a: i64, b: i64, v1: i64, v2: i64) -> i64;
138
139// Compute SHA-1 of data[0..n] into out[0..20].
140func sha1(data: *u8, n: i64, out: *u8) -> i64 {
141 let state_raw: *u8 = sys_mmap(40)
142 let state: *i64 = state_raw as *i64
143 state[0] = SHA1_H0
144 state[1] = SHA1_H1
145 state[2] = SHA1_H2
146 state[3] = SHA1_H3
147 state[4] = SHA1_H4
148
149 // 80-word message-schedule scratch, allocated ONCE and reused for every block
150 // (was an mmap syscall per block -> for a 1 MB input that was ~16k syscalls).
151 let w_raw: *u8 = sys_mmap(80 * 8)
152 let w: *i64 = w_raw as *i64
153
154 // Process full 64-byte blocks.
155 let full_blocks: i64 = n / 64
156 var i: i64 = 0
157 while i < full_blocks {
158 sha1_process_block(data, i * 64, state, w)
159 i = i + 1
160 }
161
162 // Last partial block + padding.
163 let tail_off: i64 = full_blocks * 64
164 let tail_len: i64 = n - tail_off
165
166 // Pad buffer up to 64 or 128 bytes.
167 let pad_size: i64 = 128
168 let pad_raw: *u8 = sys_mmap(pad_size)
169 var j: i64 = 0
170 while j < tail_len {
171 pad_raw[j] = data[tail_off + j]
172 j = j + 1
173 }
174 pad_raw[tail_len] = 0x80
175 j = tail_len + 1
176
177 // We need room for 8-byte length at the end. If tail_len+1 > 56
178 // we need a second block.
179 let blocks_needed: i64 = if_ge(tail_len + 1, 57, 2, 1)
180 let total_padded: i64 = blocks_needed * 64
181 while j < total_padded - 8 {
182 pad_raw[j] = 0
183 j = j + 1
184 }
185 // 64-bit BE bit-length.
186 let bits: i64 = n * 8
187 pad_raw[total_padded - 8] = (bits >> 56) & 0xFF
188 pad_raw[total_padded - 7] = (bits >> 48) & 0xFF
189 pad_raw[total_padded - 6] = (bits >> 40) & 0xFF
190 pad_raw[total_padded - 5] = (bits >> 32) & 0xFF
191 pad_raw[total_padded - 4] = (bits >> 24) & 0xFF
192 pad_raw[total_padded - 3] = (bits >> 16) & 0xFF
193 pad_raw[total_padded - 2] = (bits >> 8) & 0xFF
194 pad_raw[total_padded - 1] = bits & 0xFF
195
196 // Process 1 or 2 more blocks.
197 var b: i64 = 0
198 while b < blocks_needed {
199 sha1_process_block(pad_raw, b * 64, state, w)
200 b = b + 1
201 }
202
203 // Write big-endian state[0..5] to out.
204 var w: i64 = 0
205 while w < 5 {
206 out[w * 4] = (state[w] >> 24) & 0xFF
207 out[w * 4 + 1] = (state[w] >> 16) & 0xFF
208 out[w * 4 + 2] = (state[w] >> 8) & 0xFF
209 out[w * 4 + 3] = state[w] & 0xFF
210 w = w + 1
211 }
212 return 0
213}
214
215// Helper: if a >= b, return v1, else v2.
216func if_ge(a: i64, b: i64, v1: i64, v2: i64) -> i64 {
217 if a >= b { return v1 }
218 return v2
219}
220
221// Compile-only smoke -- hash "" should give da39a3ee5e6b4b0d...
222func main() -> i64 {
223 let out: *u8 = sys_mmap(32)
224 sha1(0 as *u8, 0, out)
225 // Expected first 4 bytes: 0xDA 0x39 0xA3 0xEE
226 if out[0] != 0xDA { return 1 }
227 if out[1] != 0x39 { return 2 }
228 if out[2] != 0xA3 { return 3 }
229 if out[3] != 0xEE { return 4 }
230
231 // "abc" -> a9993e364706816aba3e25717850c26c9cd0d89d
232 sha1("abc", 3, out)
233 if out[0] != 0xA9 { return 5 }
234 if out[1] != 0x99 { return 6 }
235 if out[2] != 0x3E { return 7 }
236 if out[3] != 0x36 { return 8 }
237 return 0
238}