TypeScript SM2 API
TypeScript SM2 API
gmkitx 的 SM2 模块提供密钥生成、公钥派生与压缩、加解密、签名验签和密钥交换。一次性操作优先使用 sm2* 具名函数;需要在多个操作之间持有密钥时使用 SM2 类。
SM2 适合数字签名、小体积密钥材料加密和协议级密钥交换。文件、图片或大段业务数据应由 SM4-GCM/CCM 处理,再用 SM2 保护随机会话密钥。
本页适用范围
以下签名和默认值按 gmkitx 0.10.1 说明。字符串消息统一按 UTF-8 编码;Hex、Base64 和原始字节的区别会在各接口下单独标明。
先完成一条可运行链路
第一次接入请先按 TypeScript SM2 使用手册 完成 DER/Base64 签名验签、C1C3C2/Base64 加解密和失败断言,再回到本页核对参数。
导入与入口选择
import {
DEFAULT_USER_ID,
InputFormat,
OutputFormat,
SM2,
SM2CipherMode,
configureRNG,
sm2,
sm2CompressPublicKey,
sm2Decrypt,
sm2DecryptBytes,
sm2DecompressPublicKey,
sm2Encrypt,
sm2GenerateKeyPair,
sm2GetPublicKeyFromPrivateKey,
sm2KeyExchange,
sm2Sign,
sm2Verify,
} from 'gmkitx';
import type {
KeyPair,
SignOptions,
SM2CurveParams,
SM2DecryptOptions,
SM2EncryptOptions,
SM2KeyExchangeParams,
SM2KeyExchangeResult,
SM2SignatureFormat,
SM2SignatureInputFormat,
VerifyOptions,
} from 'gmkitx';
// 1. 配置随机源:生产环境缺少系统 CSPRNG 时直接失败。
configureRNG('strict');命名空间中的函数名不带 sm2 前缀,例如 sm2Encrypt(...) 对应 sm2.encrypt(...)。本页以具名函数为主,两种入口的参数、返回值和失败行为相同。
输入、编码与随机源
同样是 string,在不同参数位置表示的内容不同:
已发布接口允许省略部分输入格式。该行为只用于读取旧数据;新协议必须显式保存并传入编码。兼容优先级见旧系统迁移。
密钥生成、加密、签名以及未提供临时私钥的密钥交换都需要安全随机数。浏览器通常使用 Web Crypto,Node.js 使用系统密码学随机源;受限运行环境应先注入平台 CSPRNG,详见随机源 API。
密钥与公钥格式
公开签名
interface KeyPair {
publicKey: string;
privateKey: string;
}
sm2GenerateKeyPair(compressed?: boolean): KeyPair
sm2GetPublicKeyFromPrivateKey(
privateKey: string | Uint8Array,
compressed?: boolean,
): string
sm2CompressPublicKey(publicKey: string | Uint8Array): string
sm2DecompressPublicKey(publicKey: string | Uint8Array): string| 数据 | 编码与长度 |
|---|---|
| 私钥 | 32 字节标量;标准字符串表示为 64 个 Hex 字符 |
| 非压缩公钥 | 65 字节:04 || x || y |
| 压缩公钥 | 33 字节:02/03 || x |
| 函数返回的 Hex | 小写、不带 0x 前缀 |
字符串私钥短于 64 个 Hex 字符时会在左侧补零,这是兼容行为;长于 64 个字符、包含非 Hex 字符或标量越界时会抛出 Error。公钥会校验长度、02/03/04 前缀和曲线点合法性。
import {
sm2CompressPublicKey,
sm2DecompressPublicKey,
sm2GenerateKeyPair,
sm2GetPublicKeyFromPrivateKey,
} from 'gmkitx';
// 1. 生成密钥对:默认返回 32 字节私钥和 65 字节非压缩公钥。
const keys = sm2GenerateKeyPair();
// 2. 密钥长度断言:Hex 长度必须分别为 64 和 130。
if (keys.privateKey.length !== 64 || keys.publicKey.length !== 130) {
throw new Error('SM2 key length mismatch');
}
// 3. 派生公钥:从私钥重新计算非压缩公钥。
const derived = sm2GetPublicKeyFromPrivateKey(keys.privateKey);
// 4. 公钥断言:派生结果必须与密钥对中的公钥一致。
if (derived !== keys.publicKey) {
throw new Error('SM2 public key derivation failed');
}
// 5. 压缩公钥:65 字节非压缩点转换为 33 字节压缩点。
const compressed = sm2CompressPublicKey(keys.publicKey);
// 6. 压缩长度断言:压缩公钥 Hex 长度必须为 66。
if (compressed.length !== 66) throw new Error('compressed key length mismatch');
// 7. 解压公钥:恢复非压缩点并比对同一个曲线点。
if (sm2DecompressPublicKey(compressed) !== keys.publicKey) {
throw new Error('SM2 public key round-trip failed');
}加密与解密
公开签名
interface SM2EncryptOptions {
mode?: 'C1C3C2' | 'C1C2C3';
outputFormat?: 'hex' | 'base64';
}
interface SM2DecryptOptions {
mode?: 'C1C3C2' | 'C1C2C3';
inputFormat?: 'hex' | 'base64';
}
sm2Encrypt(
publicKey: string | Uint8Array,
data: string | Uint8Array,
options?: SM2EncryptOptions,
): string
sm2Decrypt(
privateKey: string | Uint8Array,
encryptedData: string | Uint8Array,
options?: SM2DecryptOptions,
): string
sm2DecryptBytes(
privateKey: string | Uint8Array,
encryptedData: string | Uint8Array,
options?: SM2DecryptOptions,
): Uint8Arraysm2Encrypt 解决“小体积数据需要由接收方公钥保护”的问题。它不适合文件或大消息,也不提供流式接口。每次调用都会生成新的临时标量,因此同一公钥和明文的密文通常不同。
密文结构与选项
| 排列 | 字节结构 | 使用建议 |
|---|---|---|
| C1C3C2 | C1 || C3 || C2 | 当前默认值,新协议优先使用 |
| C1C2C3 | C1 || C2 || C3 | 只用于对接采用旧排列的系统 |
本库加密端固定输出 65 字节非压缩 C1、32 字节 C3,以及与明文等长的 C2。原始密文总长度为 97 + 明文字节数。加密端不输出 ASN.1 密文;解密端可以识别首字节为 0x30 的 ASN.1 SEQUENCE { x, y, hash, cipher } 密文,也能读取以 02/03 开头的压缩 C1。
sm2Decrypt 将明文字节按 UTF-8 解码。图片、压缩数据、协议包以及可能包含无效 UTF-8 的内容必须使用 sm2DecryptBytes,否则文本解码器可能替换无法解码的字节。
文本、二进制与失败断言
import {
InputFormat,
OutputFormat,
SM2CipherMode,
sm2Decrypt,
sm2DecryptBytes,
sm2Encrypt,
sm2GenerateKeyPair,
} from 'gmkitx';
// 1. 生成 SM2 密钥对并准备 UTF-8 订单明文。
const keys = sm2GenerateKeyPair();
const message = 'order=GMKIT-DEMO-0001&amount=88.00';
// 2. SM2 文本加密:显式固定 C1C3C2 排列和 Hex 编码。
const ciphertext = sm2Encrypt(keys.publicKey, message, {
mode: SM2CipherMode.C1C3C2,
outputFormat: OutputFormat.HEX,
});
// 3. SM2 文本解密:使用相同排列和编码恢复 UTF-8 文本。
const restored = sm2Decrypt(keys.privateKey, ciphertext, {
mode: SM2CipherMode.C1C3C2,
inputFormat: InputFormat.HEX,
});
// 4. 文本往返断言:解密结果必须等于订单原文。
if (restored !== message) throw new Error('SM2 text round-trip failed');
// 5. 准备二进制输入:包含无法安全经过普通文本转换的字节。
const binary = Uint8Array.of(0x00, 0xff, 0x80, 0x41);
// 6. SM2 二进制加密:原始字节直接进入加密函数。
const binaryCiphertext = sm2Encrypt(keys.publicKey, binary);
// 7. SM2 二进制解密:使用字节返回接口恢复原始数据。
const binaryRestored = sm2DecryptBytes(keys.privateKey, binaryCiphertext);
// 8. 二进制往返断言:长度和每个字节都必须一致。
if (binaryRestored.length !== binary.length
|| binaryRestored.some((value, index) => value !== binary[index])) {
throw new Error('SM2 binary round-trip failed');
}
// 9. 构造篡改密文:修改最后一个字节以破坏 C3 完整性校验。
const damaged = `${ciphertext.slice(0, -2)}${ciphertext.endsWith('00') ? '01' : '00'}`;
let rejected = false;
// 10. 失败断言:篡改密文必须抛错,不能返回部分明文。
try {
sm2Decrypt(keys.privateKey, damaged, { inputFormat: InputFormat.HEX });
} catch {
rejected = true;
}
if (!rejected) throw new Error('tampered SM2 ciphertext must be rejected');加密会拒绝空明文。加密、解密可能因为随机源、密钥、模式、编码、C1 曲线点、密文长度、ASN.1 结构或 C3 校验无效而抛出 Error;解密失败不会返回部分明文。
签名与验签
公开签名
type SM2SignatureFormat = 'raw' | 'der';
type SM2SignatureInputFormat = 'raw' | 'der' | 'auto';
interface SignOptions {
signatureFormat?: SM2SignatureFormat;
outputFormat?: 'hex' | 'base64';
userId?: string;
curveParams?: SM2CurveParams;
}
interface VerifyOptions {
signatureFormat?: SM2SignatureInputFormat;
inputFormat?: 'hex' | 'base64';
userId?: string;
curveParams?: SM2CurveParams;
}
sm2Sign(
privateKey: string | Uint8Array,
data: string | Uint8Array,
options?: SignOptions,
): string
sm2Verify(
publicKey: string | Uint8Array,
data: string | Uint8Array,
signature: string | Uint8Array,
options?: VerifyOptions,
): booleansm2Sign 对消息和用户身份产生 SM2 签名;sm2Verify 判断签名、消息、公钥和用户身份是否匹配。它们不负责证书链、密钥归属或业务授权判断。
签名选项
userId 先编码为 UTF-8,再参与 Z 值计算;编码后必须少于 8192 字节。空字符串会回落到 DEFAULT_USER_ID,当前 API 不能用空字符串表达一个独立身份。签名端和验签端必须使用完全相同的消息字节和非空 user ID。
验签只有在显式传入 signatureFormat: 'auto' 时才自动判断 raw/DER。固定协议应直接传 raw 或 der,避免把错误格式误当成另一种格式处理。
返回值与错误语义
sm2Sign成功时返回签名字符串;私钥、随机源、输出格式、user ID 或曲线声明无效时抛出Error。sm2Verify成功时返回true。- 消息或 user ID 不一致、签名被篡改、
r/s越界、签名编码错误、公钥错误或验签选项无效时,sm2Verify都返回false。该函数内部会收敛解析异常,不依靠try/catch区分失败原因。
import {
InputFormat,
OutputFormat,
sm2GenerateKeyPair,
sm2Sign,
sm2Verify,
} from 'gmkitx';
// 1. 准备输入:正常订单、篡改订单和签名身份分别保存。
const keys = sm2GenerateKeyPair();
const message = 'order=GMKIT-DEMO-0001&amount=88.00';
const tampered = 'order=GMKIT-DEMO-0001&amount=99.00';
const userId = 'merchant@gmkit.cn';
// 2. SM2 签名:DER 决定内部结构,Base64 决定外层字符串编码。
const signature = sm2Sign(keys.privateKey, message, {
userId,
signatureFormat: 'der',
outputFormat: OutputFormat.BASE64,
});
// 3. SM2 验签:使用相同消息、身份、签名格式和外层编码。
const valid = sm2Verify(keys.publicKey, message, signature, {
userId,
signatureFormat: 'der',
inputFormat: InputFormat.BASE64,
});
// 4. 成功断言:原消息必须验证成功。
if (!valid) throw new Error('SM2 verification failed');
// 5. 消息篡改断言:金额变化后必须返回 false。
if (sm2Verify(keys.publicKey, tampered, signature, {
userId,
signatureFormat: 'der',
inputFormat: InputFormat.BASE64,
})) {
throw new Error('tampered message must not verify');
}
// 6. 身份篡改断言:userId 不同必须返回 false。
if (sm2Verify(keys.publicKey, message, signature, {
userId: 'warehouse@gmkit.cn',
signatureFormat: 'der',
inputFormat: InputFormat.BASE64,
})) {
throw new Error('wrong SM2 user ID must not verify');
}标准签名固定计算 e = SM3(Z || M)。若旧系统曾使用 SM3(M),不要把它混入新协议;迁移边界和弃用成员列在本页末尾。
曲线参数兼容声明
interface SM2CurveParams {
p?: string;
a?: string;
b?: string;
Gx?: string;
Gy?: string;
n?: string;
}SM2CurveParams 为历史类型兼容而保留,不代表支持任意自定义曲线。省略它即可使用标准 sm2p256v1;字段可以带 0x 前缀且不区分 Hex 大小写,但值必须与标准参数相同。
函数式签名在 sm2Sign 时校验声明,不一致会抛错;sm2Verify 会把同类错误收敛为 false。SM2 实例的 setCurveParams 只保存声明,实际校验发生在后续签名或验签时。加解密和密钥交换不读取这个兼容字段。
密钥交换
参数与返回值
interface SM2KeyExchangeParams {
privateKey: string | Uint8Array;
publicKey?: string | Uint8Array;
userId?: string;
tempPrivateKey?: string | Uint8Array;
peerPublicKey: string | Uint8Array;
peerTempPublicKey: string | Uint8Array;
peerUserId?: string;
isInitiator: boolean;
keyLength?: number;
}
interface SM2KeyExchangeResult {
tempPublicKey: string;
sharedKey: string;
s1?: string;
s2?: string;
}
sm2KeyExchange(params: SM2KeyExchangeParams): SM2KeyExchangeResult返回字段均为小写 Hex:
tempPublicKey:己方 65 字节非压缩临时公钥。sharedKey:派生共享密钥,Hex 长度为keyLength × 2。s1、s2:32 字节确认值。类型保留为可选字段;当前实现会同时返回两项。
keyLength 没有协议级上限。若该值来自配置或请求,应用必须先设置自己的合理上限,避免一次派生分配过大缓冲区。
双方需要先交换临时公钥,再以镜像身份和相反角色调用。为了让临时公钥在调用前可发送给对方,最直接的方式是先用 sm2GenerateKeyPair() 生成临时密钥对,并把临时私钥传给 tempPrivateKey。
import { sm2GenerateKeyPair, sm2KeyExchange } from 'gmkitx';
// 1. 生成长期密钥:分别绑定 Alice 和 Bob 的稳定身份。
const alice = sm2GenerateKeyPair();
const bob = sm2GenerateKeyPair();
// 2. 生成临时密钥:每次密钥交换会话都使用新的临时密钥对。
const aliceTemp = sm2GenerateKeyPair();
const bobTemp = sm2GenerateKeyPair();
// 3. 发起方计算:Alice 使用自己的私钥和 Bob 的两个公钥派生结果。
const resultA = sm2KeyExchange({
privateKey: alice.privateKey,
publicKey: alice.publicKey,
tempPrivateKey: aliceTemp.privateKey,
peerPublicKey: bob.publicKey,
peerTempPublicKey: bobTemp.publicKey,
userId: 'Alice',
peerUserId: 'Bob',
isInitiator: true,
keyLength: 32,
});
// 4. 响应方计算:Bob 镜像身份、密钥和角色顺序。
const resultB = sm2KeyExchange({
privateKey: bob.privateKey,
publicKey: bob.publicKey,
tempPrivateKey: bobTemp.privateKey,
peerPublicKey: alice.publicKey,
peerTempPublicKey: aliceTemp.publicKey,
userId: 'Bob',
peerUserId: 'Alice',
isInitiator: false,
keyLength: 32,
});
// 5. 密钥与确认值断言:双方必须得到同一 32 字节密钥和相同确认值。
if (resultA.sharedKey.length !== 64
|| resultA.sharedKey !== resultB.sharedKey
|| resultA.s1 !== resultB.s1
|| resultA.s2 !== resultB.s2) {
throw new Error('SM2 key exchange mismatch');
}长期公私钥不匹配、任一曲线点非法、身份过长、派生长度无效、共享点为无穷远点以及 KDF 失败都会抛出 Error。角色或身份顺序没有镜像时,通常表现为双方共享密钥或确认值不一致,而不是单边调用立即抛错。因此应用必须比较确认值,不能只判断函数是否返回。sm2KeyExchange 只计算结果,不发送临时公钥或确认值;消息顺序、重放防护和会话绑定由上层协议负责。
SM2 类
公开成员
new SM2(keyPair?: Partial<KeyPair>, curveParams?: SM2CurveParams)
SM2.generateKeyPair(curveParams?: SM2CurveParams): SM2
SM2.fromPrivateKey(privateKey: string, curveParams?: SM2CurveParams): SM2
SM2.fromPublicKey(publicKey: string, curveParams?: SM2CurveParams): SM2
getPublicKey(): string
getPrivateKey(): string
encrypt(
data: string | Uint8Array,
options?: SM2EncryptOptions,
): string
decrypt(
encryptedData: string | Uint8Array,
options?: SM2DecryptOptions,
): string
decryptBytes(
encryptedData: string | Uint8Array,
options?: SM2DecryptOptions,
): Uint8Array
sign(
data: string | Uint8Array,
options?: Omit<SignOptions, 'curveParams'>,
): string
verify(
data: string | Uint8Array,
signature: string,
options?: Omit<VerifyOptions, 'curveParams'>,
): boolean
setCurveParams(curveParams: SM2CurveParams): void
getCurveParams(): SM2CurveParams | undefined
keyExchange(
peerPublicKey: string,
peerTempPublicKey: string,
isInitiator: boolean,
options?: {
userId?: string;
peerUserId?: string;
tempPrivateKey?: string;
keyLength?: number;
},
): SM2KeyExchangeResultSM2.generateKeyPair() 返回同时持有公私钥的实例;SM2.fromPrivateKey() 会派生并保存非压缩公钥;SM2.fromPublicKey() 只保存传入公钥。直接构造时,如果同时传入公钥和私钥,构造器会确认两者表示同一个曲线点。
实例没有 reset 或 close,可以重复使用。除 setCurveParams 外,密码操作不会修改实例状态。getCurveParams() 返回构造或设置时保存的对象引用,不会复制;调用方不应在外部继续修改该对象。
import { SM2 } from 'gmkitx';
// 1. 准备实例:私钥实例负责签名,公钥实例只负责验签。
const message = 'order=GMKIT-DEMO-0001&amount=88.00';
const owner = SM2.generateKeyPair();
const verifier = SM2.fromPublicKey(owner.getPublicKey());
// 2. SM2 签名:由持有私钥的 owner 创建 DER 签名。
const signature = owner.sign(message, {
userId: 'merchant@gmkit.cn',
signatureFormat: 'der',
});
// 3. SM2 验签:公钥实例使用相同 userId 验证签名。
if (!verifier.verify(message, signature, {
userId: 'merchant@gmkit.cn',
signatureFormat: 'der',
})) {
throw new Error('SM2 class verification failed');
}
// 4. 缺少私钥断言:公钥实例调用签名方法必须抛错。
let missingPrivateKeyRejected = false;
try {
verifier.sign(message);
} catch {
missingPrivateKeyRejected = true;
}
if (!missingPrivateKeyRejected) {
throw new Error('public-only SM2 instance must not sign');
}SM2.verify() 通常沿用函数式入口的 false 失败语义;但实例根本没有公钥时,getPublicKey() 会先抛出 Error。同理,缺少私钥的解密、签名和密钥交换会抛错。
失败处理速查
不要把异常文本作为稳定协议字段。对外服务应把内部解析错误统一映射为固定错误码,并避免通过错误差异泄露密钥或密文处理细节。
本页覆盖的公共 API
- 函数:
sm2GenerateKeyPair、sm2GetPublicKeyFromPrivateKey、sm2CompressPublicKey、sm2DecompressPublicKey、sm2Encrypt、sm2Decrypt、sm2DecryptBytes、sm2Sign、sm2Verify、sm2KeyExchange。 - 类型:
KeyPair、SM2CurveParams、SM2EncryptOptions、SM2DecryptOptions、SignOptions、VerifyOptions、SM2SignatureFormat、SM2SignatureInputFormat、SM2KeyExchangeParams、SM2KeyExchangeResult。 - 对象式入口:
SM2构造器、3 个静态工厂和全部公开实例方法。
兼容成员
只在维护旧调用或 no-Z 协议时展开
无算法前缀函数已弃用:
| 旧名称 | 替代名称 |
|---|---|
generateKeyPair | sm2GenerateKeyPair |
getPublicKeyFromPrivateKey | sm2GetPublicKeyFromPrivateKey |
compressPublicKey | sm2CompressPublicKey |
decompressPublicKey | sm2DecompressPublicKey |
sign | sm2Sign |
verify | sm2Verify |
keyExchange | sm2KeyExchange |
SignOptions.skipZComputation?: boolean 和 VerifyOptions.skipZComputation?: boolean 也已弃用,默认值均为 false。设为 true 时计算 e = SM3(M),不会计算标准 SM2 的 Z 值;这不是性能选项,也不是预计算摘要接口。此类签名不能由标准 Bouncy Castle SM2Signer 验证。
TypeScript 0.10.1 没有公开的预计算 e 签名接口。替代方案和互操作风险见旧系统迁移。
可执行案例
下面的测试源码覆盖标准签名、正确消息以及篡改消息返回 false。站点检查会确认引用区域存在,文档示例任务会执行同一文件。
查看测试源码
// 1. 准备输入:金额不同的订单用于成功验签和篡改验签。
const message = 'order=GMKIT-DEMO-0001&amount=88.00';
const tampered = 'order=GMKIT-DEMO-0001&amount=99.00';
// 2. 生成 SM2 密钥对:对象式入口在实例中持有私钥和公钥。
const sm2 = SM2.generateKeyPair();
// 3. SM2 签名:固定 userId 和 DER 编码。
const signature = sm2.sign(message, {
signatureFormat: 'der',
userId: 'merchant@gmkit.cn',
});
// 4. SM2 验签:相同消息和 userId 必须验证成功。
assert.equal(sm2.verify(message, signature, {
signatureFormat: 'der',
userId: 'merchant@gmkit.cn',
}), true);
// 5. 篡改断言:金额变化后必须验证失败。
assert.equal(sm2.verify(tampered, signature, {
signatureFormat: 'der',
userId: 'merchant@gmkit.cn',
}), false);相关页面
- 跨语言 SM2 协议与向量
- 输入编码、随机源与字节工具
- raw/DER 签名转换工具
- TypeScript SM4 API:大数据混合加密的对称算法部分