一、一个被长期忽略的合规盲区
动态口令几乎是企业做双因素认证的标配。它的部署成本低、用户学习成本几乎为零、不需要改造业务系统,因此在堡垒机、云桌面、远程接入、业务系统等场景中被大量使用。但也正因为"太常用",很多团队把它当成一个已经解决的问题,直到密评或信创验收时才被一记闷棍敲醒:你这套动态口令,底层哈希用的是什么算法?
很多团队在百度搜索"国密动态口令方案"时,真正想确认的其实只有一件事:现在跑得好好的那套动态口令,能不能直接改个配置项就变成国密的,还是要推倒重来。答案是——服务端改起来不算难,难的是客户端令牌、存量用户和兼容性。
现实情况是这样的:市面上绝大多数动态口令系统,无论是商业产品还是开源实现,默认算法都是 HMAC-SHA1。它是国际标准文档中定义的默认算法,主流验证器应用(谷歌验证器、微软验证器、各类手机令牌)也都以它为实现基准。这套体系在纯互联网场景下没问题,但一旦进入政务、金融、能源、交通等需要过商用密码应用安全性评估的场景,或者进入信创替代的验收范围,就立刻暴露出三个问题:
第一,算法不在合规目录内。国内密码应用相关法规与标准要求,涉及密码应用的信息系统应当使用列入商用密码目录的算法。SHA1 本身在国际上早已被认定为不安全(碰撞攻击已被实际验证),更不可能出现在合规目录里。
第二,密码运算未在合规密码产品内完成。密评关注的是"运算在哪儿做、密钥在哪儿存、算法是否可控"。大量动态口令实现是把种子以明文或简单加密形式存在关系型数据库里,验算时在应用服务器内存里做 HMAC 运算,整个链路既没有合规算法,也没有合规密码产品承载。
第三,整改窗口往往很紧。密评整改通常有明确时间节点,而动态口令牵扯到每一个最终用户的令牌,是典型的"牵一发动全身"。如果没有提前规划双算法并存与分批换发,很容易变成集中停机切换,用户体验和运维压力都不小。
所以真正要做的,不是"加一个 SM3 选项",而是把动态口令系统的算法基座、密钥保护、令牌形态和迁移路径整体重新设计一遍。下面逐层展开。
二、先定位:哈希算法在 TOTP 里到底处于哪一步
为了避免把这篇文章写成 TOTP 原理科普,这里只精确指出算法改造要动的那一步。
标准 TOTP 的计算可以拆成四步:
第一步,把当前 Unix 时间按步长换算成计数器值:counter = floor((当前时间 - 起始时间) / 步长),步长通常为 30 秒。
第二步,把计数器值序列化为 8 字节大端整数,作为消息。
第三步,用共享密钥(种子)对该消息做 HMAC 运算,得到一个消息认证码。这一步是整个 TOTP 里唯一用到哈希算法的地方。
第四步,动态截断:取消息认证码最后一个字节的低 4 位作为偏移量,从该偏移处取 4 个字节,屏蔽最高位后组成一个 31 位整数,再对 10 的 N 次幂取模,得到 N 位数字口令(常见 6 位或 8 位)。
也就是说,算法改造的切口非常集中:把第三步 HMAC 里使用的哈希原语从 SHA1 换成 SM3,其余四步的结构可以完全不动。这既是好消息——改动面小;也是需要注意的地方——因为改动面小,很容易被实现者轻率处理,从而埋下兼容性隐患。
三、为什么 HMAC-SHA1 在信创与密评场景下不合规
把理由讲透,需要区分三件事。
其一,SHA1 不是合规算法。我国商用密码算法体系中,杂凑算法对应的是 SM3。SM3 输出长度 256 位,其设计结构(Merkle-Damgård 结构搭配特定消息扩展与压缩函数)与国际上的 SHA-2 系列有相似的安全目标,但在消息扩展、常数选取、布尔函数与置换上均有独立设计,属于我国自主设计的杂凑算法。在需要过密评的系统中,杂凑运算使用 SM3 是硬性要求,SHA1 不被接受。
其二,SHA1 本身在密码学上已被攻破。学术界已经公开了 SHA1 的实际碰撞实例,虽然针对 HMAC-SHA1 的实用攻击目前仍主要停留在理论层面,但对于认证系统而言,"使用一个已被判定不安全的原语"本身就是不可接受的选型,合规测评中也不可能被放行。这一点与"是否已被实际攻破"无关,而是算法生命周期管理的基本要求。
其三,信创验收要求算法自主可控。信创的核心是自主可控,而自主可控在密码层面最直接的体现就是算法自主。一个跑在国产 CPU 和国产操作系统上、用国产数据库存储的系统,如果身份鉴别环节仍在调用 SHA1,整体链条就存在明显的断点,验收时很难自圆其说。
需要补充一点:仅仅把哈希换成 SHA256 或 SHA512 也不解决合规问题。SHA-2 系列同样不是我国商用密码算法目录中的算法。有些团队在整改时用"升级到 SHA256"来应对,这在密评中依然是不符合项。整改的唯一正确方向是 SM3(以及在密钥保护与非对称环节配套使用 SM2、SM4)。
四、国密 OTP 的算法改造:用 SM3 重构 HMAC
HMAC 的结构本身是与具体哈希算法解耦的,它定义了一套通用构造:
HMAC(K, m) = H((K' xor opad) || H((K' xor ipad) || m))其中 K’ 是密钥经过填充或哈希处理后得到的定长密钥,ipad 与 opad 是两个固定填充常数,H 是任意哈希函数。因此,把 H 替换为 SM3,就得到了 HMAC-SM3。
在具体实现上,有三个参数必须精确设定,否则不同实现之间算出来的口令不一致,这是国密 OTP 落地时最常见的事故来源。
分组长度 B。HMAC 中密钥填充使用的分组长度,应当取所使用哈希算法的输入分组长度。SM3 的输入分组为 512 位,即 64 字节。这一点与 SHA-256 相同,实现时不要误用 SM3 的输出长度。
输出长度 L。SM3 输出 256 位,即 32 字节。这比 SHA1 的 20 字节更长,动态截断时可用的字节空间更大,安全性上更宽裕。
动态截断规则。建议严格沿用原有的动态截断规则:取输出最后一个字节的低 4 位作为偏移量,从偏移处取 4 字节,屏蔽最高位(与 0x7F 做与运算)后组成 31 位整数,再取模得到指定位数的口令。这样改造后的系统在截断逻辑上与存量实现完全一致,便于同一套服务端同时支持两种算法。
下面是一段参考实现(示意代码,仅用于说明计算流程):
deftotp(seed:bytes,unix_time:int,digits:int=6,period:int=30,t0:int=0)->str:counter=(unix_time-t0)//period msg=counter.to_bytes(8,"big")# mac = HMAC-SM3(seed, msg),分组长度 64 字节,输出 32 字节mac=hmac_sm3(seed,msg)offset=mac[-1]&0x0Fbin_code=((mac[offset]&0x7F)<<24)\|((mac[offset+1]&0xFF)<<16)\|((mac[offset+2]&0xFF)<<8)\|(mac[offset+3]&0xFF)returnstr(bin_code%(10**digits)).zfill(digits)这段代码里,唯一与国密相关的就是hmac_sm3这一个函数,其余逻辑与常规实现完全一致。这也说明改造的技术难度并不高,真正的工程量在两端适配与迁移。
以安当OTP为例,其服务端同时支持 SHA1、SHA224、SHA256、SHA384、SHA512 与 SM3 多种算法,种子支持 base32 与十六进制两种编码格式,因此国密改造可以在同一套服务端内以"算法策略"的形式配置,而不是另起一套系统。
五、改造涉及的两端与兼容性风险
动态口令是典型的两端系统,只改一端必然失败。
5.1 服务端需要改什么
服务端的核心职责是种子管理与验算。改造点包括:
算法策略模型。在用户令牌记录中增加算法字段,使每条令牌记录都能明确标注自己使用的算法、位数、步长、允许窗口。这样同一个服务端可以同时服务 SHA1 存量用户与 SM3 新用户。
密码运算模块。引入 SM3 与 HMAC-SM3 实现。这里有一个关键决策:SM3 运算应当尽可能下沉到合规密码产品或密码模块内完成,而不是在应用层用一段纯软件代码算。密评中"运算是否在合规密码产品内完成"是重要的评分点。
种子保护。种子必须脱离明文存储,改为由密码模块加密保护或直接在硬件密码模块内生成与保存,见第七节。
验算接口兼容。服务端对外提供的验算接口(无论是接口服务还是传统的远程认证协议接口)不应因算法改变而改变调用方式,算法选择应当对用户与应用透明,由服务端按令牌记录自动匹配。
5.2 客户端令牌需要改什么
客户端是改造的真正难点,因为它直接面对最终用户。
手机令牌应用。通用验证器应用只实现了标准中定义的算法集合,不包含 SM3。因此国密 OTP 需要专用的手机令牌应用,或使用支持国密算法的商业令牌应用。专用应用需要内置国密算法库,并处理好国产移动操作系统与国产芯片架构的适配。
硬件令牌。硬件令牌需要选择支持国密算法的型号。选型时要确认:令牌内部是否真正实现了 SM3 运算、种子写入后能否不可导出、是否支持种子灌装与批量初始化、电池寿命与时钟精度是否满足要求。
小程序或轻量令牌。如果采用小程序形态的令牌,需要确认运行环境能否调用国密算法能力,否则仍会在算法层退回国际算法,改造成果得而复失。
5.3 兼容性风险清单
| 风险点 | 表现 | 应对方式 |
|---|---|---|
| 通用验证器不支持国密算法 | 用户扫码后算出的口令与服务端不一致 | 国密用户统一使用专用令牌应用或硬件令牌,注册入口与普通入口分离 |
| 两端分组长度或截断规则实现不一致 | 同一算法下口令仍不匹配 | 制定统一算法规范文档,两端以同一组测试向量交叉验证 |
| 种子编码格式差异 | 扫码导入后种子被错误解码 | 统一采用 base32 或十六进制,并在注册流程中做一次校验性验算 |
| 算法字段缺失导致网关错配 | 存量用户被强制按新算法验算而全部失败 | 升级前为每条存量令牌回填算法字段,默认按原算法 |
| 硬件令牌型号混杂 | 同一批用户拿到不同算法的令牌 | 按批次管理令牌型号与算法映射,台账与后台记录一一对应 |
| 第三方系统内置了自有验算逻辑 | 业务系统绕过认证服务自行验算 | 收口验算入口,禁止业务侧自行实现验算 |
六、种子的生成、分发、存储与保护
种子(共享密钥)是动态口令系统里唯一的长期秘密,它的保护水平直接决定整个系统的安全性,也是密评中的重点检查项。很多团队在百度搜索"动态口令系统安全"时,真正想确认的就是种子到底存在哪儿、谁看得见。
生成。种子应当由合规的随机数发生器产生,随机数质量必须可举证。推荐做法是在硬件密码模块内直接生成种子,生成过程与结果都不离开密码模块边界。种子长度建议不小于 128 位,与安全强度相匹配。
分发。分发是泄露风险最高的环节。常见方式有三种:其一是二维码分发,服务端生成包含种子的注册二维码,管理员或用户扫码导入;其二是种子文件分发,以加密文件形式下发,配合独立信道传递解密口令;其三是硬件令牌预灌装,由厂商在受控环境中把种子直接写入令牌,同时把种子密文交付给服务端导入。三者中,硬件预灌装的安全边界最完整,二维码方式最便捷,可按用户群体的安全等级分档采用。
存储。明文存库是必须杜绝的做法。合规的存储方式有两种:一是由密钥管理系统或硬件密码模块生成并保存种子,应用侧只保存种子的句柄或标识,验算时调用密码模块完成运算;二是由应用侧保存种子的密文,加解密密钥由密钥管理系统统一管理,密钥本身不以明文形式出现在应用侧。无论哪种方式,目标都是一致的:种子不以明文形式出现在数据库、日志、备份和内存转储中。
使用。验算时,理想状态是种子不离开密码模块,由密码模块内部完成 HMAC-SM3 运算并只返回运算结果。如果工程上暂时做不到,至少要做到"取种子密文、解密、运算、立即清零"的最小暴露窗口,并杜绝把种子写入任何形式的日志。
销毁。用户令牌注销、令牌换发、员工离职时,不仅要删除应用侧记录,还要同步销毁密码模块中对应的密钥对象,并留存销毁记录。
七、时间同步、步长窗口与漂移校准
动态口令依赖时间,时间问题是最常见的用户报障来源,国密改造后这部分逻辑不变,但仍需系统化设计。
步长与窗口。步长通常取 30 秒。服务端验算时不会只比对当前步长的口令,而是会向前向后各看若干个窗口。典型配置是前后各一个窗口,等效于给予用户约正负 30 到 60 秒的容错。窗口开得越大,用户体验越好,但被暴力猜测的成功概率也越高,因此不建议盲目放大,同时必须配合失败次数限制与锁定策略。
口令重试限制。同一个账号在短时间内连续验算失败达到阈值后,应当锁定一段时间或直接锁定令牌。这是因为 6 位数字口令的搜索空间只有一百万,如果没有重试限制,在线暴力破解是有现实可行性的。
漂移校准。硬件令牌的晶振会随温度与老化产生漂移,长期运行后累计误差可能达到数分钟,导致口令全部失效。服务端应当记录每个令牌的漂移步数:验算时若命中当前步长之后的第 N 个窗口,则把该令牌的漂移值更新为 N,后续验算自动带上这个偏移。漂移值应当有上限,超过上限(例如累计超过若干分钟)则判定令牌时钟异常,提示用户送修或换发。
时间源。服务端必须接入可靠的时间同步源,并且自身时间不得被随意修改。分布式的认证服务集群要保证节点间时间一致,否则会出现"在 A 节点能登录、在 B 节点不能登录"的诡异现象。
单次使用约束。口令在有效期内被成功使用后,应当记录该步长已被消费,禁止同一口令在同一窗口内重复使用。这能有效防御肩窥与中间人重放。
八、密评中的证据点与测评口径
这一节是很多技术团队最不熟悉、却直接决定能否通过的部分。动态口令在商用密码应用安全性评估中,通常落在"身份鉴别"这一技术层面,常见的测评关注点如下。
算法合规性。测评会确认动态口令生成过程中使用的密码算法是否为合规算法。整改后的正确证据是:SM3 用于杂凑运算,SM2 用于需要非对称密码的环节(如令牌注册时的挑战签名),SM4 用于种子等敏感数据的存储与传输保护。要能提供算法调用路径的说明与实测验证。
技术合规性。关键在于密码运算是否在合规密码产品内完成,以及产品是否取得商用密码检测认证。使用取得认证的硬件密码模块或密码产品承载种子与运算,是最直接的合规路径。
密钥管理合规性。测评关注种子的生成、存储、分发、更新、销毁全生命周期是否受控,是否存在明文存储、明文传输、越权访问等问题。需要提供密钥管理制度文件、技术实现说明与抽查验证。
防护有效性。测评会验证动态口令是否真正构成第二因子:例如拔掉令牌后是否无法登录、口令是否具备一次性、是否存在可绕过路径、失败锁定策略是否生效。
测评口径上的一个常见分歧点是:动态口令属于"密码技术"还是"非密码的认证手段"。如果动态口令系统完全没有使用密码算法(例如某些基于查表的一次性口令本),它就不属于密码技术应用,无法计入密码应用的技术得分。反之,一旦使用了 SM3 做杂凑运算,它就明确属于密码技术应用,需要完整接受算法、技术、密钥管理三个维度的检查。这一点在方案设计阶段就要与测评机构沟通确认,避免整改完才发现口径理解不一致。
九、存量 SHA 令牌的平滑替换路径
改造最忌讳一次性切换。推荐分四阶段推进。
阶段一:能力准备。服务端完成 SM3 算法支持、算法字段建模、种子保护改造(接入密钥管理系统或密码模块)、专用令牌应用与国密硬件令牌选型。这一阶段对存量用户完全无感。
阶段二:双算法并存。服务端同时支持 SHA 系列与 SM3。新注册用户默认发放 SM3 令牌,存量用户维持原算法不动。此阶段重点是跑通新链路,验证两端一致性,并积累运维经验。并存期建议不短于一个完整的换发周期。
阶段三:分批换发。按用户群体分批推进:优先换发高权限账号(系统管理员、运维人员、财务)、其次是外部与远程访问用户、最后是内部普通用户。换发方式可以是用户自助重新注册(扫码绑定新令牌),也可以是集中发放硬件令牌。每批换发后要跟踪失败率与报障量,确认稳定后再推进下一批。
阶段四:收敛下线。当存量 SHA 令牌占比降到可接受水平后,设定一个截止日期,届时停止 SHA 算法验算,对仍未换发的用户提供线下的应急换发通道。截止日期要提前充分通知,并保留管理侧的临时放行能力以应对特殊情况。
在整个迁移过程中,有两条经验特别重要。第一,迁移台账要精确到人:谁已换发、用的什么算法、什么令牌形态、什么时间。没有台账,就无法确认收敛进度。第二,永远保留逃生通道:换发期间用户令牌失效是必然会发生的事,必须提供经过身份核验的人工重置流程,而这个流程本身也要有审批与审计。
十、落地步骤与配置示例
下面给出一个可操作的落地顺序。
第一步,梳理现状。盘点现有动态口令系统:有多少用户、多少令牌、什么令牌形态、种子存在哪里、验算在哪做、对接了哪些业务系统。这一步的输出是一张现状表,也是后续风险评估的输入。
第二步,确定目标架构。明确种子由谁保护(密钥管理系统或硬件密码模块)、SM3 运算在哪一层完成、令牌采用什么形态、是否保留双算法并存。
第三步,服务端改造。引入 SM3 与 HMAC-SM3 能力,改造令牌记录模型,接入种子保护机制,改造验算接口使其按令牌记录自动匹配算法。
第四步,两端一致性验证。用统一的测试向量集验证服务端与各类令牌的算式一致性。测试向量应覆盖不同种子、不同时间点、不同位数,以及跨步长边界的时间点。
第五步,试点运行。挑选一个小规模、可控的用户群(例如安全团队自身)试用 SM3 令牌,观察一个完整周期。
第六步,分批换发与收敛。按第九节的四阶段推进。
一个典型的令牌配置示例(示意):
# 令牌策略配置(示意)otp:algorithm:SM3# 支持 SM3 / SHA1 / SHA256 / SHA512digits:6# 口令位数,可选 6 或 8period:30# 时间步长,单位秒window:1# 前后各允许的窗口数max_drift_steps:3# 允许的最大漂移步数max_attempts:5# 连续失败次数上限lock_seconds:300# 触发上限后的锁定时长seed_encoding:base32# 种子编码,可选 base32 或十六进制seed_storage:kms# 种子由密钥管理系统保护,应用侧仅存句柄one_time_use:true# 同一窗口内口令仅可使用一次这份配置的关键在于seed_storage与algorithm两项:前者决定了种子是否真正受密码模块保护,后者决定了算法是否合规。其余参数是通用工程参数,按业务容忍度调整即可。
十一、检查清单与算法对照
国密改造检查清单:
| 检查项 | 达标标准 | 常见不达标情形 |
|---|---|---|
| 杂凑算法 | 动态口令生成使用 SM3 | 仍是 SHA1,或改成了 SHA256 |
| 运算承载 | 运算在合规密码产品或密码模块内完成 | 应用服务器内存中纯软件实现 |
| 种子生成 | 由合规随机数发生器产生 | 用普通随机函数生成种子 |
| 种子存储 | 加密存储或存于密码模块,无明文 | 数据库明文存储种子 |
| 种子分发 | 加密分发或硬件预灌装 | 明文二维码长期有效、可重复扫描 |
| 种子销毁 | 令牌注销时同步销毁并留痕 | 只删应用侧记录,密码模块内残留 |
| 算法字段 | 每条令牌记录明确标注算法 | 缺少算法字段,升级后全部错配 |
| 两端一致性 | 通过统一测试向量交叉验证 | 两端分组长度或截断规则不一致 |
| 时间同步 | 服务端接入可靠时间源,集群时间一致 | 节点间时间不一致导致偶发失败 |
| 漂移校准 | 有漂移记录与上限判定 | 令牌漂移后永久失效,只能换发 |
| 重试限制 | 有失败次数上限与锁定策略 | 无限制,存在在线暴力破解风险 |
| 单次使用 | 同一窗口内口令不可重复使用 | 同一口令可重复通过验算 |
| 迁移台账 | 精确到人的换发进度记录 | 无法确认存量收敛比例 |
| 逃生通道 | 有经审批的人工重置流程 | 令牌失效后用户彻底无法登录 |
算法对照表:
| 维度 | HMAC-SHA1 | HMAC-SHA256 | HMAC-SM3 |
|---|---|---|---|
| 杂凑输出长度 | 160 位 | 256 位 | 256 位 |
| HMAC 分组长度 | 64 字节 | 64 字节 | 64 字节 |
| 是否属商用密码算法 | 否 | 否 | 是 |
| 密评可用性 | 不符合 | 不符合 | 符合 |
| 通用验证器支持 | 普遍支持 | 普遍支持 | 需专用令牌 |
| 信创适配 | 无自主可控要求 | 无自主可控要求 | 满足自主可控 |
从这张表可以清楚看到,整改的方向只有一条:SM3。把 SHA1 升级成 SHA256 看似前进了一步,但在合规维度上原地踏步。
十二、常见问题与验证方法
Q1:直接把算法配置项改成 SM3,用户手机上的通用验证器还能用吗?
不能。通用验证器不实现国密算法,改完之后它算出的仍是旧算法的口令,与服务端不匹配。国密用户必须换用支持 SM3 的专用令牌应用或硬件令牌,这也是改造中最大的用户侧工作量。
Q2:能不能服务端算两套,用户输哪个都算通过?
技术可行,但强烈不建议。同时接受两种算法会让"最弱的一环"决定整体安全强度,等同于没有整改,且在密评中会被直接判定为不合规。正确的做法是双算法并存但按令牌记录严格匹配,而不是对同一令牌放宽验算。
Q3:SM3 替换 SHA1 后,口令位数和步长需要改吗?
不需要。SM3 输出 32 字节,比 SHA1 的 20 字节更长,动态截断时可用空间更充足,6 位或 8 位口令、30 秒步长都可以保持不变。
Q4:硬件令牌的时钟漂移怎么处理?
服务端记录每个令牌的漂移步数,验算时自动补偿;超过漂移上限则判定令牌时钟异常,提示送修或换发。同时服务端自身必须接入可靠时间源。
Q5:种子能不能放在数据库里,用 SM4 加密就行?
这是可接受的过渡方案,但不是最优解。更合规的做法是把种子放在硬件密码模块或密钥管理系统内,应用侧只保存句柄,验算时调用密码模块完成运算,种子全程不出安全边界。
Q6:如何验证两端算法实现一致?
准备一组标准测试向量:固定若干个种子、固定若干时间点、覆盖跨步长边界的临界值,分别计算期望口令,然后与服务端和令牌端的输出逐一比对。这是改造过程中必须做的一次性验证,不能只靠"扫个码试试能不能登录"。
Q7:存量用户太多,一次性换发不现实怎么办?
按第九节的四阶段推进,关键是服务端具备按令牌记录匹配算法的能力,使两种算法可以长期并存,从而把换发节奏完全掌握在自己手里。
Q8:动态口令用的种子和 UKey 里的私钥,保护要求一样吗?
两者的保密性要求同等重要,区别在载体。UKey 的私钥天然不可导出,由硬件保证;动态口令的种子是共享密钥,服务端也必须持有,因此必须借助密码模块或密钥管理系统来构造同等的保护边界。
方案参考
安当OTP是上海安当技术推出的动态口令产品,可作为国密改造与信创合规场景的参考方案。其核心能力如下:
- 算法支持:同时支持 SHA1、SHA224、SHA256、SHA384、SHA512 与 SM3,便于存量国际算法令牌与国密令牌长期并存、分批替换。
- 令牌形态:支持手机令牌应用、硬件令牌与短信等多种形态;手机令牌支持扫码注册,可与主流验证器兼容使用,国密场景则配套支持 SM3 的专用令牌。
- 口令参数:支持 30 秒时间步长,默认 6 位口令,可按安全策略调整位数与窗口容错。
- 种子管理:种子支持 base32 与十六进制编码,可结合密钥管理系统实现种子的安全生成、存储与保护,避免明文落库。
- 对接方式:支持远程认证协议接口与接口服务两种对接方式,一套后台可同时服务多个应用系统,业务侧改造量小。
- 部署形态:支持本地化部署与服务化订阅两种模式,满足不同行业对数据驻留与运维模式的要求。
- 用户自助:支持用户自注册与自主绑定令牌,可显著降低批量换发阶段的运维压力。
在实施国密改造时,建议按本文第十一步的检查清单逐项自评,优先解决"算法是否合规"与"种子是否受保护"两项,再推进令牌换发与收敛下线。