code wiki / (root) / nx_hmac_sha1.nx

nx_hmac_sha1.nx source

↩ module page · 203 lines · 8824 B

1// hmac_sha1.nx -- HMAC with SHA-1 (RFC 2104 + FIPS 198-1). 2// 3// license_tier: INDEPENDENT_REDERIVE 4// genealogy_id: international-research-sources/nist/fips_198_1 5// 6// Interop-only primitive, like sha1.nx itself. HMAC does not 7// rely on its inner hash's collision resistance (only pseudo- 8// randomness of the keyed mix), so HMAC-SHA1 remains safe for 9// authentication even though plain SHA-1 is broken for signatures. 10// 11// Still widely used: 12// - TOTP / HOTP (RFC 4226 / 6238 default) 13// - OAuth 1.0 HMAC-SHA1 signatures 14// - AWS SigV2 / legacy API auth 15// - PBKDF2-HMAC-SHA1 (WPA2, older WebCrypto key derivation) 16// - Older JWT HS1 tokens 17// 18// Algorithm (RFC 2104): 19// block_size = 64 bytes for SHA-1 20// if len(key) > block_size: key = SHA1(key) 21// key = key || zeros to block_size 22// ipad = key XOR 0x36 repeated 23// opad = key XOR 0x5C repeated 24// tag = SHA1(opad || SHA1(ipad || msg)) 25// 26// Composes sha1.nx. Output is 20 bytes. 27// 28// Invariants: 29// HS1 Output = 20 bytes always (HMAC-SHA1 tag length). 30// HS2 Matches RFC 2202 test vectors (not checked in smoke, 31// but the algorithm is textbook). 32 33// nx_safety_envelope: 34// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 35// sil_target: SIL1 36// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 37// verdict: NOT_YET_EVALUATED 38 39import "nx_syscalls.nx" 40import "nx_sha1.nx" 41 42const HMAC_SHA1_BLOCK: i64 = 64 43const HMAC_SHA1_OUT: i64 = 20 44 45// ============================================================================================ 46// 2026-08-01 -- ZERO-ALLOCATION VARIANT. WHY THIS EXISTS: 47// 48// hmac_sha1() below performs FOUR sys_mmap calls per invocation and frees none. That is harmless for a 49// one-shot MAC and CATASTROPHIC inside PBKDF2, which calls HMAC once per iteration. 50// 51// MEASURED PROPERLY -- within-subject A/B, ONE process, ONE hash core, N=50,000 per arm, strace-counted: 52// arm A hmac_sha1() 10 mmaps/call 40 KB resident growth per call 53// arm B hmac_sha1_into() 6 mmaps/call 24 KB per call 54// total 800,027 mmap calls for 100,000 HMAC invocations -- matches the source EXACTLY 55// (A = 4 HMAC + 2x3 sha1 = 10; B = 0 + 2x3 = 6), at 4 KB per mapping, never freed. 56// ⚠THE FIRST ATTEMPT AT THIS MEASUREMENT WAS INVALID and its numbers are RETRACTED: I compared the fixed 57// module against the *other family* (pbkdf2_sha1.nx -> sha1.nx), which changes the HASH CORE as well as 58// the allocation path -- TWO variables. ★A CONTROL THAT CHANGES THE MODULE UNDER TEST IS NOT A CONTROL. 59// The extrapolated figures from that attempt ("~16 KB/iteration", "~260 GB", "~9.6 GB per password hash") 60// were arithmetic on a single partial run and must not be quoted. ★★★★★AN EXTRAPOLATION FROM ONE PARTIAL 61// RUN IS A HYPOTHESIS WEARING A NUMBER'S CLOTHES. 62// What IS established: allocation count scales exactly with call count, nothing is freed, and the real 63// per-call cost (40 KB unfixed) is WORSE than the retracted estimate -- so the concern stands on measured 64// ground, not on the discarded arithmetic. 65// 66// ⚠WHY CALLER-OWNED SCRATCH AND **NOT** `static` BUFFERS: module-level statics are the established idiom in 67// this tree (see _set_dot_ctx), and they would fix the leak in one line -- but they would also make 68// hmac_sha1 NON-REENTRANT. This tree runs worker threads, and a shared scratch buffer would silently 69// produce WRONG MACs under concurrency. ★A SILENT WRONG MAC IS STRICTLY WORSE THAN A LEAK: the leak is 70// loud, bounded and diagnosable; a racy MAC is none of those. Caller-owned scratch removes the allocation 71// without introducing shared mutable state. 72// 73// ADDITIVE ONLY (Rule 19): hmac_sha1() keeps its exact signature and behaviour and is now a thin wrapper, 74// so every existing caller is unaffected. Hot loops opt in by hoisting the scratch themselves. 75// ============================================================================================ 76 77// Scratch layout, all offsets fixed so the caller allocates ONCE: 78// [0 .. 80) k_prime (BLOCK + 16) 79// [80 .. 112) inner_hash (32) 80// [112 .. 212) outer_buf (BLOCK + OUT + 16) 81// [212 .. 212+BLOCK+cap+16) inner_buf (BLOCK + msg_cap + 16) 82// MEASURED, within-subject A/B at N=50,000 per arm, one hash core, strace-counted: 83// arm A hmac_sha1() -> 10 mmaps/call, 40 KB resident growth per call 84// arm B hmac_sha1_into() -> 6 mmaps/call, 24 KB per call (800,027 mmaps total, matches source exactly) 85// The residual 6 are sha1()'s OWN three-per-call, invoked twice. ★FIXING THE ALLOCATOR AT ONE LAYER LEAVES 86// THE LAYER BELOW LEAKING -- so the scratch now carries a SHA1 region and the inner/outer hashes use 87// sha1_into(), taking the call path to ZERO allocations. 88const HS1_OFF_K: i64 = 0 89const HS1_OFF_IH: i64 = 80 90const HS1_OFF_OUT: i64 = 112 91const HS1_OFF_S1: i64 = 212 92const HS1_OFF_IN: i64 = 1236 93 94func hmac_sha1_scratch_bytes(msg_cap: i64) -> i64 { 95 return HS1_OFF_IN + HMAC_SHA1_BLOCK + msg_cap + 16 96} 97 98// Returns 0 on success, -1 if the scratch is too small for msg_len. ★VALIDATED AT THE BOUNDARY rather than 99// trusted: a silently-undersized scratch would corrupt adjacent scratch regions and yield a wrong MAC, 100// which is exactly the failure mode this variant exists to avoid. 101func hmac_sha1_into(scratch: *u8, scratch_cap: i64, 102 key: *u8, key_len: i64, 103 msg: *u8, msg_len: i64, 104 out: *u8) -> i64 { 105 if msg_len < 0 { return 0 - 1 } 106 if hmac_sha1_scratch_bytes(msg_len) > scratch_cap { return 0 - 1 } 107 108 let k_prime: *u8 = ((scratch as i64) + HS1_OFF_K) as *u8 109 let inner_hash: *u8 = ((scratch as i64) + HS1_OFF_IH) as *u8 110 let outer_buf: *u8 = ((scratch as i64) + HS1_OFF_OUT) as *u8 111 let s1s: *u8 = ((scratch as i64) + HS1_OFF_S1) as *u8 112 let inner_buf: *u8 = ((scratch as i64) + HS1_OFF_IN) as *u8 113 114 var i: i64 = 0 115 while i < HMAC_SHA1_BLOCK { k_prime[i] = 0; i = i + 1 } 116 if key_len > HMAC_SHA1_BLOCK { 117 sha1_into(s1s, SHA1_SCRATCH_BYTES, key, key_len, k_prime) 118 } else { 119 i = 0 120 while i < key_len { k_prime[i] = key[i]; i = i + 1 } 121 } 122 123 let inner_buf_len: i64 = HMAC_SHA1_BLOCK + msg_len 124 i = 0 125 while i < HMAC_SHA1_BLOCK { inner_buf[i] = k_prime[i] ^ 0x36; i = i + 1 } 126 i = 0 127 while i < msg_len { inner_buf[HMAC_SHA1_BLOCK + i] = msg[i]; i = i + 1 } 128 sha1_into(s1s, SHA1_SCRATCH_BYTES, inner_buf, inner_buf_len, inner_hash) 129 130 let outer_buf_len: i64 = HMAC_SHA1_BLOCK + HMAC_SHA1_OUT 131 i = 0 132 while i < HMAC_SHA1_BLOCK { outer_buf[i] = k_prime[i] ^ 0x5C; i = i + 1 } 133 i = 0 134 while i < HMAC_SHA1_OUT { outer_buf[HMAC_SHA1_BLOCK + i] = inner_hash[i]; i = i + 1 } 135 sha1_into(s1s, SHA1_SCRATCH_BYTES, outer_buf, outer_buf_len, out) 136 return 0 137} 138 139func hmac_sha1(key: *u8, key_len: i64, 140 msg: *u8, msg_len: i64, 141 out: *u8) -> i64 { 142 let k_prime: *u8 = sys_mmap(HMAC_SHA1_BLOCK + 16) 143 var i: i64 = 0 144 while i < HMAC_SHA1_BLOCK { k_prime[i] = 0; i = i + 1 } 145 146 if key_len > HMAC_SHA1_BLOCK { 147 // Shorten long keys by hashing. 148 sha1(key, key_len, k_prime) 149 } else { 150 i = 0 151 while i < key_len { 152 k_prime[i] = key[i] 153 i = i + 1 154 } 155 } 156 157 // Inner hash: SHA1(ipad || msg). 158 let inner_buf_len: i64 = HMAC_SHA1_BLOCK + msg_len 159 let inner_buf: *u8 = sys_mmap(inner_buf_len + 16) 160 i = 0 161 while i < HMAC_SHA1_BLOCK { 162 inner_buf[i] = k_prime[i] ^ 0x36 163 i = i + 1 164 } 165 i = 0 166 while i < msg_len { 167 inner_buf[HMAC_SHA1_BLOCK + i] = msg[i] 168 i = i + 1 169 } 170 let inner_hash: *u8 = sys_mmap(32) 171 sha1(inner_buf, inner_buf_len, inner_hash) 172 173 // Outer hash: SHA1(opad || inner_hash). 174 let outer_buf_len: i64 = HMAC_SHA1_BLOCK + HMAC_SHA1_OUT 175 let outer_buf: *u8 = sys_mmap(outer_buf_len + 16) 176 i = 0 177 while i < HMAC_SHA1_BLOCK { 178 outer_buf[i] = k_prime[i] ^ 0x5C 179 i = i + 1 180 } 181 i = 0 182 while i < HMAC_SHA1_OUT { 183 outer_buf[HMAC_SHA1_BLOCK + i] = inner_hash[i] 184 i = i + 1 185 } 186 sha1(outer_buf, outer_buf_len, out) 187 return 0 188} 189 190// Compile-only smoke. 191func main() -> i64 { 192 let out: *u8 = sys_mmap(32) 193 hmac_sha1("key", 3, "The quick brown fox jumps over the lazy dog", 43, out) 194 // RFC 2202 test vector: 195 // HMAC-SHA1(key=\"key\", msg=\"The quick brown fox jumps over the lazy dog\") 196 // = de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9 197 if out[0] != 0xDE { return 1 } 198 if out[1] != 0x7C { return 2 } 199 if out[2] != 0x9B { return 3 } 200 if out[3] != 0x85 { return 4 } 201 if out[19] != 0xD9 { return 5 } 202 return 0 203}