HTTPS接口国密安全设计
一、需求
1、总体安全需求
接口需要使用国密进行安全传输、防重放、防篡改和防伪造,敏感字段加密。
敏感字段是idCard和amount。
2、场景设定
客户端调用服务端接口
- 客户端(App 或前端)向服务端(API 接口)发送请求。
- 请求包含敏感参数(如用户身份证号、金额等),需加密。
- 接口需防重放、防篡改、防伪造。
3、原始接口参数
json
{
"userId": "1001",
"idCard": "110101199001011234",
"amount": "5000.00",
"timestamp": "1731123456789",
"nonce": "abc123xyz"
}
二、设计与实现步骤
简单来说客户端实现
-
将需要加密的字段通过sm4加密,然后进行base64编码
-
将报文通过sm3进行报文摘要计算
-
将报文摘要进行签名,然后进行base64编码
-
将base64编码后的字符串(可选的公钥)作为参数
-
完成请求参数构造
实现简单代码示例如下。
java
// 原始报文
{
"userId": "1001",
"idCard": "110101199001011234",
"amount": "5000.00",
"timestamp": "1731123456789",
"nonce": "abc123xyz"
}
// 需要加密的字段
"encryptedData": {
"idCard": "110101199001011234",
"amount": "5000.00"
}
// 1、将需要加密的字段通过sm4加密,然后进行base64编码
// cipherBytes 是二进制数据,不能直接放在 JSON 中传输
byte[] cipherBytes = SM4_CBC_Encrypt(encryptedData, key, iv);
// 经过Base64编码后的密文
String cipherText = Base64.encode(cipherBytes);
// 2、将报文通过sm3进行报文摘要计算
// 待签名的字符串,一般需要按照需要设定
待签名字符串="userId=1001×tamp=1731123456789&nonce=abc123xyz&cipherText=Base64编码的SM4密文..."
digest = SM3(待签名字符串)
// 3、将报文摘要进行签名,然后进行base64编码
byte[] derSignature = SM2_Sign(client_private_key, digest); // 已经是 DER 编码
String signature = Base64.encode(derSignature); // 用于 JSON 传输
// 4、最终构造的请求体
{
"userId": "1001",
"cipherText": "Base64编码的SM4密文",// 来自于第一步
"timestamp": 1731123456789,
"nonce": "abc123xyz",
"signature": "Base64编码的SM2签名", // 来自于第三步
"publicKey": "客户端SM2公钥(首次调用时提供,可缓存)" // 一般是约定共享方式或作为参数传递
}
服务端实现
-
将报文通过sm3进行摘要计算
-
将公钥、报文摘要和客户端的签名进行sm2验证签名
-
签名验证通过进行加密字段sm4解密
-
正常业务处理
实现简单代码示例如下。
java
// 原始报文
{
"userId": "1001",
"cipherText": "Base64编码的SM4密文",// 来自于第一步
"timestamp": 1731123456789,
"nonce": "abc123xyz",
"signature": "Base64编码的SM2签名", // 来自于第三步
"publicKey": "客户端SM2公钥(首次调用时提供,可缓存)" // 一般是约定共享方式或作为参数传递
}
// 0、在进行报文sm3摘要计算之前可能会做些基础性的校验,比如防重放等
// 1、将报文通过sm3进行摘要计算
待签名字符串="userId=1001×tamp=1731123456789&nonce=abc123xyz&cipherText=Base64编码的SM4密文..."
digest_server = SM3(待签名字符串)
// 2、将公钥、报文摘要和客户端的签名进行sm2验证签名
isValid = SM2_Verify(publicKey, digest_server, signature)
// 3、验证通过则进行sm4解密
// 通过共享的sm4密钥进行解密并结构化得到
"encryptedData": {
"idCard": "110101199001011234",
"amount": "5000.00"
}
// 4、正常的业务逻辑处理
...
1、总体目标
- 传输安全:使用国密 HTTPS(即基于 SM2/SM3/SM4 的 TLS 协议)
- 接口参数加密:在应用层对敏感参数进行国密加密(使用 SM4)
- 身份认证与防篡改:使用 SM2 数字签名 + SM3 摘要验证请求合法性
- 符合国家密码管理局要求:满足等保、密评等合规需求
2、实现步骤
1)、建立国密 HTTPS 通道(传输层安全)
-
使用算法
SM2:用于服务器证书和密钥交换
SM3:用于消息摘要和证书指纹
SM4:用于加密会话数据(替代 AES)
-
实现方式
服务端申请国密数字证书(含 SM2 公钥)
客户端使用支持国密的浏览器或 SDK(如 CFCA 国密浏览器、GmSSL 库)
-
握手过程
服务器发送 SM2 证书
双方通过 SM2 密钥协商(ECDH)生成会话密钥
使用 SM4 加密后续通信数据
使用 SM3 计算握手消息完整性
结果:传输通道已加密,防止中间人窃听。
2)、客户端准备请求(应用层加密与签名)
在 HTTPS 基础上,对接口参数进行额外加密和签名,增强安全性。
- 请求参数(原始)
json
{
"userId": "1001",
"idCard": "110101199001011234",
"amount": "5000.00",
"timestamp": "1731123456789",
"nonce": "abc123xyz"
}
- 使用算法
SM4:加密敏感字段(如 idCard、amount)
SM3:计算待签名数据的摘要
SM2:对摘要进行签名,生成数字签名 - 实现步骤
(1)选择需加密的敏感字段
json
"encryptedData": {
"idCard": "110101199001011234",
"amount": "5000.00"
}
(2)使用 SM4 加密敏感数据(CBC 模式 + 随机 IV)
客户端和服务端预先协商或通过安全通道分发一个 SM4 会话密钥(可用于一次请求或多请求),使用 SM4-CBC 加密 encryptedData,得到密文(Base64 编码)。
json
"cipherText": "Base64编码的SM4密文"
实现过程如下。
先通过SM4-CBC加密算法得到二进制密文,然后进行Base64编码得到经过Base64编码的密文。
txt
原始数据(JSON对象) → 转为字节数组 → SM4-CBC加密 → 二进制密文(byte[]) → Base64编码 → Base64字符串(可传输)
java实现示例
java
// cipherBytes 是二进制数据,不能直接放在 JSON 中传输
byte[] cipherBytes = SM4_CBC_Encrypt(encryptedData, key, iv);
// 经过Base64编码后的密文
String cipherText = Base64.encode(cipherBytes);
(3)构造待签名字符串(拼接关键字段)
待签名字符串示例如下。
txt
userId=1001×tamp=1731123456789&nonce=abc123xyz&cipherText=Base64编码的SM4密文...
(4)使用 SM3 计算摘要
java
digest = SM3(待签名字符串)
(5)使用客户端 SM2 私钥签名
java
// 示例(伪代码)
byte[] derSignature = SM2_Sign(client_private_key, digest); // 已经是 DER 编码
String signature = Base64.encode(derSignature); // 用于 JSON 传输
使用成熟的国密库(如 Bouncy Castle + 国密补丁、Hutool、GmSSL、SM-CRYPTO)时,签名方法通常会自动返回 DER 编码后的字节数组,只需再做一次 Base64 编码即可用于传输。
(6)构造最终请求体
json
{
"userId": "1001",
"cipherText": "Base64编码的SM4密文",
"timestamp": 1731123456789,
"nonce": "abc123xyz",
"signature": "Base64编码的SM2签名",
"publicKey": "客户端SM2公钥(首次调用时提供,可缓存)"
}
3)、服务端验证与解密
服务端收到请求后,执行以下验证流程:
(1)基础校验
- 检查
timestamp是否过期(防重放攻击,如超过5分钟拒绝) - 检查
nonce是否已使用(防重放) - 校验
userId合法性
(2)获取客户端公钥
- 若是首次调用,保存
publicKey - 否则从数据库或缓存中取出该用户的 SM2 公钥
- 或者另行约定的方式
(3)重构待签名字符串(同客户端规则)
根据约定的签名字段、算法等要素进行构建待签名的字符串。
txt
userId=1001×tamp=...&nonce=...&cipherText=...
(4)使用 SM3 计算摘要
java
digest_server = SM3(重构字符串)
(5)使用客户端公钥验证 SM2 签名
java
isValid = SM2_Verify(client_public_key, digest_server, signature)
- 若验证失败 → 拒绝请求(可能被篡改或伪造)
(6)使用 SM4 解密数据
- 使用预共享的 SM4 密钥解密
cipherText - 得到原始敏感数据(如
idCard,amount)
(7)处理业务逻辑
- 使用解密后的数据完成业务操作
(8)返回响应(可选加密)
- 响应也可使用 SM4 加密,并附 SM2 签名,确保下行安全
三、SM2、SM3、SM4 使用环节总结
| 环节 | 使用算法 | 用途 |
|---|---|---|
| 1. 传输层安全 | SM2 + SM3 + SM4 | 国密 HTTPS,保障通信通道安全 |
| 2. 敏感参数加密 | SM4 | 对身份证、金额等字段加密 |
| 3. 数据完整性校验 | SM3 | 计算请求参数摘要,防篡改 |
| 4. 身份认证与抗抵赖 | SM2 | 客户端私钥签名,服务端公钥验签 |
| 5. 密钥协商 | SM2(ECDH) | 在 HTTPS 握手中协商会话密钥 |
| 6. 证书体系 | SM2 + SM3 | 国密数字证书,SM3 用于证书指纹 |
四、关键技术点与建议
- SM4 密钥管理
- 可通过国密 HSM(硬件安全模块)生成和存储
- 支持定期轮换,提高安全性
- SM2 密钥对
- 客户端需持有自己的 SM2 密钥对(可存储在 U盾、TEE 或安全容器中)
- 服务端需维护客户端公钥白名单
- 时间戳与随机数(nonce)
- 必须使用,防止重放攻击
- 开发库推荐
- Java:Bouncy Castle + 国密补丁、Hutool、Lang-Crypto
- 前端:sm-crypto(JavaScript 国密库)
- 合规要求
- 通过国家密码管理局的商用密码应用安全性评估(密评)
- 使用经认证的密码产品(如国密 U盾、HSM)
五、总结
要实现一个国密合规的 HTTPS 接口调用系统,应采用"双层防护"策略:
第一层:国密 HTTPS
使用 SM2/SM3/SM4 构建安全传输通道,防止窃听。
第二层:应用层加密 + 签名
使用 SM4 加密敏感参数,SM3 + SM2 实现防篡改和身份认证。
这样既能满足高安全性要求,又能通过国家合规审查,适用于金融、政务、医疗等高敏感场景。
不能直接对原始消息进行签名,而是先对消息计算 SM3 摘要,再对摘要值进行签名。
原因1:性能效率(Efficiency)
- 问题:如果直接对一个大文件(比如几MB的网页内容)进行 SM2 签名,计算量极大,速度慢。
- 解决 :使用 SM3 将任意长度的数据压缩成 固定 256 位(32 字节) 的摘要,SM2 只需对这 32 字节签名,极大提升效率。
原因2:安全性(Security)
(1)防止"长度扩展攻击"等潜在风险
- 直接对原始数据签名可能暴露结构信息,增加被攻击的风险。
- 使用哈希函数(SM3)作为"中介",可以隔离原始数据与签名过程,增强安全性。
(2)SM2 算法设计要求输入为固定长度
- SM2 基于椭圆曲线数学运算,其签名算法要求输入是一个固定长度的整数(通常是哈希值)。
- 如果直接输入可变长度的原始消息,会导致算法不稳定或不安全。
国密标准 GM/T 0009-2012《SM2密码算法使用规范》明确规定:
"在使用 SM2 进行数字签名时,应先使用 SM3 杂凑算法对消息进行摘要处理,然后对摘要值进行签名。"
原因3:完整性验证的标准化流程
在 HTTPS 握手过程中,数字证书和握手消息的签名验证流程如下:
发送方(服务器):
1. 计算握手消息的摘要:digest = SM3(handshake_messages)
2. 使用私钥对摘要签名:signature = SM2_Sign(private_key, digest)
3. 发送签名值和原始消息
接收方(浏览器):
1. 接收到消息后,重新计算摘要:digest' = SM3(received_messages)
2. 使用服务器公钥验证签名:SM2_Verify(public_key, digest', signature)
3. 如果验证通过 → 说明消息未被篡改,且来源可信
这个流程中,SM3 保证了数据完整性,SM2 保证了身份认证和不可否认性,二者缺一不可。
接口调用防重放设计
使用 防重放随机数(Nonce) + 时间戳(Timestamp) 是最常用且有效的防御手段。
一、核心原理:Nonce + Timestamp
| 参数 | 作用 |
|---|---|
timestamp |
请求时间戳(毫秒级),用于判断请求是否过期 |
nonce |
一次性随机数,确保每次请求唯一 |
规则 :服务端只接受在一定时间窗口内(如5分钟)且 nonce 未被使用过的请求。
二、完整实现流程
步骤1:客户端生成请求
json
{
"userId": "1001",
"amount": "100.00",
"timestamp": 1731123456789, // 当前时间毫秒
"nonce": "aB3x9Kl2mNpQrStUvWzY", // 随机字符串
"signature": "MEUCIQD..." // 对 (timestamp + nonce + ...) 签名
}
nonce 生成建议:
- 长度:16~32位
- 字符集:大小写字母 + 数字(避免特殊字符)
- 生成方式:
- 使用安全随机数生成器(如 Java 的
SecureRandom) - 或使用 UUID 截取(去掉
-后取部分)
- 使用安全随机数生成器(如 Java 的
java
// Java 示例
String nonce = new BigInteger(130, new SecureRandom()).toString(36).substring(0, 20);
步骤2:服务端验证流程
txt
收到请求 → 校验 timestamp → 检查 nonce 是否已使用 → 记录 nonce → 验证签名 → 处理业务
(1)校验时间戳(防过期)
java
long currentTime = System.currentTimeMillis();
long requestTime = request.getTimestamp();
if (Math.abs(currentTime - requestTime) > 300_000) { // 5分钟 = 300,000ms
throw new SecurityException("请求已过期");
}
可接受范围通常设为 ±5分钟,避免因客户端时钟偏差导致误判。
(2)检查 nonce 是否已使用(防重放)
- 使用 Redis 缓存已使用的
nonce - 设置过期时间略大于时间窗口(如 6 分钟)
java
String key = "nonce:" + request.getUserId() + ":" + request.getNonce();
Boolean exists = redis.set(key, "1", Duration.ofMinutes(6));
if (!exists) {
throw new SecurityException("重复请求,nonce 已使用");
}
SET key value EX 360 NX 命令可原子性完成"设置 + 过期 + 不存在才设"
(3)记录 nonce 并继续后续验证(如签名)
只有通过 timestamp 和 nonce 验证后,才进行 SM2 签名验证等耗时操作,防止被恶意刷接口。
三、关键设计要点
| 要点 | 说明 |
|---|---|
| 1. nonce 必须全局唯一 | 同一用户短时间内不能重复,建议结合用户ID做 key |
| 2. 使用 Redis 缓存 nonce | 高性能、自动过期,适合分布式系统 |
| 3. 过期时间 > 时间窗口 | 如时间窗口 5 分钟,nonce 缓存 6 分钟,防止临界问题 |
| 4. nonce 参与签名 | 必须将 nonce 和 timestamp 加入签名原文,防止被篡改 |
| 5. 不依赖客户端 IP | 移动端 IP 经常变化,不可靠 |
四、签名原文构造示例(含 nonce 和 timestamp)
txt
userId=1001&
timestamp=1731123456789&
nonce=aB3x9Kl2mNpQrStUvWzY&
cipherText=...
然后对上述字符串进行 SM3 摘要 ,再用 SM2 私钥签名。
如果攻击者修改 nonce 或 timestamp,签名将验证失败。
五、可选增强方案
方案1:递增序列号(Sequence Number)
- 客户端维护一个递增的
seq(从1开始) - 服务端记录每个用户的最新
seq - 每次请求
seq必须比上次大 - 优点:绝对防重放
- 缺点:需持久化状态,客户端断线重连需同步
方案2:挑战-应答机制(Challenge-Response)
- 服务端先返回一个
challenge(随机数) - 客户端在请求中包含该
challenge - 服务端验证后作废
- 适用于高安全场景(如金融交易)
六、总结:防重放随机数实现要点
| 步骤 | 实现方式 |
|---|---|
| 1. 客户端生成 | nonce = 安全随机字符串(16-32位),timestamp = 当前毫秒时间 |
| 2. 加入请求 | 将 nonce 和 timestamp 放入请求参数 |
| 3. 参与签名 | 将两者加入签名原文,防止篡改 |
| 4. 服务端校验 | 检查时间窗口(±5分钟),检查 nonce 是否已使用(Redis) |
| 5. 记录 nonce | 使用 Redis 缓存 nonce,TTL = 6分钟 |
最佳实践口诀:"一户一 nonce,时效五分钟,Redis 记录,签名保完整"
通过 nonce + timestamp + Redis + 签名 四重防护,可有效抵御接口重放攻击,满足国密合规和高安全系统要求。