code wiki / _hdl_build / nx_totp_extvec_gate.nx

nx_totp_extvec_gate.nx source

↩ module page · 289 lines · 13455 B

1// nx_totp_extvec_gate.nx -- TOTP vs RFC 6238 Appendix B, all SIX published SHA-1 vectors. 2// 3// COMPLETES THE 2FA CHAIN. nx_hotp_extvec_gate proved HOTP (RFC 4226, 10/10). This proves TOTP, which adds 4// the TIME-STEP DERIVATION T = floor(unix_time / X) on top of HOTP and uses EIGHT digits rather than six. 5// ★Neither addition is implied by the other: an 8-digit dynamic truncation exercises a different modulus 6// than 6, and T-derivation is integer division that nothing below it can validate. 7// 8// TWO INDEPENDENT CHECKS PER ROW, because the table publishes enough to separate them: 9// 1. DERIVED T -- our own time/30 must equal the document's "Value of T (Hex)" column 10// 2. TOTP CODE -- totp_sha1_value(seed, 20, time, 30, 8) must equal the document's TOTP column 11// ★IF ONLY THE CODE WERE CHECKED, A WRONG T AND A COMPENSATING TRUNCATION BUG COULD CANCEL. Checking the 12// intermediate the authority happened to publish makes that impossible -- USE EVERY COLUMN THE AUTHORITY 13// GIVES YOU; THE INTERMEDIATE ONES ARE THERE TO LOCALISE FAILURES. 14// 15// ⚠SCOPE, STATED HONESTLY: RFC 6238 publishes EIGHTEEN rows -- 6 time points x {SHA1, SHA256, SHA512}. 16// This gate grades the SIX SHA-1 rows only, because this tree has TOTP over SHA-1 and not over SHA-256 or 17// SHA-512. The other twelve are NOT skipped-and-forgotten: they are counted and reported as 18// UNIMPLEMENTED-MODE, which is a capability gap, not a pass. ★A ROW YOU CANNOT COMPUTE IS NOT A ROW YOU 19// PASSED, AND CALLING IT "N/A" IN SILENCE IS HOW COVERAGE INFLATES. 20// 21// ⚠THE SEED IS READ FROM THE DOCUMENT (`String seed = "3132...3930";`) -- 20 bytes, never typed from 22// recall. ★THE KEY IS AN ANSWER TOO. 23// ⚠ROW SELECTION IS BY THE EXACT CELL "| SHA1 |" (two spaces). "SHA1" alone is NOT a safe discriminator 24// here because SHA256/SHA512 rows exist; a substring test would grade all eighteen against a SHA-1 25// implementation. ★A SUBSTRING NEEDLE FLATTERS SILENTLY. 26// license_tier: ORIGINAL expect_exit: 0 27import "nx_syscalls.nx" 28import "nx_sha256_wasm.nx" 29import "nx_totp_sha1.nx" 30import "nx_gate_verdict.nx" 31 32func w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 33func wb(b: *u8, n: i64) -> i64 { sys_write(1, b, n); return 0 } 34 35func nn(v: i64) -> i64 { 36 var m: i64 = v 37 if m < 0 { w("-" as *u8); m = 0 - m } 38 let t: *u8 = sys_mmap(32) 39 var k: i64 = 0 40 if m == 0 { t[0] = 48 as u8; k = 1 } 41 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 42 let b: *u8 = sys_mmap(32) 43 var j: i64 = 0 44 while j < k { b[j] = t[k - 1 - j]; j = j + 1 } 45 sys_write(1, b, k) 46 return 0 47} 48 49func hexnib(v: i64) -> i64 { if v < 10 { return 48 + v } return 87 + v } 50func hexval(c: i64) -> i64 { 51 if c >= 48 { if c <= 57 { return c - 48 } } 52 if c >= 97 { if c <= 102 { return c - 87 } } 53 if c >= 65 { if c <= 70 { return c - 55 } } 54 return 0 - 1 55} 56func isws(c: i64) -> i64 { 57 if c == 32 { return 1 } 58 if c == 10 { return 1 } 59 if c == 13 { return 1 } 60 if c == 9 { return 1 } 61 return 0 62} 63func starts(b: *u8, n: i64, at: i64, s: *u8) -> i64 { 64 var i: i64 = 0 65 while s[i] != (0 as u8) { 66 if at + i >= n { return 0 } 67 if b[at + i] != s[i] { return 0 } 68 i = i + 1 69 } 70 return 1 71} 72func findfrom(b: *u8, n: i64, s: *u8, from: i64) -> i64 { 73 var p: i64 = from 74 while p < n { if starts(b, n, p, s) == 1 { return p } p = p + 1 } 75 return 0 - 1 76} 77 78func parsehex_run(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// next whitespace/pipe-delimited DECIMAL token 95func next_dec(b: *u8, n: i64, p0: i64, endp: *i64) -> i64 { 96 var p: i64 = p0 97 var d: i64 = 0 98 while d == 0 { 99 if p >= n { return 0 - 1 } 100 let c: i64 = b[p] as i64 101 if isws(c) == 1 { p = p + 1 } else { if c == 124 { 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// next hex token of exactly `want` nibbles -> i64 120func next_hex_i64(b: *u8, n: i64, p0: i64, want: i64, endp: *i64) -> i64 { 121 var p: i64 = p0 122 var d: i64 = 0 123 while d == 0 { 124 if p >= n { return 0 - 1 } 125 let c: i64 = b[p] as i64 126 if isws(c) == 1 { p = p + 1 } else { if c == 124 { p = p + 1 } else { d = 1 } } 127 } 128 var v: i64 = 0 129 var k: i64 = 0 130 while k < want { 131 if p >= n { return 0 - 1 } 132 let hv: i64 = hexval(b[p] as i64) 133 if hv < 0 { return 0 - 1 } 134 v = v * 16 + hv 135 p = p + 1 136 k = k + 1 137 } 138 endp[0] = p 139 return v 140} 141 142func main() -> i64 { 143 w("nx_totp_extvec_gate -- TOTP vs RFC 6238 Appendix B, READ FROM THE FETCHED DOCUMENT\n" as *u8) 144 145 let lp: *i64 = sys_mmap(16) as *i64 146 lp[0] = 0 147 let b: *u8 = sys_read_file("knowledge/extvec/rfc6238.txt\x00" as *u8, lp) 148 if lp[0] <= 0 { w("RED: fetched vector file absent -- run nx_vecfetch.\n" as *u8); return 1 } 149 150 let ctx: *u8 = sys_mmap(1024) 151 let dg: *u8 = sys_mmap(64) 152 nx_sha256_one_shot(b, lp[0], ctx, dg) 153 let hx: *u8 = sys_mmap(80) 154 var i: i64 = 0 155 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 } 156 let wnt: *u8 = "82947ed9064450850547f55959dc79d2de775f0fa33f7b3f9622fb6c93e69a7a\x00" as *u8 157 var pin: i64 = 1 158 i = 0 159 while i < 64 { if hx[i] != wnt[i] { pin = 0 } i = i + 1 } 160 w(" acquisition digest: " as *u8); wb(hx, 64); w("\n" as *u8) 161 if pin == 0 { w("RED: PIN FAILED -- not the corroborated document.\n" as *u8); return 1 } 162 w(" PIN OK -- CORROBORATED (sovereign fetch and .NET WebClient agree)\n" as *u8) 163 164 // ---- seed, read from the document ---- 165 let sat: i64 = findfrom(b, lp[0], "String seed = \"" as *u8, 0) 166 if sat < 0 { w("RED: no seed declaration in document\n" as *u8); return 1 } 167 let seed: *u8 = sys_mmap(64) 168 if parsehex_run(b, lp[0], sat + 15, seed, 20) < 0 { w("RED: seed short\n" as *u8); return 1 } 169 var sok: i64 = 0 170 if seed[0] == (49 as u8) { if seed[19] == (48 as u8) { sok = 1 } } 171 if sok == 0 { w("RED: PARSE SELF-CHECK FAILED -- seed is not the ASCII digit run; the READER.\n" as *u8); return 1 } 172 w(" seed parsed: 20 bytes, ASCII digit run -- self-check OK\n\n" as *u8) 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 var othermode: i64 = 0 179 180 // count the non-SHA1 rows so the unimplemented modes are REPORTED, not silently dropped 181 var q: i64 = 0 182 var dc: i64 = 0 183 while dc == 0 { 184 let h: i64 = findfrom(b, lp[0], "| SHA256 |" as *u8, q) 185 if h < 0 { dc = 1 } else { othermode = othermode + 1; q = h + 5 } 186 } 187 q = 0 188 dc = 0 189 while dc == 0 { 190 let h: i64 = findfrom(b, lp[0], "| SHA512 |" as *u8, q) 191 if h < 0 { dc = 1 } else { othermode = othermode + 1; q = h + 5 } 192 } 193 194 var cur: i64 = 0 195 var done: i64 = 0 196 while done == 0 { 197 let hit: i64 = findfrom(b, lp[0], "| SHA1 |" as *u8, cur) 198 if hit < 0 { w(" [diag] no more SHA1 rows at cur\n" as *u8); done = 1 } 199 else { 200 // scan back to the start of this line 201 var ls: i64 = hit 202 while ls > 0 { if b[ls - 1] == (10 as u8) { ls = 0 - ls } else { ls = ls - 1 } } 203 if ls < 0 { ls = 0 - ls } 204 ep[0] = 0 205 let tsec: i64 = next_dec(b, lp[0], ls, ep) // Time(sec) 206 if tsec < 0 { w(" [diag] tsec parse failed at line start\n" as *u8); done = 1 } 207 else { 208 // skip the UTC date cell (contains digits and dashes) -- jump to the T(Hex) cell, which is 209 // the 16-nibble run. Locate it by the next '|' after the date cell. 210 var p2: i64 = findfrom(b, lp[0], "|" as *u8, ep[0]) 211 if p2 < 0 { w(" [diag] first pipe not found\n" as *u8); done = 1 } 212 else { 213 p2 = findfrom(b, lp[0], "|" as *u8, p2 + 1) // end of date cell 214 if p2 < 0 { w(" [diag] first pipe not found\n" as *u8); done = 1 } 215 else { 216 ep[0] = 0 217 let thex: i64 = next_hex_i64(b, lp[0], p2 + 1, 16, ep) 218 if thex < 0 { w(" [diag] T hex parse failed\n" as *u8); done = 1 } 219 else { 220 // ⚠⚠DO NOT ZERO ep[0] HERE. `ep` is BOTH the cursor-out slot and the start 221 // position for the next read: next_hex_i64 wrote the post-T position into it, 222 // and `ep[0] = 0` destroyed that, restarting the scan at byte 0 of the RFC -- 223 // where the header text has no leading digits, so the parse returned -1 and the 224 // whole row loop exited with seen=0. 225 // ★★★★★AN IN/OUT PARAMETER USED AS BOTH CURSOR AND RESULT WILL EVENTUALLY BE 226 // RESET BEFORE IT IS READ. The sibling HOTP gate is correct only because it 227 // happened to carry the cursor in a SEPARATE variable. 228 // ★And note what found this: not a hypothesis, but a one-line [diag] print at 229 // every early exit. Four wrong guesses earlier this session cost far more than 230 // this build did. 231 let after_t: i64 = ep[0] 232 let want: i64 = next_dec(b, lp[0], after_t, ep) 233 if want < 0 { w(" [diag] TOTP decimal parse failed\n" as *u8); done = 1 } 234 else { 235 seen = seen + 1 236 // CHECK 1: our own time-step derivation vs the document's published T 237 let tderiv: i64 = tsec / 30 238 var okT: i64 = 0 239 if tderiv == thex { okT = 1 } 240 // CHECK 2: the full TOTP value 241 let got: i64 = totp_sha1_value(seed, 20, tsec, 30, 8) 242 var okC: i64 = 0 243 if got == want { okC = 1 } 244 if okT == 1 { 245 if okC == 1 { 246 pass = pass + 1 247 w(" PASS t=" as *u8); nn(tsec); w(" T=" as *u8); nn(thex) 248 w(" TOTP=" as *u8); nn(want); w("\n" as *u8) 249 } else { 250 fail = fail + 1 251 w(" FAIL t=" as *u8); nn(tsec); w(" T derived OK but TOTP expected " as *u8) 252 nn(want); w(" got " as *u8); nn(got); w(" -> truncation/HOTP layer\n" as *u8) 253 } 254 } else { 255 fail = fail + 1 256 w(" FAIL t=" as *u8); nn(tsec); w(" T derivation: expected " as *u8) 257 nn(thex); w(" got " as *u8); nn(tderiv); w(" -> time-step layer\n" as *u8) 258 } 259 cur = hit + 10 260 } 261 } 262 } 263 } 264 } 265 } 266 } 267 268 if seen < 6 { 269 w("\n RED: only " as *u8); nn(seen); w(" of 6 published SHA-1 rows graded -- refusing GREEN on a partial read.\n" as *u8) 270 fail = fail + 1 271 } 272 273 w("\n refsrc=https://www.rfc-editor.org/rfc/rfc6238.txt\n" as *u8) 274 w(" refsrcdig=" as *u8); wb(hx, 64); w("\n" as *u8) 275 w(" ref=RFC6238-AppendixB-SHA1 gate=nx_totp_extvec_gate\n" as *u8) 276 w(" BOUND: nx_totp_sha1.nx -> nx_hmac_sha1.nx -> nx_sha1.nx (the repaired family)\n" as *u8) 277 w(" UNIMPLEMENTED-MODE rows (SHA256/SHA512), counted NOT passed: " as *u8); nn(othermode); w("\n" as *u8) 278 w("nx_totp_extvec_gate: sha1_rows=" as *u8); nn(seen) 279 w(" pass=" as *u8); nn(pass); w(" fail=" as *u8); nn(fail) 280 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check 281 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled 282 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify. 283 let ctr__dry: *i64 = gv_ctr() 284 ctr__dry[0] = pass 285 ctr__dry[1] = pass + fail 286 let rc__dry: i64 = gv_verdict("TOTP-EXTVEC-GATE" as *u8, ctr__dry, "teeth unchanged; verdict emission migrated onto the shared base class" as *u8) 287 sys_exit(rc__dry) 288 return rc__dry 289}