动态口令密码应用的技术原理与 GM/T 0021 标准深度解读:从 HOTP/TOTP 到国密算法实现
概述
动态密码(Dynamic Password),又称一次性密码(One-Time Password, OTP),是现代多因子认证(MFA)体系中最广泛部署的第二因子技术。与静态密码不同,动态口令每隔一定时间或每认证一次就变化一次,即使被截获也无法重复使用。
当前主流的 OTP 技术基于两个 IETF RFC 标准:RFC 4226(HOTP,基于 HMAC 的事件同步 OTP)和 RFC 6238(TOTP,基于时间同步的 OTP)。两者都采用 SHA-1 作为底层哈希算法,使用 6-8 位数字作为口令输出。
GM/T 0021《动态口令密码应用技术规范》是中华人民共和国密码行业标准,最早发布于 2012 年(GM/T 0021-2012),后在 2023 年更新(GM/T 0021-2023)。该标准针对国密合规场景,要求使用 SM3 或 SM4 算法替代 SHA-1,并对密钥管理、动态因子生成、截断函数等环节提出了严格要求。
本文将从工程实现角度,系统解析动态口令的技术原理,深入解读 GM/T 0021 的核心设计,并提供生产环境部署建议。
OTP 算法基础
核心概念模型
动态口令的生成基于三个核心要素:
┌─────────────────────────────────────────────────────────┐
│ OTP 生成模型 │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 种子密钥 │ + │ 动态因子 │ → │ 哈希/加密运算 │ │
│ │ (Secret) │ │ (Counter/ │ │ (SHA-1/ │ │
│ │ │ │ Timestamp) │ │ SM3/SM4) │ │
│ └──────────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 截断函数 │ │
│ │ (Truncation) │ │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 6-8位数字OTP │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────┘种子密钥(Secret/Seed Key):服务端和客户端共享的对称密钥,通常以 Base32 编码或二维码形式分发给用户。种子的安全性是整个 OTP 体系的核心——种子泄露意味着 OTP 保护失效。
动态因子(Dynamic Factor):
- HOTP 使用递增计数器(Counter),客户端每次生成口令后计数器 +1
- TOTP 使用 Unix 时间戳除以时间步长(Time Step),通常步长为 30 秒
截断函数(Truncation):从摘要中提取 4 字节,转换为 0-999999 范围的整数,再取模得到 6 位数字(可扩展至 7-8 位)。
国际标准:HOTP 与 TOTP
#### HOTP(RFC 4226)
HOTP 的核心公式:
HOTP(K, C) = Truncate(HMAC-SHA-1(K, C))
其中:
- K:种子密钥(≥128 bit)
- C:8 字节大端递增计数器
- Truncate():从 HMAC 输出中提取 4 字节并转换为整数截断函数详解:
def hmac_sha1_truncate(hmac_result: bytes) -> int:
"""RFC 4226 截断函数"""
# 取低 4 位作为偏移量(0-15)
offset = hmac_result[-1] & 0x0F
# 从偏移量开始取 4 字节
code = (
((hmac_result[offset] & 0x7F) << 24) |
((hmac_result[offset + 1] & 0xFF) << 16) |
((hmac_result[offset + 2] & 0xFF) << 8) |
(hmac_result[offset + 3] & 0xFF)
)
# 取模得到 6 位数字
return code % 1_000_000注意 0x7F 掩码:防止符号位问题,确保结果为正整数。
#### TOTP(RFC 6238)
TOTP 建立在 HOTP 之上,将计数器替换为时间戳:
TOTP(K, T) = HOTP(K, T)
其中:
- T = floor((UnixTime - T0) / TimeStep)
- T0:起始 Unix 时间戳(通常 0,即 1970-01-01)
- TimeStep:时间步长(通常 30 秒)
- UnixTime:当前 Unix 时间(秒)关键参数:
| 参数 | 典型值 | 说明 |
|---|---|---|
| T0 | 0 | 起始时间戳(UTC 1970-01-01 00:00:00) |
| TimeStep | 30s | 口令更新间隔 |
| Digit | 6 | 输出位数(或 8) |
| Look-ahead | ±1 | 服务端向后查看的步数(容忍时钟漂移) |
服务端的验证逻辑
服务端验证不仅仅是"比对当前值的 OTP 是否匹配",还需要处理客户端时钟漂移和计数器不同步:
┌─────────────────────────────────────────────────────────┐
│ TOTP 服务端验证流程 │
│ │
│ 1. 接收客户端OTP + 时间戳(可选) │
│ 2. 计算当前时间步 index = floor(current_time / 30) │
│ 3. 遍历窗口 [index - lookahead, index + lookahead] │
│ 对每个 index_i: │
│ otp_i = HOTP(secret, index_i) │
│ 如果 otp_i == client_otp → 验证通过 │
│ 4. 全部不匹配 → 验证失败 │
│ 5. 验证通过后: │
│ - HOTP: 更新服务端计数器 = matched_index + 1 │
│ - TOTP: 记录已使用的 (otp, time_step) 防重放 │
└─────────────────────────────────────────────────────────┘GM/T 0021 标准对动态口令的特殊要求
算法选择:国密密码体系的必由之路
GM/T 0021 要求动态口令使用国密算法替代国际标准中的 SHA-1:
| 环节 | 国际标准 | GM/T 0021 要求 |
|---|---|---|
| 哈希运算 | SHA-1 | SM3 密码杂凑算法 |
| 分组加密(可选) | AES | SM4 分组密码算法 |
| MAC 运算 | HMAC | 自定义拼接杂凑或 SM4-CBC-MAC |
GM/T 0021 中,动态口令的 SM3 国密实现并非使用 HMAC-SM3,而是采用种子密钥与动态因子拼接后直接进行 SM3 杂凑的方式:
OTP_SM3(K, F) = Truncate(SM3(K || F))
其中:
- K:种子密钥
- F:动态因子(计数器或时间戳)
- ||:拼接操作(具体拼接方式由标准定义,参考 GM/T 0021-2012 第 6.2 节)注意:这种设计使得 GM/T 0021 的 SM3 实现与 HMAC-SM3 不等价。HMAC 使用 ipad/opad 进行密钥填充和两层哈希,而直接拼接只做一次杂凑。两者的安全性依赖不同的密码学假设。
密钥管理要求
GM/T 0021 对种子密钥的全生命周期提出了严格管理要求:
- 密钥生成:种子密钥必须由密码随机数生成器(符合 GM/T 0005)产生,长度不小于 128 位(16 字节)
- 密钥分发:种子密钥在服务端和客户端之间分发时,必须使用 SM2 密钥封装或 SM4 加密传输,明文不得出现在传输信道中
- 密钥存储:服务端种子密钥应存储在密码机或安全介质中(参考 GM/T 0028 对应安全等级要求);客户端种子密钥存储在身份验证器安全区域或智能密码钥匙中
- 密钥更新:种子密钥应定期更新,建议在以下情况下强制更新:用户挂失、设备更换、密码机重置
- 密钥销毁:密钥生命周期结束后必须安全清零,防止残留数据被恢复
动态因子的安全要求
GM/T 0021 强调动态因子的不可预测性:
- HOTP 计数器:计数器服务端必须单调递增,认证成功后立即更新,防止重放
- TOTP 时间戳:时间戳必须通过可信时间源获取,服务端和客户端的时钟漂移容忍窗口不得配置过大(建议 ±2 个时间步)
- 动态因子长度:动态因子至少应为 8 字节,确保足够的熵值
截断函数的设计
GM/T 0021 截断函数的设计思想与 RFC 4226 类似,但针对 SM3 输出做了适配:
SM3 输出为 32 字节(256 位),截断偏移量取值范围 0-27(最后 5 位)
取 4 字节(32 位),取低 31 位确保正整数
OTP = Code % 10^Digit
其中 Digit 通常为 6 或 8国密 OTP 的工程实现
SM3-based TOTP 实现(伪代码)
以下伪代码展示 GM/T 0021 风格的国密 TOTP 生成逻辑,用于原理说明(非实际运行代码):
# ⚠️ 原理演示代码,非完整生产实现
# SM3 算法通过 gmssl 库或 cryptography(v41+)调用
import struct
import time
def sm3_otp_generate(seed_key: bytes, time_step: int = 30, digits: int = 6, current_time: int = None) -> str:
"""
GM/T 0021 风格的 SM3 TOTP 生成(伪代码)
参数 current_time 用于指定时间,默认使用当前时间
"""
# 1. 计算时间因子
t = (current_time if current_time is not None else int(time.time())) // time_step
time_factor = struct.pack(">Q", t) # 8 字节大端
# 2. 拼接种子密钥与时间因子
# 具体拼接顺序和编码参考 GM/T 0021 标准原文
data = seed_key + time_factor
# 3. SM3 哈希运算
# digest = sm3_hash(data) # 通过国密库调用,输出 32 字节
# 4. 截断(类似 RFC 4226)
# offset = digest[-1] & 0x1F # SM3 32 字节,取低 5 位 → 0-31
# 取 4 字节,掩码高位:
# code = ((digest[offset] & 0x7F) << 24) | \
# ((digest[offset+1] & 0xFF) << 16) | \
# ((digest[offset+2] & 0xFF) << 8) | \
# (digest[offset+3] & 0xFF)
# 5. 取模生成 OTP
# otp = code % (10 ** digits)
# return str(otp).zfill(digits)
# 完整实现需引入国密 SM3 函数库
# 推荐使用 gmssl(pip install gmssl)或 Tongsuo OpenSSL 绑定
pass
def sm3_otp_verify(
seed_key: bytes,
client_otp: str,
current_time: int = None,
look_ahead: int = 2,
time_step: int = 30,
digits: int = 6
) -> dict:
"""
GM/T 0021 风格的服务端验证
返回:{"valid": bool, "matched_step": int}
"""
if current_time is None:
current_time = int(time.time())
current_step = current_time // time_step
# 搜索窗口内的所有可能口令
for offset in range(-look_ahead, look_ahead + 1):
check_step = current_step + offset
# 使用 check_step 计算预期 OTP
check_time = check_step * time_step
expected_otp = sm3_otp_generate(seed_key, time_step, digits, check_time)
if expected_otp == client_otp:
return {"valid": True, "matched_step": check_step}
return {"valid": False, "matched_step": -1}服务端重放攻击防护
TOTP 的重放防护是工程中的关键挑战。常见方案:
-- 方案一:记录已使用的 (otp, step) 对
CREATE TABLE used_otp_records (
user_id INTEGER NOT NULL,
time_step INTEGER NOT NULL,
otp_value VARCHAR(8) NOT NULL,
used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, time_step, otp_value)
);
-- 验证时先检查是否已使用
-- INSERT 失败(主键冲突)→ 重放攻击
-- 定期清理过期记录(窗口外的记录不再需要)
DELETE FROM used_otp_records
WHERE time_step < (current_step - look_ahead);# 方案二:基于步长的单调递增验证(HOTP 方式适配 TOTP)
# 记录每个用户最后成功认证的步长,拒绝任何 <= last_step 的认证请求
def verify_with_monotonic_step(user_id: int, otp: str, step: int, db_session):
last_step = db_session.query(User.otp_last_step).filter_by(id=user_id).scalar()
if step <= last_step:
return False # 重放或时钟回退攻击
# 验证 OTP...
if otp_matches:
db_session.query(User).filter_by(id=user_id).update({"otp_last_step": step})
db_session.commit()
return True
return False工程建议:方案二(步长单调递增)更适合高并发场景,避免了数据库写入已用 OTP 的性能开销。但在 TOTP 中需要处理合理的时钟漂移(用户设备时间偏差),建议允许 look_ahead = 1(即 ±30 秒),同时使用步长单调性确保同一窗口内的重放被拒绝。
国密认证产品生态
GM/T 0021 认证的动态口令产品
截至 2026 年,多家国内密码产品厂商的 OTP 产品已获得 GM/T 0021 认证,安全等级通常为二级或三级:
| 厂商 | 产品型号 | 安全等级 | 证书编号示例 |
|---|---|---|---|
| 宁盾科技 | NDKEYTPASS V8.0 | 安全二级 | GM013111420250947 |
| 天安电子 | TASS OTP | 安全二级 | GM013111420XXXXXXXX |
| 众人科技 | JustKey OTP | 安全二级 | — |
- 软件身份验证器(手机 App):种子密钥加密存储在设备安全区(如 iOS Keychain、Android Keystore),通过 SM3/SM4 算法生成 OTP
- 硬件动态令牌(显示设备):内置安全芯片执行国密算法,LCD 显示动态口令,防物理拆解设计
- 智能密码钥匙(USB Key):符合 GM/T 0027 规范,执行 SM2 签名 + SM3 OTP 双重认证
与 GM/T 0016 的协同应用
GM/T 0016《智能密码钥匙密码应用接口规范》定义了智能密码钥匙的应用接口,厂商可基于此接口实现:
- 密钥生成:在 Key 内部生成 SM2 密钥对,私钥不可导出
- OTP 生成:在 Key 内部完成 SM3 哈希和截断,输出最终 OTP 到主机
- 双因子认证:SM2 数字签名(证明 Key 持有)+ SM3 OTP(证明用户知道 PIN 码)
密钥派生与分散
密钥分散的概念
在大规模企业或金融系统中,为每个用户生成独立的种子密钥在管理上极为复杂。GM/T 0021 引入了密钥分散机制:
┌─────────────────────────────────────────────────────────┐
│ 密钥分散模型 │
│ │
│ 根密钥(Root Key,存储在密码机中) │
│ │ │
│ │ 分散因子(如用户ID/手机号/身份证号) │
│ ▼ │
│ ┌─────────────┐ │
│ │ 密钥派生函数 │ KDF = SM3(RootKey || Factor) │
│ │ (SM3 杂凑) │ 或 SM4-KDF(RootKey, Factor) │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ 用户专属种子密钥(User Seed) │
│ │ │
│ │ 动态因子 │
│ ▼ │
│ 用户动态口令 │
└─────────────────────────────────────────────────────────┘密钥分散的核心思想:从单一的根密钥派生出大量用户专属种子,系统只需保护根密钥的安全,而不需要为每用户存储独立的种子。
GM/T 0021 中的分散操作
根据标准原文(GM/T 0021-2012 第 6.3 节),分散操作通常采用以下方式:
分散种子 = SM3(根密钥 || 分散因子 || 分散标识)
其中:
- 根密钥:密码机内生成和存储的主密钥
- 分散因子:足够区分用户的标识(如手机号、用户编号、身份证号散列)
- 分散标识:区分不同应用或用途的固定字符串(如 "OTP-AUTH-v1")
- SM3 输出 32 字节,取前 16 字节作为 128 位种子密钥安全要求:根密钥必须存储在符合 GM/T 0028 要求的密码机或安全介质中,分散运算也在密码机内完成,根密钥不可导出。
安全性分析与合规要求
抗重放攻击
动态口令天然具有抗重放特性(一次性使用),但工程实现中仍需防范以下攻击:
| 攻击类型 | 攻击方式 | GM/T 0021 防护要求 |
|---|---|---|
| 重放攻击 | 截获已使用的 OTP 再次提交 | 服务端记录已用 OTP/步长 |
| 中间人攻击 | 在网络信道中截获 OTP | 国密 TLS 传输加密(推荐 GM/T 0024) |
| 暴力破解 | 穷举所有可能的 OTP 值 | 6 位数字限 10^6 种可能,服务端实施速率限制 |
| 种子泄露 | 获取种子密钥 | 根密钥存储在密码机内,不可导出 |
时钟漂移容忍
TOTP 的安全性依赖于服务端和客户端的时钟同步。实际工程中:
- 服务端时钟:必须通过 NTP 协议与国家标准时间同步(建议精度 ±1 秒以内)
- 客户端时钟:移动设备通常通过网络自动同步,但存在断网后时钟漂移的场景
- 容忍窗口:look_ahead = ±1(即 ±30 秒)可覆盖绝大多数场景;增大到 ±2 会增加暴力破解风险
密评合规要点
根据密评实践经验,动态口令系统的合规检查重点:
- 算法合规:必须使用 SM3 或 SM4,禁止使用 SHA-1(在高风险判定时,基于 SHA-1 的 OTP 会被判定为高风险)
- 密钥管理合规:种子生成、分发、存储、使用、销毁各环节需有完整记录
- 密码模块合规:动态口令生成模块需通过 GM/T 0028/0039 检测认证
- 传输合规:服务端与验证器之间的种子分发需使用国密算法加密传输
总结
- GM/T 0021 是国密 OTP 的标准基石:使用 SM3/SM4 替代 SHA-1,满足密评合规要求
- 种子密钥安全是核心:根密钥存储在密码机内,通过密钥分散派生用户种子
- 重放防护不能依赖算法本身:必须配合服务端的步长记录或 OTP 去重机制
- 时钟同步是 TOTP 的生命线:建议部署 NTP 服务并监控时钟偏差
- 与智能密码钥匙协同:SM2 签名 + SM3 OTP 提供硬件级双因子认证
参考来源
- RFC 4226 — HOTP: An HMAC-Based One-Time Password Algorithm
- RFC 6238 — TOTP: Time-Based One-Time Password Algorithm
- GM/T 0021-2012《动态口令密码应用技术规范》 — 国家密码管理局
- GM/T 0021-2023《动态口令密码应用技术规范》 — 更新版本,含最新技术调整
- 密码标准使用指南(2021版) — 密码行业标准化技术委员会
- 密码应用安全性评估 — 动态口令常见问题探讨
- 智能密码钥匙技术规范 GM/T 0027
相关实践
- SM3 密码杂凑算法原理详解:从消息填充到压缩函数——理解动态口令中 SM3 的底层原理
- SM4 分组密码算法原理详解:从设计思想到安全分析——动态口令备选算法 SM4 的深入分析
- FIDO2 与 WebAuthn:无密码认证协议的原理与架构——从 OTP 到无密码认证的演进路径
- 密码学安全随机数生成:CSPRNG 原理与熵源管理——种子密钥生成的技术基础