TypeScript SM2 使用手册
TypeScript SM2 使用手册
本章用于两类任务:以身份绑定的 SM2 签名确认消息来源;使用接收方 SM2 公钥保护小体积数据。文件和大段业务数据应使用 SM4-GCM 加密,再用 SM2 保护会话 key。
本章固定的协议
签名使用标准路径 e = SM3(Z || M)。不要把 userId 当作展示名称随意修改;它是签名协议字段。
签名、验签、加密和解密
样例使用随机密钥、随机签名和随机密文,因此只断言长度、验签结果、明文往返和篡改失败,不断言随机输出的固定字符串。
// 1. 准备参数:固定业务消息、篡改消息和非空 SM2 用户标识。
const plaintext = 'order=GMKIT-DEMO-0001&amount=88.00';
const tampered = 'order=GMKIT-DEMO-0001&amount=99.00';
const userId = 'merchant@gmkit.cn';
// 2. 生成 SM2 密钥:私钥为 32 字节 Hex,公钥默认为 65 字节非压缩点 Hex。
const { privateKey, publicKey } = sm2GenerateKeyPair();
assert.equal(privateKey.length, 64);
assert.equal(publicKey.length, 130);
assert.equal(publicKey.startsWith('04'), true);
// 3. SM2 签名:计算 e = SM3(Z || M),签名使用 DER,外层使用 Base64。
const signature = sm2Sign(privateKey, plaintext, {
userId,
signatureFormat: 'der',
outputFormat: OutputFormat.BASE64,
});
// 4. SM2 验签:输入编码、签名结构和 userId 必须与签名端一致。
assert.equal(sm2Verify(publicKey, plaintext, signature, {
userId,
signatureFormat: 'der',
inputFormat: InputFormat.BASE64,
}), true);
// 5. 篡改断言:金额变化或 userId 变化后,SM2 验签必须返回 false。
assert.equal(sm2Verify(publicKey, tampered, signature, {
userId,
signatureFormat: 'der',
inputFormat: InputFormat.BASE64,
}), false);
assert.equal(sm2Verify(publicKey, plaintext, signature, {
userId: 'other@gmkit.cn',
signatureFormat: 'der',
inputFormat: InputFormat.BASE64,
}), false);
// 6. SM2 加密:密文排列固定为 C1C3C2,外层使用 Base64。
const ciphertext = sm2Encrypt(publicKey, plaintext, {
mode: SM2CipherMode.C1C3C2,
outputFormat: OutputFormat.BASE64,
});
// 7. SM2 解密:显式指定 C1C3C2 和 Base64,恢复 UTF-8 文本。
const decrypted = sm2Decrypt(privateKey, ciphertext, {
mode: SM2CipherMode.C1C3C2,
inputFormat: InputFormat.BASE64,
});
assert.equal(decrypted, plaintext);
// 8. 密文篡改断言:修改 C3/C2 后,SM2 解密必须校验失败并抛错。
const tamperedCiphertextBytes = base64ToBytes(ciphertext);
tamperedCiphertextBytes[tamperedCiphertextBytes.length - 1] ^= 0x01;
const tamperedCiphertext = bytesToBase64(tamperedCiphertextBytes);
assert.throws(() => sm2Decrypt(privateKey, tamperedCiphertext, {
mode: SM2CipherMode.C1C3C2,
inputFormat: InputFormat.BASE64,
}));
// 9. 公钥压缩往返:压缩点可用于存储,解压后必须恢复同一非压缩公钥。
const compressedPublicKey = sm2CompressPublicKey(publicKey);
assert.equal(compressedPublicKey.length, 66);
assert.equal(sm2DecompressPublicKey(compressedPublicKey), publicKey);调用参数
私钥固定 32 字节;默认非压缩公钥固定 65 字节(04 || x || y)。压缩公钥固定 33 字节(02/03 || x)。字符串均为 Hex。
失败行为
- 消息或
userId变化:sm2Verify返回false。 - 签名编码、DER 结构、公钥或参数无法用于验签:0.10.1 的
sm2Verify捕获错误并返回false。 - SM2 明文为空:
sm2Encrypt抛出Error。 - 私钥、公钥、密文模式或输入编码非法:加解密抛出
Error。 - 密文被修改或使用错误私钥:C3 校验失败并抛出
Error,不得返回部分明文。
服务端可把验签的所有 false 统一映射为“签名无效”;不要根据解析细节向外暴露不同响应。
二进制数据
sm2Encrypt 接受 Uint8Array。解密时必须选择 sm2DecryptBytes:
Uint8Array 明文
→ sm2Encrypt(..., { mode: C1C3C2, outputFormat: BASE64 })
→ Base64 密文
→ sm2DecryptBytes(..., { mode: C1C3C2, inputFormat: BASE64 })
→ Uint8Array 明文不要把文件字节先交给 bytesToString。无法表示的 UTF-8 序列会被替换,之后不能还原原文件。
公钥压缩
压缩只改变点编码,不改变公钥本身。数据库使用压缩公钥时,协议字段要保存 compressed 或通过首字节 02/03 明确识别;需要非压缩点的系统可用 sm2DecompressPublicKey 转换。
公钥压缩不是加密,也不隐藏公钥。
密钥交换的使用条件
只有协议明确要求 SM2 密钥交换时才使用 sm2KeyExchange。双方必须预先拥有可信的长期公钥,并为每次会话生成临时密钥对。参数中的 keyLength 单位是 byte,默认值为 16。
双方的 sharedKey 必须相同。确认值按对端关系校验:一方发出的确认值必须与另一方期望接收的确认值一致。完整可执行流程见高级能力中的 SM2 密钥交换。
// 1. 生成长期密钥:A 和 B 的公钥必须通过业务系统预先完成身份绑定。
const longTermA = sm2GenerateKeyPair();
const longTermB = sm2GenerateKeyPair();
// 2. 生成临时密钥:每次会话分别生成 A、B 的临时密钥对。
const ephemeralA = sm2GenerateKeyPair();
const ephemeralB = sm2GenerateKeyPair();
// 3. 发起方计算:A 使用自己的长期/临时私钥和 B 的两个公钥派生 16 字节 key。
const exchangeA = sm2KeyExchange({
privateKey: longTermA.privateKey,
publicKey: longTermA.publicKey,
userId: 'merchant@gmkit.cn',
tempPrivateKey: ephemeralA.privateKey,
peerPublicKey: longTermB.publicKey,
peerTempPublicKey: ephemeralB.publicKey,
peerUserId: 'warehouse@gmkit.cn',
isInitiator: true,
keyLength: 16,
});
// 4. 响应方计算:B 交换 self/peer 参数,并把角色固定为响应方。
const exchangeB = sm2KeyExchange({
privateKey: longTermB.privateKey,
publicKey: longTermB.publicKey,
userId: 'warehouse@gmkit.cn',
tempPrivateKey: ephemeralB.privateKey,
peerPublicKey: longTermA.publicKey,
peerTempPublicKey: ephemeralA.publicKey,
peerUserId: 'merchant@gmkit.cn',
isInitiator: false,
keyLength: 16,
});
// 5. 派生密钥断言:双方必须得到相同的 16 字节共享 key。
assert.equal(exchangeA.sharedKey.length, 32);
assert.equal(exchangeA.sharedKey, exchangeB.sharedKey);
// 6. 确认标签断言:双方计算的 S1 与 S2 必须分别一致后才能接受会话。
assert.equal(exchangeA.s1, exchangeB.s1);
assert.equal(exchangeA.s2, exchangeB.s2);
// 7. 身份错误断言:B 的 userId 被替换后,派生 key 和确认标签不得通过比对。
const wrongIdentity = sm2KeyExchange({
privateKey: longTermA.privateKey,
publicKey: longTermA.publicKey,
userId: 'merchant@gmkit.cn',
tempPrivateKey: ephemeralA.privateKey,
peerPublicKey: longTermB.publicKey,
peerTempPublicKey: ephemeralB.publicKey,
peerUserId: 'other@gmkit.cn',
isInitiator: true,
keyLength: 16,
});
assert.notEqual(wrongIdentity.sharedKey, exchangeB.sharedKey);
assert.notEqual(wrongIdentity.s1, exchangeB.s1);0.10.1 的结果同时返回 s1 和 s2。应用协议应明确谁先发送哪个确认值、在什么时点销毁临时私钥,并在确认失败时丢弃 sharedKey。示例中的 sharedKey 只做双方一致性断言,不直接当作长期存储 key。
不在主流程使用的兼容能力
- 空
userId在 0.10.1 中会回落到DEFAULT_USER_ID,不能表达独立的空身份。 - no-Z 签名计算
SM3(M),不是标准 SM2 签名,也不能由标准 BCSM2Signer直接验证。 - 传入的
SM2CurveParams只能重复声明标准sm2p256v1参数;0.10.1 不支持自定义曲线。 - 既有系统的格式分支统一在迁移层处理,不混入本页的标准调用。
替代方案和风险见旧系统迁移。全部函数、选项和类成员见 TypeScript SM2 API。