Java SM9 使用手册
Java SM9 使用手册
SM9 位于独立制品 cn.gmkit:gmkit-sm9:0.10.1,依赖随 JAR 分发的本地动态库。0.10.1 提供签名/验签和基于身份的加密(IBE),不提供 SM9 密钥交换。
在采用 SM9 前,先确认对端实现、密钥生成中心(KGC)职责、主密钥保管、身份编码、支持平台和合规要求。不能只因为 API 可调用就省略协议设计。
支持平台与启动检查
0.10.1 构建流水线覆盖:
应用启动时按顺序记录:
SM9.nativePlatform():当前运行环境映射的平台标识,不支持时为unsupported。SM9.nativeVersion():JNI bridge 版本标识。SM9.isAvailable():依赖库和 bridge 是否已成功加载。SM9.nativeLoadErrorMessage():不可用时的诊断;可用时为null。
isAvailable() == false 时不要调用生成密钥、签名或加密方法。测试在没有本地产物的开发机上会跳过;五平台 Action 通过 -Dgmkit.sm9.requireNative=true 禁止这种跳过。
四种密钥角色
公开主密钥和主私钥在 Java 中使用同一个句柄类型表示,是因为它们来自不同导入/生成路径;这不表示主私钥可以分发。
端到端样例
下面的测试包含平台检查、KGC 派生、加密 PEM、重新导入、签名验签、错误身份、IBE、255 字节上限和流式签名。所有本地句柄均由 try-with-resources 关闭。
package cn.gmkit.sm9;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.api.io.TempDir;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@EnabledIf("cn.gmkit.sm9.SM9Assumptions#nativeAvailable")
class ManualJavaSm9UserGuideTest {
@Test
void signsEncryptsAndReloadsPem(@TempDir Path directory) {
// 1. 检查平台:执行密码操作前确认本地动态库已加载,并记录版本与平台。
assertTrue(SM9.isAvailable());
assertNotNull(SM9.nativeVersion());
assertNotNull(SM9.nativePlatform());
// 2. 准备参数:身份、正常订单、篡改订单、PEM 口令和临时文件路径分别保存。
String recipientId = "warehouse@gmkit.cn";
byte[] plaintext =
"order=GMKIT-DEMO-0001&amount=88.00".getBytes(StandardCharsets.UTF_8);
byte[] tampered =
"order=GMKIT-DEMO-0001&amount=99.00".getBytes(StandardCharsets.UTF_8);
String password = "Passw0rd!";
String signPublicFile = directory.resolve("sm9-sign-master-public.pem").toString();
String signKeyFile = directory.resolve("sm9-sign-user-private.pem").toString();
String encPublicFile = directory.resolve("sm9-enc-master-public.pem").toString();
String encKeyFile = directory.resolve("sm9-enc-user-private.pem").toString();
// 3. 生成签名 KGC 主密钥:主私钥留在 KGC,只把主公钥 PEM 分发给验签方。
try (SM9SignMasterKey signMaster = SM9.generateSignMasterKey()) {
signMaster.exportPublicMasterKeyPem(signPublicFile);
// 4. 派生签名身份私钥:私钥与 recipientId 绑定,并用口令加密后保存为 PEM。
try (SM9SignKey signKey = signMaster.extractKey(recipientId)) {
signKey.exportEncryptedPrivateKeyInfoPem(password, signKeyFile);
}
}
// 5. 导入签名材料:业务方只加载身份私钥,验签方只加载主公钥。
byte[] signature;
try (SM9SignKey signKey =
SM9SignKey.importEncryptedPrivateKeyInfoPem(password, signKeyFile, recipientId);
SM9SignMasterKey signPublic =
SM9SignMasterKey.importPublicMasterKeyPem(signPublicFile)) {
// 6. SM9 签名:身份私钥对订单字节签名。
signature = SM9.sign(signKey, plaintext);
// 7. SM9 验签:主公钥、同一身份和原消息必须验证成功。
assertTrue(SM9.verify(signPublic, recipientId, plaintext, signature));
// 8. 验签失败断言:身份或订单金额变化后必须返回 false。
assertFalse(SM9.verify(signPublic, "other@gmkit.cn", plaintext, signature));
assertFalse(SM9.verify(signPublic, recipientId, tampered, signature));
}
// 9. 生成加密 KGC 主密钥:导出加密主公钥,并派生接收方 IBE 私钥。
try (SM9EncMasterKey encMaster = SM9.generateEncMasterKey()) {
encMaster.exportPublicMasterKeyPem(encPublicFile);
try (SM9EncKey encKey = encMaster.extractKey(recipientId)) {
encKey.exportEncryptedPrivateKeyInfoPem(password, encKeyFile);
}
}
// 10. 导入加密材料:发送方加载主公钥,接收方加载绑定身份的 IBE 私钥。
try (SM9EncMasterKey encPublic =
SM9EncMasterKey.importPublicMasterKeyPem(encPublicFile);
SM9EncKey encKey =
SM9EncKey.importEncryptedPrivateKeyInfoPem(password, encKeyFile, recipientId)) {
// 11. SM9 IBE 加密:发送方用主公钥和 recipientId 保护小消息。
byte[] ciphertext = SM9.encrypt(encPublic, recipientId, plaintext);
// 12. SM9 IBE 解密:接收方身份私钥恢复订单字节。
byte[] decrypted = SM9.decrypt(encKey, ciphertext);
assertArrayEquals(plaintext, decrypted);
// 13. 身份失败断言:为其他身份生成的密文不能由当前身份私钥解密。
byte[] wrongIdentityCiphertext = SM9.encrypt(encPublic, "other@gmkit.cn", plaintext);
assertThrows(SM9Exception.class, () -> SM9.decrypt(encKey, wrongIdentityCiphertext));
// 14. 长度失败断言:单次 IBE 明文超过 255 字节时必须抛出 SM9Exception。
byte[] tooLong = new byte[SM9EncMasterKey.MAX_PLAINTEXT_SIZE + 1];
assertThrows(SM9Exception.class, () -> SM9.encrypt(encPublic, recipientId, tooLong));
}
// 15. 流式 SM9 签名:大消息分块 update 后签名,再按相同分块顺序验签。
try (SM9SignKey signKey =
SM9SignKey.importEncryptedPrivateKeyInfoPem(password, signKeyFile, recipientId);
SM9SignMasterKey signPublic =
SM9SignMasterKey.importPublicMasterKeyPem(signPublicFile);
SM9Signature signer = new SM9Signature(true);
SM9Signature verifier = new SM9Signature(false)) {
signer.update(plaintext, 0, 10).update(plaintext, 10, plaintext.length - 10);
byte[] streamingSignature = signer.sign(signKey);
verifier.update(plaintext, 0, 10).update(plaintext, 10, plaintext.length - 10);
assertTrue(verifier.verify(streamingSignature, signPublic, recipientId));
}
}
}签名与流式签名
SM9.sign/SM9.verify为一次性门面,内部创建并关闭SM9Signature。SM9Signature(true)是签名上下文,SM9Signature(false)是验签上下文。update(byte[])或update(data, offset, length)可分块输入。sign输出 DER 编码的签名。- 身份或消息不匹配时验签返回
false;空参数、关闭后的句柄或本地调用错误抛SM9Exception。 - 同一个上下文不能并发处理两条消息;需要重用时先
reset(doSign),并保证前一操作已结束。
IBE 明文限制
SM9.encrypt 的单次明文必须为 1–255 字节。这个入口适合会话 key、短令牌和协议规定的小字段,不适合文件或业务报文。
大数据流程:
- 生成随机 SM4 key 和唯一 GCM nonce。
- 使用 SM4-GCM 加密业务数据并保存 AAD、ciphertext、tag。
- 使用 SM9 IBE 保护短 SM4 会话 key。
- 接收方以身份私钥恢复会话 key,再完成 GCM 认证解密。
GMKit 0.10.1 没有公开 SM9 + SM4 组合载荷类型;应用必须定义带 schema 版本的协议。
PEM
文件路径是宿主文件系统路径。服务端应设置最小文件权限;PEM 口令来自秘密管理系统,不写入源码、命令历史和日志。导入用户私钥时传入的 ID 必须与派生时一致。
资源关闭
以下类型都实现 AutoCloseable:
SM9SignMasterKeySM9EncMasterKeySM9SignKeySM9EncKeySM9Signature
close() 可重复调用;关闭后继续使用会抛 SM9Exception。不要依赖 GC 或 finalizer 释放本地句柄。
标准证据
SM9 Native Action 的验证顺序固定为:
- 锁定 GmSSL commit
d655c06b3a6b0fe8cff900f293bf0e5aac6eb0a2。 - 执行 GmSSL
sm9test.c对应的ctest ... -R ^sm9$,覆盖ks/ds/ke/de固定派生向量。 - 运行 Java/JNI 的签名、错误身份、篡改、IBE、PEM和本手册测试。
- 在 Linux x86-64/AArch64、macOS Intel/Apple Silicon、Windows x86-64 上分别构建和测试本地产物。
随机签名和随机 IBE 密文只验证可验签、可解密和篡改失败,不冒充固定国标向量。
完整句柄成员、异常和诊断入口见 Java SM9 API。