SM2 密钥协商
QXJ 在鸿蒙安全浏览器(客户端 A)与鉴权及策略管理子系统(服务器 B)之间,运行 一套基于 GM/T 0003.3-2012《SM2 椭圆曲线公钥密码算法 · 密钥交换协议》 的 四步握手协议,一次完成四件事:
- 双向身份认证——双方确认对方持有合法长期私钥,中间人无法冒充;
- 会话密钥协商——双方独立推导出同一把会话密钥(K_client / K_session);
- 前向安全——身份只由长期密钥签名担保,密钥材料全部来自一次性临时密钥对;
- 令牌签发——服务器向客户端下发带 HMAC 的访问 Token,作为后续业务请求凭证。
阅读本页前若对「对称/非对称、哈希、有限域、椭圆曲线点加法、ECDH」不熟悉, 请先读 密码学基础,本页只在必要处简要复述。
协议全景
协商完成后双方持有:
| 产物 | 客户端位置 | 服务器位置 | 用途 |
|---|---|---|---|
| 会话密钥 K(16B) | HUKS 关键资产(非持久,卸载即清) | Redis {prefix}sm2:device_session_key:{device_id},TTL 7 天 | SM4-GCM 业务加解密 |
| 访问 Token | 凭据存储 | 可按 HMAC 校验 | 业务请求身份凭证 |
| 对方身份确认 | S1==SB | S2==SA | 双向认证成立的判据 |
协议符号表
设计文档《密钥协商协议 v8》表 1-1 定义了协议使用的全部符号:
| 符号 | 含义 |
|---|---|
| A、B | 使用公钥密码系统的两个用户:客户端 A 和服务器 B |
| dA、dB | A、B 的长期私钥 |
| PA、PB | A、B 的长期公钥 |
| ID_A、ID_B | A、B 的长度为 entlen_A / entlen_B 比特的可辨别标识 |
| entlen_A、entlen_B | ID_A、ID_B 的比特长度 |
| ENTL_A、ENTL_B | 身份长度字段:entlen 转换而成的 2 字节 |
| H₂₅₆() / Hash() | SM3 杂凑函数(GM/T 0004),输入长度小于 2⁶⁴,输出 256 bit |
| ZA、ZB | 双方身份杂凑值(定义见下) |
| G | 椭圆曲线基点,阶为素数 n |
| O | 群单位元(无穷远点) |
| n | 基点 G 的阶(n 是 #E(Fq) 的素因子) |
| h | 余因子,h = #E(Fq)/n,SM2 取 1 |
| q | 有限域 Fq 的元素个数(SM2 为 256 bit 素数 p) |
| a、b | 椭圆曲线方程参数,定义 Fq 上曲线 E |
| x_G、y_G | 基点 G 坐标转换的比特串 |
| x_A、y_A | 公钥 PA 坐标转换的比特串 |
| x‖y | x 与 y 的拼接(比特串或字节串) |
| KDF | 密钥派生函数 |
| mod n | 模 n 运算,如 23 mod 7 = 2 |
| [k]P | 点 P 的 k 倍点:P+P+…+P(k 个) |
| #E(Fq) | 曲线点的数目(曲线的阶) |
| ⌈x⌉ / ⌊x⌋ | 向上 / 向下取整 |
| [x, y] | 整数区间 x ≤ t ≤ y |
| & | 按位与 |
工程中 A、B 的身份标识统一采用 36 字节 ASCII UUID(形如 8-4-4-4-12 五组十六进制),设备注册时生成。
通信帧整体格式
协商帧与后续加密数据帧使用统一帧头。设计文档 v8 的帧头为 32 字节;工程 落地为 64 字节(v2 帧头)——发送方标识由 4B 扩展为 36B UUID,其余字段 口径不变:
v8 设计帧头(32B)
1B 版本 + 1B 主命令 + 2B 子命令 + 2B 总长度 + 2B 序号
+ 4B 发送方ID + 1B 加密认证模式 + 16B IV + 3B 保留
当前实现帧头(64B,大端)
偏移 Size 字段 说明
0 1 version 协议版本(0x01)
1 1 main_cmd 主命令:0x00 协商 / 0x01 报警 / 0x02 加密完保
2 2 sub_cmd 子命令(最高位=上下行;低 15 位=功能编号)
4 2 total_len 整帧总长度(帧头 + 数据域;加密帧另含 tag)
6 2 seq 帧序号(协商期置 0;业务期双方各自递增抗重放)
8 36 sender_id 发送方标识(ASCII UUID,不足补 0x00)
44 1 enc_auth 加密认证模式(握手帧 0x00;SM4-GCM 帧 0x40)
45 16 IV 初始向量(握手帧全零;加密帧为随机 IV)
61 3 reserved 保留字段2
3
4
5
6
7
8
9
10
11
12
13
14
15
帧头字段释义(v8 原文口径)
- 版本号(1B):满足规范的协议版本,目前为 1;
- 主命令码(1B):功能模块标识;
- 子命令码(2B):最高位表示上下行——Pad→鉴权子系统为上行(0), 鉴权子系统→Pad 为下行(1);低 15 位为模块内部标识,协商协议使用
2024 ~ 3072区间; - 总长度(2B):整个报文总长度(协议头部 + 认证与校验域之和);
- 帧序号(2B):双方各自维护、依次递增,用于抗重放;协商阶段可为任意 值(如全 0),接收方不处理,协商完成后才启用;
- 发送方 ID(v8 为 4B):高字节系统类型、低字节同类型编号(APP
0x20、 MCS 鉴权子系统0x21);当前实现改为 36B UUID; - 加密认证模式(1B):v8 设计为两段式——高 4 位加密模式(
0未加密 /1SM4-CBC /2SM4-ECB /3SM4-CTR),低 4 位认证模式(0未签名 /1SM2 签名 /2SM4-CMAC);当前实现统一为 SM4-GCM 单模式0x40; - IV(16B):加密方产生随机数,解密时取回送入算法;
- 保留字段(3B):暂时保留。
主子命令码全量表
| 主命令 | 方向位 | 子命令(低 15 位) | 备注 |
|---|---|---|---|
| 密钥协商(0) | 0 上行 | 发起密钥协商 0x01 | Pad → 鉴权子系统 |
| 1 下行 | 客户端认证和密钥交换 0x02 | 鉴权子系统 → Pad | |
| 0 上行 | 服务器认证、密钥交换、会话密钥确认 0x03 | Pad → 鉴权子系统 | |
| 1 下行 | 会话密钥确认、Token 生成 0x04 | 鉴权子系统 → Pad | |
| 报警(1) | 0 / 1 | 不满足椭圆曲线方程 0x01 | 上下行同编号 |
| 0 / 1 | 计算椭圆曲线点失败 0x02 | ||
| 0 / 1 | 密钥确认失败 0x03 | ||
| 0 / 1 | 意外消息 0x04 | ||
| 0 / 1 | 结束通知 0x05 | ||
| 加密完保(2) | 0 上行 | 加密完保消息 0x01 | Pad → MCS 加解密接口 |
| 1 下行 | 加密完保消息 0x02 | MCS → Pad |
数据域与四包结构
每个帧在帧头之后接数据域。数据域的统一开头为:数据长度(2B)+ 握手数据 (nB)。四个握手包的数据域布局如下(BE = 大端)。
第一包 INIT(数据域 216B,整帧 280B)
| 偏移(数据域内) | Size | 字段 |
|---|---|---|
| 0 | 2 | 数据长度 |
| 2 | 2 | IDA 长度(BE) |
| 4 | 2 | IDB 长度(BE) |
| 6 | 2 | 会话密钥长度 klen(BE) |
| 8 | 8 | 时间戳 T(毫秒,BE) |
| 16 | 36 | IDA(ASCII UUID) |
| 52 | 36 | IDB(ASCII UUID) |
| 88 | 64 | RA(临时公钥 x‖y) |
| 152 | 64 | SigA(SM2 签名 RAW r‖s) |
长度验证:16(定长头)+ 72(双方 ID)+ 128(RA+SigA)= 216; 64 + 216 = 280B ✓
第二包 RESP(数据域 98B,整帧 162B)
| 偏移 | Size | 字段 |
|---|---|---|
| 0 | 2 | 数据长度 |
| 2 | 64 | RB(临时公钥 x‖y) |
| 66 | 32 | SB(共享密钥校验值) |
64 + 2 + 64 + 32 = 162B ✓
第三包 ACK(数据域 34B,整帧 98B)
| 偏移 | Size | 字段 |
|---|---|---|
| 0 | 2 | 数据长度 |
| 2 | 32 | SA(共享密钥校验值) |
64 + 2 + 32 = 98B ✓
第四包 TOKEN(数据域 82B,整帧 146B)
| 偏移 | Size | 字段 |
|---|---|---|
| 0 | 2 | 数据长度 |
| 2 | 2 | IDA 长度(BE) |
| 4 | 8 | expiration_ms(有效期毫秒数,BE;是时长不是时间戳) |
| 12 | 2 | hmac_len(BE) |
| 14 | 36 | IDA(ASCII UUID) |
| 50 | 32 | HMAC(HMAC-SM3,当前实现定长 32B) |
64 + 2 + 2 + 8 + 2 + 36 + 32 = 146B ✓ (v8 草拟时 HMAC 长度标注「64 字节?」未定,工程落地取 SM3 输出 32B。)
四包长度速查
| 包 | 方向 | 整帧长度 | HTTP 接口 |
|---|---|---|---|
| INIT | A→B | 280B | POST /api/v3/keymgr/sm2/build_resp/ |
| RESP | B→A | 162B | 上一接口响应 |
| ACK | A→B | 98B | POST /api/v3/keymgr/sm2/build_token/ |
| TOKEN | B→A | 146B | 上一接口响应 |
完整协商流程(逐步推导)
准备:身份杂凑 ZA / ZB
设备注册阶段,每方计算自己的身份杂凑,把「身份标识 + 曲线参数 + 长期公钥」 全部绑定:
ZA = SM3( ENTL_A ‖ ID_A ‖ a ‖ b ‖ xG ‖ yG ‖ xA ‖ yA )
ZB = SM3( ENTL_B ‖ ID_B ‖ a ‖ b ‖ xG ‖ yG ‖ xB ‖ yB )2
ZA/ZB 随后被放入 KDF 与校验值的输入——这是「把设备身份自然融入 ECDH」的 关键:最终密钥在数学上是「A 与 B 之间」的密钥,而不是「两个匿名临时点」 之间的密钥,任何一方身份被替换都会导致派生结果不同。
消息 ① INIT —— A 执行 Step A1
1. 由 ID_A、PA 计算 ZA;由 ID_B、PB 计算 ZB;
2. 生成随机 rA ∈ [1, n−1],计算临时公钥 RA = [rA]G = (x1, y1);
3. 计算签名 SigA = Sign(dA, ID_A ‖ RA ‖ T),T 为毫秒时间戳;
4. 将 ID_A、RA、T、SigA 组帧发送。2
3
4
消息 ② RESP —— B 执行 Step B1~B5
B1 · 认证 A
1. 用(可信渠道获得的)PA 校验 SigA = Sign(dA, ID_A‖RA‖T);
2. 检查 T 的新鲜度(时间窗内)且未被重放;
3. 验证 RA:在曲线上、坐标域内、非无穷远点。2
3
B2 · 生成临时密钥:随机 rB ∈ [1, n−1],RB = [rB]G = (x2, y2)。
B3 · 中间参数与共享点
先定义坐标混淆函数(SM2 取 w = 127,因为 n ≈ 2²⁵⁶):
w = ⌈ ⌈log₂ n⌉ / 2 ⌉ − 1
x̄ = 2^w + (x & (2^w − 1)) // 取 x 低 w 位并把第 w+1 位置 12
工程实现的中间参数与共享点(注意与国标文本的下标区别,详见下文对照):
tB = (dB + x̄_B · rB) mod n // x̄_B 取自本方临时公钥 RB 的 x 坐标
V = [h · tB] ( PA + [x̄_A] RA ) // x̄_A 取自对方 RA 的 x 坐标
若 V 为无穷远点 O,协商失败。2
3
B4 · 派生会话密钥
KB = KDF( xV ‖ yV ‖ ZA ‖ ZB , klen )B5 · 共享密钥校验值:SB 的构造见 S 值的三种口径, 随后将 RB、SB 发送给 A。
消息 ③ ACK —— A 执行 Step A3~A5
A3 · 认证 B 的临时公钥:验证 RB 在曲线上、坐标域内、非无穷远;随后
tA = (dA + x̄_A · rA) mod n // x̄_A 取自本方 RA
U = [h · tA] ( PB + [x̄_B] RB ) // x̄_B 取自对方 RB
若 U 为无穷远点 O,协商失败。2
3
A4 · 派生会话密钥
KA = KDF( xU ‖ yU ‖ ZA ‖ ZB , klen )A5 · 密钥确认:先计算 S1 并与收到的 SB 比对(一致则 U=V,B 身份成立); 再计算 SA 发送给 B 确认:
S1 = S_tag(0x02, …, xU, yU);要求 S1 == SB
SA = S_tag(0x03, …, xU, yU);将 SA 放入 ACK2
消息 ④ TOKEN —— B 执行 Step B6
1. 重算共享点与 S2 = S_tag(0x03, …, xV, yV),与收到的 SA 比对;
2. 生成 Token:ID_A ‖ expirationTime ‖ HMAC(ID_A ‖ expirationTime);
3. 将会话密钥发布到 Redis,向 A 回 TOKEN 帧。2
3
为什么双方必然得到相同密钥
展开工程变体的共享点(PA=[dA]G 等关系代入):
V = [h(dB + x̄_B rB)](PA + [x̄_A]RA)
= h·dB·dA·G + h·dB·x̄_A·rA·G + h·x̄_B·rB·dA·G + h·x̄_B·rB·x̄_A·rA·G
U = [h(dA + x̄_A rA)](PB + [x̄_B]RB)
= h·dA·dB·G + h·dA·x̄_B·rB·G + h·x̄_A·rA·dB·G + h·x̄_A·rA·x̄_B·rB·G2
3
4
四项逐项相同(乘法可交换),故 U = V,进而 KA = KB、S1 = SB、S2 = SA。 安全性上:长期私钥只证明身份,会话密钥的全部机密性来自 rA/rB 两个一次性 随机数,临时私钥用后即毁 → 前向安全。
S 值的三种口径
协议校验值在「设计文档草案、GM/T 国标、工程实现」三处有细微差异,这是读 文档时最容易困惑的地方,一并列出:
① v8 草案文字:单层哈希,把共享点与双方杂凑、临时公钥全部喂入:
SB = Hash( V ‖ ZA ‖ ZB ‖ RA ‖ RB )② GM/T 0003.3 国标:双层 SM3,外层带标签字节与共享点 y 坐标:
inner = SM3( ZA ‖ ZB ‖ x1 ‖ y1 ‖ x2 ‖ y2 )
SB = SM3( 0x02 ‖ yV ‖ inner )
SA = SM3( 0x03 ‖ yU ‖ inner )2
3
③ 当前工程实现(源码 nsp_sm2_make_S_tag):国标双层结构,但 inner 中 显式补入共享点 x 坐标 xV(国标只在外层绑 yV):
inner = SM3( xV ‖ ZA ‖ ZB ‖ x1 ‖ y1 ‖ x2 ‖ y2 )
S = SM3( tag ‖ yV ‖ inner ) tag=0x02 得 SB/S1,tag=0x03 得 SA/S22
工程实现 = v8「把共享点纳入校验」的意图 + 国标的双层标签结构。双方必须使用 同一口径,握手才能通过。
Token 生成
设备 Token(握手第四包)
Token = ID_A ‖ expirationTime ‖ HMAC( H_key, ID_A[0:16] ‖ expirationTime )工程细节(见 C++ 源码):HMAC 的待签内容只取 IDA 固定区的前 16 字节与 8 字节有效期;HMAC 底层为 HMAC-SM3,输出 32B。expiration_ms 是有效时长 (毫秒数),不是绝对时间戳,客户端据此自行换算到期时刻。
用户级 Token
v8 第 1.1.5 节定义了面向具体用户(IDu)的令牌数据域,用于区分同一设备上的 不同用户身份:
| 顺序 | Size | 字段 |
|---|---|---|
| 1 | 2 | 数据长度 |
| 2 | 2 | IDA 长度(BE) |
| 3 | IDA 长度 | IDA |
| 4 | 4 | IDu 长度 |
| 5 | IDu 长度 | IDu |
| 6 | 8 | expiration_ms(BE) |
| 7 | 2 | hmac_len(BE) |
| 8 | hmac_len | HMAC |
HMAC 可由服务器私钥签名,或由会话密钥做 HMAC——前者不可否认但计算 较慢,后者性能高,按场景选取。
C 组包接口(v8 第 1.1.4 节)
设计文档按密码设备 SDF 风格定义了 4 个组包函数,是工程 NAPI 6 函数的原型。 密钥统一使用 ECCrefPrivateKey_st / ECCrefPublicKey_st 结构(GM/T 0018):
typedef struct ECCrefPrivateKey_st {
unsigned int bits; /* 密钥位长 */
unsigned char K[32]; /* 私钥 */
} ECCrefPrivateKey_st;
typedef struct ECCrefPublicKey_st {
unsigned int bits;
unsigned char x[64]; /* 公钥 x 坐标 */
unsigned char y[64]; /* 公钥 y 坐标 */
} ECCrefPublicKey_st;2
3
4
5
6
7
8
9
10
① 握手第一包
int nsp_sm2_build_init_packet(
ECCrefPrivateKey_st *myPriKey, /* [IN] 发起方长期私钥 dA */
ECCrefPublicKey_st *myPubKey, /* [IN] 发起方长期公钥 PA */
ECCrefPrivateKey_st *myTmpPriKey, /* [IN] 临时私钥 rA */
ECCrefPublicKey_st *myTmpPub, /* [OUT] 临时公钥 RA=[rA]G */
ECCrefPublicKey_st *hePubKey, /* [IN] 响应方长期公钥 PB */
unsigned char *pucSponsorId, /* [IN] ID_A */
unsigned int uiSponsorIdLen,
unsigned char *pucResponsorId, /* [IN] ID_B */
unsigned int uiResponsorIdLen,
unsigned int uiKeyLen, /* [IN] 期望会话密钥长度 */
unsigned int *puiOutLen, /* [IN/OUT] 缓冲区容量→实际长度 */
unsigned char *pucOutFrame); /* [OUT] 完整帧 */2
3
4
5
6
7
8
9
10
11
12
13
② 握手第二包
int nsp_sm2_build_resp_packet(
ECCrefPrivateKey_st *pstMyPriKey, /* [IN] dB */
ECCrefPublicKey_st *pstMyPubKey, /* [IN] PB=(x‖y) */
ECCrefPrivateKey_st *pstMyTmpPriKey, /* [IN] rB */
ECCrefPublicKey_st *pstMyTmpPubKey, /* [IN] RB=[rB]G */
ECCrefPublicKey_st *pstHePubKey, /* [IN] PA */
unsigned char *pucSponsorId, unsigned int uiSponsorIdLen,
unsigned char *pucResponsorId, unsigned int uiResponsorIdLen,
unsigned int uiKeyLen,
unsigned char *pucKey, /* [OUT] 会话密钥 KB */
const unsigned char *pucRA, unsigned int uiRALen,
const unsigned char *pucSigA, unsigned int uiSigALen, /* RAW r‖s 64B */
unsigned long long ulltstampMs, /* [IN] A 的时间戳 */
unsigned int *puiOutLen, unsigned char *pucOutFrame);2
3
4
5
6
7
8
9
10
11
12
13
14
③ 握手第三包
int nsp_sm2_build_ack_packet(
ECCrefPrivateKey_st *pstMyPriKey, /* [IN] dA */
ECCrefPublicKey_st *pstMyPubKey, /* [IN] PA */
ECCrefPrivateKey_st *pstMyTmpPriKey, /* [IN] rA */
ECCrefPublicKey_st *pstMyTmpPubKey, /* [IN] RA */
ECCrefPublicKey_st *pstHePubKey, /* [IN] PB */
unsigned char *pucSponsorId, unsigned int uiSponsorIdLen,
unsigned char *pucResponsorId, unsigned int uiResponsorIdLen,
unsigned int uiKeyLen,
unsigned char *pucKey, /* [OUT] 会话密钥 KA */
const unsigned char *pucRB, unsigned int uiRBLen, /* 64B */
const unsigned char *pucSB, unsigned int uiSBLen, /* 32B */
unsigned int *puiOutLen, unsigned char *pucOutFrame);2
3
4
5
6
7
8
9
10
11
12
13
④ 握手第四包
int nsp_sm2_build_token_packet(
ECCrefPrivateKey_st *pstMyPriKey, /* [IN] dB */
ECCrefPublicKey_st *pstMyPubKey, /* [IN] PB */
ECCrefPrivateKey_st *pstMyTmpPriKey, /* [IN] rB */
ECCrefPublicKey_st *pstMyTmpPubKey, /* [IN] RB */
ECCrefPublicKey_st *pstHePubKey, /* [IN] PA */
unsigned char *pucSponsorId, unsigned int uiSponsorIdLen,
unsigned char *pucResponsorId, unsigned int uiResponsorIdLen,
unsigned int uiKeyLen,
unsigned char *pucKey, /* [OUT] KB */
const unsigned char *pucRA, unsigned int uiRALen,
const unsigned char *pucSA, unsigned int uiSALen, /* 32B */
unsigned long long ullExpirationMs, /* [IN] Token 有效期毫秒数 */
const unsigned char *pucWaitedHmacData, /* [IN] 待签内容:用户ID、到期时间 */
unsigned char *pucHmacData, /* [OUT] HMAC */
unsigned int *puiOutLen, unsigned char *pucOutFrame);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
所有函数返回值非 0 即失败。演进到 ArkTS 后,SDF 结构体被拍平为 hex 字符串 参数,组包与 SM4 加解密统一收进 6 个 JSBIND_GLOBAL 函数(见 鸿蒙架构 · qxj_sdk)。
密钥安全存储(电子盘密管设计)
口径说明:以下内容为 v8 文档第五章描述的密钥管理系统电子盘存储设计, 对应密码卡 / 密管设备体系(对称与非对称密钥以文件形式密态落盘)。当前鸿蒙 APP 进程内的对应实现是 HUKS 关键资产 + Redis 会话密钥,不落这些文件; 本节保留设计全貌以便对照与学习。
三级密钥、四类对象
| 层级 | 密钥对象 | 落盘形式 |
|---|---|---|
| 根 | 设备保护密钥(因子 G1/G2 合成) | 系统文件 |
| 中 | 设备密钥对、鉴权子系统密钥对 | 非对称密钥文件 |
| 叶 | 会话(工作)密钥 | 对称密钥文件 |
需要写入电子盘的对称密钥:设备保护密钥因子 G2、会话密钥;非对称密钥: 设备密钥对、鉴权子系统密钥对。
| 类型 | 密钥种类 | 文件名 | 描述 | 备注 |
|---|---|---|---|---|
| 系统文件 | 保护密钥因子 G2 | Device_G.dat | 保护密钥 | 独立调用 |
| 对称密钥 | 会话密钥 | Work_Key.dat | 存储工作密钥 | 后台封装 |
| Work_key.sta | 工作密钥状态 | |||
| 非对称密钥 | 设备签名密钥对 | Device_SignKey.dat | ||
| 设备加密密钥对 | Device_EncKey.dat | |||
| 密钥对 PIN | Device_Pin.dat | |||
| 签名密钥对索引状态 | Device_Sign.idx | 含证书索引 | ||
| 加密密钥对索引状态 | Device_Enc.idx | 含证书索引 |
扩展名约定:.dat 存密钥内容,.idx 存索引与状态,.sta 存密钥状态。
对称密钥文件
工作密钥长度 16B,用设备保护密钥加密后仍为定长 16B,因此可按索引直接定位:
偏移量(密钥 i) = 16 · i
Work_Key.dat = [加密密钥0][加密密钥1]…[加密密钥n−1]2
状态文件 Work_key.sta:前 4B 为当前密钥管理个数(= 存在密钥的最大索引+1), 其后每个密钥用 2 bit 表示状态,每字节装 4 个:
b7b6 = 密钥 4i 状态;b5b4 = 密钥 4i+1;b3b2 = 4i+2;b1b0 = 4i+3
状态:00 不存在 / 01 存在未使用 / 10 正在使用
MCS 口径:10 = 已生成、正常可用;11 = 已过期、不可用
状态信息字节数 = (密钥个数 + 3) / 42
3
4
非对称密钥文件
私钥 32B(设备保护密钥加密后仍 32B),公钥 64B 不加密。数据文件以 96B 为 单位:
单位 i:[公钥 64B 明文][私钥 32B 密态]
公钥偏移 = 96·i;私钥偏移 = 96·i + 64
(MCS 密管系统不存储设备私钥)2
3
证书长度不固定,单独存证书文件,按索引状态定位:
Device_SignCert.dat / Device_EncCert.dat = [证书0][证书1]…(不按索引顺序)索引状态文件:前 4B 为密钥/证书个数,其后每对密钥+证书用 7B:
第 1 字节:b7b6 公钥状态 / b5b4 私钥状态 / b3b2 证书状态 / b1b0 保留
(00 不存在 / 01 存在未使用 / 10 正在使用)
第 2~5 字节(4B):证书在证书文件中的起始位置
第 6~7 字节(2B):证书长度
密钥 i 的索引位于文件第 4 + 7·i 字节处2
3
4
5
系统文件 SysFile.dat
集中存放设备 ID、保护密钥因子 G2、鉴权子系统管理员信息:
| 位置 | 长度 | 内容 |
|---|---|---|
| 第 1 字节高 4 bit | — | 系统状态(0 出厂 / 1 已安装) |
| 第 1 字节低 4 bit | — | G2 状态(0 不存在 / 1 已存在) |
| 第 2 字节起 | 4B | 设备 ID |
| 第 6 字节起 | 16B | 保护密钥因子 G2(与 G1 异或合成设备保护密钥) |
| 第 22 字节起 | 4B | 管理员数目 N |
| 其后 | 198B·N | 管理员信息 |
管理员信息(198B 定长):
| 位置 | 长度 | 内容 |
|---|---|---|
| 1 | 4B | 管理员 ID |
| 5 | 1B | 类别:1 超级 / 2 设备 / 3 用户 / 4 审计管理员 |
| 6 | 1B | 权限位(见下) |
| 7 起 | 96B | 签名公钥 64B + 签名私钥 32B(密态) |
| 103 起 | 96B | 加密公钥 64B + 加密私钥 32B(密态) |
权限位字节 b7…b0:b7 预留 / b5 超级管理 / b4 设备管理 / b3 用户管理 / b2 对称密钥 / b1 备份恢复 / b0 审计——这是「四权分立」(系统/安全/审计 分离)在存储结构上的落地。
离线导出公钥
APP 端生成设备公钥后离线导出给服务器(文件名 设备名称#设备ID.dat), 封装格式:
[设备ID 36B][# 1B][秒级时间戳 10B][# 1B][设备公钥 128B]- 设备 ID:36 字符 UUID(16B 的 32 个 hex + 4 个连字符);
- 时间戳:10 字符 NTP 秒级时间(自 1900-01-01 起);
- 设备公钥:128 字符 hex(x‖y)。
工程内扫码绑定内容 server_id#timestamp#server_pubkey 即此格式。
工程实现与现网
v8 → 当前实现的演进
| 维度 | v8 设计(2026-01) | 当前实现 |
|---|---|---|
| 承载 | TCP 独立进程、指定端口,协商后再用于 HTTP | HTTP application/octet-stream 直承载(POST Django) |
| 发送方标识 | 4B(系统类型 0x20/0x21 + 编号) | 36B ASCII UUID |
| 加密模式 | SM4-CBC/ECB/CTR + 签名/CMAC 两段式 | SM4-GCM 单模式 0x40 |
| 状态机 | 明确要求状态机实现 | 帧类型 + 报警帧,异常即清理重协商 |
| 并发要求 | 1W 并发与协商时延测试 | 服务器无状态接口 + Redis 会话存储 |
报警与错误码
服务器失败统一返回 HTTP 200 + 报警帧,不回显内部细节。v8 的 5 类报警子命令 在工程中收敛为三段:
| 现网码 | 触发段 | 涵盖 v8 子命令 |
|---|---|---|
| 0x8002 | build_resp:INIT 解析 / 设备或公钥查询 / C 库失败 | 0x01、0x02、0x04 |
| 0x8003 | build_token:ACK 定位会话 / SA 自证失败 | 0x03 |
| 0x8004 | build_token:TOKEN 构造 / 会话密钥发布失败 | 0x02、0x04 |
客户端任何一步失败(解析、超时、报警帧)都清理本地残留密钥与临时密钥对, 下次 login 重新完整协商;自签证书错误按 CertValidationError(错误码 2300060) 单独处理,API ≥ 18 且用户知情确认后 remoteValidation:skip。
代码位置
| 侧 | 文件 |
|---|---|
| 客户端帧工具 | [QxjFrameUtil.ets](file:///c:/Users/etsuyou/Desktop/qxj-project/qxj_harmony_next_pad_nsp_browser/common/src/main/ets/util/QxjFrameUtil.ets) |
| 客户端协商编排 | [KeyNegotiationUtil.ets](file:///c:/Users/etsuyou/Desktop/qxj-project/qxj_harmony_next_pad_nsp_browser/common/src/main/ets/util/KeyNegotiationUtil.ets) |
| 客户端 HTTP/证书 | [HttpUtil.ets](file:///c:/Users/etsuyou/Desktop/qxj-project/qxj_harmony_next_pad_nsp_browser/common/src/main/ets/util/HttpUtil.ets) |
| Native 协商实现 | [sm2_agree.cpp](file:///c:/Users/etsuyou/Desktop/qxj-project/qxj_harmony_next_pad_nsp_browser/features/qxj_sdk/src/main/cpp/src/sm2_agree.cpp) |
| 服务端帧解析 | [packet_parser.py](file:///c:/Users/etsuyou/Desktop/qxj-project/qxj-backend-admin/apps/keymgr/utils/packet_parser.py) |
| 抓包解析 | qxj-wireshark-binary-parser(Lua 解析器) |
面试要点速记
Q:为什么不用 RSA? 256 bit SM2 ≈ 3072 bit RSA 的安全强度,密钥与签名更短、计算与功耗更低, 适合移动终端,且 SM2 为我国商用标准、可自主可控。
Q:ECDH 已经能协商密钥,为什么还要签名和 ZA/ZB? 朴素 ECDH 不认证对方身份,存在中间人;长期密钥签名 + 身份杂凑进入 KDF, 使密钥数学绑定双方身份。
Q:前向安全怎么实现? 长期密钥只签名,密钥材料全部来自一次性 rA/rB,临时私钥协商后销毁;长期 密钥日后泄露也无法解密历史会话。
Q:GCM 相比 CBC+CMAC 组合有什么优势? 一次完成加密与认证、可并行、无填充(杜绝 Padding Oracle);但严禁 nonce 重用。
Q:协商中途失败怎么办? 客户端 fail-closed,清理全部残留密钥与临时密钥对,不做半成品复用,下次 重新完整握手。