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}