一、种子密钥为何是 OTP 体系的命门
在基于时间的一次性口令(TOTP)体系中,服务端和用户令牌必须持有相同的种子密钥,再结合当前时间窗口(典型为 30 秒)通过哈希算法(如 SHA1、SHA256、SHA512,或国密 SM3)生成一个 6 位动态口令。从密码学角度看,TOTP 是一个对称算法:只要掌握了种子密钥与时间计数器,任何人都能复现出正确口令。这意味着种子密钥本身,而不是那串短暂有效、随时变化的动态数字,才是整个双因素认证体系真正的信任根。
很多早期或自研的 OTP 系统在处理种子密钥时存在一种危险但普遍的简化做法:把种子密钥当作普通字段,直接写入业务数据库的一张表(例如 user_otp_secret 表)里,落库前最多做一次静态加密或干脆明文存储。这种做法在工程上最省事,但埋下了三个层面的隐患。
第一是数据库层面的横向风险。一旦数据库被拖库(无论是通过注入、备份泄露还是内部越权),攻击者可以一次性拿到全部用户的种子密钥。由于 TOTP 是离线的、无需联网验证的算法,攻击者拿到种子后,即使业务系统完全正常,他也能在手机上自行计算任意时间窗口的动态口令,从而绕过双因素认证。换句话说,数据库泄露即等于双因素认证的全面失守。
第二是运维与备份链路的扩散风险。明文种子密钥会随着数据库备份、跨机房同步、开发测试环境的数据脱敏不彻底而不断复制扩散。安全团队往往只盯着生产库,却忽略了测试库、数据仓库、日志归档中同样存在一份份明文的种子拷贝。攻击面从一座堡垒变成了一整片田野。
第三是内部人员的滥用风险。DBA、运维或具有数据库直连权限的人,无需触碰任何应用代码,仅凭一条 SQL 就能导出所有种子密钥。传统的数据库审计只能记录"谁访问了哪张表",却无法证明访问者是否把数据带走。在金融、保险、海关这类对内部合规要求极严的行业,这种"看得见明文"的设计本身就是审计红线。
理解了种子密钥是命门这一事实后,正确的工程目标就清晰了:让种子密钥在任何时刻、任何位置都不以明文形态出现,唯一允许它参与运算的地方,是一台受物理与逻辑双重保护的 HSM 内部。
二、HSM 托管的核心设计原则
HSM(Hardware Security Module,硬件安全模块)是一种专门负责密钥生成、存储与密码运算的硬件设备。它的核心承诺是:私钥或种子一旦以"密钥对象"的形式进入 HSM,就永远不能以明文形式导出。所有需要用到密钥的运算(签名、校验、HMAC 计算等)都必须把数据送进 HSM,由 HSM 在内部完成计算后只把结果返回。这个特性恰好能对症下药地解决上一节提到的三类风险。
把 HSM 引入 OTP 体系,需要遵循五条设计原则。
原则一:种子密钥在 HSM 内生成,永不明文离开。种子密钥应当由 HSM 的随机数发生器直接产生,并以"不可导出(non-exportable)"的密钥句柄(handle)形式留在 HSM 中。业务数据库里只保存一个无意义的引用标识符(如 key_id),以及该密钥对应的算法、位数、状态等元数据。即便数据库被完整拖走,攻击者拿到的也只是一堆无法反推种子的 key_id 字符串。
原则二:TOTP 校验全程在 HSM 内完成。校验动态口令时,服务端把"用户 ID 对应的 key_id + 当前挑战时间 + 用户输入的 6 位口令"发送给 HSM,由 HSM 用内部保存的种子密钥计算期望口令并比对,只返回"通过/失败"。种子密钥本身从不出现在服务端的内存或日志中。
原则三:密钥按用户与应用双维度隔离。HSM 内每个种子密钥都绑定到具体的用户标识和具体的应用标识,调用时必须同时提供正确的句柄与访问权限上下文,避免越权校验。
原则四:密钥生命周期由 HSM 与密钥管理服务共同管理。种子的创建、激活、轮换、吊销、销毁都通过受控的管理接口进行,并留下防篡改的审计日志,而不是靠人工去数据库里改字段。
原则五:性能与高可用必须提前规划。HSM 是专用硬件,单次运算延迟虽低(通常在毫秒级),但吞吐受设备规格限制。TOTP 校验属于高频、低算力但请求量大的场景,需要在架构上做好连接池、批量与限流设计,避免 HSM 成为瓶颈。
以安当OTP为例,其服务端并不直接持有种子明文,而是把种子的密码学运算下沉到 HSM 或等价的密钥保护边界内,业务侧只持有密钥句柄。这种设计使得即便 OTP 服务端进程被攻破、内存被 dump,攻击者也只能拿到 key_id 而拿不到可用种子,从而把"攻破应用"与"攻破双因素认证"这两件事彻底解耦。
三、HSM 内 TOTP 校验的完整流程
下面给出 HSM 内 TOTP 校验的逻辑流程。把一个动态口令的校验拆开来看,本质就是"时间分片 + HMAC + 截断取模"三个步骤(即 RFC 6238 定义的 TOTP 算法),差异只在于 HMAC 的密钥(种子)必须留在 HSM 里。
// 输入:key_id(HSM 内种子密钥句柄)、challenge_time(挑战时间,可由服务端传入或 HSM 取当前时间) // 输入:user_code(用户提交的 6 位动态口令) // 输出:PASS / FAIL,以及匹配的时间窗口偏移(用于抗时钟漂移) function hsm_verify_totp(key_id, challenge_time, user_code): // 1. 计算时间计数器,T = floor((challenge_time - T0) / X),X=30 T = (challenge_time - T0) // X // 2. 在允许的漂移窗口内逐一尝试,例如 [-1, 0, +1] 共三个窗口 for drift in [-1, 0, +1]: counter = T + drift // 3. 关键:HMAC 运算在 HSM 内部完成,种子密钥不离开 HSM expected = hsm_hmac_sha(key_id, big_endian_8bytes(counter)) // 4. 动态截断(dynamic truncation)得到 6 位 code = totp_truncate(expected, 6) if code == user_code: return PASS with drift return FAIL注意上面hsm_hmac_sha这一步的语义:种子密钥key_id指向的明文只有 HSM 能访问,服务端进程调用的是 HSM 提供的"用这个句柄做一次 HMAC"的指令,而不是"把这个密钥返回给我"。这是整个方案安全性的基石。
把视角切换到一次真实的登录校验,全链路的时序如下。
[用户手机令牌] [OTP 业务服务端] [HSM] | | | | 当前 6 位口令 | | |----------------------->| | | | 查 key_id(用户,应用) | | |------------------------->| | | | 取出种子句柄 | | 校验(key_id,时间,口令) | | |------------------------->| | | | HMAC*N窗口 | |<-------------------------| PASS/FAIL | |<-------------------------| |<-----------------------| | | 登录成功/失败 | |在这个时序里,种子密钥只存在于 HSM 的受保护内存中,业务服务端全程只搬运"口令、key_id、结果"三类无害数据。即便在传输链路上被抓包,攻击者能看到的也只是 key_id 与时间戳,无法复原出任何用户口令。
算法选择上,除了通行的 SHA1、SHA256、SHA512 之外,在金融、政务、关基等受监管场景,还应支持国密 SM3 作为 HMAC 的基础哈希。SM3 是我国自主设计的密码杂凑算法,其输出长度为 256 位,足以支撑 TOTP 的截断取模运算。当算法标识被置为 SM3 时,上面的hsm_hmac_sha实际调用的是 HSM 内的 SM3-HMAC 指令,算法标签随 key_id 一起作为元数据持久化,校验时自动选用,对业务代码透明。
四、种子密钥的安全分发与令牌注册
校验侧把种子锁进 HSM 之后,新的问题出现了:种子在 HSM 里生成,用户手里的手机令牌或硬件令牌又必须持有同一份种子才能算出相同的动态口令——种子究竟如何安全地从 HSM "分身"到用户令牌,而不在传输途中泄露?
这里要先澄清一个容易混淆的点:HSM 不导出种子明文,并不等于用户令牌拿不到种子。实际工程里有两种合规做法。
第一种做法,称为"注册期注入"。在用户注册动态口令的环节,由 HSM 内部生成一个种子,HSM 直接把这个种子封装进一个一次性的、受保护的注册凭证(例如一个带签名与时间戳的加密二维码票据)。手机令牌扫码后,在令牌应用的安全区内解密并存储种子,HSM 本身并未把明文种子吐给服务端。整个过程中,服务端从未以明文形式接触过种子。
第二种做法,称为"令牌预置"。对于硬件令牌或批量发放的企业场景,种子在符合安全标准的制卡/烧录环境中注入令牌,同时把同一份种子的句柄(而非明文)登记进 HSM。这种方式下种子从生成到注入都在受控边界内完成。
无论哪种做法,扫码注册环节都必须走安全信道并做完整性保护,否则二维码在展示、传输、截屏的任一环被中间人替换,用户就会把攻击者控制的种子扫进手机,从而让攻击者也能算出正确口令。典型防护要点如下:
// 手机令牌扫码注册的安全时序 [后台/HSM] [用户浏览器/展示端] [手机令牌APP] [中间人] | | | | | 生成种子(句柄k) | | | | 构造注册票据: | | | | enc(seed,k_session) | | | | +签名 +有效期 | | | |----------------------->| | | | | 展示二维码(HTTPS信道) | | | |-----------------------------\ | | | | 替换二维码? | | |<----------------------------- 篡改票据 | | | | | | | 用户扫码 | | | |----------------------->| | | | | 验签名+有效期 | | | | 解密得到种子 | | | | 本地安全存储 | |<----------------------(仅返回注册成功确认)------| |要点归纳:
- 展示二维码必须基于加密信道(如管理后台本身的 HTTPS 会话),且二维码内容应当包含服务端签名与时间戳,手机令牌在扫码时先验签再解密,拒绝过期或签名不符的票据,从而抵御中间人替换。
- 种子在票据内使用一次性会话密钥加密,即便票据被截获,没有对应的会话密钥也无法还原种子。
- 手机令牌把种子保存在自身的安全存储区(如系统的 Keychain / Keystore 或令牌应用私有加密区),不落明文文件、不写入可截图可读的界面缓存。
- 硬件令牌通过出厂烧录或专用灌密钥设备注入,种子的明文形态只在该受控环境内存活极短时间。
- 兼容性是现实需求:由于 TOTP 是开放标准,同一份种子用 base32 或 hex 编码后,可以写入符合 RFC 6238 的第三方令牌(如谷歌、微软、腾讯验证器)。采用开放编码格式分发,能在不牺牲安全边界的前提下,保证用户用熟悉的工具完成注册。
需要强调的是,开放编码(base32/hex)只是种子在"注入令牌那一刻"的表示形式,它解决的是互通问题,并不削弱 HSM 托管的安全性——因为种子在注入前始终在受保护边界内,编码动作也只是边界内的最后一次转换。
五、种子轮换、吊销与密钥版本管理
再坚固的密钥也有生命周期。员工离职、令牌丢失、疑似泄露、合规要求的定期更换,都会触发种子的轮换与吊销。HSM 托管的体系下,这些操作不是去数据库改一个字段,而是对 HSM 内密钥对象的状态机进行管理。
建议为每把种子维护如下状态:
| 状态 | 含义 | 允许的操作 |
|---|---|---|
| INIT | 已生成,待激活 | 激活、销毁 |
| ACTIVE | 正常服务中 | 校验、轮换、吊销 |
| ROTATING | 轮换中(新旧并存) | 双密钥校验、确认新密钥 |
| REVOKED | 已吊销 | 仅留审计,拒绝校验 |
| DESTROYED | 已销毁 | 不可恢复 |
轮换(rotation)的关键点是"新旧并存"。当触发轮换时,HSM 内生成新种子句柄,旧种子进入 ROTATING 状态并保留一个短暂的重叠期(例如 24 小时或若干个时间窗口),期间两种子都可校验,方便用户在不中断登录的前提下完成手机令牌重新扫码注册。重叠期结束后旧种子转为 REVOKED,不再参与校验。
吊销(revocation)则用于令牌丢失或泄露的紧急处置:把对应 key_id 直接置为 REVOKED,HSM 对该句柄的校验指令立即返回失败,攻击者即便手握旧种子也无法通过认证。由于种子明文从未离开 HSM,吊销动作是即时且不可逆的,不存在"明文还在某处"的尾巴。
为了支持上述并存校验,HSM 校验逻辑需要能根据用户当前的有效密钥集合返回匹配结果。伪代码层面,一次校验应遍历该用户在该应用下的所有"非吊销"密钥:
function verify_user(user_id, app_id, user_code, t): keys = list_active_keys(user_id, app_id) // 返回 ACTIVE + ROTATING 的 key_id 列表 for k in keys: if hsm_verify_totp(k, t, user_code) == PASS: return PASS return FAIL这种多密钥并存模型同时解决了"换手机重新注册"和"旧令牌回收"两类常见运维诉求,又不会让旧种子以明文形式游离在系统之外。
六、一个后台对接多应用的命名空间设计
在实际企业环境里,同一个组织往往要把双因素认证同时挂到多个业务系统上:办公门户、堡垒机、云桌面、代码仓库(如 GitLab)等。如果每个系统各自部署一套 OTP 后台与 HSM,成本与运维复杂度都会失控。更好的做法是"一个后台对接多应用",而 HSM 内的种子密钥天然适合用命名空间来隔离。
建议的密钥标识结构为三元组:(tenant_id, app_id, user_id)。其中 tenant_id 用于多租户隔离(集团下不同子公司),app_id 区分具体接入的业务系统,user_id 标识用户。HSM 内每把种子句柄都绑定到这个三元组,校验时必须携带完整上下文,HSM 在内部做权限校验,确保"A 应用的 key_id 不能被 B 应用的请求拿来校验"。
接入方式上,业务系统通常通过两种协议把校验请求转发给 OTP 后台:其一是标准 Radius 协议,适合堡垒机、云桌面、网络设备这种原生支持 Radius 的远程接入场景;其二是 REST API,适合办公门户、GitLab 这类 Web 业务在登录流程中嵌入二次认证。无论哪种方式,OTP 后台在收到请求后,都先解析出(tenant_id, app_id, user_id),再拿着对应的 key_id 去 HSM 完成校验,对上游业务系统而言只看到一个"认证成功/失败"的干净结果。
[业务系统A] --Radius--> [OTP后台] --key_id+code--> [HSM] --> PASS/FAIL [业务系统B] --REST ---> [OTP后台] --key_id+code--> [HSM] --> PASS/FAIL [业务系统C] --Radius--> [OTP后台] --key_id+code--> [HSM] --> PASS/FAIL (同一套后台,按 app_id 路由到不同种子命名空间)用户自注册能力也应纳入这个命名空间:用户在某个应用的注册页完成手机令牌扫码后,OTP 后台自动在其(tenant_id, app_id, user_id)命名空间下登记一把种子句柄,无需管理员逐人手工录入。如此一来,多应用共享一个后台,却各自拥有独立、隔离、可独立轮换吊销的种子集合。
七、性能、限流与防爆破
TOTP 校验看似只是一次 HMAC,但放在企业全量用户的登录高峰面前,其请求量是相当可观的。HSM 作为专用硬件,单设备吞吐有限,必须在架构层做好三件事。
第一是连接与句柄复用。HSM 访问通常通过 PKCS#11 或厂商 SDK 建立会话,频繁开闭会话代价很高。应在 OTP 后台侧维护一个到 HSM 的连接池,并把 key_id 到会话的映射做本地缓存(注意缓存的只是句柄引用,绝不是种子明文)。
第二是限流与熔断。针对单个用户,应对单位时间内的校验尝试次数做限流(例如每分钟最多 5 次、连续失败 10 次锁定一段时间)。这能直接压制针对某一个种子的在线爆破——即便攻击者知道算法,他每试一个口令都要走一次 HSM 校验并受到限流约束,而 TOTP 的 6 位数字在单窗口内只有 100 万种组合,叠加时间窗口与限流后,在线爆破在现实里几乎不可行。同时对 HSM 整体做并发熔断,当 HSM 延迟或错误率超阈值时快速失败,避免请求在 HSM 前堆积雪崩。
第三是校验窗口与漂移控制。TOTP 依赖两端时钟同步,手机令牌与服务端时间存在漂移时必须允许有限的前后窗口(常见为 ±1 个 30 秒窗口)。但这个容忍窗口同时会被攻击者利用来扩大爆破空间,因此窗口不宜过大,并应在日志中记录匹配到的偏移量,用于事后发现时钟异常或可疑尝试。
// 单用户限流 + HSM 熔断的校验骨架 rate_key = "otp_verify:" + user_id + ":" + app_id if rate_limiter.allow(rate_key) == false: return FAIL_TOO_MANY // 触发限流,直接拒绝,不碰 HSM try: with hsm_pool.connection() as hsm: result = hsm_verify_totp(key_id, now(), user_code) except HSMTimeout: circuit_breaker.record_failure() return FAIL_TEMPORARY else: circuit_breaker.record_success() if result == PASS: rate_limiter.reset(rate_key) return result值得注意的是,由于种子在 HSM 内、明文不外泄,攻击者无法把"猜测口令"这件事搬到自己机器上离线穷举——他每一次猜测都必须经过 HSM,而 HSM 前面的限流与熔断正是为这种在线尝试量身定制的防护。这也是 HSM 托管相比"明文种子 + 应用内直接计算"在抗爆破维度上的结构性优势。
八、国密 SM3 与算法可配置落地
在受监管的关基行业,密码算法的合规性是硬指标。前面多次提到 SM3,这里把它的落地要点单独说明。SM3 作为 HMAC 的底层哈希时,与 SHA 系列在结构上对等,只是压缩函数与常数表不同。OTP 后台应在每把种子的元数据中记录算法标识(SHA1/SHA256/SHA512/SHA224/SHA384/SM3),HSM 校验时按标识自动选择对应的 HMAC 指令,业务调用方无需关心底层差异。
选型上有几点建议:新系统不应再使用 SHA1,因其抗碰撞性已被削弱,尽管 TOTP 实际依赖的是 HMAC 而非直接碰撞,但从合规与前瞻角度应默认 SHA256 起步;对密码合规要求明确的场景直接选用 SM3;同一后台可同时容纳多种算法,按应用或按用户粒度配置,从而平滑支持存量令牌(已发行的老令牌可能只支持 SHA1)向新算法的迁移——过渡期让新旧算法对应的种子并存即可,与第五节的轮换模型天然契合。
方案参考
把动态口令的种子密钥托管进 HSM 并不是某一个产品的专属能力,而是一套可以复用的工程范式。若正在规划或改造自家的双因素认证体系,可参考以下落地步骤,不必拘泥于具体品牌:
- 先做资产盘点:梳理当前种子密钥的存储位置(数据库、配置文件、备份、日志),确认是否存在明文拷贝,这是改造的出发点。
- 选定密钥保护边界:根据合规要求与预算,确定是用物理 HSM、云端的密钥管理服务,还是具备等价"不可导出"特性的软件根。核心判据是"种子明文是否可能离开保护边界"。
- 改造校验路径:把 TOTP 的 HMAC 计算从应用进程内迁移到保护边界内,应用侧只保留 key_id 与校验结果,数据库只存密钥句柄而非种子明文。
- 重做分发链路:为手机令牌扫码注册引入带签名、带时效的一次性注册票据,并强制走加密信道;硬件令牌走受控烧录环境。对外互通时采用 base32/hex 开放编码以兼容标准令牌。
- 建立生命周期管理:为种子定义 INIT/ACTIVE/ROTATING/REVOKED/DESTROYED 状态机,实现新旧密钥并存轮换与即时吊销,并保留防篡改审计。
- 规划多应用隔离:用
(租户, 应用, 用户)三元组作为密钥命名空间,一套后台通过 Radius 或 API 对接多个业务系统,按 app_id 路由到各自的种子集合。 - 补齐性能与防护:维护 HSM 连接池、做单用户限流与 HSM 熔断,控制时间漂移窗口并记录偏移量,从架构上压制在线爆破。
- 落实算法合规:默认启用 SHA256 以上强度,在受监管场景支持 SM3,并允许按应用/用户粒度共存多种算法以平滑迁移。
上述步骤的价值不在于堆砌功能,而在于把"种子密钥"这一信任根,从"人能看、库能拖、日志能留"的明文状态,收敛到"只在受保护硬件内运算、处处只留句柄"的受控状态。当攻击者即便攻破应用、拖走数据库、抓到链路包,依然拿不到任何可用种子时,双因素认证才真正名副其实。