ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

安当OTP:从HOTP/TOTP算法推导到对时容灾,一份可直接复用的排障手册

安当OTP:从HOTP/TOTP算法推导到对时容灾,一份可直接复用的排障手册 一、先把话说明白OTP的故障九成不是算法问题动态口令上线之后运维收到的工单高度集中在同一句话上——“码不对”。而这句话背后真正的原因分布和大多数人想的正好相反算法实现写错的极少绝大多数是四类工程问题。第一类是参数不一致服务端配的是六十秒步长令牌端按默认三十秒算服务端用八位令牌显示六位服务端哈希选了 SHA-256令牌只支持 SHA-1。第二类是时钟问题服务端时钟漂了或者手机被用户手动改了时间。第三类是密钥问题换手机重新绑定后旧密钥没作废或者密钥在迁移时编码出错。第四类是防重放误伤同一窗口内重复提交被一次性消费机制拒绝用户以为是自己输错了。这四类问题的排查方法完全不同但它们的外在表现一模一样都是码不对。所以本文的做法是先把算法推导到底让你能自己手算出正确答案然后再给排障手册。因为只有知道正确值应该是什么才能判断错的到底是时间、密钥还是参数。二、从 HOTP 推导 TOTPTOTP 只是 HOTP 的一个特例动态口令有两大家族HOTP基于事件计数器与 TOTP基于时间计数器。理解它们的关系很多设计取舍就自然清楚了。HOTP 的定义是HOTP(K, C) Truncate(HMAC-H(K, C))其中 K 是客户端与服务端共享的密钥C 是一个八字节的计数器值H 是哈希算法Truncate 是截断函数。每认证一次双方各自把计数器加一因此两端的计数器必须保持同步。HOTP 的优点是不依赖时钟缺点也正来自这里用户在令牌上多按了几下、或者认证请求在网络上丢了服务端和令牌的计数器就会错开这就是常说的跳号失步。TOTP 的推导非常直接——把计数器从事件计数换成时间计数T floor( (UnixTime - T0) / X ) TOTP(K, T) HOTP(K, T) Truncate(HMAC-H(K, T))其中 T0 是起始时间默认为 0即 Unix 纪元X 是时间步长默认三十秒。也就是说TOTP 在数学上完全就是 HOTP唯一的区别是计数器的来源不再需要双方同步维护而是各自从自己的时钟算出来。这个替换带来两个直接后果。好的一面不需要维护计数器状态两端各自独立计算天然支持离线出码。坏的一面时钟成了新的同步依赖于是所有时钟相关的问题漂移、改时间、时区误用都变成了认证故障的来源。这也解释了为什么 TOTP 的工程复杂度几乎全部集中在时间同步上。三、HMAC 内部构造密钥先被加工成两把子密钥截断函数作用在 HMAC 的输出上所以要把 HMAC 本身拆开看。HMAC 的定义是HMAC(K, m) H( (K ⊕ opad) ‖ H( (K ⊕ ipad) ‖ m ) )展开说明每一步的工程含义块长 B哈希算法的分块长度。SHA-1、SHA-256、国密 SM3 的块长都是 64 字节SHA-384、SHA-512 的块长是 128 字节。密钥预处理如果密钥 K 的长度大于 B先做一次哈希压缩即K H(K)然后把 K 右侧补零到 B 字节得到 K’。两把子密钥ipad是 0x36 重复 B 次opad是 0x5c 重复 B 次。分别做异或得到两把不同的子密钥。两次哈希先用内层子密钥包裹消息做一次哈希再用外层子密钥包裹这个结果做第二次哈希。输出长度由哈希算法决定SHA-1 输出 20 字节SHA-256 与 SM3 输出 32 字节SHA-512 输出 64 字节。这个长度后面会直接影响截断偏移的取值范围。在国密场景中做法是把哈希算法整体替换为 SM3即HMAC-SM3。由于 SM3 的块长也是 64 字节、输出也是 32 字节替换在结构上是平替的不需要改动 HMAC 框架只需要底层哈希实现支持 SM3。这也是国密动态口令改造在工程上可行的原因。需要特别注意的是替换必须两端同时生效服务端切了 SM3 而令牌端仍是 SHA-1算出来的码必然对不上而且这类不一致在日志里只表现为校验失败没有更明显的报错排障时极易被误判为时钟问题。以安当OTP为例其服务端同时支持 SHA1/256/512/224/384 与国密 SM3切换时应当先在灰度用户上验证两端一致性再全量推开并把切换时间点与参数基线记录下来作为后续密评的证据材料。四、动态截断每一个比特为什么这样取截断Dynamic Truncation的目的是把一个二十字节到六十四字节的哈希输出稳定地压缩成一个六位或八位十进制数。它的每一步都有明确理由。4.1 偏移量 offset为什么取最后一个字节的低四位offset mac[len(mac) - 1] 0x0F取最后一个字节的低四位得到的取值范围是 0 到 15。之所以用四位是因为需要保证offset 3不越界最坏情况下 offset 取 15需要访问到第 15、16、17、18 字节而最短的哈希输出SHA-1是 20 字节因此安全。如果换成输出更短的哈希这一行的边界就必须重新校验。用最后一个字节而不是第一个字节是为了让偏移量本身也随输入变化从而让取出的四字节位置具有良好的散布性——这是截断方案设计上的刻意选择不是随手写的。4.2 高位清零为什么是 0x7F 而不是 0xFFbin (mac[offset] 0x7F) 24 | (mac[offset 1] 0xFF) 16 | (mac[offset 2] 0xFF) 8 | (mac[offset 3] 0xFF)第一个字节与 0x7F 相与是清掉最高位得到一个31 位的正整数取值范围 0 到 2147483647。这么做的理由很实际不同编程语言、不同平台对最高位为 1 的 32 位整数的解释不一致有符号整型会变成负数如果保留符号位同一个哈希输出在 Java、C、Python、Go 里可能算出不同的动态码。清掉最高位就把这个跨平台歧义从根上消除了。4.3 取模与位数otp bin mod 10^Digit # Digit 默认 6可选 8取模得到 0 到 999999或 0 到 99999999之间的数然后不足位数时左侧补零。这里的补零是最容易被漏掉的一步当取模结果小于 10 的 (Digit-1) 次方时正确的输出应当带前导零例如六位模式下值为 488 时应输出000488。漏掉补零的实现会输出488用户照着输进去必然失败。八位模式下出现前导零的概率约为百分之零点四七即值小于一千万的概率六位模式约为百分之零点零零四七。这个概率看着很低但在万人规模、每天多次认证的场景下一周之内就会出现若干次而且每次都表现为用户坚称自己输对了排查起来非常费时。因此上线前必须用边界值做一次专门测试构造出一个带前导零的码确认前后端显示与比对都正确。4.4 一次完整的手算示例下面用一个示意值把全过程走一遍方便对照检查自己的实现。输入T0 0X 30 秒Digit 6哈希算法为 SHA-1当前 Unix 时间 1735689600。第一步计算时间计数器T floor(1735689600 / 30) 57856320第二步把 T 编码为八字节大端序。57856320 的十六进制是 0x0372D140补齐八字节为00 00 00 00 03 72 D1 40第三步对这八字节做 HMAC此处用示意输出真实结果取决于共享密钥3c 7a 6f 21 8b 4d 90 e5 12 a7 3f 6c d8 41 9e b2 05 7d fa 63第四步取偏移量。最后一个字节是 0x63与 0x0F 相与0x63 0x0F 0x03 → offset 3第五步取第 3 到第 6 个字节从 0 开始计数mac[3] 0x21 mac[4] 0x8b mac[5] 0x4d mac[6] 0x90首字节清零高位0x21 0x7F 0x21拼接得到 0x218B4D90即十进制的 562777488。第六步取模并补零6 位562777488 mod 1000000 777488 → 输出 777488 8 位562777488 mod 100000000 62777488 → 输出 62777488把这个手算过程和自己的实现逐行对齐是排查算法实现错误最快的方法。实操建议写一个单元测试固定时间与密钥硬编码期望值任何一次升级哈希库或改动编码方式之后都重跑一次。五、时间步长三十秒与容差窗口的数学5.1 为什么是三十秒三十秒不是数学上的必然而是三个约束的平衡输入耗时人读一个六位码再敲进去通常需要几秒到十几秒。步长太短比如十秒用户还没输完码就过期了失败率陡增。暴露时长码一旦显示出来就有被肩窥、被截屏、被中间人拿走的可能。步长越长单个码的有效生命越长。重放窗口即使服务端做了容差一个码最长可存活的时间约为30 × (N 1)秒N 为容差窗口数。步长翻倍暴露时长也翻倍。六十秒会显著降低来不及输的投诉但把重放窗口扩大一倍十五秒则相反。三十秒是长期实践中收敛下来的默认值除非有明确理由不建议改动。5.2 容差窗口 N 的取舍服务端校验时通常不只校验当前窗口而是校验[C - N, C N]共2N 1个候选窗口。N 取多大直接影响两件事。可用性N 越大时钟偏差容忍度越高用户失败率越低。安全性这是容易被忽略的一面。攻击者随机猜一个六位码命中单个窗口的概率是百万分之一如果服务端接受2N 1个候选窗口命中概率就变成约(2N 1) / 10^6。也就是说把 N 从 1 放大到 5爆破成功率会提高数倍。同时一个被截获的码最长可存活30 × (N 1)秒。结论很明确容差不能靠开大一点来解决时钟问题。推荐做法是 N 控制在 1 到 2即容忍正负三十到六十秒并强制配套两件事——认证失败限流同一账号单位时间内失败次数超限即锁定与窗口一次性消费同一窗口的码只能用一次。有了限流容差放大的爆破风险就被压住了。5.3 一次性消费防重放的最后一道闸时间窗口只让码过期并不能阻止同一窗口内的重放。因此服务端必须记录某个用户在某个窗口的码已被消费校验通过后立即写入消费记录下次同一窗口的同一码再次提交则拒绝。这里有个工程细节消费记录必须存在共享存储里不能只放在应用节点本地内存。否则做高可用时主节点校验通过后请求被切到备节点消费记录查不到重放就成功了。同时消费记录的保留时长应大于最大可能的窗口跨度N 2 时至少保留五个窗口即一百五十秒以上实际部署中通常保留更久以便审计追溯。六、漂移补偿从扫窗口到学习偏移静态扫描每次都要算2N 1次 HMAC在大规模认证场景下是可观的开销而且无法解决偏差超过 N的用户。工程上有三种递进的做法。第一种静态扫描。每次校验按顺序计算候选窗口命中即通过。简单可靠是基线实现。第二种学习偏移。为每个用户记录上一次成功校验时的窗口偏移d 命中窗口 - 当前窗口下次校验时优先尝试C d命中率高且计算量小。学习到的偏移应有边界比如限制在正负 N 以内和老化机制长时间未使用则清零否则一次异常成功的偏移会被长期记住。第三种受控重同步。当用户连续失败达到阈值时说明两端偏差可能已经超出 N此时进入重同步模式在更大范围比如正负十个窗口内扫描。关键的安全要求是重同步必须要求用户连续提交两个相邻窗口的码且两个都正确才接受。两个连续的码可以唯一确定时钟偏移而攻击者要在宽窗口里同时撞中两个码难度是百万分之一再平方实际上不可行。如果重同步只要求一个码攻击者就能用暴力尝试在宽窗口里撞进去等于主动把容差放大了十倍。完整校验流程可以写成下面这段伪代码function verify(user, input_otp): secret load_secret(user) # 密钥加密存储敏感场景由密码模块保护 if rate_limited(user): return LOCKED C floor((now_utc() - T0) / X) # 必须用 UTC Unix 时间戳 d clamp(load_drift(user), -N, N) # 学习到的偏移默认 0超出边界则清零 # 先试学习偏移再试当前窗口最后扫容差窗口 order dedup([C d, C, C - 1, C 1, ...]) # 按策略展开到 ±N for cand in order: if abs(cand - C) N: continue if not const_time_equal(gen(secret, cand, Digit, Hash), input_otp): continue if not mark_consumed(user, cand): audit(user, REPLAY_REJECT, cand) return REPLAY # 同一窗口已消费 save_drift(user, cand - C) # 更新学习偏移 audit(user, OTP_OK, cand, cand - C) return OK audit(user, OTP_FAIL, C) return FAIL三个实现细节值得强调比对要用常数时间比较函数避免通过响应耗时进行逐位猜测记录日志时要记下命中的窗口偏移这个字段是后续分析时钟问题的关键数据失败要限流否则容差窗口的存在会让在线爆破变得可行。七、对时工程时钟是OTP的隐藏基础设施TOTP 把同步依赖从计数器转移到了时钟于是时钟本身成了必须被工程化保障的基础设施。7.1 服务端时钟架构建议的最小可靠架构是内网部署至少两台时钟服务器各自向上游至少两个不同的可信时间源同步两台之间互相监测偏差。关键监控指标包括与上游的偏移量、层级、同步状态、以及与上游失联的持续时长。告警阈值建议分层设置比如偏移超过两百毫秒触发提醒超过一秒触发严重告警并暂停部分敏感操作。需要特别注意的是时钟跃变的处理。如果服务端时钟被大幅回拨已经消费过的窗口可能重新变得有效重放保护会失效。因此生产环境应配置时钟平滑调整缓慢追赶而非一次性跳变并禁止在认证服务运行时手动大改系统时间。7.2 虚拟化与容器环境的两个陷阱虚拟机虚拟机的时钟由宿主机提供在负载高、发生迁移或快照恢复时可能出现明显漂移甚至跳变。应在虚拟机内启用时钟同步代理并把漂移速率纳入监控而不是只监控偏移量。容器容器共享宿主机内核时钟本身问题不大但时区配置是高频故障源。TOTP 的计算输入是 Unix 时间戳UTC 基准与时区无关但如果代码里错误地用本地时间字符串去换算时间戳就会整体偏移若干小时表现为所有人的码都不对。因此规范做法是内部一律使用 UTC 时间戳只在展示层做时区转换并在单元测试里加一条非 UTC 时区环境下结果不变的用例。7.3 客户端侧手机令牌依赖手机系统时间。绝大多数手机通过网络自动校时漂移很小但存在两类例外用户手动关闭自动校时并改了时间设备长期离线比如某些现场终端导致晶振漂移累积。应对方式是服务端侧的学习偏移与受控重同步同时在客户端引导中提示保持自动校时。硬件令牌则不同它自带时钟与电池长期运行后晶振会缓慢漂移电池耗尽时时钟停摆。因此硬件令牌要有有效期与定期校验机制——在认证日志里监控各令牌的偏移趋势偏移持续变大的令牌提前更换而不是等它彻底失效。八、容灾与降级动态口令服务一旦不可用所有依赖它的系统都无法完成双因素登录影响面是全局的。因此容灾设计必须提前做而不是出事时临时想办法。校验服务高可用。至少双活部署消费记录与学习偏移存共享存储切换后行为一致。要实测一次主节点强制下线的切换确认切换期间不出现重放保护失效。密钥库备份与恢复演练。密钥库是动态口令的全部信任根一旦损坏且无备份等于全员需要重新绑定令牌这是最严重的故障场景。备份必须加密、异地、定期恢复演练演练要记录恢复耗时。降级与逃生通道。认证服务全停时必须有管理员应急放行流程。正确形态是可用但要付出高昂的审计代价破窗操作触发告警、强制审批、全程留痕、事后复盘。完全没有逃生通道认证挂了全公司停工在可用性上不可接受逃生通道完全不设限任何人都能绕过在安全性上同样不可接受。应急码。为每个用户生成一组一次性应急码供令牌丢失时登录并重新绑定。应急码要限量、生成即记录、用后作废并提示用户离线保管。没有应急码用户丢手机就等于被锁在门外只能走人工流程成本极高。多应用一后台的收敛收益。用一个后台对接多个应用容灾的对象也从每套系统各自的动态口令收敛为一套服务这本身就是容灾成本的大幅下降。以安当OTP为例一个后台可对接多个应用支持服务端本地化或 SaaS 两种部署形态通过 RADIUS 或 API 对接堡垒机、云桌面、代码仓库与各类业务系统的远程接入二次认证容灾设计只需围绕这一套服务展开。九、排障手册按症状定位下面这张表按你看到的症状组织是本文最建议打印出来的部分。症状最可能的原因定位步骤处置全部用户、全部应用都失败服务端时钟跃变或哈希库与配置被变更核对服务端 Unix 时间与标准时间差检查最近变更记录平滑校正时钟回滚变更若曾回拨临时收紧容差并加强限流全部用户失败但只在某个新接入的应用上该应用与动态口令后台参数不一致核对四元组T0、X、Digit、Hash统一四元组建立参数基线文档并纳入变更评审个别用户一直失败密钥绑定错误、换了设备未重绑、本地时间被改查该用户最近成功记录与窗口偏移核对绑定时间与密钥版本重新扫码绑定作废旧密钥引导开启自动校时时好时坏间歇性失败跨窗口边界提交或容差过小查看失败日志中的窗口偏移若集中在正负一边界适当放宽到 N 为 1 到 2优化页面提示剩余有效期刚绑定成功过一会儿就失败两端时钟偏差大绑定时恰好落在容差内比较令牌端与服务端的时间戳差值校正客户端时钟启用学习偏移同一窗口内第二次提交被拒一次性消费机制正常生效查审计中是否有重放拒绝记录属预期行为向用户说明优化提示文案迁移或换手机后全量失败密钥编码错误base32 与 hex 混用、大小写、多余空格用固定时间做一次离线比对分别按两种编码试算统一编码规范迁移脚本加校验用例八位对不上、六位可以位数参数两端不一致核对位数参数与令牌显示位数统一位数前导零逻辑一并回归测试硬件令牌陆续失效电池耗尽或晶振漂移累积统计各令牌偏移趋势按序列号排查提前更换建立令牌有效期台账部分应用能过、部分不能过存在多套后台策略不一致梳理后台清单与各自参数收敛到一个后台过渡期建立参数对照表失败集中在某个终端或网络区域该区域终端时间源异常或请求被重试放大抽查该区域终端时间检查链路上是否有重复提交修正该区域时钟源对重试做幂等处理认证时延明显变长容差窗口过大导致每次计算候选过多统计单次校验的哈希运算次数引入学习偏移减少平均候选数使用这张表时有一条通用原则先看审计日志里的窗口偏移字段再动手改参数。窗口偏移是区分时钟问题与参数问题的分水岭——偏移稳定为 0 但仍失败几乎一定是参数或密钥问题偏移非零且随用户不同是客户端时钟问题偏移随时间单向增大是漂移问题。没有这个字段排障就只能靠猜。十、上线前验收清单四元组T0、X、Digit、Hash两端一致并写入参数基线文档用固定时间加固定密钥的单元测试通过期望值硬编码前导零边界用例通过六位与八位各构造一次常数时间比较函数已用于码值比对窗口一次性消费已开启且消费记录存于共享存储容差窗口 N 控制在 1 到 2且已配套失败限流与账号锁定学习偏移已启用偏移有边界与老化重同步流程要求连续两个相邻窗口的码且已做越权测试服务端时钟双源、互监、告警阈值分层禁止运行时手动大幅改时间容器时区用例通过非 UTC 环境下结果不变密钥库加密存储敏感场景由密码模块保护备份加密、异地、恢复演练有记录高可用切换实测通过切换后重放保护仍生效应急码已生成、限量、用后作废丢失补办流程端到端走过一遍逃生通道已设计并演练触发即告警、全程留痕审计日志覆盖成功、失败、重放拒绝三类事件且结构化外送到日志分析平台国密场景确认两端均已切到 HMAC-SM3并保留切换记录作为密评证据。十一、常见问题 FAQ问一三十秒太短用户来不及输怎么办先确认是不是真的来不及——多数情况是跨窗口边界提交导致的偶发失败而不是普遍来不及。如果确有需要可放宽到六十秒但要同步收紧失败限流策略并接受重放窗口翻倍。更推荐的做法是优化交互在页面上显示当前码的剩余有效秒数并提示接近刷新时请等下一个码。问二手机没信号能出码吗能。TOTP 是两端各自计算不依赖网络。只要手机时钟基本准确离线状态下也能出码。这一点对现场、车间、外场等弱网环境很重要。问三用户手动改了手机时间能骗过系统吗只能造成一次性失败不能形成持续威胁改时间会让用户自己的码对不上反而先被挡住。真正的防线是服务端限流与异常检测——同一账号短时间内大量失败、或窗口偏移长期异常都应触发告警。问四共享密钥泄露了怎么办立即在服务端重置该用户的密钥并作废旧密钥用户重新扫码绑定。这也说明密钥存储必须加密、敏感场景应由密码模块保护绝不能明文落库。问五SHA-1 还能用吗作为 HMAC 的底层哈希SHA-1 目前尚未出现实用的碰撞攻击但新系统不应再默认使用它。建议选 SHA-256 及以上有国密合规要求的场景直接选 SM3并把切换记录保留下来作为密评证据。问六硬件令牌和手机令牌能混用吗可以而且是常态。做法是按人群分有智能手机的员工用软令牌无手机或受限环境如不准安装第三方应用、长期无网络用硬件令牌或小程序令牌。关键是后台要能统一管理不同形态的密钥与偏移。问七为什么要在意前导零这种小概率问题因为它的故障表现最迷惑——用户会坚定地说我输的就是屏幕上显示的而排查者往往先去查时钟。八位模式下约百分之零点四七的概率在万人规模下一周内就会出现若干次。上线前专门测一次能省下大量工单。问八容差开大一点是不是更省事短期省事长期埋雷。容差每放大一档在线爆破成功率就线性上升同时截获码的存活时间增加三十秒。正确做法是容差保持在正负六十秒以内配合限流与一次性消费再用学习偏移去解决真实的漂移问题。方案参考安当OTP是上海安当技术推出的基于 OATH TOTP 的动态口令产品可作为本文所述算法实现与对时容灾工程实践的落地参考。其能力要点与本文各节的对应关系如下算法与参数三十秒步长、六位动态码的标准 TOTP支持 SHA1/256/512/224/384 及国密 SM3 多种哈希算法。SM3 支持对应本文第三节的国密变体与验收清单中的国密确认项。密钥编码与注册支持 base32 与 hex 两种共享密钥编码手机令牌扫码注册兼容主流验证器实际部署中应对照排障手册中编码不一致一条建立统一规范。令牌形态手机软令牌、硬件令牌、微信小程序令牌三类并存可按人群与环境分配对应第七节客户端侧的讨论。部署形态服务端支持本地化或 SaaS本地化适合对数据主权与密钥保护要求高的场景也是容灾设计中需要自主保障的对象。对接方式通过 RADIUS 或 API 对接堡垒机、云桌面、代码仓库与业务系统等远程接入的二次认证场景一个后台可对接多个应用把容灾对象从多套收敛为一套。易用性支持用户自注册配合身份核验流程可显著降低IT逐人录入的成本同时把密钥重置与丢失补办纳入自助流程。建议的落地顺序是先用本文第四节的示例做一次算法正确性验证把四元组与前导零边界测通再按第六节实现学习偏移与受控重同步并按第七节搭建时钟监控随后按第八节完成高可用切换与密钥库恢复演练上线后把第九节的排障表与第十节的验收清单固化进运维手册并持续关注审计日志中的窗口偏移分布。
返回列表