设备时钟漂移时 TOTP 验证码为何失效
验证码是密钥与当前时间步的哈希。比较两端的偏差,然后修正时钟,而不是放宽窗口。
一次性验证码在手机上可能完全正确,服务器却会在同一秒拒绝它。双方都用共享密钥和当前时间步计算同一个函数,因此时间上的分歧会导致验证码上的分歧。这让时钟漂移成为一个与安全相关的故障,而不只是恼人的小问题。
这里有两个时钟都重要,而修正看得见的那个是人们最先做的部分。通常真正需要处理的,是另一个。
验证码是时间的函数,而不是密码
TOTP 取共享密钥和由当前时间推导出的计数器,把两者送入带密钥的哈希,再把结果截断成一段简短的十进制字符串,规范见 RFC 6238。计数器每个时间步前进一次,通常为三十秒,因此时间步变化时同一个密钥会产生新值。验证方重新计算当前时间步的期望值并加以比较,这要求相同的密钥和对“此刻”的相同理解。密码在被更换前可以反复使用,而这个值会在客户端看不见的边界上过期。
所以一个验证码有效,下一个过几秒就失败了。这也解释了为什么从聊天消息里复制验证码是如此不可靠的习惯。
两个时钟必须一致,而不是一个
注册材料是第一项共享内容,时间是第二项。如果密钥正确,而两个时钟的差异超过允许窗口,那么生成的每个值都会落在错误的时间步上并被拒绝。如果时钟一致,而密钥错误,每个值同样会被拒绝,而且 两种失败在登录界面上看起来完全一样。要区分它们,就得分别检查账户条目和时间,并按这个顺序进行。
验证方的时钟也值得像设备时钟一样审视。时间源损坏的服务器会同时拒绝所有用户的正确验证码,而报告会描述成突然故障,而不是漂移。这个模式值得记住,因为修复应该在服务器上,而不是在一百部手机上。
放宽窗口的代价
实现可以接受相邻时间步的验证码以吸收细小差异,而这种容忍是设计决定,不是免费的调整。每多一个时间步,任意时刻有效的值的数量就成倍增加,而这正是猜测型攻击者最喜欢的。防重放规则同样重要,因为被接受两次的验证码就是已经有用泄露的验证码。管理员为平息投诉而放宽窗口,是用真实属性换取表面改善。
修正时钟是修复,放宽窗口是镇静剂。前者消除原因,后者掩盖原因。
重试之前先检查两个时钟
读取设备和服务器上的时间,比较偏差而不是显示的本地时间。下面的命令显示 Linux 主机或服务器上的同步状态和原始 UTC 时刻。在更改任何身份验证设置之前,请两端都运行一遍。
timedatectl
date -u +'%Y-%m-%dT%H:%M:%SZ'
chronyc tracking
timedatectl timesync-status
以秒计的偏差无害,以分钟计的偏差能解释拒绝,而完全不报告同步来源的系统则两者都有可能。直接比较两个 UTC 时间戳,因为本地时间包含算法永远看不到的时区。如果偏差很小而验证码仍然失败,就停止调整时钟,转而检查注册条目。对已锁定账户反复尝试,会让原始故障更难观察。
记录拒绝时间、偏差和错误文本,并把共享密钥排除在外。支持人员可以用前三项工作,永远不应看到第四项。