国密 KMS 使用与开发对接实践
蚁景网安实验室
蚁景网安实验室
蚁景网络安全
2026年10月29日 17:40
湖南
去阅读
国密KMS同时支持国密算法与国际算法,本实验以国密算法为主线。
| 算法 | 类型 | 标准依据 | 关键参数与用途 |
|---|---|---|---|
| SM2 | 椭圆曲线公钥 | GB/T 32918.1~.4-2016 | 256 位曲线;签名、公钥加密、密钥交换 |
| SM3 | 密码杂凑 | GB/T 32905-2016 | 输出 256 位(64 个十六进制字符) |
| SM4 | 分组密码 | GB/T 32907-2016 | 128 位分组、128 位密钥;CBC 模式与 PKCS#7 填充按 GB/T 17964-2021 |
| SM9 | 标识密码 | GB/T 38635 系列 | 以标识为公钥;主密钥对派生用户密钥 |
实现细节:
- SM2 签名计算的是
ZA || M的 SM3 摘要,其中ZA = SM3(ENTL ‖ ID ‖ a ‖ b ‖ xG ‖ yG ‖ x ‖ y)。使用标准附录 A 的默认用户标识1234567812345678,签名输出 ASN.1(DER) 的(r, s)。 - SM2 加密中采用
C1‖C3‖C2排列(与 GM/T 0018 的 ECCCipher 结构、TLCP 工程接口一致)。教材示例若为 C1C2C3,与外部系统互通时必须做排列转换。 - SM4-CBC 使用 PKCS#7 填充;还把附加数据 AAD 的 SM3 摘要嵌入密文信封,解密时校验 AAD 一致性。
- SM9 由主密钥对(签名主密钥、加密主密钥)按标识派生用户私钥;签名用
hid=1,加密用hid=3。标识即公钥,省去证书分发,但用户私钥的交付必须安全。 - 所有密码用途随机数(CMK/DEK/IV/API Key、SM2 签名随机数)统一来自符合 GM/T 0105-2021 的 SM3 Hash DRBG;
sdf模式下设备侧随机数由密码机SDF_GenerateRandom提供。 - 国际算法仅作兼容:AES-256-GCM、3DES-EDE-CBC、RSA-2048(OAEP/PSS + SHA-256);杂凑服务支持 SM3、SHA-1、SHA-256、SHA-512。
密钥全生命周期
GB/T 39786-2021 把密钥管理划分为生成、分发、存储、使用、更换、销毁等环节:
禁用 计划删除(7~30 天窗口)
启用 ───────────────→ 禁用 ───────────────────────→ 待删除 ──到期──→ 物理销毁
↑ │ ↑ │
└──────── 启用 ────────┘ └─────── 取消删除 ───────────┘
- 多版本并存。轮换不覆盖旧材料,而是追加新版本(v1、v2……)。加密自动使用最新版本,密文信封内记录
key_id + version,解密时按信封中的版本选择对应材料,因此轮换后旧密文仍可解。 - 材料不产生明文。SM4/SM2 的 CMK 由密码设备生成并以设备内部保护密钥(IPK/ISK)包裹;AES/3DES/RSA/SM9 的材料在进程内生成后,经 IPK 直包或全局 MPK-SM4 二次包裹后入库。数据库中只有包裹体,没有明文密钥。
信封加密:DEK + KEK
直接用主密钥加密海量业务数据有两个问题:KMS 成为吞吐瓶颈;业务数据必须离开本地。信封加密可以解决这两个问题:
- KEK(Key Encryption Key):即 KMS 中的 CMK,只负责包裹/解包数据密钥。
- DEK(Data Encryption Key):一次性数据密钥,在业务侧本地加密数据。
写入:生成 DEK → 明文 DEK 本地加密业务数据 → 持久化 {业务密文, 密文 DEK} → 丢弃明文 DEK
读取:提交密文 DEK → KMS 用 CMK 解包 → 得到明文 DEK → 本地解密业务数据 → 丢弃明文 DEK
只有很小的 DEK 包裹体经过 KMS,海量数据的加解密全部在本地完成。SM4 CMK 产出 16 字节 DEK,AES CMK 产出 32 字节 DEK。
四种自描述信封:
| 信封 | 结构 | 用途 |
|---|---|---|
GKE1 |
GKE1‖key_id_len‖key_id‖version(2B)‖iv(16B)‖SM4-CBC 密文 ,密文内含 SM3(AAD) 摘要 |
SM4 敏感数据、SM4 包裹的 DEK |
GKE2 |
GKE2‖key_id_len‖key_id‖version(2B)‖SM2 密文(C1C3C2) |
SM2 公钥加密的小数据 |
GKE3 |
GKE3‖key_id_len‖key_id‖version(2B)‖alg(1B)‖payload ;alg:02=AES-GCM、03=3DES、04=RSA-OAEP、05=SM9 |
国际算法与 SM9 |
GKL1 |
GKL1‖ctDEK_len(2B)‖ctDEK‖iv(16B)‖SM4-CBC(PKCS7(数据)) |
kmsctl seal-file 生成的文件信封 |
信封自描述的重要工程性质:密文自己带着密钥标识和版本,轮换密钥、按别名迁移都不影响历史数据的解密。
三权分立原则
| 角色 | 权限 | 典型使用人 |
|---|---|---|
admin 管理员 |
全部:密钥管理、凭证管理、证书管理、系统信息 | 安全管理员 |
operator 操作员 |
仅在授权密钥范围内做密码运算;可只读查看密钥台账 | 业务系统、开发人员 |
auditor 审计员 |
审计日志查询/导出/链校验;无任何业务与密钥管理权限 | 审计员 |
GM/T 0018 密码设备接口(SDF)
系统通过 GM/T 0018 的 SDF 抽象层访问密码设备,包含两种实现:
| provider | 实现方式 | 适用场景 |
|---|---|---|
software |
进程内软件密码机(gmsm 实现),设备密钥保存在本地目录 | 开发、教学、测试 |
sdf |
dlopen 加载标准 SDF 动态库(SoftSDF 或厂商 libsdf.so) |
演示真实密码机链路、生产 |
software 与 sdf 的 REST API、数据格式完全一致,切换只需要改配置。SDF 接口子集包括 OpenDevice/OpenSession/GenerateKeyWithIPK/ImportKeyWithISK/Encrypt/Decrypt/InternalEncrypt/Decrypt_ECC/GenerateKeyPair/GenerateRandom 等;系统用会话池(sdf_pool_size,默认 8)支持并发。
TLCP 双向认证
TLCP 是 GB/T 38636-2020 定义的国密传输层密码协议,握手使用国密套件(如 ECC-SM4-SM3-CBC-SM3)。实验中的证书身份通道具有三个要点:
- 双证书:客户端持有 SM2 签名证书和 SM2 加密证书,分别用于签名与密钥交换。
- 双向认证:服务端配置
client_auth: true后要求客户端出示证书;服务端自身使用自签双证书(生产应换 CA 签发)。 - 按指纹鉴别:服务端取客户端签名证书 DER 的 SM3 指纹,到证书身份库中查找对应角色与授权;未注册或已吊销的证书一律 401。
USBKey 私钥不以明文存放,而是用 PIN 派生密钥做 SM4 加密后封装在 Key 目录中(GM/T 0016 SKF 抽象的 software 实现);PIN 错误则无法解封私钥。
SM3链式审计
每条审计记录的哈希为:
hash = SM3(prev_hash ‖ ts ‖ log_type ‖ actor ‖ action ‖ key_id ‖ detail ‖ result ‖ ip)
各字段按「4 字节大端长度 + 字段内容」拼接,避免字段边界歧义。写入时在同一事务内读取上一条记录的哈希,形成链。校验时从第一条开始全链重算并逐条比对:任何一条记录被增删改,其后所有记录的 prev_hash 都对不上,校验立即报出断点。
实验环境
| 角色 | 系统 |
|---|---|
| 服务端(主实例) | 远端 Linux 主机(Ubuntu 22.04/24.04),Docker + Docker Compose |
| 服务端(开发实例) | 同一主机本地源码运行(Go 1.26、Node 22、CGO、gcc) |
| 客户端 | Windows 11 / Linux 桌面,Chrome 或 Edge 浏览器 |
| 密码设备 | SoftSDF 软件密码机(Docker 内 sdf 模式)/ 进程内软件密码机 |
访问地址:
| 入口 | 地址 | 说明 |
|---|---|---|
| Web 管理台(主实例) | http://127.0.0.1:8200/ |
Docker 实例 |
| REST API(主实例) | http://127.0.0.1:8200/api/v1 |
同上 |
| Web 管理台(开发实例) | http://127.0.0.1:8202/ |
测试实例 |
| TLCP 证书通道 | https://127.0.0.1:8444/api/v1 |
测试实例 |
辅助工具:
| 工具名称 | 说明 |
|---|---|
curl |
调用 REST API,观察状态码与原始响应 |
jq |
解析 JSON 响应 |
openssl |
生成密钥材料、SM4 本地加解密、SM3 对照 |
| 浏览器开发者工具 | 查看网络请求与响应 |
kmsctl / gm-usbkey |
自带命令行工具 |
javac / java |
Java 应用对接实例 |
一、部署和初始化
cd gm-kms/deploy
# 进程内软件密码机,零外部依赖,适合开发调试
docker compose up -d
# SoftSDF 软件密码机:走真实的 GM/T 0018 设备链路
SDF_PROVIDER=sdf docker compose up -d
# 再叠加国密 TLS(TLCP,8443 端口)
TLCP_ENABLED=true SDF_PROVIDER=sdf docker compose up -d
# 首次启动生成管理员 Key,只显示这一次
docker logs gm-kms 2>&1 | grep \"API Key\"
配置文件参考:
server:
listen: \"0.0.0.0:8202\"
data_dir: \"./data\"
sdf_pool_size: 8
tlcp:
enabled: true
listen: \"0.0.0.0:8444\"
client_auth: true
sdf:
provider: sdf# software / sdf
sdf:
lib: third_party/softsdf/libsoftsdf.so
isk_index: 1
password: \"P@ssw0rd\"
work_dir: ./data/sdfdev
启动日志里看到类似这几行,就说明服务、设备和 TLCP 通道都起来了:
[sdf] provider=sdf 设备=Soft SDF 序列号=202406220010200 会话池=8
首次启动:已生成管理员 API Key(仅显示一次)
API Key: gak-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
[tlcp] 国密 TLS 监听 0.0.0.0:8444(客户端证书鉴别)
再确认健康检查:
curl http://127.0.0.1:8202/api/v1/health
# {\"status\":\"ok\"}
software 和 sdf 的区别只在密码运算发生在哪里:前者纯软件实现,后者 dlopen 标准 SDF 动态库,走设备会话。两者 API 完全一致,换成硬件密码机只改 lib 指向厂商的 libsdf.so。
生产环境要用 sdf 模式对接经过认证的硬件密码机。等保和密评都要求密码运算在密码设备内完成,密钥材料也以设备内部保护密钥包裹的形态存储,明文只在设备会话里短暂出现。
二、Web管理控制台功能介绍
打开管理台,输入管理员 API Key 就能进,不需要用户名密码。
Python调用示例
import base64, json, os, urllib.request
BASE = \"http://127.0.0.1:8202/api/v1\"
KEY = os.environ[\"KMS_API_KEY\"]
def protect(plain: str) -> str:
body = {
\"plaintext\": base64.b64encode(plain.encode()).decode(),
\"aad\": base64.b64encode(b\"app/config.yaml\").decode(),
}
req = urllib.request.Request(
BASE + \"/keys/demo-db-password/encrypt\",
data=json.dumps(body).encode(),
headers={\"Content-Type\": \"application/json\",
\"Authorization\": \"Bearer \" + KEY},
)
with urllib.request.urlopen(req) as r:
return json.loads(r.read())[\"ciphertext\"]
解密时把同样的 AAD 传回去,AAD 不一致就解不开。信封加密的 Python 版在 examples/python-demo/kms_demo.py,本地加密需要 cryptography 包。
Java开发示例
java.net.http 发请求,javax.crypto 做本地 AES-GCM。需要 SM2 签名或 SM9 时可以引 BouncyCastle。完整示例在 examples/java-demo/KmsClient.java。
// Maven: 无第三方依赖(java.net.http + javax.crypto)
import java.net.URI;
import java.net.http.*;
import java.security.SecureRandom;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class KmsClient {
static final String BASE = \"http://127.0.0.1:8200/api/v1\";
static final String KEY= System.getenv(\"KMS_API_KEY\"); // ① 环境变量
static HttpClient http = HttpClient.newHttpClient();
// 通用 POST
static String post(String path, String json) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.header(\"Content-Type\", \"application/json\")
.header(\"Authorization\", \"Bearer \" + KEY)
.POST(HttpRequest.BodyPublishers.ofString(json)).build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() != 200)
throw new RuntimeException(\"KMS \" + path + \": \" + resp.body());
return resp.body();
}
// 信封加密示例(业务字段)
record Sealed(String dataCipherB64, String wrappedDek) {}
static Sealed seal(byte[] plaintext) throws Exception {
String r = post(\"/keys/demo-aes-key/generate-data-key\", \"{}\");
String dekB64 = io.Json.get(r, \"plaintext_dek\"); //
String wrap= io.Json.get(r, \"ciphertext_dek\");
byte[] dek = Base64.getDecoder().decode(dekB64);
byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce);
Cipher c = Cipher.getInstance(\"AES/GCM/NoPadding\");
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(dek, \"AES\"), new GCMParameterSpec(128, nonce));
byte[] ct = c.doFinal(plaintext);
byte[] out = new byte[nonce.length + ct.length];
System.arraycopy(nonce, 0, out, 0, nonce.length);
System.arraycopy(ct, 0, out, nonce.length, ct.length);
return new Sealed(Base64.getEncoder().encodeToString(out), wrap);
}
static byte[] open(String dataCipherB64, String wrappedDek) throws Exception {
String r = post(\"/decrypt-data-key\",
\"{\\\"ciphertext_dek\\\":\\\"\" + wrappedDek + \"\\\"}\");
byte[] dek = Base64.getDecoder().decode(io.Json.get(r, \"plaintext_dek\"));
byte[] raw = Base64.getDecoder().decode(dataCipherB64);
Cipher c = Cipher.getInstance(\"AES/GCM/NoPadding\");
c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(dek, \"AES\"),
new GCMParameterSpec(128, raw, 0, 12));
return c.doFinal(raw, 12, raw.length - 12);
}
public static void main(String[] args) throws Exception {
Sealed s = seal(\"订单敏感字段-110101199001011234\".getBytes(\"UTF-8\"));
System.out.println(\"业务密文: \" + s.dataCipherB64().substring(0, 40) + \"…\");
System.out.println(\"解密还原: \" + new String(open(s.dataCipherB64(), s.wrappedDek()), \"UTF-8\"));
}
}
// 迷你 JSON 取值(生产请用 Jackson/Gson)
class io { class Json {
static String get(String json, String key) {
int i = json.indexOf(\"\\\"\" + key + \"\\\":\\\"\") + key.length() + 4;
return json.substring(i, json.indexOf('\"', i));
}
}}
USBKey / TLCP-免 API Key 的接入方式
需要“无 API Key”或者强合规场景时,发一张证书:
# 管理员发卡并注册,角色和授权同凭证模型
gm-usbkey issue --cn alice --pin 123456 --out /tmp/alice --role operator \\
--register http://127.0.0.1:8202 --admin-key gak-
# 应用侧用证书身份访问 TLCP 通道
gm-usbkey request --key /tmp/alice --pin 123456 \\
--url https://127.0.0.1:8444/api/v1/keys
服务端开了 client_auth 之后,客户端不带证书无法握手通过;带了证书但没在服务端注册,也认不出身份。自签证书场景客户端要跳过校验,如果是生产环境换成 CA 签发,需要去掉这个开关。
示例应用:口令保险箱
配置里的数据库口令,用 KMS 加密后写回配置文件,启动时解到内存。单文件实现 examples/password-vault/main.go:
export KMS_API_KEY=gak-<operator-或-admin>
go run ./examples/password-vault -kms http://127.0.0.1:8202/api/v1 \\
-alias demo-db-password -put \"Prod-MySQL-Root-2026!\" -out vault.json
go run ./examples/password-vault -kms http://127.0.0.1:8202/api/v1 \\
-alias demo-db-password -get -out vault.json
# → Prod-MySQL-Root-2026!
vault.json 里只有 GKE1 信封,口令明文不落盘。轮换密钥之后,旧信封照样能解。
大文件信封加密
任意大小的文件在本地分块加密,KMS 只托管 DEK。examples/python-demo/seal_file.py 按 1MiB 分块做 AES-GCM,内存占用不随文件增长;密文文件内嵌密文 DEK,解密时不用再指定别名。
python examples/python-demo/seal_file.py encrypt --alias demo-aes-key \\
--in big.bin --out big.bin.gkenv
python examples/python-demo/seal_file.py decrypt --in big.bin.gkenv --out big.bin.out
微服务网关统一加解密(架构参考)
┌─────────────── KMS(8202) ───────────────┐
│ 密钥台账 / 信封加密 / 审计 / 三权分立 │
└──────▲──────────────▲──────────────────┘
│ operator凭证A │ operator凭证B
┌────────────┴───┐ ┌──────┴─────────┐
│ 订单服务 │ │ 用户服务 │
│ 授权: order-encrypt│ │ 授权: user-pii │
└────────────────┘ └────────────────┘
每个服务发一张独立的 operator 凭证,只授权自己用的密钥。订单服务拿不到用户隐私数据(PII)的密钥,横向越权在服务端就被拦住。敏感字段各自加密入库。数据库被拖走,攻击者拿到的是密文和一堆包着 DEK 的信封,没有明文,也没有主密钥。所有运算进审计链。能查到哪个服务、哪把密钥、什么时间。
在线实验
实验已在蚁景实验室上线,限时免费:https://www.yijinglab.com/exp-detail.shtml?w=exp\_ass&ec=66b94d5c-3c9b-41cc-8755-86a00629d45f
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论