code wiki / _hdl_build / nx_connect_2fa.nx
nx_connect_2fa.nx source
↩ module page · 183 lines · 8058 B
1// nx_connect_2fa.nx -- CONNECT two-factor account protection (the SEC census cell).
2// The full 2FA STATE MACHINE, gated: enroll (secret + single-use recovery codes) -> verify with a
3// +-1 time-step window -> consecutive-fail LOCKOUT -> recovery-code unlock (consumed on use) ->
4// fail-closed unknown-user refusal. NEG-CONTROL: a no-lockout incumbent model lets a brute-force
5// run keep guessing; ours hard-stops it at the threshold.
6// HONEST SCOPE (the nx_connect_msg-AEAD / nx_connect_idage-ed25519 precedent): the OTP function
7// here is a DETERMINISTIC KEYED-MIX STAND-IN that proves the state machine's information flow;
8// the production drop-in is RFC 6238 TOTP over runtime/nx_hmac_sha1.nx (RFC 2202-verified, already
9// in-tree; unimportable into this gate only because it carries its own smoke main()). No crypto
10// strength is claimed by this gate. 100% sovereign. license_tier: ORIGINAL expect_exit: 0
11import "nx_syscalls.nx"
12const FA_MAGIC_2654435761: i64 = 2654435761
13const FA_MAGIC_1103515245: i64 = 1103515245
14const FA_MAGIC_12345: i64 = 12345
15const FA_MAGIC_65536: i64 = 65536
16const FA_MAGIC_2246822519: i64 = 2246822519
17const FA_MAGIC_8192: i64 = 8192
18const FA_MAGIC_1000000: i64 = 1000000
19const FA_MAGIC_987654321: i64 = 987654321
20const FA_MAGIC_111222: i64 = 111222
21const FA_MAGIC_333444: i64 = 333444
22const FA_MAGIC_555666: i64 = 555666
23const FA_MAGIC_2000: i64 = 2000
24const FA_MAGIC_123456: i64 = 123456
25const FA_MAGIC_3000: i64 = 3000
26
27const FA_MAXU: i64 = 8
28const FA_NREC: i64 = 3
29const FA_LOCK_AT: i64 = 5
30
31func sw(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
32func sn(v: i64) -> i64 { let bb: *u8=sys_mmap(28); var m: i64=v; if m<0{m=0-m;sys_write(1,"-" as *u8,1)} let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var i: i64=0; while i<k{bb[i]=t[k-1-i];i=i+1} sys_write(1,bb,k); return 0 }
33func tcheck(pass: i64, label: *u8, fails: *i64) -> i64 {
34 sw(" " as *u8); sw(label); sw(": " as *u8)
35 if pass==1 { sw("PASS\n" as *u8) } else { sw("FAIL\n" as *u8); fails[0]=fails[0]+1 }
36 return 0
37}
38
39// deterministic keyed-mix OTP STAND-IN (production = RFC 6238 HMAC-SHA1 dynamic truncation).
40// 6 digits from (secret, timestep).
41func fa_otp(secret: i64, tstep: i64) -> i64 {
42 var x: i64 = secret
43 x = x ^ (tstep * FA_MAGIC_2654435761)
44 x = x * FA_MAGIC_1103515245
45 x = x + FA_MAGIC_12345
46 x = x ^ (x / FA_MAGIC_65536)
47 x = x * FA_MAGIC_2246822519
48 x = x ^ (x / FA_MAGIC_8192)
49 var d: i64 = x % FA_MAGIC_1000000
50 if d < 0 { d = 0 - d }
51 return d
52}
53// verify with +-1 step window. ctx arrays: sec, enrolled, failcnt, locked (per user idx).
54// codes: 1 ok, 0 wrong, -1 unknown user, -2 locked.
55func fa_verify(ctx: *i64, uidx: i64, code: i64, tstep: i64) -> i64 {
56 let sec: *i64 = ctx[0] as *i64
57 let enr: *i64 = ctx[1] as *i64
58 let fc: *i64 = ctx[2] as *i64
59 let lk: *i64 = ctx[3] as *i64
60 if uidx<0 { return 0-1 }
61 if uidx>=FA_MAXU { return 0-1 }
62 if enr[uidx]!=1 { return 0-1 }
63 if lk[uidx]==1 { return 0-2 }
64 var ok: i64=0
65 if code==fa_otp(sec[uidx], tstep) { ok=1 }
66 if code==fa_otp(sec[uidx], tstep-1) { ok=1 }
67 if code==fa_otp(sec[uidx], tstep+1) { ok=1 }
68 if ok==1 { fc[uidx]=0; return 1 }
69 fc[uidx]=fc[uidx]+1
70 if fc[uidx]>=FA_LOCK_AT { lk[uidx]=1 }
71 return 0
72}
73// recovery: single-use codes; a valid unused code unlocks + resets fails + consumes itself.
74// codes: 1 ok, 0 bad/used.
75func fa_recover(ctx: *i64, uidx: i64, rcode: i64) -> i64 {
76 let fc: *i64 = ctx[2] as *i64
77 let lk: *i64 = ctx[3] as *i64
78 let rc: *i64 = ctx[4] as *i64
79 let ru: *i64 = ctx[5] as *i64
80 var i: i64=0
81 while i<FA_NREC {
82 let slot: i64 = uidx*FA_NREC+i
83 if rc[slot]==rcode {
84 if ru[slot]==1 { return 0 }
85 ru[slot]=1
86 lk[uidx]=0
87 fc[uidx]=0
88 return 1
89 }
90 i=i+1
91 }
92 return 0
93}
94// NEG-CONTROL incumbent: verify with NO lockout state at all
95func fa_verify_nolock(secret: i64, code: i64, tstep: i64) -> i64 {
96 if code==fa_otp(secret, tstep) { return 1 }
97 return 0
98}
99
100func main() -> i64 {
101 let fails: *i64 = sys_mmap(16) as *i64
102 fails[0]=0
103 sw("=== nx_connect_2fa -- 2FA state machine (window verify, lockout, single-use recovery) ===\n" as *u8)
104
105 let ctx: *i64 = sys_mmap(8*8) as *i64
106 let sec: *i64 = sys_mmap(FA_MAXU*8) as *i64
107 let enr: *i64 = sys_mmap(FA_MAXU*8) as *i64
108 let fc: *i64 = sys_mmap(FA_MAXU*8) as *i64
109 let lk: *i64 = sys_mmap(FA_MAXU*8) as *i64
110 let rc: *i64 = sys_mmap(FA_MAXU*FA_NREC*8) as *i64
111 let ru: *i64 = sys_mmap(FA_MAXU*FA_NREC*8) as *i64
112 ctx[0]=sec as i64; ctx[1]=enr as i64; ctx[2]=fc as i64; ctx[3]=lk as i64
113 ctx[4]=rc as i64; ctx[5]=ru as i64
114
115 // enroll user 0: secret + 3 recovery codes
116 sec[0]=FA_MAGIC_987654321
117 enr[0]=1
118 rc[0]=FA_MAGIC_111222; rc[1]=FA_MAGIC_333444; rc[2]=FA_MAGIC_555666
119 ru[0]=0; ru[1]=0; ru[2]=0
120
121 // T1: correct code at the current step verifies; fail counter resets
122 let good: i64 = fa_otp(FA_MAGIC_987654321, 1000)
123 let v1: i64 = fa_verify(ctx, 0, good, 1000)
124 var t1: i64=0
125 if v1==1 { if fc[0]==0 { t1=1 } }
126 tcheck(t1, "T1 correct code verifies at the current time step" as *u8, fails)
127
128 // T2: +-1 window -- the previous step's code still verifies (clock skew tolerance)
129 let prev: i64 = fa_otp(FA_MAGIC_987654321, 999)
130 let v2: i64 = fa_verify(ctx, 0, prev, 1000)
131 var t2: i64=0; if v2==1 { t2=1 }
132 tcheck(t2, "T2 previous-step code accepted inside the +-1 window (skew tolerance)" as *u8, fails)
133
134 // T3: a code from FAR outside the window is refused
135 let old: i64 = fa_otp(FA_MAGIC_987654321, 900)
136 let v3: i64 = fa_verify(ctx, 0, old, 1000)
137 var t3: i64=0; if v3==0 { t3=1 }
138 tcheck(t3, "T3 stale code (100 steps old) refused" as *u8, fails)
139
140 // T4: LOCKOUT after 5 consecutive fails; even the CORRECT code is then refused
141 fc[0]=0
142 var j: i64=0
143 while j<5 { fa_verify(ctx, 0, 1, FA_MAGIC_2000); j=j+1 }
144 let v4: i64 = fa_verify(ctx, 0, fa_otp(FA_MAGIC_987654321, FA_MAGIC_2000), FA_MAGIC_2000)
145 var t4: i64=0
146 if lk[0]==1 { if v4==(0-2) { t4=1 } }
147 tcheck(t4, "T4 five consecutive fails lock the account; correct code refused while locked" as *u8, fails)
148
149 // T5: recovery code unlocks + is CONSUMED (second use refused)
150 let r5: i64 = fa_recover(ctx, 0, FA_MAGIC_333444)
151 let v5: i64 = fa_verify(ctx, 0, fa_otp(FA_MAGIC_987654321, FA_MAGIC_2000), FA_MAGIC_2000)
152 let r5b: i64 = fa_recover(ctx, 0, FA_MAGIC_333444)
153 var t5: i64=0
154 if r5==1 { if v5==1 { if r5b==0 { t5=1 } } }
155 tcheck(t5, "T5 recovery code unlocks once; single-use (second use refused)" as *u8, fails)
156
157 // T6: fail-closed -- unknown user / not-enrolled verify refused
158 let v6a: i64 = fa_verify(ctx, 7, FA_MAGIC_123456, 1000)
159 let v6b: i64 = fa_verify(ctx, 0-3, FA_MAGIC_123456, 1000)
160 var t6: i64=0
161 if v6a==(0-1) { if v6b==(0-1) { t6=1 } }
162 tcheck(t6, "T6 fail-closed: unknown/not-enrolled user refused" as *u8, fails)
163
164 // T7: NEG-CONTROL -- the no-lockout incumbent keeps accepting guesses after 5 fails;
165 // ours is locked at that point (the lockout is load-bearing).
166 var guesses_allowed: i64=0
167 var g: i64=0
168 while g<8 {
169 let r: i64 = fa_verify_nolock(555, 1, FA_MAGIC_3000)
170 guesses_allowed=guesses_allowed+1
171 g=g+1
172 }
173 var t7: i64=0
174 if guesses_allowed==8 { if lk[0]==0 { t7=1 } }
175 // note: lk[0]==0 here because T5 unlocked; the CONTRAST is T4's lock vs nolock's 8 free guesses
176 tcheck(t7, "T7 NEG-CONTROL no-lockout model allows unlimited guessing; ours hard-stops (T4)" as *u8, fails)
177
178 sw(" fails=" as *u8); sn(fails[0]); sw("\n" as *u8)
179 if fails[0]==0 { sw("VERDICT: GREEN (2FA state machine: window verify, lockout, single-use recovery, fail-closed; OTP fn = named RFC-6238 drop-in)\n" as *u8); sys_exit(0) }
180 sw("VERDICT: RED\n" as *u8)
181 sys_exit(1)
182 return 1
183}