nx_mfa.nx source
↩ module page · 73 lines · 4196 B
1// nx_mfa.nx -- ADMIN MFA / second factor (closes hosting_research gap #12 "mfa-admin", 2/0 CONFIRMED:
2// admin login is single-factor password only). Sovereign TOTP verification (RFC 6238, HMAC-SHA1 -- the
3// Google Authenticator / Authy / Microsoft Authenticator / Duo default) on top of the already-KAT'd
4// nx_totp_sha1 + nx_base32. The login composes mfa_verify AFTER the argon2id password check, so a stolen
5// password alone is not enough. Why SHA-1 (not nx_otp's SHA-256): authenticator apps default to and widely
6// ONLY support HMAC-SHA1 TOTP; SHA-1's collision weakness is irrelevant to HMAC's PRF security (NIST + RFC
7// 6238 both keep HMAC-SHA1 for OTP). Standard params: 6 digits, 30s step, +/-1 step skew window.
8//
9// module: nishi-core.auth.mfa capability: ACCESS_CONTROL / auth
10import "nx_totp_sha1.nx" // hotp_sha1_value / totp_sha1_render
11import "nx_base32.nx" // base32_decode / base32_encode
12import "nx_syscalls.nx"
13
14const MFA_STEP: i64 = 30 // RFC 6238 default time-step (seconds)
15const MFA_DIGITS: i64 = 6 // authenticator-app default
16const MFA_WINDOW: i64 = 1 // accept the code for step-1 / step / step+1 (RFC 6238 SS5.2 skew tolerance)
17const MFA_KEY_BYTES: i64 = 20 // 160-bit secret (RFC 4226 SS4 recommends >= 128 bits, 160 preferred)
18
19// constant-time compare of two equal-length digit strings (no early-out timing oracle on the code).
20func mfa_ct_eq(a: *u8, b: *u8, n: i64) -> i64 {
21 var diff: i64 = 0; var i: i64 = 0
22 while i < n { diff = diff | ((a[i] as i64) ^ (b[i] as i64)); i = i + 1 }
23 if diff == 0 { return 1 }
24 return 0
25}
26
27// verify a submitted code (ASCII digits, code_len long) against a base32 secret at now_unix, within
28// +/-window steps. Returns 1 = valid, 0 = invalid. Decodes the secret, walks the skew window, ct-compares.
29func mfa_verify(secret_b32: *u8, b32_len: i64, code: *u8, code_len: i64, now_unix: i64, window: i64) -> i64 {
30 if code_len != MFA_DIGITS { return 0 }
31 let key: *u8 = sys_mmap(64)
32 let klen: i64 = base32_decode(secret_b32, b32_len, key)
33 if klen <= 0 { return 0 }
34 let base_counter: i64 = now_unix / MFA_STEP
35 let cand: *u8 = sys_mmap(16)
36 var w: i64 = 0 - window
37 var ok: i64 = 0
38 while w <= window {
39 let v: i64 = hotp_sha1_value(key, klen, base_counter + w, MFA_DIGITS)
40 totp_sha1_render(v, MFA_DIGITS, cand)
41 if mfa_ct_eq(cand, code, MFA_DIGITS) == 1 { ok = 1 } // no early break: walk all steps (timing-flat)
42 w = w + 1
43 }
44 return ok
45}
46
47// is MFA enrolled? -- a non-empty base32 secret is present. (login: enrolled -> require code; not -> password-only)
48func mfa_enrolled(secret_b32: *u8, b32_len: i64) -> i64 { if b32_len >= 16 { return 1 } return 0 }
49
50// ENROLLMENT: generate a fresh 160-bit secret from /dev/urandom and base32-encode it (-> out_b32). Returns
51// the base32 length, or 0 on failure. The operator enters/scans this into their authenticator once.
52func mfa_gen_secret(out_b32: *u8) -> i64 {
53 let raw: *u8 = sys_mmap(64)
54 let fd: i64 = sys_openat_rd("/dev/urandom" as *u8); if fd < 0 { return 0 }
55 var got: i64 = 0
56 while got < MFA_KEY_BYTES { let r: i64 = sys_read(fd, ((raw as i64)+got) as *u8, MFA_KEY_BYTES-got); if r <= 0 { got = MFA_KEY_BYTES } else { got = got + r } }
57 sys_close(fd)
58 return base32_encode(raw, MFA_KEY_BYTES, out_b32)
59}
60
61func mfa_p(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
62
63func main() -> i64 {
64 // demo enrollment + a live code for the just-generated secret (shows the round trip end-to-end).
65 let sec: *u8 = sys_mmap(64); let sl: i64 = mfa_gen_secret(sec)
66 mfa_p("=== NISHI ADMIN MFA (TOTP-SHA1, RFC 6238) ===\n" as *u8)
67 mfa_p("enrollment secret (base32, put in your authenticator): " as *u8); sys_write(1, sec, sl); mfa_p("\n" as *u8)
68 let now: i64 = sys_now_realtime_sec()
69 let key: *u8 = sys_mmap(64); let kl: i64 = base32_decode(sec, sl, key)
70 let cur: *u8 = sys_mmap(16); totp_sha1_render(hotp_sha1_value(key, kl, now / MFA_STEP, MFA_DIGITS), MFA_DIGITS, cur)
71 mfa_p("current 6-digit code (this 30s window): " as *u8); sys_write(1, cur, 6); mfa_p("\n" as *u8)
72 sys_exit(0); return 0
73}