code wiki / _hdl_build / nx_hmacsha1_extvec_gate.nx

nx_hmacsha1_extvec_gate.nx source

↩ module page · 444 lines · 21798 B

1// nx_hmacsha1_extvec_gate.nx -- validated against RFC2202-HMAC-SHA1, read from the pinned+corroborated RFC2202 document. 2// ⚠HEADER CORRECTED 2026-08-01: this file was CLONED from nx_hmac_extvec_gate.nx and inherited its 3// header verbatim, so it claimed to be that gate validating HMAC-SHA-256 against RFC 4231. Nine of 4// twenty-three extvec gates carried the same wrong self-description. ★★★★★A CLONED FILE INHERITS ITS 5// PARENT'S CLAIMS, AND ON AN EVIDENCE ARTIFACT THE HEADER IS A PROVENANCE CLAIM, NOT A COMMENT -- 6// an auditor reading headers would have concluded RFC 4231 validated all nine subjects. 7// 8// SUPERSEDES the single-case version. Going from 1 vector to 7 matters because the cases are deliberately 9// chosen by the authority to hit DIFFERENT code paths, and the ones I was NOT running are the interesting 10// ones: case 3 uses a 50-byte repeated data block, cases 6 and 7 use a 131-BYTE KEY (longer than the 64-byte 11// SHA-256 block, so the key must be HASHED first), and case 5 publishes a TRUNCATED 128-bit MAC. 12// ★A gate that ran only case 1 would never touch the key-longer-than-block branch -- the single most 13// commonly botched part of HMAC. Running one vector from a seven-vector suite is not "validated against 14// RFC 4231"; it is validated against one line of it. 15// 16// ⚠VARIABLE-LENGTH FIELDS, HANDLED BY TERMINATOR NOT BY LENGTH. Key/Data lengths differ per case and their 17// annotations are inconsistent -- "(20 bytes)" for keys but ("Hi There") for data -- so a length cannot be 18// read uniformly. Instead the hex run is read until the first `(`, which terminates both forms. 19// ⚠THE MAC HAS NO `(` TERMINATOR and case 5's is TRUNCATED to 16 bytes, so it is read as hex pairs until a 20// pair is not both-hex, capped at 32. That correctly stops at the section heading that follows -- note 21// "4.3." begins with '4', a HEX DIGIT, and is only rejected because '.' is not: the PAIR rule saves this, 22// a single-nibble rule would have swallowed it. 23// 24// Construction unchanged: no expected value in this source, document pinned to a socket-time digest, every 25// key/data/MAC read from that pinned document, and a completeness check that refuses GREEN below 7. 26// license_tier: ORIGINAL expect_exit: 0 27import "nx_syscalls.nx" 28import "nx_sha256_wasm.nx" 29import "hmac_sha1.nx" 30import "hkdf_sha1.nx" 31import "nx_sha1.nx" 32import "nx_gate_verdict.nx" 33 34func w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 35func wb(b: *u8, n: i64) -> i64 { sys_write(1, b, n); return 0 } 36 37func nn(v: i64) -> i64 { 38 var m: i64 = v 39 if m < 0 { w("-" as *u8); m = 0 - m } 40 let t: *u8 = sys_mmap(32) 41 var k: i64 = 0 42 if m == 0 { t[0] = 48 as u8; k = 1 } 43 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 44 let b: *u8 = sys_mmap(32) 45 var j: i64 = 0 46 while j < k { b[j] = t[k - 1 - j]; j = j + 1 } 47 sys_write(1, b, k) 48 return 0 49} 50 51func hexnib(v: i64) -> i64 { if v < 10 { return 48 + v } return 87 + v } 52 53func hexval(c: i64) -> i64 { 54 if c >= 48 { if c <= 57 { return c - 48 } } 55 if c >= 97 { if c <= 102 { return c - 87 } } 56 if c >= 65 { if c <= 70 { return c - 55 } } 57 return 0 - 1 58} 59 60func isws(c: i64) -> i64 { 61 if c == 32 { return 1 } 62 if c == 10 { return 1 } 63 if c == 13 { return 1 } 64 if c == 9 { return 1 } 65 return 0 66} 67 68func starts(b: *u8, n: i64, at: i64, s: *u8) -> i64 { 69 var i: i64 = 0 70 while s[i] != (0 as u8) { 71 if at + i >= n { return 0 } 72 if b[at + i] != s[i] { return 0 } 73 i = i + 1 74 } 75 return 1 76} 77 78func findfrom(b: *u8, n: i64, s: *u8, from: i64) -> i64 { 79 var p: i64 = from 80 while p < n { 81 if starts(b, n, p, s) == 1 { return p } 82 p = p + 1 83 } 84 return 0 - 1 85} 86 87// Read hex PAIRS (whitespace between pairs is skipped) until a pair is not both-hex, or `(` is reached, 88// or cap is hit. Returns the byte count. 89func parserun(b: *u8, n: i64, from: i64, out: *u8, cap: i64) -> i64 { 90 var p: i64 = from 91 var got: i64 = 0 92 var done: i64 = 0 93 while done == 0 { 94 if got >= cap { done = 1 } 95 else { 96 // skip whitespace, REMEMBERING whether the skip crossed a line boundary 97 var d1: i64 = 0 98 var crossed: i64 = 0 99 while d1 == 0 { 100 if p >= n { d1 = 1 } 101 else { if isws(b[p] as i64) == 1 { if b[p] == (10 as u8) { crossed = 1 } p = p + 1 } else { d1 = 1 } } 102 } 103 // ★"Da" IS VALID HEX. Same defect, same file family: the `=`-discriminator was applied only to 104 // the `(`-annotated branch, so a `digest =` value (which has no `(` annotation) ran past 105 // end-of-line and swallowed the "da" of the following "data_len" as byte 0xDA -- 21 bytes for a 106 // 20-byte digest. Ported here from nx_hmacsha1fix_extvec_gate, where it was MEASURED: 107 // expected e8e9…1a91 DA vs computed e8e9…1a91 00 (first 20 bytes IDENTICAL) 108 // ★A FIX APPLIED TO ONE COPY OF A CLONED READER LEAVES EVERY OTHER COPY BROKEN -- and this gate 109 // sat at 8/9 looking like a crypto residue when it was the same reader bug all along. 110 if crossed == 1 { 111 var eq2: i64 = 0 112 var sc2: i64 = p 113 var de2: i64 = 0 114 while de2 == 0 { 115 if sc2 >= n { de2 = 1 } 116 else { if b[sc2] == (10 as u8) { de2 = 1 } 117 else { if b[sc2] == (61 as u8) { eq2 = 1; de2 = 1 } else { sc2 = sc2 + 1 } } } 118 } 119 if eq2 == 1 { p = n } 120 } 121 if p + 1 >= n { done = 1 } 122 else { 123 // `(` ends a LINE SEGMENT, not the value: RFC 4231 annotates EVERY wrapped line, e.g. 124 // Data = 7768...6e7420 ("what do ya want ") 125 // 666f...693f ("for nothing?") 126 // Treating `(` as the value terminator stopped case 2 at 16 of its 28 bytes. On `(`, skip 127 // to the next line and continue ONLY if it resumes with a hex pair; otherwise stop. 128 if b[p] == (40 as u8) { 129 var dl: i64 = 0 130 while dl == 0 { 131 if p >= n { dl = 1 } 132 else { if b[p] == (10 as u8) { p = p + 1; dl = 1 } else { p = p + 1 } } 133 } 134 var dw: i64 = 0 135 while dw == 0 { 136 if p >= n { dw = 1 } 137 else { if isws(b[p] as i64) == 1 { p = p + 1 } else { dw = 1 } } 138 } 139 // ⚠"Da" IS VALID HEX. The next line may be ` Data = 7768...`, and testing only 140 // "do the first two chars parse as hex" accepted D,a and read the LABEL as byte 0xDA -- 141 // adding one phantom byte to every key (case 1 read 21 of 20, case 2 read 5 of 4). 142 // Third form of the same hazard today: an ASCII gutter, then English prose, now a 143 // FIELD LABEL that happens to spell hex. 144 // STRUCTURAL DISCRIMINATOR: a continuation line has NO '='; every label line has one. 145 var eqfound: i64 = 0 146 var sc: i64 = p 147 var de: i64 = 0 148 while de == 0 { 149 if sc >= n { de = 1 } 150 else { if b[sc] == (10 as u8) { de = 1 } 151 else { if b[sc] == (61 as u8) { eqfound = 1; de = 1 } else { sc = sc + 1 } } } 152 } 153 if eqfound == 1 { done = 1 } 154 else { 155 if p + 1 >= n { done = 1 } 156 else { 157 if hexval(b[p] as i64) < 0 { done = 1 } 158 else { if hexval(b[p + 1] as i64) < 0 { done = 1 } } 159 } 160 } 161 } 162 else { 163 let h1: i64 = hexval(b[p] as i64) 164 let h2: i64 = hexval(b[p + 1] as i64) 165 if h1 < 0 { done = 1 } 166 else { if h2 < 0 { done = 1 } 167 else { 168 out[got] = ((h1 * 16) + h2) as u8 169 got = got + 1 170 p = p + 2 171 } } 172 } 173 } 174 } 175 } 176 return got 177} 178 179// Find the next "Key" FIELD LABEL at line start. Verified against the raw bytes of RFC 4231: 180// line 192 " Key = 0b0b..." <- cases 1,2,4,5,6,7 181// line 251 " Key aaaa..." <- CASE 3: no '=' at all 182// line 177 " Keys, data, and digests..." <- PROSE. "Key" is a PREFIX of "Keys". 183// Anchoring on "\n Key" alone matched that prose line and drove the parse to ZERO cases. So the label is 184// accepted only when the character AFTER "Key" is a SPACE or '=' -- which "Keys" fails on 's'. 185// ★A prefix match is not a token match. Every anchor in this file is now checked against the byte AFTER it. 186func find_key_label(b: *u8, n: i64, from: i64) -> i64 { 187 var p: i64 = from 188 var done: i64 = 0 189 while done == 0 { 190 let h: i64 = findfrom(b, n, "\n Key" as *u8, p) 191 if h < 0 { return 0 - 1 } 192 let c: i64 = b[h + 7] as i64 193 if c == 32 { return h } 194 if c == 61 { return h } 195 p = h + 7 196 } 197 return 0 - 1 198} 199func readdec(b: *u8, n: i64, from: i64) -> i64 { 200 var p: i64 = from 201 var v: i64 = 0 202 var got: i64 = 0 203 var done: i64 = 0 204 while done == 0 { 205 if p >= n { done = 1 } 206 else { 207 let c: i64 = b[p] as i64 208 if c == 32 { if got == 1 { done = 1 } else { p = p + 1 } } 209 else { if c >= 48 { if c <= 57 { v = (v*10)+(c-48); got = 1; p = p + 1 } else { done = 1 } } else { done = 1 } } 210 } 211 } 212 if got == 0 { return 0 - 1 } 213 return v 214} 215 216// A field is EITHER 0x-hex OR a quoted ASCII string. RFC 2202 uses both for `data` across its cases 217// (0xdd... for the repeated-byte cases, "Hi There" for the readable ones), so the reader must dispatch on 218// the first non-space character rather than assume one form. 219func readdec_skipeq(b: *u8, n: i64, from: i64) -> i64 { 220 var p: i64 = from 221 var d: i64 = 0 222 while d == 0 { 223 if p >= n { d = 1 } 224 else { if b[p] == (32 as u8) { p = p + 1 } else { if b[p] == (61 as u8) { p = p + 1 } else { d = 1 } } } 225 } 226 return readdec(b, n, p) 227} 228 229func readfield(b: *u8, n: i64, from: i64, out: *u8, cap: i64) -> i64 { 230 var p: i64 = from 231 var d: i64 = 0 232 while d == 0 { 233 if p >= n { d = 1 } 234 else { if b[p] == (32 as u8) { p = p + 1 } else { d = 1 } } 235 } 236 if p >= n { return 0 - 1 } 237 if b[p] == (34 as u8) { 238 // ⚠A WRAPPED QUOTED STRING: the newline REPLACES a space, it does not delete one. 239 // data = "Test Using Larger Than Block-Size Key and Larger 240 // Than One Block-Size Data" 241 // Copying raw gave 89 (47 + newline + 16 indent + 25); dropping the wrap entirely gives 72; the 242 // document declares 73. So a newline plus its following indentation collapses to EXACTLY ONE SPACE. 243 // ★Verified against the document's own data_len, not assumed -- the declared length is what 244 // distinguishes "delete the wrap" from "replace the wrap", and those differ by one byte. 245 var k: i64 = 0 246 p = p + 1 247 while p < n { 248 if b[p] == (34 as u8) { return k } 249 if k >= cap { return 0 - 1 } 250 if b[p] == (10 as u8) { 251 out[k] = 32 as u8 252 k = k + 1 253 p = p + 1 254 var ds: i64 = 0 255 while ds == 0 { 256 if p >= n { ds = 1 } 257 else { if b[p] == (32 as u8) { p = p + 1 } else { if b[p] == (13 as u8) { p = p + 1 } else { ds = 1 } } } 258 } 259 } else { 260 if b[p] == (13 as u8) { p = p + 1 } 261 else { 262 out[k] = b[p] 263 k = k + 1 264 p = p + 1 265 } 266 } 267 } 268 return 0 - 1 269 } 270 if p + 1 < n { if b[p] == (48 as u8) { if b[p+1] == (120 as u8) { 271 let got: i64 = parserun(b, n, p + 2, out, cap) 272 // ⚠RFC 2202 EXPRESSES BULK DATA AS PROSE: `data = 0xdd repeated 50 times`. That is ONE hex byte 273 // followed by a repetition count in ENGLISH. A literal hex reader returns 1 byte, and the 274 // document-declared data_len=50 is what caught it -- the self-check named the READER instead of 275 // letting HMAC be blamed for a 1-byte input. 276 // ★A value can be expressed as a PROGRAM ("repeat this"), not just as data. Tenth notation form. 277 if got == 1 { 278 let rp: i64 = findfrom(b, n, "repeated" as *u8, p) 279 if rp >= 0 { if rp < p + 40 { 280 let cnt: i64 = readdec(b, n, rp + 8) 281 if cnt > 1 { if cnt <= cap { 282 let fill: i64 = out[0] as i64 283 var q: i64 = 0 284 while q < cnt { out[q] = fill as u8; q = q + 1 } 285 return cnt 286 } } 287 } } 288 } 289 return got 290 } } } 291 return 0 - 1 292} 293 294// REFERENCE HMAC-SHA-1 BUILT HERE FROM THE EXONERATED sha1() ONLY. 295// RFC 2104: HMAC(K,m) = H((K' xor opad) || H((K' xor ipad) || m)), K' = K padded to 64 (hashed first if 296// longer). This shares ONLY the hash core with hmac_sha1 -- and that core is independently PROVEN correct 297// against RFC 3174. So if THIS matches the published digest and hmac_sha1 does not, the wrapper is at 298// fault; if BOTH miss, the reader is. ★A differential is only worth running when its shared set excludes 299// the suspect -- round 53's did not, this one does. 300func ref_hmac_sha1(key: *u8, klen: i64, msg: *u8, mlen: i64, out: *u8) -> i64 { 301 let kp: *u8 = sys_mmap(128) 302 var i: i64 = 0 303 while i < 64 { kp[i] = 0 as u8; i = i + 1 } 304 if klen > 64 { 305 let kh: *u8 = sys_mmap(64) 306 sha1(key, klen, kh) 307 i = 0 308 while i < 20 { kp[i] = kh[i]; i = i + 1 } 309 } else { 310 i = 0 311 while i < klen { kp[i] = key[i]; i = i + 1 } 312 } 313 let inner: *u8 = sys_mmap(4096) 314 i = 0 315 while i < 64 { inner[i] = (((kp[i] as i64) ^ 54) & 255) as u8; i = i + 1 } 316 i = 0 317 while i < mlen { inner[64 + i] = msg[i]; i = i + 1 } 318 let ih: *u8 = sys_mmap(64) 319 sha1(inner, 64 + mlen, ih) 320 let outer: *u8 = sys_mmap(256) 321 i = 0 322 while i < 64 { outer[i] = (((kp[i] as i64) ^ 92) & 255) as u8; i = i + 1 } 323 i = 0 324 while i < 20 { outer[64 + i] = ih[i]; i = i + 1 } 325 sha1(outer, 84, out) 326 return 0 327} 328func main() -> i64 { 329 w("nx_hmacsha1_extvec_gate -- HMAC-SHA-1 vs RFC 2202, READ FROM THE FETCHED DOCUMENT\n" as *u8) 330 let lp: *i64 = sys_mmap(16) as *i64 331 lp[0] = 0 332 let b: *u8 = sys_read_file("knowledge/extvec/rfc2202.txt\x00" as *u8, lp) 333 if lp[0] <= 0 { w("RED: fetched vector file absent.\n" as *u8); return 1 } 334 let ctx: *u8 = sys_mmap(1024) 335 let dg: *u8 = sys_mmap(64) 336 nx_sha256_one_shot(b, lp[0], ctx, dg) 337 let hx: *u8 = sys_mmap(80) 338 var i: i64 = 0 339 while i < 32 { hx[i*2] = hexnib(((dg[i] as i64)/16)&15) as u8; hx[i*2+1] = hexnib((dg[i] as i64)&15) as u8; i = i + 1 } 340 let wnt: *u8 = "c19effeca47e801be304460da3b2fb05f595e2309abceb050e40e024868fe483\x00" as *u8 341 var pin: i64 = 1 342 i = 0 343 while i < 64 { if hx[i] != wnt[i] { pin = 0 } i = i + 1 } 344 w(" acquisition digest: " as *u8); wb(hx, 64); w("\n" as *u8) 345 if pin == 0 { w("RED: PIN FAILED.\n" as *u8); return 1 } 346 w(" PIN OK -- bytes match the digest computed in-process at the socket\n" as *u8) 347 348 let key: *u8 = sys_mmap(512) 349 let data: *u8 = sys_mmap(512) 350 let exp: *u8 = sys_mmap(128) 351 let got: *u8 = sys_mmap(128) 352 353 var pass: i64 = 0 354 var fail: i64 = 0 355 var seen: i64 = 0 356 var skipped: i64 = 0 357 // ANCHOR ON THE SECTION, NOT ON A DIGEST-LENGTH GUESS. RFC 2202 has 7 HMAC-MD5 cases (sec 2) and 358 // NINE HMAC-SHA-1 markers (sec 3 -- cases 6 and 7 appear TWICE, once with longer data). Dispatching by 359 // digest length mis-sorted them 8/8 against the true 7/9. Starting at the section heading makes every 360 // case found a SHA-1 case BY CONSTRUCTION -- no inference required. 361 let sec: i64 = findfrom(b, lp[0], "3. Test Cases for HMAC-SHA-1" as *u8, 0) 362 if sec < 0 { w("RED: no HMAC-SHA-1 section heading\n" as *u8); return 1 } 363 var cur: i64 = sec 364 var done: i64 = 0 365 while done == 0 { 366 let tc: i64 = findfrom(b, lp[0], "test_case =" as *u8, cur) 367 if tc < 0 { done = 1 } 368 else { 369 let lk: i64 = findfrom(b, lp[0], "key =" as *u8, tc) 370 let lkl: i64 = findfrom(b, lp[0], "key_len" as *u8, tc) 371 let ld: i64 = findfrom(b, lp[0], "data =" as *u8, tc) 372 let ldl: i64 = findfrom(b, lp[0], "data_len" as *u8, tc) 373 let lg: i64 = findfrom(b, lp[0], "digest =" as *u8, tc) 374 if lk < 0 { done = 1 } 375 else { if lg < 0 { done = 1 } 376 else { 377 let kn: i64 = readfield(b, lp[0], lk + 5, key, 400) 378 let dn: i64 = readfield(b, lp[0], ld + 6, data, 400) 379 let kdecl: i64 = readdec_skipeq(b, lp[0], lkl + 7) 380 let ddecl: i64 = readdec_skipeq(b, lp[0], ldl + 8) 381 let gn: i64 = readfield(b, lp[0], lg + 8, exp, 100) 382 if gn < 0 { done = 1 } 383 else { 384 // THE DOCUMENT DECLARES ITS OWN LENGTHS -- grade the reader by the source. 385 if kn != kdecl { 386 w(" RED: key parsed " as *u8); nn(kn); w(" but document declares key_len=" as *u8); nn(kdecl) 387 w(" -- the READER.\n" as *u8) 388 fail = fail + 1 389 done = 1 390 } else { if dn != ddecl { 391 w(" RED: data parsed " as *u8); nn(dn); w(" but document declares data_len=" as *u8); nn(ddecl) 392 w(" -- the READER.\n" as *u8) 393 fail = fail + 1 394 done = 1 395 } else { 396 // dispatch on the PUBLISHED digest length: 20 = SHA-1, 16 = the MD5 section 397 if gn > 0 { 398 hmac_sha1(key, kn, data, dn, got) 399 // DIFFERENTIAL: HKDF-Extract(salt, ikm) IS DEFINED AS HMAC-Hash(salt, ikm) 400 // (RFC 5869 sec 2.2), so hkdf_sha1_extract is an INDEPENDENT internal path to 401 // the same function. If one matches the published vector and the other does 402 // not, the defect is localised to the one that disagrees -- adjudicated by the 403 // AUTHORITY, not by which of our two implementations we happen to trust. 404 let alt: *u8 = sys_mmap(64) 405 ref_hmac_sha1(key, kn, data, dn, alt) 406 var altsame: i64 = 1 407 var z: i64 = 0 408 while z < gn { if alt[z] != exp[z] { altsame = 0 } z = z + 1 } 409 if altsame == 1 { w(" [differential] REFERENCE HMAC (built from the exonerated sha1) MATCHES -> the WRAPPER is at fault\n" as *u8) } 410 else { w(" [differential] reference HMAC also differs -> the READER is at fault, crypto exonerated\n" as *u8) } 411 // Compare only the PUBLISHED digest length: section 3 case 5 is the truncation 412 // case, whose published digest is SHORTER than HMAC-SHA-1's 20-byte output. 413 // Comparing a fixed 20 would fail a correct implementation on that case. 414 var same: i64 = 1 415 i = 0 416 while i < gn { if got[i] != exp[i] { same = 0 } i = i + 1 } 417 seen = seen + 1 418 if same == 1 { pass = pass + 1; w(" PASS sha1 keylen=" as *u8); nn(kn); w(" datalen=" as *u8); nn(dn); w(" maclen=" as *u8); nn(gn); w("\n" as *u8) } 419 else { fail = fail + 1; w(" FAIL sha1 case keylen=" as *u8); nn(kn); w(" datalen=" as *u8); nn(dn); w("\n" as *u8) } 420 } else { skipped = skipped + 1 } 421 cur = lg + 8 422 } } 423 } 424 } } 425 } 426 } 427 428 w("\n refsrc=https://www.rfc-editor.org/rfc/rfc2202.txt\n" as *u8) 429 w(" refsrcdig=" as *u8); wb(hx, 64); w("\n" as *u8) 430 w(" ref=RFC2202-HMAC-SHA1 gate=nx_hmacsha1_extvec_gate\n" as *u8) 431 w(" NOTE: key_len and data_len are cross-checked against the document on EVERY case.\n" as *u8) 432 w(" 16-byte-digest cases are the HMAC-MD5 section and are counted as SKIPPED, not passed.\n" as *u8) 433 w("nx_hmacsha1_extvec_gate: sha1_cases=" as *u8); nn(seen); w(" skipped_md5=" as *u8); nn(skipped) 434 w(" pass=" as *u8); nn(pass); w(" fail=" as *u8); nn(fail) 435 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check 436 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled 437 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify. 438 let ctr__dry: *i64 = gv_ctr() 439 ctr__dry[0] = pass 440 ctr__dry[1] = pass + fail 441 let rc__dry: i64 = gv_verdict("HMACSHA1-EXTVEC-GATE" as *u8, ctr__dry, "teeth unchanged; verdict emission migrated onto the shared base class" as *u8) 442 sys_exit(rc__dry) 443 return rc__dry 444}