国密KMS使用与开发对接实践"

admin 2026-11-01 01:48:01 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

国密 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 天窗口)
 启用 ───────────────→ 禁用 ───────────────────────→ 待删除 ──到期──→ 物理销毁
  ↑                      │  ↑                            │
  └──────── 启用 ────────┘  └─────── 取消删除 ───────────┘
  1. 多版本并存。轮换不覆盖旧材料,而是追加新版本(v1、v2……)。加密自动使用最新版本,密文信封内记录 key_id + version,解密时按信封中的版本选择对应材料,因此轮换后旧密文仍可解。
  2. 材料不产生明文。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)。实验中的证书身份通道具有三个要点:

  1. 双证书:客户端持有 SM2 签名证书和 SM2 加密证书,分别用于签名与密钥交换。
  2. 双向认证:服务端配置 client_auth: true 后要求客户端出示证书;服务端自身使用自签双证书(生产应换 CA 签发)。
  3. 按指纹鉴别:服务端取客户端签名证书 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

国密KMS使用与开发对接实践" 网络安全文章

国密KMS使用与开发对接实践"

文档介绍国密KMS的算法支持(SM2/SM3/SM4/SM9)、密钥全生命周期管理、信封加密机制、三权分立安全设计、GM/T0018SDF接口对接、TLCP双向认证、SM3链式审计等核心功能,并提供部署配置与Web管理控制台操作指南,对国密改造实践具有较强参考价值。
【实验上新】国密KMS使用与开发对接实践" 网络安全文章

【实验上新】国密KMS使用与开发对接实践"

本文介绍国密KMS使用与开发对接实践,涵盖SM2/SM3/SM4/SM9算法细节、密钥全生命周期管理、信封加密机制、三权分立原则、GM/T0018SDF接口、TLCP双向认证及SM3链式审计。提供部署配置与操作指南,强调生产环境需用硬件密码机满足等保密评要求,具有较强实践参考价值。
评论:0   参与:  0