code wiki / _hdl_build / nx_hmacalt_extvec_gate.nx

nx_hmacalt_extvec_gate.nx source

↩ module page · 287 lines · 13676 B

1// nx_hmacalt_extvec_gate.nx -- ADJUDICATES the hmac.nx FORK against RFC 4231 (all 7 HMAC-SHA-256 vectors). 2// 3// One import apart from nx_hmac_extvec_gate: 4// nx_hmac_extvec_gate -> nx_hmac.nx -> nx_sha256.nx (7/7 GREEN) 5// nx_hmacalt_extvec_gate -> hmac.nx -> sha256.nx (9 live importers, NEVER GRADED) 6// Symbol parity verified: hmac.nx's functions all exist in nx_hmac.nx. 7// NOTE sha256.nx is a SHIM re-exporting nx_sha256.nx, so the CORE beneath this fork is already the 8// validated one -- what is under test here is the HMAC WRAPPER itself (key padding, ipad/opad, the 9// key-longer-than-block branch that RFC 4231 cases 6 and 7 exercise with a 131-byte key). 10// * TESTING A WRAPPER WHOSE CORE IS ALREADY EXONERATED IS STILL WORTH DOING -- HMAC's most commonly botched 11// part is the key-shortening branch, which lives in the wrapper, not the hash. 12//// nx_hmacalt_extvec_gate.nx -- HMAC-SHA-256 vs RFC 4231, ALL SEVEN published test cases. 13// 14// SUPERSEDES the single-case version. Going from 1 vector to 7 matters because the cases are deliberately 15// chosen by the authority to hit DIFFERENT code paths, and the ones I was NOT running are the interesting 16// ones: case 3 uses a 50-byte repeated data block, cases 6 and 7 use a 131-BYTE KEY (longer than the 64-byte 17// SHA-256 block, so the key must be HASHED first), and case 5 publishes a TRUNCATED 128-bit MAC. 18// ★A gate that ran only case 1 would never touch the key-longer-than-block branch -- the single most 19// commonly botched part of HMAC. Running one vector from a seven-vector suite is not "validated against 20// RFC 4231"; it is validated against one line of it. 21// 22// ⚠VARIABLE-LENGTH FIELDS, HANDLED BY TERMINATOR NOT BY LENGTH. Key/Data lengths differ per case and their 23// annotations are inconsistent -- "(20 bytes)" for keys but ("Hi There") for data -- so a length cannot be 24// read uniformly. Instead the hex run is read until the first `(`, which terminates both forms. 25// ⚠THE MAC HAS NO `(` TERMINATOR and case 5's is TRUNCATED to 16 bytes, so it is read as hex pairs until a 26// pair is not both-hex, capped at 32. That correctly stops at the section heading that follows -- note 27// "4.3." begins with '4', a HEX DIGIT, and is only rejected because '.' is not: the PAIR rule saves this, 28// a single-nibble rule would have swallowed it. 29// 30// Construction unchanged: no expected value in this source, document pinned to a socket-time digest, every 31// key/data/MAC read from that pinned document, and a completeness check that refuses GREEN below 7. 32// license_tier: ORIGINAL expect_exit: 0 33import "nx_syscalls.nx" 34import "nx_sha256_wasm.nx" 35import "hmac.nx" 36import "nx_gate_verdict.nx" 37 38func w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 39func wb(b: *u8, n: i64) -> i64 { sys_write(1, b, n); return 0 } 40 41func nn(v: i64) -> i64 { 42 var m: i64 = v 43 if m < 0 { w("-" as *u8); m = 0 - m } 44 let t: *u8 = sys_mmap(32) 45 var k: i64 = 0 46 if m == 0 { t[0] = 48 as u8; k = 1 } 47 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 48 let b: *u8 = sys_mmap(32) 49 var j: i64 = 0 50 while j < k { b[j] = t[k - 1 - j]; j = j + 1 } 51 sys_write(1, b, k) 52 return 0 53} 54 55func hexnib(v: i64) -> i64 { if v < 10 { return 48 + v } return 87 + v } 56 57func hexval(c: i64) -> i64 { 58 if c >= 48 { if c <= 57 { return c - 48 } } 59 if c >= 97 { if c <= 102 { return c - 87 } } 60 if c >= 65 { if c <= 70 { return c - 55 } } 61 return 0 - 1 62} 63 64func isws(c: i64) -> i64 { 65 if c == 32 { return 1 } 66 if c == 10 { return 1 } 67 if c == 13 { return 1 } 68 if c == 9 { return 1 } 69 return 0 70} 71 72func starts(b: *u8, n: i64, at: i64, s: *u8) -> i64 { 73 var i: i64 = 0 74 while s[i] != (0 as u8) { 75 if at + i >= n { return 0 } 76 if b[at + i] != s[i] { return 0 } 77 i = i + 1 78 } 79 return 1 80} 81 82func findfrom(b: *u8, n: i64, s: *u8, from: i64) -> i64 { 83 var p: i64 = from 84 while p < n { 85 if starts(b, n, p, s) == 1 { return p } 86 p = p + 1 87 } 88 return 0 - 1 89} 90 91// Read hex PAIRS (whitespace between pairs is skipped) until a pair is not both-hex, or `(` is reached, 92// or cap is hit. Returns the byte count. 93func parserun(b: *u8, n: i64, from: i64, out: *u8, cap: i64) -> i64 { 94 var p: i64 = from 95 var got: i64 = 0 96 var done: i64 = 0 97 while done == 0 { 98 if got >= cap { done = 1 } 99 else { 100 // skip whitespace 101 var d1: i64 = 0 102 while d1 == 0 { 103 if p >= n { d1 = 1 } 104 else { if isws(b[p] as i64) == 1 { p = p + 1 } else { d1 = 1 } } 105 } 106 if p + 1 >= n { done = 1 } 107 else { 108 // `(` ends a LINE SEGMENT, not the value: RFC 4231 annotates EVERY wrapped line, e.g. 109 // Data = 7768...6e7420 ("what do ya want ") 110 // 666f...693f ("for nothing?") 111 // Treating `(` as the value terminator stopped case 2 at 16 of its 28 bytes. On `(`, skip 112 // to the next line and continue ONLY if it resumes with a hex pair; otherwise stop. 113 if b[p] == (40 as u8) { 114 var dl: i64 = 0 115 while dl == 0 { 116 if p >= n { dl = 1 } 117 else { if b[p] == (10 as u8) { p = p + 1; dl = 1 } else { p = p + 1 } } 118 } 119 var dw: i64 = 0 120 while dw == 0 { 121 if p >= n { dw = 1 } 122 else { if isws(b[p] as i64) == 1 { p = p + 1 } else { dw = 1 } } 123 } 124 // ⚠"Da" IS VALID HEX. The next line may be ` Data = 7768...`, and testing only 125 // "do the first two chars parse as hex" accepted D,a and read the LABEL as byte 0xDA -- 126 // adding one phantom byte to every key (case 1 read 21 of 20, case 2 read 5 of 4). 127 // Third form of the same hazard today: an ASCII gutter, then English prose, now a 128 // FIELD LABEL that happens to spell hex. 129 // STRUCTURAL DISCRIMINATOR: a continuation line has NO '='; every label line has one. 130 var eqfound: i64 = 0 131 var sc: i64 = p 132 var de: i64 = 0 133 while de == 0 { 134 if sc >= n { de = 1 } 135 else { if b[sc] == (10 as u8) { de = 1 } 136 else { if b[sc] == (61 as u8) { eqfound = 1; de = 1 } else { sc = sc + 1 } } } 137 } 138 if eqfound == 1 { done = 1 } 139 else { 140 if p + 1 >= n { done = 1 } 141 else { 142 if hexval(b[p] as i64) < 0 { done = 1 } 143 else { if hexval(b[p + 1] as i64) < 0 { done = 1 } } 144 } 145 } 146 } 147 else { 148 let h1: i64 = hexval(b[p] as i64) 149 let h2: i64 = hexval(b[p + 1] as i64) 150 if h1 < 0 { done = 1 } 151 else { if h2 < 0 { done = 1 } 152 else { 153 out[got] = ((h1 * 16) + h2) as u8 154 got = got + 1 155 p = p + 2 156 } } 157 } 158 } 159 } 160 } 161 return got 162} 163 164// Find the next "Key" FIELD LABEL at line start. Verified against the raw bytes of RFC 4231: 165// line 192 " Key = 0b0b..." <- cases 1,2,4,5,6,7 166// line 251 " Key aaaa..." <- CASE 3: no '=' at all 167// line 177 " Keys, data, and digests..." <- PROSE. "Key" is a PREFIX of "Keys". 168// Anchoring on "\n Key" alone matched that prose line and drove the parse to ZERO cases. So the label is 169// accepted only when the character AFTER "Key" is a SPACE or '=' -- which "Keys" fails on 's'. 170// ★A prefix match is not a token match. Every anchor in this file is now checked against the byte AFTER it. 171func find_key_label(b: *u8, n: i64, from: i64) -> i64 { 172 var p: i64 = from 173 var done: i64 = 0 174 while done == 0 { 175 let h: i64 = findfrom(b, n, "\n Key" as *u8, p) 176 if h < 0 { return 0 - 1 } 177 let c: i64 = b[h + 7] as i64 178 if c == 32 { return h } 179 if c == 61 { return h } 180 p = h + 7 181 } 182 return 0 - 1 183} 184func main() -> i64 { 185 w("nx_hmacalt_extvec_gate -- HMAC-SHA-256 vs RFC 4231, ALL 7 cases, READ FROM THE FETCHED DOCUMENT\n" as *u8) 186 187 let lp: *i64 = sys_mmap(16) as *i64 188 lp[0] = 0 189 let b: *u8 = sys_read_file("knowledge/extvec/rfc4231.txt\x00" as *u8, lp) 190 if lp[0] <= 0 { w("RED: fetched vector file absent -- run nx_vecfetch.\n" as *u8); return 1 } 191 192 let ctx: *u8 = sys_mmap(1024) 193 let dg: *u8 = sys_mmap(64) 194 nx_sha256_one_shot(b, lp[0], ctx, dg) 195 let hx: *u8 = sys_mmap(80) 196 var i: i64 = 0 197 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 } 198 let wnt: *u8 = "72178527ce93500e730bc8eb182b857e583096d652b64ece0879c52ba1df973b\x00" as *u8 199 var pin: i64 = 1 200 i = 0 201 while i < 64 { if hx[i] != wnt[i] { pin = 0 } i = i + 1 } 202 w(" acquisition digest: " as *u8); wb(hx, 64); w("\n" as *u8) 203 if pin == 0 { w("RED: PIN FAILED -- not the file nx_vecfetch acquired.\n" as *u8); return 1 } 204 w(" PIN OK -- bytes match the digest computed in-process at the socket\n" as *u8) 205 206 let key: *u8 = sys_mmap(512) 207 let data: *u8 = sys_mmap(512) 208 let exp: *u8 = sys_mmap(64) 209 let got: *u8 = sys_mmap(64) 210 211 var pass: i64 = 0 212 var fail: i64 = 0 213 var seen: i64 = 0 214 var cur: i64 = 0 215 var done: i64 = 0 216 while done == 0 { 217 let lm: i64 = findfrom(b, lp[0], "HMAC-SHA-256 =" as *u8, cur) 218 if lm < 0 { done = 1 } 219 else { 220 // the Key and Data for this case are the ones immediately BEFORE this MAC line 221 // ⚠RFC 4231 CASE 3 WRITES `Key` WITH NO `=` -- every other case writes `Key =`. Anchoring on 222 // "Key =" skipped case 3 entirely and matched CASE 4's key, which sits AFTER case 3's MAC. 223 // The lk>lm guard below caught that and halted rather than pairing case 4's key with case 3's 224 // MAC -- which would have produced a confident WRONG FAIL against a correct HMAC. 225 // ★THE AUTHORITY'S OWN DOCUMENT IS INCONSISTENT, AND A READER MUST SURVIVE THAT WITHOUT 226 // SILENTLY MIS-PAIRING. Anchor on "Key" and skip an OPTIONAL "=". 227 let lk: i64 = find_key_label(b, lp[0], cur) 228 let ld: i64 = findfrom(b, lp[0], "Data =" as *u8, cur) 229 if lk < 0 { done = 1 } 230 else { if ld < 0 { done = 1 } 231 else { if lk > lm { done = 1 } 232 else { 233 var ks: i64 = lk + 7 234 var dks: i64 = 0 235 while dks == 0 { 236 if ks >= lp[0] { dks = 1 } 237 else { if b[ks] == (32 as u8) { ks = ks + 1 } 238 else { if b[ks] == (61 as u8) { ks = ks + 1 } else { dks = 1 } } } 239 } 240 let klen: i64 = parserun(b, lp[0], ks, key, 400) 241 let dlen: i64 = parserun(b, lp[0], ld + 6, data, 400) 242 let elen: i64 = parserun(b, lp[0], lm + 14, exp, 32) 243 if klen <= 0 { done = 1 } 244 else { if elen <= 0 { done = 1 } 245 else { 246 hmac_sha256(key, klen, data, dlen, got) 247 var same: i64 = 1 248 i = 0 249 while i < elen { if got[i] != exp[i] { same = 0 } i = i + 1 } 250 seen = seen + 1 251 if same == 1 { 252 pass = pass + 1 253 w(" PASS case " as *u8); nn(seen) 254 w(": keylen=" as *u8); nn(klen); w(" datalen=" as *u8); nn(dlen) 255 w(" maclen=" as *u8); nn(elen); w("\n" as *u8) 256 } else { 257 fail = fail + 1 258 w(" FAIL case " as *u8); nn(seen); w(": keylen=" as *u8); nn(klen) 259 w(" datalen=" as *u8); nn(dlen); w(" maclen=" as *u8); nn(elen); w("\n" as *u8) 260 } 261 cur = lm + 14 262 } } 263 } } } 264 } 265 } 266 267 // RFC 4231 publishes SEVEN cases. Grading fewer while printing GREEN is coverage-gaming -- this exact 268 // check is what exposed a swallowed vector in the MD5 gate earlier today. 269 if seen != 7 { 270 w(" RED: parsed " as *u8); nn(seen); w(" of 7 published cases -- refusing GREEN on a partial read.\n" as *u8) 271 fail = fail + 1 272 } 273 274 w("\n refsrc=https://www.rfc-editor.org/rfc/rfc4231.txt\n" as *u8) 275 w(" refsrcdig=" as *u8); wb(hx, 64); w("\n" as *u8) 276 w(" ref=RFC4231-all-cases gate=nx_hmacalt_extvec_gate\n" as *u8) 277 w("nx_hmacalt_extvec_gate: cases=" as *u8); nn(seen); w(" pass=" as *u8); nn(pass); w(" fail=" as *u8); nn(fail) 278 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check 279 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled 280 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify. 281 let ctr__dry: *i64 = gv_ctr() 282 ctr__dry[0] = pass 283 ctr__dry[1] = pass + fail 284 let rc__dry: i64 = gv_verdict("HMAC-EXTVEC-GATE" as *u8, ctr__dry, "teeth unchanged; verdict emission migrated onto the shared base class" as *u8) 285 sys_exit(rc__dry) 286 return rc__dry 287}