Java SM2 使用手册
Java SM2 使用手册
本章固定标准签名 e = SM3(Z || M)、非空 userId、DER 签名、C1C3C2 密文和 Base64 外层编码。主流程使用一个 SM2 实例。
完整流程
package cn.gmkit;
import cn.gmkit.core.Base64Codec;
import cn.gmkit.core.GmkitException;
import cn.gmkit.core.SM2CipherMode;
import cn.gmkit.core.SM2SignatureFormat;
import cn.gmkit.core.SM2SignatureInputFormat;
import cn.gmkit.core.Texts;
import cn.gmkit.sm2.SM2;
import cn.gmkit.sm2.SM2KeyExchangeOptions;
import cn.gmkit.sm2.SM2KeyExchangeResult;
import cn.gmkit.sm2.SM2KeyPair;
import cn.gmkit.sm2.SM2SignOptions;
import cn.gmkit.sm2.SM2VerifyOptions;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ManualJavaSm2Test {
@Test
void signsEncryptsAndExchangesKeys() {
// 1. 准备参数:固定业务消息、篡改消息和非空 SM2 用户标识。
String plaintext = "order=GMKIT-DEMO-0001&amount=88.00";
String tampered = "order=GMKIT-DEMO-0001&amount=99.00";
String userId = "merchant@gmkit.cn";
SM2 sm2 = new SM2();
// 2. 生成 SM2 密钥:私钥为 32 字节 Hex,公钥默认为 65 字节非压缩点 Hex。
SM2KeyPair keyPair = sm2.generateKeyPair();
assertEquals(64, keyPair.privateKey().length());
assertEquals(130, keyPair.publicKey().length());
// 3. SM2 签名:计算 e = SM3(Z || M),签名使用 DER,外层使用 Base64。
SM2SignOptions signOptions = SM2SignOptions.builder()
.userId(userId)
.signatureFormat(SM2SignatureFormat.DER)
.build();
String signatureBase64 = Base64Codec.encode(
sm2.sign(keyPair.privateKey(), plaintext, StandardCharsets.UTF_8, signOptions));
// 4. SM2 验签:显式 Base64 解码,并固定 DER 结构和相同 userId。
SM2VerifyOptions verifyOptions = SM2VerifyOptions.builder()
.userId(userId)
.signatureFormat(SM2SignatureInputFormat.DER)
.build();
byte[] signature = Base64Codec.decode(signatureBase64, "SM2 signature");
assertTrue(sm2.verify(keyPair.publicKey(), plaintext, StandardCharsets.UTF_8, signature, verifyOptions));
// 5. 篡改断言:金额变化或 userId 变化后,SM2 验签必须返回 false。
assertFalse(sm2.verify(keyPair.publicKey(), tampered, StandardCharsets.UTF_8, signature, verifyOptions));
assertFalse(sm2.verify(
keyPair.publicKey(),
plaintext,
StandardCharsets.UTF_8,
signature,
SM2VerifyOptions.builder()
.userId("other@gmkit.cn")
.signatureFormat(SM2SignatureInputFormat.DER)
.build()));
// 6. SM2 加密:文本先按 UTF-8 转为字节,密文固定为 C1C3C2 并使用 Base64 传输。
byte[] ciphertext = sm2.encrypt(
keyPair.publicKey(),
Texts.bytes(plaintext, StandardCharsets.UTF_8),
SM2CipherMode.C1C3C2);
String ciphertextBase64 = Base64Codec.encode(ciphertext);
// 7. SM2 解密:显式 Base64 解码和 C1C3C2,恢复 UTF-8 文本。
byte[] decrypted = sm2.decrypt(
keyPair.privateKey(),
Base64Codec.decode(ciphertextBase64, "SM2 ciphertext"),
SM2CipherMode.C1C3C2);
assertEquals(plaintext, Texts.text(decrypted, StandardCharsets.UTF_8));
// 8. 密文篡改断言:修改 C3/C2 后,SM2 解密必须校验失败并抛错。
byte[] tamperedCiphertext = ciphertext.clone();
tamperedCiphertext[tamperedCiphertext.length - 1] ^= 0x01;
assertThrows(
GmkitException.class,
() -> sm2.decrypt(keyPair.privateKey(), tamperedCiphertext, SM2CipherMode.C1C3C2));
// 9. 公钥压缩往返:压缩点解压后必须恢复同一非压缩公钥。
String compressedPublicKey = sm2.compressPublicKey(keyPair.publicKey());
assertEquals(66, compressedPublicKey.length());
assertEquals(keyPair.publicKey(), sm2.decompressPublicKey(compressedPublicKey));
// 10. 生成交换密钥:A、B 分别创建长期密钥和本次会话临时密钥。
SM2KeyPair staticA = sm2.generateKeyPair();
SM2KeyPair ephemeralA = sm2.generateKeyPair();
SM2KeyPair staticB = sm2.generateKeyPair();
SM2KeyPair ephemeralB = sm2.generateKeyPair();
// 11. 响应方计算:B 先派生 128-bit key,并生成发给 A 的 S1。
SM2KeyExchangeResult responder = sm2.keyExchangeWithConfirmation(
staticB.privateKey(),
ephemeralB.privateKey(),
staticA.publicKey(),
ephemeralA.publicKey(),
SM2KeyExchangeOptions.builder()
.initiator(false)
.keyBits(128)
.selfId("warehouse@gmkit.cn")
.peerId("merchant@gmkit.cn")
.build());
// 12. 发起方计算:A 验证 B 发来的 S1,并生成供 B 验证的 S2。
SM2KeyExchangeResult initiator = sm2.keyExchangeWithConfirmation(
staticA.privateKey(),
ephemeralA.privateKey(),
staticB.publicKey(),
ephemeralB.publicKey(),
SM2KeyExchangeOptions.builder()
.initiator(true)
.keyBits(128)
.selfId("merchant@gmkit.cn")
.peerId("warehouse@gmkit.cn")
.confirmationTag(responder.s1())
.build());
// 13. 派生密钥断言:双方共享 key 必须相同且长度为 16 字节。
assertEquals(16, initiator.key().length);
assertArrayEquals(responder.key(), initiator.key());
// 14. 确认标签断言:B 必须验证 A 返回的 S2 后才能接受会话。
assertTrue(sm2.confirmResponder(responder.s2(), initiator.s2()));
// 15. 身份错误断言:替换响应方身份后,A 对 S1 的验证必须失败。
assertThrows(
IllegalStateException.class,
() -> sm2.keyExchangeWithConfirmation(
staticA.privateKey(),
ephemeralA.privateKey(),
staticB.publicKey(),
ephemeralB.publicKey(),
SM2KeyExchangeOptions.builder()
.initiator(true)
.keyBits(128)
.selfId("merchant@gmkit.cn")
.peerId("other@gmkit.cn")
.confirmationTag(responder.s1())
.build()));
}
}签名和密文含随机量,因此测试验证性质与往返,不把某次随机输出写成固定向量。
签名协议
使用 byte[] 验签时,消息或签名不匹配返回 false。签名字节无法按指定 RAW/DER 解析也返回 false。无效公钥会在进入该捕获边界前抛 GmkitException。
userId 是签名协议字段,不是随意变化的账户昵称。签名端和验签端必须使用相同 UTF-8 字节。
加密协议
图片、文件和协议包直接使用 byte[],不要先构造 String。密文校验失败或使用错误私钥时抛 GmkitException。
公钥和格式转换
- 私钥:32 字节标量,字符串为 64 个 Hex 字符。
- 非压缩公钥:65 字节,字符串为 130 个 Hex 字符。
- 压缩公钥:33 字节,字符串为 66 个 Hex 字符。
compressPublicKey/decompressPublicKey只改变点编码,不隐藏公钥。- 签名 RAW/DER、密文 C1C3C2/C1C2C3/ASN.1 的转换成员用于已有协议;新协议固定一种格式后无需往返转换。
密钥交换
确认顺序是:
- B 先计算结果,把
s1发给 A。 - A 通过
confirmationTag(responder.s1())校验 B 的标签并计算结果。 - A 把自己的
s2发给 B。 - B 调用
confirmResponder(responder.s2(), initiator.s2())。 - 两次确认和共享 key 比对全部成功后,双方才接受会话。
keyBits 单位是 bit,默认 128。确认失败在 0.10.1 中由 BC 抛出 IllegalStateException。失败后丢弃派生 key 和临时私钥,不继续协议。
兼容边界
空 userId 会被 Builder 改成 SM2.DEFAULT_USER_ID,不能表达独立的空身份。已发布兼容成员见旧系统迁移;C1C2C3 只在对端协议明确要求时使用。
全部构造器、重载、Builder 和转换成员见 Java SM2 API。