code wiki / (root) / nx_totp_verify.nx

nx_totp_verify.nx source

↩ module page · 107 lines · 4206 B

1// totp_verify.nx -- TOTP code verification with clock-skew window. 2// 3// Companion to totp_sha1.nx. A bare "is the code equal to 4// totp(secret, now)?" check fails whenever the user's device 5// clock drifts by a few seconds past the step boundary. RFC 6238 6// ยง5.2 recommends accepting the code from step N-1 and N+1 as 7// well, for a 3-window tolerance. We generalise to caller-chosen 8// window size. 9// 10// Uses constant-time comparison (ct.nx) across all candidate 11// windows so timing doesn't leak which window matched (hardening 12// against code-enumeration attacks from a malicious client). 13// 14// Invariants: 15// TV1 Provided code compared against every window in 16// [now_step - window, now_step + window], inclusive. 17// TV2 Match check uses ct_eq on the integer value -- no 18// timing side-channel per window. 19// TV3 Returns matched window offset (negative meaning older 20// step, 0 = current, positive = newer) on success, or 21// TV_ERR_NO_MATCH if nothing in range matches. 22 23// nx_safety_envelope: 24// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 25// sil_target: SIL1 26// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 27// verdict: NOT_YET_EVALUATED 28 29import "nx_syscalls.nx" 30import "nx_totp_sha1.nx" 31import "nx_ct.nx" 32const TV_MAGIC_287082: i64 = 287082 33const TV_MAGIC_999999: i64 = 999999 34 35const TV_ERR_NO_MATCH: i64 = -1000 // way outside any reasonable window 36 37// Verify a TOTP code against a secret. Returns matched window 38// offset (usually -1, 0, or 1) on success. Walks all windows 39// unconditionally -- the caller learns only (matched, which_offset) 40// not which individual step hit. 41func totp_sha1_verify(secret: *u8, secret_len: i64, 42 provided_code: i64, 43 now_unix: i64, 44 step: i64, 45 digits: i64, 46 window: i64) -> i64 { 47 let base_counter: i64 = now_unix / step 48 var match_offset: i64 = TV_ERR_NO_MATCH 49 var match_mask: i64 = 0 // OR-accumulator for timing 50 var off: i64 = 0 - window 51 while off <= window { 52 let cand: i64 = hotp_sha1_value(secret, secret_len, 53 base_counter + off, digits) 54 let hit: i64 = ct_eq(cand, provided_code) 55 // Record first match but keep iterating so total work 56 // equals 2*window+1 HMAC invocations regardless. 57 if hit == 1 { 58 if match_mask == 0 { 59 match_offset = off 60 match_mask = 1 61 } 62 } 63 off = off + 1 64 } 65 return match_offset 66} 67 68// Convenience: verify with default (step=30, digits=6, window=1). 69func totp_sha1_verify_default(secret: *u8, secret_len: i64, 70 provided_code: i64, 71 now_unix: i64) -> i64 { 72 return totp_sha1_verify(secret, secret_len, provided_code, 73 now_unix, 30, 6, 1) 74} 75 76// Compile-only smoke using RFC 6238 Appendix B vector: 77// secret = "12345678901234567890" (20 bytes) 78// T=59 -> code 94287082 (8 digits) -- we use 6 here: 287082 79func main() -> i64 { 80 let k: *u8 = "12345678901234567890" 81 82 // 6-digit code at T=59 is 287082. 83 let correct: i64 = totp_sha1_value(k, 20, 59, 30, 6) 84 if correct != TV_MAGIC_287082 { return 1 } 85 86 // Verify at the same time -> match offset 0. 87 let r1: i64 = totp_sha1_verify(k, 20, TV_MAGIC_287082, 59, 30, 6, 1) 88 if r1 != 0 { return 2 } 89 90 // Late: provided code is from 30s in the past (still in window=1). 91 let r2: i64 = totp_sha1_verify(k, 20, TV_MAGIC_287082, 89, 30, 6, 1) 92 if r2 != -1 { return 3 } 93 94 // Early: provided code is from 30s in the future. 95 let r3: i64 = totp_sha1_verify(k, 20, TV_MAGIC_287082, 29, 30, 6, 1) 96 if r3 != 1 { return 4 } 97 98 // Outside window -> no match. 99 let r4: i64 = totp_sha1_verify(k, 20, TV_MAGIC_287082, 200, 30, 6, 1) 100 if r4 != TV_ERR_NO_MATCH { return 5 } 101 102 // Wrong code -> no match even at right time. 103 let r5: i64 = totp_sha1_verify(k, 20, TV_MAGIC_999999, 59, 30, 6, 1) 104 if r5 != TV_ERR_NO_MATCH { return 6 } 105 106 return 0 107}