NTP时间同步 - 网络如何实现毫秒级精度
NTP通过层级结构和巧妙的往返时间计算实现跨网络的时钟同步。本文涵盖Stratum层级、偏移量计算、chrony和systemd-timesyncd等现代实现,以及2020年引入的NTS安全扩展。
基于时间的一次性密码(TOTP)是 Google Authenticator、Authy、Microsoft Authenticator 和 1Password 背后的算法。服务器和用户设备共享一个密钥,双方通过将该密钥与当前时间结合来计算一个 6 位数字。这个数字每 30 秒更换一次,成功登录需要用户输入当前窗口内有效的数字。RFC 6238 于 2011 年对此进行了标准化,确保了厂商无关的互操作性。
TOTP 最大的实用优势是,一旦密钥共享完成,它无需任何网络连接即可工作。服务器不需要向设备发送任何内容,设备也不需要查询服务器。这使得 TOTP 在大规模运营时比基于短信的一次性密码更经济,同时还避开了困扰短信认证的 SIM 卡交换攻击。
TOTP 建立在基于 HMAC 的一次性密码(HOTP)之上,后者定义于 RFC 4226。HOTP 使用计数器和密钥,通过 HMAC-SHA-1 对它们进行哈希运算,并从结果中提取一个 6 位数字。每次成功登录后,双方的计数器都会递增,因此下一个验证码是不同的。HOTP 无需时间源,这使其适用于没有时钟的离线硬件令牌。
HOTP 的缺点是计数器失步。如果用户多次打开认证器应用而未使用生成的验证码,设备的计数器会前进而服务器端保持不变。因此服务器必须向前查看若干个值以找到匹配项,这增加了运维复杂度。TOTP 用当前时间除以 30 秒的时间窗口来替代计数器,优雅地解决了这个问题。
| 观点 | HOTP | TOTP |
|---|---|---|
| 标准规范 | RFC 4226 | RFC 6238 (2011 年标准化) |
| 计数器是什么 | 给验证码计数的值。设备每生成一次验证码就加 1,服务器则在每次认证成功时加 1 | Unix 时间戳除以 30 的商。每 30 秒加 1 |
| 是否需要时钟 | 不需要 - 没有时钟的硬件令牌也能运行 | 需要 - 前提是设备和服务器都知道正确的时间 |
| 失步的原因 | 生成了验证码却不使用,只有设备一侧的计数器前进 | 设备或服务器的时钟偏差 |
| 失步后的处理 | 服务器向后预读几个值来逐一比对 | 重新把时钟同步到正确时间 |
把共享密钥交给 HMAC-SHA-1 并从结果中取出 6 位数字的部分,两者完全相同。区别只在于交给 HMAC 的计数器是由“成功次数”算出还是由“时间”算出,TOTP 直接沿用了 HOTP 的计算。
TOTP 将计数器计算为当前 Unix 时间戳除以 30(整数除法)。例如,在 Unix 时间 1716000000(2024 年 5 月 18 日 UTC 02:40)时,计数器为 57200000。该计数器与共享密钥一起输入 HOTP 算法,生成一个 6 位数字。30 秒后,计数器变为 57200001,生成一个完全不同的数字。
RFC 6238 规定的 30 秒步长在可用性和安全性之间取得平衡。太短用户来不及输入验证码;太长则暴力破解有更大的成功窗口。服务器通常还会接受前一个和下一个 30 秒窗口,总共给予大约 90 秒的容差。不同服务对此略有调整:Steam Guard 使用 30 秒步长但有自定义调整,而某些企业令牌使用 60 秒。
只交换一次共享密钥 - 启用两步验证时显示的二维码 (otpauth URI) 中含有 Base32 编码的共享密钥。此后这把密钥不再经过网络。
把 Unix 时间戳除以 30 - 商就是计数器。Unix 时间戳为 1716000000 (2024-05-18 02:40:00 UTC) 时,计数器是 57200000。
用 HMAC-SHA-1 处理计数器和共享密钥 - 输出是 20 字节的哈希值。这一步与 HOTP 完全相同。
从哈希值中截取 4 个字节 - 最后一个字节的低 4 位指出截取位置,取出的 4 字节再去掉最高位,得到 31 位的正整数。
取除以 1000000 的余数并按 6 位显示 - 不足的位用 0 补齐。30 秒后计数器变为 57200001,出现完全不同的 6 位数字。
服务器会自己走一遍同样的步骤,再把算出的 6 位数字与收到的比对。多数实现还会接受前后各 1 步 (合计约 90 秒),正是这份容差吸收了设备与服务器之间的微小时钟偏差。otpauth URI 的 digits 参数也可以要求 8 位,但除 6 位以外的长度客户端支持有限。
启用双因素认证时,屏幕上的二维码编码了一个包含共享密钥的 otpauth:// URI。一个典型的 URI 形如 otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example。密钥使用 Base32 编码(使用 Base32 是因为它避免了在显示字体中容易混淆的字符)。认证器应用解析此 URI 并将密钥本地存储。
任何拍到二维码照片的人都可以克隆 OTP 生成器,因此设置界面应仅显示一次,且绝不应在屏幕共享会话期间显示。大多数服务会在 TOTP 设置时配备一组一次性备份码。每个备份码是一个单次使用的长随机字符串,可以绕过 TOTP,在手机丢失时使用。将备份码存储在另一个密码管理器中或打印后放入保险箱是标准做法。
当 TOTP 认证失败时,原因通常是服务器和设备对当前时间存在分歧。服务器通常通过 NTP 同步到 UTC,但手动调整过时间的手机,或刚从国际旅行返回设置混乱的手机,可能漂移一分钟甚至更多。Google Authenticator 过去曾在应用内提供“时间校正”设置,但它在 7.0 版本中已被移除,现在应用直接使用操作系统的时间设置。设备一侧的应对办法是保持开启系统的自动时间同步。
服务端时钟漂移同样危险。没有正确 NTP 同步的容器可能漂移数十秒,尤其是在嵌套虚拟化环境中。服务器一侧的漂移格外棘手,因为它不会只影响少数运气不好的用户,而是表现为所有用户同时被锁在系统之外。对于实现 TOTP 的系统,定期 NTP 同步和时间同步失败告警是不可妥协的运维要求。
TOTP 默认使用 HMAC-SHA-1,RFC 6238 认为这对 OTP 用例来说已经足够。更新的实现可以通过 otpauth URI 中的 algorithm 参数选择 SHA-256 或 SHA-512。难点在于客户端的兼容性:Google Authenticator 会完全忽略 algorithm 参数,一律按 SHA-1 处理,因此出于互操作性考虑,大多数服务仍然停留在 SHA-1。对于仅面向现代认证器应用的全新实现,SHA-256 是合理的升级选择。
服务器还必须防止重放攻击。如果两次登录尝试在同一个 30 秒窗口内到达,只有第一次应该成功;否则,捕获了验证码的攻击者可以重复使用它。存储每个用户最近一次成功使用的计数器值,并拒绝任何等于或小于该值的后续请求,是经典的防御方式。6 位是标准长度,虽然 digits 参数允许要求 8 位,但客户端支持有限,采用它的服务很少。
这篇文章对您有帮助吗?