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}