使用 sm-crypto 进行 SM2 加解密、签名、验签
Haiya Lv3

安装依赖

安装 sm-crypto 依赖,如果需要进行密钥格式转换,则还需要安装 asn1.js 依赖。

1
npm install --save sm-crypto asn1.js

转换函数

Hex 转 Base64:

1
2
3
4
5
6
7
function hexToBase64(hex) {
var b64 = "";
for (var i = 0; i < hex.length; i += 2) {
b64 += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
return btoa(b64);
}

Base64 转 Hex:

1
2
3
4
5
6
7
8
function base64ToHex(b64) {
const decodedBytes = atob(b64); // might be window.atob(b64) in browser
let hexString = "";
for (let i = 0; i < decodedBytes.length; i++) {
hexString += decodedBytes.charCodeAt(i).toString(16).padStart(2, "0"); // padStart is defined in the ES2017 standard
}
return hexString;
}

如果你的环境不支持 padStart 函数(ES2017),可以使用以下函数代替

1
2
3
4
5
6
7
8
9
function base64Tohex(b64) {
var decodedBytes = atob(b64); // might be window.atob(b64) in browser
var hexString = "";
for (var i = 0; i < decodedBytes.length; i++) {
var hex = decodedBytes.charCodeAt(i).toString(16);
hexString += hex.length === 1 ? "0" + hex : hex; // manually pad with "0" if needed
}
return hexString;
}

Base64 URL Encode:

1
2
3
4
5
6
7
function base64urlEncode(buffer) {
return buffer
.toString("base64")
.replace(/=/g, "") // 去掉 padding
.replace(/\+/g, "-") // URL safe
.replace(/\//g, "_"); // URL safe
}

生成密钥对

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 生成密钥对
* @returns {Object} keyPair
*/
function generateKeyPair() {
const sm2 = require("sm-crypto").sm2;
let keyPair = sm2.generateKeyPairHex();
return {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
};
}

本文中使用以下密钥对为例

1
2
3
4
5
6
7
8
9
10
{
"publicKey": {
"hex": "0440d268d2636e855b88984353da1c1d93d7ea365375f39eb351910b62a598ef26ce83bfbf5d3137657385380d571e08eacb17c4d8e5d5c5d8c365308dc2049fe5",
"base64": "BEi45oUS52pagpvrIZ/KMaBb7hzwFlDoXo5TQ7L0ztSEFPQj5YTaQoP/IkWzRUpSXewqFwKIZPVJr+moF6U+L6k="
},
"privateKey": {
"hex": "d75feda9f92bac889ed5c76792054f54e6149a745fad5f029ef5e1614e795c20",
"base64": "11/tqfkrrIie1cdnkgVPVOYUmnRfrV8CnvXhYU55XCA="
}
}
1
2
let { publicKey, privateKey } = generateKeyPair();
console.info({ publicKey, privateKey });

如果需要 Base64 格式的,可以进行转换:

1
2
3
4
5
const keyPairBase64 = {
publicKeyBase64: hexToBase64(publicKey),
privateKeyBase64: hexToBase64(privateKey),
};
console.info({ keyPairBase64 });

导入 Base64 密钥

sm-crypto 加解密、验签操作时,公私钥均作为参数传入,值为 Hex 字符串;但有些情况下,我们拿到的是 Base64 字符串,这种情况需要进行转换。

转换时分两种情况,第一种比较简单,是直接使用 Hex 转 Base64,比如:

1
2
3
4
5
const publicKey = base64ToHex(
"BEi45oUS52pagpvrIZ/KMaBb7hzwFlDoXo5TQ7L0ztSEFPQj5YTaQoP/IkWzRUpSXewqFwKIZPVJr+moF6U+L6k="
);
const privateKey = base64ToHex("11/tqfkrrIie1cdnkgVPVOYUmnRfrV8CnvXhYU55XCA=");
console.info({ privateKey, publicKey });

第二种复杂一些,是 DER 编码的密钥对,此时需要用 asn1.js 进行转换

公钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const asn1 = require("asn1.js");

const SM2PublicKeyASN = asn1.define("SM2PublicKey", function () {
this.seq().obj(
this.key("algorithm")
.seq()
.obj(this.key("algorithm").objid(), this.key("parameters").objid()),
this.key("publicKey").bitstr()
);
});

function convertDerToHexPublicKey(derPublicKey) {
const derBuffer = Buffer.from(derPublicKey, "base64");
const decoded = SM2PublicKeyASN.decode(derBuffer, "der");
const pubKeyHex = decoded.publicKey.data.toString("hex");
return pubKeyHex;
}

const derPublicKey =
"MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEQNJo0mNuhVuImENT2hwdk9fqNlN1856zUZELYqWY7ybOg7+/XTE3ZXOFOA1XHgjqyxfE2OXVxdjDZTCNwgSf5Q==";
const publicKey = convertDerToHexPublicKey(derPublicKey);

console.info({ publicKey });

私钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const ECPrivateKeyASN = asn1.define("ECPrivateKey", function () {
this.seq().obj(
this.key("version").int(),
this.key("privateKey").octstr(),
this.key("parameters").explicit(0).objid().optional(),
this.key("publicKey").explicit(1).bitstr().optional()
);
});

const PKCS8PrivateKeyASN = asn1.define("PrivateKeyInfo", function () {
this.seq().obj(
this.key("version").int(),
this.key("privateKeyAlgorithm")
.seq()
.obj(this.key("algorithm").objid(), this.key("parameters").objid()),
this.key("privateKey").octstr()
);
});

function parsePKCS8ToHex(derPrivateKey) {
const pkcs8Der = Buffer.from(derPrivateKey, "base64");
const pkcs8Obj = PKCS8PrivateKeyASN.decode(pkcs8Der, "der");
const ecPrivateKeyDer = pkcs8Obj.privateKey;
const ecPrivateKeyObj = ECPrivateKeyASN.decode(ecPrivateKeyDer, "der");

const privateKey = ecPrivateKeyObj.privateKey;
let publicKey = null;

if (ecPrivateKeyObj.publicKey) {
publicKey = ecPrivateKeyObj.publicKey.data;
}

return {
privateKey: {
hex: privateKey.toString("hex"),
base64: privateKey.toString("base64"),
},
publicKey: {
hex: publicKey ? publicKey.toString("hex") : null,
base64: publicKey ? publicKey.toString("base64") : null,
},
};
}

const derPrivateKey =
"MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQg11/tqfkrrIie1cdnkgVPVOYUmnRfrV8CnvXhYU55XCCgCgYIKoEcz1UBgi2hRANCAARA0mjSY26FW4iYQ1PaHB2T1+o2U3XznrNRkQtipZjvJs6Dv79dMTdlc4U4DVceCOrLF8TY5dXF2MNlMI3CBJ/l";
const keyPair = parsePKCS8ToHex(derPrivateKey);
const privateKey = keyPair.privateKey.hex;

console.log({ privateKey });

加解密

有了密钥对,就可以进行加解密了

1
2
3
4
5
6
7
8
9
10
11
12
const sm2 = require("sm-crypto").sm2;
const cipherMode = 1; // 1 - C1C3C2,0 - C1C2C3,默认为1

const message = "plaintext";
let encryptData = sm2.doEncrypt(message, publicKey, cipherMode); // 加密结果
let decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode); // 解密结果

console.info({
encryptData,
decryptData,
encryptDataBase64: hexToBase64(encryptData),
});

签名验签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const msg = "text";

// 纯签名 + 生成椭圆曲线点
let sigValueHex = sm2.doSignature(msg, privateKey);
let verifyResult = sm2.doVerifySignature(msg, sigValueHex, publicKey);

console.log({
sigValueHex, // c2c6f39113869b776d1080c77637edd1f1fd3fd0386f49df3495f372c5d80b5ecd753c54c124e7608babde061b9db0b19201178bf883e7df6bca030909da7c51
sigValueBase64: hexToBase64(sigValueHex), // wsbzkROGm3dtEIDHdjft0fH9P9A4b0nfNJXzcsXYC17NdTxUwSTnYIur3gYbnbCxkgEXi/iD599rygMJCdp8UQ==
verifyResult,
});

// 纯签名 + 生成椭圆曲线点 + der编解码
let sigValueHex3 = sm2.doSignature(msg, privateKey, {
der: true,
});
let verifyResult3 = sm2.doVerifySignature(msg, sigValueHex3, publicKey, {
der: true,
});

console.log({
sigValueHex3, // 30460221009e8ec722c1a3ed08a0e964915bbd77f1b0ae2924a9b07e49c8457faa14ba294b0221008f706524686e471e1688db39fb41703fb7b7e5a0240d626a2b2a216305cbf029
sigValueBase64: hexToBase64(sigValueHex3), // MEYCIQCejsciwaPtCKDpZJFbvXfxsK4pJKmwfknIRX+qFLopSwIhAI9wZSRobkceFojbOftBcD+3t+WgJA1iaisqIWMFy/Ap
verifyResult3,
});

// 纯签名 + 生成椭圆曲线点 + sm3杂凑
let sigValueHex4 = sm2.doSignature(msg, privateKey, {
hash: true,
});
let verifyResult4 = sm2.doVerifySignature(msg, sigValueHex4, publicKey, {
hash: true,
});

console.log({
sigValueHex4, // 58f462defe828dde54f11b0f14575b8fe2b915137fabbc804513d6fe7837e05741bafd8900bfabd5eb5b050f2c37cf4889700e7821197ae92cc5f80d36d18958
sigValueBase64: hexToBase64(sigValueHex4), // WPRi3v6Cjd5U8RsPFFdbj+K5FRN/q7yARRPW/ng34FdBuv2JAL+r1etbBQ8sN89IiXAOeCEZeuksxfgNNtGJWA==
verifyResult4,
});

// 纯签名 + 生成椭圆曲线点 + sm3杂凑(不做公钥推导)
let sigValueHex5 = sm2.doSignature(msg, privateKey, {
hash: true,
publicKey, // 传入公钥的话,可以去掉sm3杂凑中推导公钥的过程,速度会比纯签名 + 生成椭圆曲线点 + sm3杂凑快
});
let verifyResult5 = sm2.doVerifySignature(msg, sigValueHex5, publicKey, {
hash: true,
publicKey,
});
console.log({
sigValueHex5, // 4116254962afe9cae9e7e6f1d6d6026e6e4323c9f868a7585a2c58169ac6ec34a32b2663e02703286f82bf71fbb19dc17fbcc2892446e67c85e8249b64a08279
sigValueBase64: hexToBase64(sigValueHex5), // QRYlSWKv6crp5+bx1tYCbm5DI8n4aKdYWixYFprG7DSjKyZj4CcDKG+Cv3H7sZ3Bf7zCiSRG5nyF6CSbZKCCeQ==
verifyResult5,
});

提醒:签名验签的时候一定要注意,是不是用了 DER 编解码和 Hash

doVerifySignature 定义如下:

1
2
3
4
5
function doVerifySignature(msg: string | number[], signHex: string, publicKey: string, options?: {
der?: boolean | undefined;
hash?: boolean | undefined;
userId?: string | undefined;
}): boolean;

derhash 两个参数的值非常重要,如果和签名的时候不一致,会导致验签失败。

举个例子,Java 端使用 Hutool 进行签名:

1
2
3
4
5
6
7
8
String privateKey = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQg11/tqfkrrIie1cdnkgVPVOYUmnRfrV8CnvXhYU55XCCgCgYIKoEcz1UBgi2hRANCAARA0mjSY26FW4iYQ1PaHB2T1+o2U3XznrNRkQtipZjvJs6Dv79dMTdlc4U4DVceCOrLF8TY5dXF2MNlMI3CBJ/l";
SM2 sm2 = SmUtil.sm2(privateKey, null);
byte[] sign = sm2.sign("plaintext".getBytes(StandardCharsets.UTF_8));
String signHex = HexUtil.encodeHexStr(sign);
String signBase64 = Base64.encode(sign);

System.out.println(signHex); // 304402202eab6424eac67b67bfdb6991d785b5712e7e3e2b15c15d406b47972d32935f7c02200711318c8fac6bf7a6e4f0e22f1a0dda9293d890034a55994a10dbf3f62fd128
System.out.println(signBase64); // MEQCIC6rZCTqxntnv9tpkdeFtXEufj4rFcFdQGtHly0yk198AiAHETGMj6xr96bk8OIvGg3akpPYkANKVZlKENvz9i/RKA==

如果用以下代码进行验签是不通过的:

1
2
3
4
5
6
7
8
9
10
let verifyResult = sm2.doVerifySignature(
"plaintext",
base64ToHex(
"MEYCIQDOcEzIb29f96l+Tu0eGPAANwOUhp7wJ3+rWe0fWPIvKwIhALe5iZme4E8c6Q0e7lLyM2I3lJ74BaGvQx1Le/zFAIll"
),
publicKey
);
console.log({
verifyResult,
});

必须使用 DER+Hash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let verifyResult = sm2.doVerifySignature(
"plaintext",
base64ToHex(
"MEYCIQDOcEzIb29f96l+Tu0eGPAANwOUhp7wJ3+rWe0fWPIvKwIhALe5iZme4E8c6Q0e7lLyM2I3lJ74BaGvQx1Le/zFAIll"
),
publicKey,
{
hash: true,
der: true,
}
);
console.log({
verifyResult,
});

附录:Hex 转 DER

公钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function convertHexToDerPublicKey(hexPubKey) {
if (!hexPubKey.startsWith("04")) {
throw new Error("Invalid SM2 public key format, must start with 04");
}
const pubKeyBuffer = Buffer.from(hexPubKey, "hex");
const derBuffer = SM2PublicKeyASN.encode(
{
algorithm: {
algorithm: [1, 2, 840, 10045, 2, 1],
parameters: [1, 2, 156, 10197, 1, 301],
},
publicKey: { data: pubKeyBuffer },
},
"der"
);

return {
hex: derBuffer.toString("hex"),
base64: derBuffer.toString("base64"),
};
}

const hexPublicKey =
"0440d268d2636e855b88984353da1c1d93d7ea365375f39eb351910b62a598ef26ce83bfbf5d3137657385380d571e08eacb17c4d8e5d5c5d8c365308dc2049fe5";
const derPublicKey = convertHexToDerPublicKey(hexPublicKey);
console.info({ derPublicKey });

私钥(PKCS#8)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function convertHexToPKCS8PrivateKey(hexPrivateKey, hexPublicKey) {
if (hexPrivateKey.length !== 64) {
throw new Error("Invalid SM2 private key length (must be 32 bytes in hex)");
}
if (hexPublicKey.length !== 130 || !hexPublicKey.startsWith("04")) {
throw new Error(
"Invalid SM2 public key length (must be 65 bytes in hex with 04 prefix)"
);
}

const privKeyBuffer = Buffer.from(hexPrivateKey, "hex");
const pubKeyBuffer = Buffer.from(hexPublicKey, "hex");

const ecPrivateKeyDER = ECPrivateKeyASN.encode(
{
version: 1,
privateKey: privKeyBuffer,
parameters: [1, 2, 156, 10197, 1, 301],
publicKey: { data: pubKeyBuffer },
},
"der"
);

const pkcs8PrivateKeyDER = PKCS8PrivateKeyASN.encode(
{
version: 0,
privateKeyAlgorithm: {
algorithm: [1, 2, 840, 10045, 2, 1],
parameters: [1, 2, 156, 10197, 1, 301],
},
privateKey: ecPrivateKeyDER,
},
"der"
);

return {
hex: pkcs8PrivateKeyDER.toString("hex"),
base64: pkcs8PrivateKeyDER.toString("base64"),
};
}

// 32字节私钥
const hexPrivateKey =
"d75feda9f92bac889ed5c76792054f54e6149a745fad5f029ef5e1614e795c20";
// 65字节公钥
const hexPublicKey =
"0440d268d2636e855b88984353da1c1d93d7ea365375f39eb351910b62a598ef26ce83bfbf5d3137657385380d571e08eacb17c4d8e5d5c5d8c365308dc2049fe5";

const derPrivateKey = convertHexToPKCS8PrivateKey(hexPrivateKey, hexPublicKey);
console.log({ derPrivateKey });

附录:PEM 和 DER 互转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class PEMFormatter {
private static readonly PEM_HEADER = "-----BEGIN PUBLIC KEY-----";
private static readonly PEM_FOOTER = "-----END PUBLIC KEY-----";

/**
* DER to PEM
* @param {string} der - DER
* @returns {string} - PEM
*/
static derToPem(der: string): string {
if (typeof der !== "string" || !/^[A-Za-z0-9+/=]+$/.test(der)) {
throw new Error("Input must be a valid Base64 string");
}
const formattedKey = der.replace(/(.{64})/g, "$1\n").trim();
return `${PEMFormatter.PEM_HEADER}\n${formattedKey}\n${PEMFormatter.PEM_FOOTER}\n`;
}

/**
* PEM to DER
* @param {string} pem - PEM
* @returns {string} - DER
*/
static pemToDer(pem: string): string {
if (typeof pem !== "string") {
throw new Error("Input must be a string");
}

if (
!pem.startsWith(PEMFormatter.PEM_HEADER) ||
!pem.endsWith(PEMFormatter.PEM_FOOTER + "\n")
) {
throw new Error("Invalid PEM format: Missing header or footer");
}

const base64Data = pem
.slice(
PEMFormatter.PEM_HEADER.length,
-PEMFormatter.PEM_FOOTER.length - 1
)
.replace(/\s/g, ""); // 去掉所有空白字符
if (!/^[A-Za-z0-9+/=]+$/.test(base64Data)) {
throw new Error("Invalid Base64 data in PEM file");
}

return base64Data;
}
}

export default PEMFormatter;
1
2
3
4
5
6
7
8
9
const { default: PEMFormatter } = require("./pem");

const pem = PEMFormatter.derToPem(
"MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQg11/tqfkrrIie1cdnkgVPVOYUmnRfrV8CnvXhYU55XCCgCgYIKoEcz1UBgi2hRANCAARA0mjSY26FW4iYQ1PaHB2T1+o2U3XznrNRkQtipZjvJs6Dv79dMTdlc4U4DVceCOrLF8TY5dXF2MNlMI3CBJ/l"
);
console.log(pem);

const raw = PEMFormatter.pemToDer(pem);
console.log(raw);

附录:DER 和 JWK 互转

DER 转 JWK

私钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function privateKeyDerToJwk(privateKey) {
const pkcs8Der = Buffer.from(privateKey, "base64");
const pkcs8Obj = PKCS8PrivateKeyASN.decode(pkcs8Der, "der");
const ecPrivateKeyDer = pkcs8Obj.privateKey;
const ecPrivateKeyObj = ECPrivateKeyASN.decode(ecPrivateKeyDer, "der");
const d = ecPrivateKeyObj.privateKey;
let x = null,
y = null;

if (ecPrivateKeyObj.publicKey) {
const pubKeyBuffer = ecPrivateKeyObj.publicKey.data; // 64 字节
if (pubKeyBuffer.length === 65) {
// 如果是65字节,说明多了一个04,去掉
pubKeyBuffer = pubKeyBuffer.slice(1);
}
x = pubKeyBuffer.slice(0, 32);
y = pubKeyBuffer.slice(32, 64);
}
const jwk = {
hex: {
kty: "EC",
crv: "SM2",
x: x ? x.toString("hex") : undefined,
y: y ? y.toString("hex") : undefined,
d: d ? d.toString("hex") : undefined,
},
base64: {
kty: "EC",
crv: "SM2",
x: x ? base64urlEncode(x) : undefined,
y: y ? base64urlEncode(y) : undefined,
d: d ? base64urlEncode(d) : undefined,
},
};

return jwk;
}

const der =
"MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQg11/tqfkrrIie1cdnkgVPVOYUmnRfrV8CnvXhYU55XCCgCgYIKoEcz1UBgi2hRANCAARA0mjSY26FW4iYQ1PaHB2T1+o2U3XznrNRkQtipZjvJs6Dv79dMTdlc4U4DVceCOrLF8TY5dXF2MNlMI3CBJ/l";
const jwk = privateKeyDerToJwk(der);
console.info({ jwk });

公钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function publicKeyDerToJwk(publicKey) {
const publicKeyDer = Buffer.from(publicKey, "base64");
const pubKeyInfo = SM2PublicKeyASN.decode(publicKeyDer, "der");
const pubKeyBuffer = pubKeyInfo.publicKey.data;
if (pubKeyBuffer.length !== 65 || pubKeyBuffer[0] !== 0x04) {
throw new Error("Invalid SM2 public key format");
}
const x = pubKeyBuffer.slice(1, 33);
const y = pubKeyBuffer.slice(33, 65);
const jwk = {
base64: {
kty: "EC",
crv: "SM2",
x: base64urlEncode(x),
y: base64urlEncode(y),
},
hex: {
kty: "EC",
crv: "SM2",
x: x.toString("hex"),
y: y.toString("hex"),
},
};

return jwk;
}

const publicKeyDer =
"MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEQNJo0mNuhVuImENT2hwdk9fqNlN1856zUZELYqWY7ybOg7+/XTE3ZXOFOA1XHgjqyxfE2OXVxdjDZTCNwgSf5Q==";
const jwk = publicKeyDerToJwk(publicKeyDer);
console.info({ jwk });

JWK 转 DER

参考 附录:Hex 转 DER 部分,使用 04+x+y 的形式组装 Hex 字符串后,再转 DER,此处不再赘述。

References

由 Hexo 驱动 & 主题 Keep