code wiki / _hdl_build / nx_hotp_extvec_gate.nx

nx_hotp_extvec_gate.nx source

↩ module page · 234 lines · 9998 B

1// nx_hotp_extvec_gate.nx -- HOTP vs RFC 4226 Appendix D, all TEN published values. 2// 3// ★WHY THIS GATE EXISTS: I claimed all session that HOTP/TOTP two-factor codes were BROKEN (they rode the 4// defective sha1.nx core) and that repointing hmac_sha1.nx REPAIRED them. The first half was proven; the 5// second half was INFERRED. I had shown HMAC-SHA-1 now matches RFC 2202 -- I had never once computed an 6// actual HOTP code and compared it to a published one. 7// ★★★★★"THE PRIMITIVE UNDERNEATH IT IS NOW CORRECT" IS NOT THE SAME CLAIM AS "THIS PRODUCES CORRECT 8// OUTPUT." HOTP adds dynamic truncation and modular reduction ON TOP of HMAC; a correct HMAC does not 9// prove a correct truncation, and the truncation is where HOTP implementations actually go wrong. 10// 11// EVERYTHING IS READ FROM THE PINNED DOCUMENT, INCLUDING THE SECRET. RFC 4226 Appendix D states it as 12// Secret = 0x3132333435363738393031323334353637383930 13// and typing those 20 bytes from memory would put the agent back in the byte path for the one input the 14// whole test depends on. ★THE KEY IS AN ANSWER TOO. 15// 16// ⚠ANCHOR HAZARD, HIT AND FIXED: Appendix D has TWO tables -- 17// Table 1: "Count Hexadecimal HMAC-SHA-1(secret, count)" (raw HMAC values) 18// Table 2: "Count Hexadecimal Decimal HOTP" (truncated + HOTP) 19// Anchoring on "Count Hexadecimal" matches TABLE 1 FIRST and would grade HOTP codes against 20-byte 20// HMAC strings. The anchor below is the full Table-2 header. ★AN ANCHOR THAT IS A PREFIX OF AN EARLIER 21// HEADING FINDS THE EARLIER ONE -- the same class as matching a table of contents instead of the body. 22// 23// Document pinned to a CORROBORATED digest (sovereign fetch and .NET WebClient independently agree -- 24// knowledge/extvec/CORROBORATION.tsv). 25// license_tier: ORIGINAL expect_exit: 0 26import "nx_syscalls.nx" 27import "nx_sha256_wasm.nx" 28import "nx_hotp_sha1.nx" 29import "nx_gate_verdict.nx" 30 31func w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 32func wb(b: *u8, n: i64) -> i64 { sys_write(1, b, n); return 0 } 33 34func nn(v: i64) -> i64 { 35 var m: i64 = v 36 if m < 0 { w("-" as *u8); m = 0 - m } 37 let t: *u8 = sys_mmap(32) 38 var k: i64 = 0 39 if m == 0 { t[0] = 48 as u8; k = 1 } 40 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 41 let b: *u8 = sys_mmap(32) 42 var j: i64 = 0 43 while j < k { b[j] = t[k - 1 - j]; j = j + 1 } 44 sys_write(1, b, k) 45 return 0 46} 47 48func hexnib(v: i64) -> i64 { if v < 10 { return 48 + v } return 87 + v } 49func hexval(c: i64) -> i64 { 50 if c >= 48 { if c <= 57 { return c - 48 } } 51 if c >= 97 { if c <= 102 { return c - 87 } } 52 if c >= 65 { if c <= 70 { return c - 55 } } 53 return 0 - 1 54} 55func isws(c: i64) -> i64 { 56 if c == 32 { return 1 } 57 if c == 10 { return 1 } 58 if c == 13 { return 1 } 59 if c == 9 { return 1 } 60 return 0 61} 62func starts(b: *u8, n: i64, at: i64, s: *u8) -> i64 { 63 var i: i64 = 0 64 while s[i] != (0 as u8) { 65 if at + i >= n { return 0 } 66 if b[at + i] != s[i] { return 0 } 67 i = i + 1 68 } 69 return 1 70} 71func findfrom(b: *u8, n: i64, s: *u8, from: i64) -> i64 { 72 var p: i64 = from 73 while p < n { if starts(b, n, p, s) == 1 { return p } p = p + 1 } 74 return 0 - 1 75} 76 77// Read a run of contiguous hex pairs (no whitespace tolerance -- the secret is one unbroken run). 78func parsehex(b: *u8, n: i64, from: i64, out: *u8, want: i64) -> i64 { 79 var p: i64 = from 80 var got: i64 = 0 81 while got < want { 82 if p + 1 >= n { return 0 - 1 } 83 let h1: i64 = hexval(b[p] as i64) 84 let h2: i64 = hexval(b[p + 1] as i64) 85 if h1 < 0 { return 0 - 1 } 86 if h2 < 0 { return 0 - 1 } 87 out[got] = ((h1 * 16) + h2) as u8 88 got = got + 1 89 p = p + 2 90 } 91 return p 92} 93 94// Read the next whitespace-separated DECIMAL token starting at/after `p`. Returns the value; writes the 95// position just past the token into endp. -1 if the next token is not decimal. 96func next_dec(b: *u8, n: i64, p0: i64, endp: *i64) -> i64 { 97 var p: i64 = p0 98 var d: i64 = 0 99 while d == 0 { 100 if p >= n { return 0 - 1 } 101 if isws(b[p] as i64) == 1 { p = p + 1 } else { d = 1 } 102 } 103 var v: i64 = 0 104 var any: i64 = 0 105 var done: i64 = 0 106 while done == 0 { 107 if p >= n { done = 1 } 108 else { 109 let c: i64 = b[p] as i64 110 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); any = 1; p = p + 1 } else { done = 1 } } 111 else { done = 1 } 112 } 113 } 114 if any == 0 { return 0 - 1 } 115 endp[0] = p 116 return v 117} 118 119// Skip one whitespace-separated token of ANY kind (used for the hex column, which is not decimal). 120func skip_token(b: *u8, n: i64, p0: i64) -> i64 { 121 var p: i64 = p0 122 var d: i64 = 0 123 while d == 0 { 124 if p >= n { return p } 125 if isws(b[p] as i64) == 1 { p = p + 1 } else { d = 1 } 126 } 127 while p < n { if isws(b[p] as i64) == 1 { return p } p = p + 1 } 128 return p 129} 130 131func main() -> i64 { 132 w("nx_hotp_extvec_gate -- HOTP vs RFC 4226 Appendix D, READ FROM THE FETCHED DOCUMENT\n" as *u8) 133 134 let lp: *i64 = sys_mmap(16) as *i64 135 lp[0] = 0 136 let b: *u8 = sys_read_file("knowledge/extvec/rfc4226.txt\x00" as *u8, lp) 137 if lp[0] <= 0 { w("RED: fetched vector file absent -- run nx_vecfetch.\n" as *u8); return 1 } 138 139 let ctx: *u8 = sys_mmap(1024) 140 let dg: *u8 = sys_mmap(64) 141 nx_sha256_one_shot(b, lp[0], ctx, dg) 142 let hx: *u8 = sys_mmap(80) 143 var i: i64 = 0 144 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 } 145 let wnt: *u8 = "db6974cd02ca33560ec3a191ee9b1016067cd11d1f5d89e7efa36be69482229d\x00" as *u8 146 var pin: i64 = 1 147 i = 0 148 while i < 64 { if hx[i] != wnt[i] { pin = 0 } i = i + 1 } 149 w(" acquisition digest: " as *u8); wb(hx, 64); w("\n" as *u8) 150 if pin == 0 { w("RED: PIN FAILED -- not the corroborated document.\n" as *u8); return 1 } 151 w(" PIN OK -- CORROBORATED (sovereign fetch and .NET WebClient agree)\n" as *u8) 152 153 // ---- the SECRET, read from the document ---- 154 let sat: i64 = findfrom(b, lp[0], "Secret = 0x" as *u8, 0) 155 if sat < 0 { w("RED: no 'Secret = 0x' in the document\n" as *u8); return 1 } 156 let secret: *u8 = sys_mmap(64) 157 if parsehex(b, lp[0], sat + 11, secret, 20) < 0 { w("RED: secret short\n" as *u8); return 1 } 158 // PARSE SELF-CHECK: RFC 4226's secret is the ASCII digits "12345678901234567890", so byte 0 must be 159 // '1' and byte 19 '0'. A mis-read secret would fail every vector and look like a broken HOTP. 160 // ★NAME THE READER BEFORE BLAMING THE SUBJECT. 161 var sok: i64 = 0 162 if secret[0] == (49 as u8) { if secret[19] == (48 as u8) { sok = 1 } } 163 if sok == 0 { w("RED: PARSE SELF-CHECK FAILED -- secret is not the ASCII digit run; the READER.\n" as *u8); return 1 } 164 w(" secret parsed: 20 bytes, ASCII digit run -- self-check OK\n\n" as *u8) 165 166 // ---- Table 2 ---- 167 let tab: i64 = findfrom(b, lp[0], "Count Hexadecimal Decimal HOTP" as *u8, 0) 168 if tab < 0 { w("RED: Table 2 header not found\n" as *u8); return 1 } 169 // move past the header line 170 var p: i64 = tab 171 var dh: i64 = 0 172 while dh == 0 { if p >= lp[0] { dh = 1 } else { if b[p] == (10 as u8) { p = p + 1; dh = 1 } else { p = p + 1 } } } 173 174 let ep: *i64 = sys_mmap(16) as *i64 175 var pass: i64 = 0 176 var fail: i64 = 0 177 var seen: i64 = 0 178 179 var row: i64 = 0 180 while row < 10 { 181 ep[0] = 0 182 let cnt: i64 = next_dec(b, lp[0], p, ep) 183 if cnt < 0 { row = 10 } 184 else { 185 p = ep[0] 186 p = skip_token(b, lp[0], p) // hexadecimal column (not decimal) 187 ep[0] = 0 188 let dec: i64 = next_dec(b, lp[0], p, ep) // decimal column 189 if dec < 0 { row = 10 } 190 else { 191 p = ep[0] 192 ep[0] = 0 193 let want: i64 = next_dec(b, lp[0], p, ep) // HOTP column 194 if want < 0 { row = 10 } 195 else { 196 p = ep[0] 197 let got: i64 = hotp_sha1_code(secret, 20, cnt, 6) 198 seen = seen + 1 199 if got == want { 200 pass = pass + 1 201 w(" PASS count=" as *u8); nn(cnt); w(" HOTP=" as *u8); nn(want); w("\n" as *u8) 202 } else { 203 fail = fail + 1 204 w(" FAIL count=" as *u8); nn(cnt); w(" expected " as *u8); nn(want) 205 w(" got " as *u8); nn(got); w("\n" as *u8) 206 } 207 row = row + 1 208 } 209 } 210 } 211 } 212 213 // RFC 4226 publishes TEN. Grading fewer while printing GREEN is coverage-gaming. 214 if seen < 10 { 215 w("\n RED: only " as *u8); nn(seen); w(" of 10 published values graded -- refusing GREEN on a partial read.\n" as *u8) 216 fail = fail + 1 217 } 218 219 w("\n refsrc=https://www.rfc-editor.org/rfc/rfc4226.txt\n" as *u8) 220 w(" refsrcdig=" as *u8); wb(hx, 64); w("\n" as *u8) 221 w(" ref=RFC4226-AppendixD-Table2 gate=nx_hotp_extvec_gate\n" as *u8) 222 w(" BOUND: nx_hotp_sha1.nx -> nx_hmac_sha1.nx -> nx_sha1.nx (the repaired family)\n" as *u8) 223 w("nx_hotp_extvec_gate: values=" as *u8); nn(seen) 224 w(" pass=" as *u8); nn(pass); w(" fail=" as *u8); nn(fail) 225 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check 226 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled 227 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify. 228 let ctr__dry: *i64 = gv_ctr() 229 ctr__dry[0] = pass 230 ctr__dry[1] = pass + fail 231 let rc__dry: i64 = gv_verdict("HOTP-EXTVEC-GATE" as *u8, ctr__dry, "teeth unchanged; verdict emission migrated onto the shared base class" as *u8) 232 sys_exit(rc__dry) 233 return rc__dry 234}