code wiki / (root) / totp_verify.nx

totp_verify.nx source

↩ module page · 99 lines · 3919 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 23import "syscalls.nx" 24import "totp_sha1.nx" 25import "ct.nx" 26 27const TV_ERR_NO_MATCH: i64 = -1000 // way outside any reasonable window 28 29// Verify a TOTP code against a secret. Returns matched window 30// offset (usually -1, 0, or 1) on success. Walks all windows 31// unconditionally -- the caller learns only (matched, which_offset) 32// not which individual step hit. 33func totp_sha1_verify(secret: *u8, secret_len: i64, 34 provided_code: i64, 35 now_unix: i64, 36 step: i64, 37 digits: i64, 38 window: i64) -> i64 { 39 let base_counter: i64 = now_unix / step 40 var match_offset: i64 = TV_ERR_NO_MATCH 41 var match_mask: i64 = 0 // OR-accumulator for timing 42 var off: i64 = 0 - window 43 while off <= window { 44 let cand: i64 = hotp_sha1_value(secret, secret_len, 45 base_counter + off, digits) 46 let hit: i64 = ct_eq(cand, provided_code) 47 // Record first match but keep iterating so total work 48 // equals 2*window+1 HMAC invocations regardless. 49 if hit == 1 { 50 if match_mask == 0 { 51 match_offset = off 52 match_mask = 1 53 } 54 } 55 off = off + 1 56 } 57 return match_offset 58} 59 60// Convenience: verify with default (step=30, digits=6, window=1). 61func totp_sha1_verify_default(secret: *u8, secret_len: i64, 62 provided_code: i64, 63 now_unix: i64) -> i64 { 64 return totp_sha1_verify(secret, secret_len, provided_code, 65 now_unix, 30, 6, 1) 66} 67 68// Compile-only smoke using RFC 6238 Appendix B vector: 69// secret = "12345678901234567890" (20 bytes) 70// T=59 -> code 94287082 (8 digits) -- we use 6 here: 287082 71func main() -> i64 { 72 let k: *u8 = "12345678901234567890" 73 74 // 6-digit code at T=59 is 287082. 75 let correct: i64 = totp_sha1_value(k, 20, 59, 30, 6) 76 if correct != 287082 { return 1 } 77 78 // Verify at the same time -> match offset 0. 79 let r1: i64 = totp_sha1_verify(k, 20, 287082, 59, 30, 6, 1) 80 if r1 != 0 { return 2 } 81 82 // Late: provided code is from 30s in the past (still in window=1). 83 let r2: i64 = totp_sha1_verify(k, 20, 287082, 89, 30, 6, 1) 84 if r2 != -1 { return 3 } 85 86 // Early: provided code is from 30s in the future. 87 let r3: i64 = totp_sha1_verify(k, 20, 287082, 29, 30, 6, 1) 88 if r3 != 1 { return 4 } 89 90 // Outside window -> no match. 91 let r4: i64 = totp_sha1_verify(k, 20, 287082, 200, 30, 6, 1) 92 if r4 != TV_ERR_NO_MATCH { return 5 } 93 94 // Wrong code -> no match even at right time. 95 let r5: i64 = totp_sha1_verify(k, 20, 999999, 59, 30, 6, 1) 96 if r5 != TV_ERR_NO_MATCH { return 6 } 97 98 return 0 99}