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}