椭圆曲线公钥密码算法
SM2 是我国自主研发的 256 位椭圆曲线公钥密码算法(ECC),属于国密(商用密码)核心非对称算法,用于替代 RSA/ECDSA/ECDH,主打数字签名、密钥交换、公钥加密三大功能,安全强度等效 RSA-3072、速度远快于 RSA。
曲线方程:
曲线参数,都是 32 字节 长度(256位):
p=FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFFa=FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFCb=28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93n=FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123Gx=32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7Gy=BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0
p: 素数域的模,
- 公式里所有计算最后都需要
mod p - 决定了坐标 x,y 的取值范围
0 ~ p-1
a: 曲线系数b: 曲线系数n: 基点 G 的阶。表示:G 加自己 n 次会回到 “无穷远点”。
G: G 是椭圆曲线上一个固定的点 (Gx, Gy)。
简单来说:p, a, b 定义了曲线本身;(Gx, Gy) 定义起点 G;n 定义私钥能用多大。以上这些值都是在标准/规范中定义好的。
公钥计算:
注意:这里的 * 指的是椭圆曲线的点乘法。也就是从起点 G(Gx,Gy) 经过 k 次计算后得到的结果坐标Q(Qx,Qy) 仍然还在椭圆曲线上。这里的 k 就是私钥。
总结:椭圆曲线密码就是用椭圆曲线上的点做公钥和私钥。公钥可由私钥计算得出,但是不能通过公钥反推出私钥。
参考:
SM2 加解密算法
SM2 加解密算法是基于国家密码管理局发布的《GM/T 0003.4-2012 椭圆曲线公钥密码算法 第 4 部分:公钥加密算法》标准。
椭圆曲线设计的目的是“密钥交换”,而不是“直接加密数据”
椭圆曲线设计的目的是“密钥交换”,而不是“直接加密数据”
椭圆曲线设计的目的是“密钥交换”,而不是“直接加密数据”
加密流程:
- 生成随机数
k,计算临时公钥 C1 = k * G。由坐标 (x,y) 组成,也是 64(32 + 32) 字节长度 - 计算共享密钥
t:
- 根据第一步中的随机数
k 和 公钥 Q 进行点乘,计算椭圆坐标点 S = k * Q。类似公钥计算规则 - 执行密钥派生函数
KDF: t = KDF(Sx||Sy, len(M)),M 表示明文,生成和明文长度一样的密钥 t
- 计算密文
C2:使用上一步生成的共享密钥 t 对明文 M 进行异或操作,得到密文 C2 - 计算校验值
C3: 将椭圆坐标点 S 和 明文 M 的拼接串进行 hash 操作 C3 = SM3(Sx||M||Sy) - 拼接最终密文
C1||C2||C3:SM2 有两种加密模式 C1C2C3 和 C1C3C2。
解密流程:
- 解析密文为
C1C2C3 - 计算共享密钥
t':
- 根据
C1 和 私钥 d 计算椭圆坐标点 S' = d * C1 该点和加密时计算得到的 S 相同,因为 d * C1 = d * (k * G) = k * (d * G) = k * Q - 同样执行密钥派生函数
KDF: t = KDF(Sx||Sy, len(C2)),由于 C2 长度和明文长度相同,这里生成的 t' 和加密时生成的 t 也相同
- 计算明文
M': 使用上一步生成的密钥 t' 对密文 C2 进行异或操作得到明文 M' - 计算校验值
C3' : 将椭圆坐标点 S' 和 明文 M' 的拼接串进行 hash 操作 C3' = SM3(Sx'||M'||Sy')。 - 比较
C3' 和 C3:如果解密得到的 C3' 和接收到的 C3 相同,说明 明文 M' 有效,没有被篡改;否则解密结果就是无效的。
这也就解释了为什么说 椭圆曲线设计的目的是“密钥交换”,而不是“直接加密数据”,加密和解密过程中都能计算得到相同的密钥 t,加密端仅需要将生成的临时公钥给到解密端就i完成了 “密钥交换”,全程没有传输密钥,而实际加密过程是使用的 异或 这种对称的加密处理。
公钥压缩
通过 k * G 计算出来的公钥包含 x 坐标和 y 坐标的完整信息总共 64(32 + 32) 字节长度,但是某些场景需要占用空间更小的公钥数据格式,因此会对公钥信息进行压缩,因此在原本的坐标信息下在开头增加一个字节的标志位:
04: 表示完整的公钥信息,总共 65(1 + 32 + 32) 字节03: 公钥压缩格式,y 坐标是奇数,总共 33(1 + 32) 字节02: 公钥压缩格式,y 坐标是偶数,总共 33(1 + 32) 字节
为什么能压缩?根据椭圆曲线方程 y^2=x^3 + ax + b (mod p) 可知,根据 x 就能计算出 y 的值,但是可以得出两个 y 值,因此使用 02 和 03 标志位加以区分。
因此一个完整的 SM2 公钥信息是应该包括标志位信息的,在处理 SM2 公钥数据时,要根据对应的工具库的处理方式,特别是非压缩格式时,有没有特殊说明需要补充或者截取 04 标志位。
密钥封装
通常在加解密工程中:d 值指的就是原始的 密钥 数据,而 q 值指的原始的 公钥 数据。为了让公私钥数据在不同的系统或语言中能够通用,通常会在*原始数据**之上封装一层格式,增加了算法类型等元数据信息,常见封装格式:
| 格式 | 数据格式 | 封装对象 | 核心用途 | 典型特征 | 常见算法 |
|---|
| X.509 | ASN.1 | 公钥(仅公钥) | 公钥的标准化传输 / 存储 | Base64 编码,带-----BEGIN PUBLIC KEY-----头 | SM2/RSA/ECDSA |
| PKCS#1 | ASN.1 | RSA 私钥(仅 RSA) | RSA 私钥的传统封装 | Base64 编码,带-----BEGIN RSA PRIVATE KEY-----头 | 仅 RSA |
| PKCS#8 | ASN.1 | 任意私钥(通用) | 私钥的通用标准化封装 | Base64 编码,带-----BEGIN PRIVATE KEY-----头 | SM2/RSA/ECDSA |
像后端 Hutool 这种通用工具类,生成的 密钥对 都是带格式封装的,为了通用性;而像前端 sm-crypto.js 这种专门针对 SM2 的加解密工具,它所使用的都是原始的密钥数据。因此,在使用不同的加解密工具时,需要注意数据格式的处理和转换。
后端工具
Hutool SmUtil 使用详解:
无参实例化 SM2 工具
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.SM2;
// 初始化 SM2 实例,无参构造,会自动创建新的密钥对
SM2 sm2 = SmUtil.sm2();
// 设置加密模式:默认是 C1C3C2
sm2.setMode(SM2Engine.Mode.C1C3C2);
// 直接获取公钥和私钥信息
String privateKey = sm2.getPrivateKeyBase64();
String publicKey = sm2.getPublicKeyBase64();
// 获取原始密钥对信息
byte[] dBytes = sm2.getD();
byte[] qBytes = sm2.getQ(false); // false 表示无压缩公钥信息
有参实例化 SM2 工具
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.SM2;
// 缓存中获取公钥和私钥信息
String privateKeyStr = "xxxxxxx";
String publicKeyStr = "xxxxxxx";
// 初始化 SM2 实例,有参构造,使用已有密钥数据
// 格式可以是 HexStr 或 Base64 形式的字符串
// 也可以单独传入一个,只用来进行加密或解密
SM2 sm2 = SmUtil.sm2(privateKeyStr, publicKeyStr);
// 设置加密模式:默认是 C1C3C2
sm2.setMode(SM2Engine.Mode.C1C3C2);
// 获取原始密钥对信息
byte[] dBytes = sm2.getD();
byte[] qBytes = sm2.getQ(false); // false 表示无压缩公钥信息
加密数据
String publicKeyStr = "xxxx"; // 公钥数据
String plainText = "xxxxxx"; // 待加密数据
// 初始化 SM2,加密端通常不具有私钥信息,因此私钥可以为空
SM2 sm2 = SmUtil.sm2(null, publicKeyStr);
// 设置加密模式:默认是 C1C3C2
sm2.setMode(SM2Engine.Mode.C1C3C2);
try {
byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8);
byte[] cipherBytes = sm2.encrypt(bytes, KeyType.PublicKey);
String cipherText = Base64.encode(cipherBytes);
} catch (Exception e) {
// 加密失败
}
解密数据
String publicKeyStr = "xxxx"; // 公钥数据
String privateKeyStr = "xxxx"; // 私钥数据
String cipherText = "xxxxxx"; // 加密数据,假如 base64 格式
// 初始化 SM2
SM2 sm2 = SmUtil.sm2(privateKeyStr, publicKeyStr);
// 设置加密模式:默认是 C1C3C2
sm2.setMode(SM2Engine.Mode.C1C3C2);
try {
byte[] cipherBytes = Base64.decode(cipherText);
byte[] plainBytes = sm2.decrypt(cipherBytes, KeyType.PrivateKey);
String plainText = new String(plainBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
// 解密失败
}
注意:通过 sm2.getXXXKeyBase64() 获取到的是 base64 编码,且带有 X.509 或 PKCS#8 封装格式的,前端 sm-crypto.js 需要的是原始的 公钥数据(不带封装格式的 Hex 字符串),因此可以直接使用 String pubkey = HexUtil.encodeHexStr(sm2.getQ(false)); 获取原始公钥数据的 Hex 字符串。
前端工具
sm-crypto.js 使用详解:
生成密钥对
import{sm2}from'sm-crypto'constkeypair=sm2.generateKeyPairHex()constpublicKey=keypair.publicKey// 公钥,Hex 字符串constprivateKey=keypair.privateKey// 私钥,Hex 字符串加密数据
import{sm2}from'sm-crypto'constcipherMode=1;// 加密模式:1 - C1C3C2,0 - C1C2C3,默认为1constpublicKey="xxxxx";// web 作为加密端,通常公钥是从后台获取的,不需要本地生成constplainText="xxxx";// 待加密数据// 注意这里加密生成的 C1 没有 04 前缀,如果给 Hutool 工具使用,最好手动添加 04,否则可能服务端解密失败consthexStr="04"+sm2.doEncrypt(plainText,publicKey,cipherMode);// 另外 base64 字符串比 Hex 字符串更节省空间,通常会进行转换constcipherText=hex2Base64(hexStr);// hex2Base64 是工具函数解密数据
import{sm2}from'sm-crypto'// 如果需要服务端加密,客户端解密。则需要客户端生成密钥,// 客户端请求接口时需将 publicKey 放置在 header 中给到服务端// 服务端响应数据时,则使用客户端传递的 publicKey 进行加密// 客户端收到响应之后,再使用本地的 privateKey 进行解密constkeypair=sm2.generateKeyPairHex()constcipherMode=1;// 加密模式:1 - C1C3C2,0 - C1C2C3,默认为1constpublicKey=keypair.publicKey// 公钥,Hex 字符串。请求接口时需要给到后端constprivateKey=keypair.privateKey// 私钥,Hex 字符串constcipherText="xxxx";// 待解密数据,假设为 base64 字符串constcipherBytes=base64ToBytes(cipherText);// base64ToBytes 是 base64 转 Uint8Array 工具函数consthexStr=bytesToHex(cipherBytes);// bytesToHex 是 Uint8Array 转 hex 字符串工具// 解密服务端数据// 这里将 hexStr 进行了截取,去掉了头两个字符串,因为 sm-crypto.js 在解密时会自动添加 04 前缀// 而服务端 Hutool 工具生成的密文包含 04 前缀,因此这里需要手动截取掉constplainText=sm2.doDecrypt(hexStr.slice(2),privateKey,cipherMode)服务端客户端加解密交互时需要注意以下两点:
- 加密模式
cipherMode 要保持一致:都是 C1C3C2 或者 C1C2C3 模式。 C1 临时公钥格式处理:是否需要手动添加或截取开头的 04 前缀。- 公钥
publicKey 格式确认:是否包含 X.509 格式封装,如果各端公钥格式要求不一样,则需要手动转换。 - 数据 base64 和 hex 字符串处理:确认各端
sm2 需要的数据格式,看是否需要进行转换。
椭圆曲线公钥密码算法
SM2 是我国自主研发的 256 位椭圆曲线公钥密码算法(ECC),属于国密(商用密码)核心非对称算法,用于替代 RSA/ECDSA/ECDH,主打数字签名、密钥交换、公钥加密三大功能,安全强度等效 RSA-3072、速度远快于 RSA。
曲线方程:
曲线参数,都是 32 字节 长度(256位):
p: 素数域的模,mod p0 ~ p-1a: 曲线系数b: 曲线系数n: 基点G的阶。表示:G加自己n次会回到 “无穷远点”。k的范围:1 ≤ k ≤ n-2G: G 是椭圆曲线上一个固定的点 (Gx, Gy)。简单来说:
p, a, b定义了曲线本身;(Gx, Gy)定义起点 G;n定义私钥能用多大。以上这些值都是在标准/规范中定义好的。公钥计算:
注意:这里的
*指的是椭圆曲线的点乘法。也就是从起点G(Gx,Gy)经过k次计算后得到的结果坐标Q(Qx,Qy)仍然还在椭圆曲线上。这里的k就是私钥。参考:
SM2 加解密算法
SM2加解密算法是基于国家密码管理局发布的《GM/T 0003.4-2012 椭圆曲线公钥密码算法 第 4 部分:公钥加密算法》标准。椭圆曲线设计的目的是“密钥交换”,而不是“直接加密数据”
椭圆曲线设计的目的是“密钥交换”,而不是“直接加密数据”
椭圆曲线设计的目的是“密钥交换”,而不是“直接加密数据”
加密流程:
k,计算临时公钥C1 = k * G。由坐标(x,y)组成,也是64(32 + 32)字节长度t:k和 公钥Q进行点乘,计算椭圆坐标点S = k * Q。类似公钥计算规则KDF:t = KDF(Sx||Sy, len(M)),M表示明文,生成和明文长度一样的密钥tC2:使用上一步生成的共享密钥t对明文M进行异或操作,得到密文C2C3: 将椭圆坐标点S和 明文M的拼接串进行hash操作C3 = SM3(Sx||M||Sy)C1||C2||C3:SM2有两种加密模式C1C2C3和C1C3C2。解密流程:
C1C2C3t':C1和 私钥d计算椭圆坐标点S' = d * C1该点和加密时计算得到的S相同,因为d * C1 = d * (k * G) = k * (d * G) = k * QKDF:t = KDF(Sx||Sy, len(C2)),由于C2长度和明文长度相同,这里生成的t'和加密时生成的t也相同M': 使用上一步生成的密钥t'对密文C2进行异或操作得到明文M'C3': 将椭圆坐标点S'和 明文M'的拼接串进行hash操作C3' = SM3(Sx'||M'||Sy')。C3'和C3:如果解密得到的C3'和接收到的C3相同,说明 明文M'有效,没有被篡改;否则解密结果就是无效的。公钥压缩
通过
k * G计算出来的公钥包含 x 坐标和 y 坐标的完整信息总共64(32 + 32)字节长度,但是某些场景需要占用空间更小的公钥数据格式,因此会对公钥信息进行压缩,因此在原本的坐标信息下在开头增加一个字节的标志位:04: 表示完整的公钥信息,总共65(1 + 32 + 32)字节03: 公钥压缩格式,y坐标是奇数,总共33(1 + 32)字节02: 公钥压缩格式,y坐标是偶数,总共33(1 + 32)字节因此一个完整的
SM2公钥信息是应该包括标志位信息的,在处理SM2公钥数据时,要根据对应的工具库的处理方式,特别是非压缩格式时,有没有特殊说明需要补充或者截取04标志位。密钥封装
通常在加解密工程中:
d值指的就是原始的 密钥 数据,而q值指的原始的 公钥 数据。为了让公私钥数据在不同的系统或语言中能够通用,通常会在*原始数据**之上封装一层格式,增加了算法类型等元数据信息,常见封装格式:-----BEGIN PUBLIC KEY-----头-----BEGIN RSA PRIVATE KEY-----头-----BEGIN PRIVATE KEY-----头像后端
Hutool这种通用工具类,生成的 密钥对 都是带格式封装的,为了通用性;而像前端sm-crypto.js这种专门针对SM2的加解密工具,它所使用的都是原始的密钥数据。因此,在使用不同的加解密工具时,需要注意数据格式的处理和转换。后端工具
Hutool SmUtil 使用详解:
无参实例化
SM2工具有参实例化
SM2工具加密数据
解密数据
前端工具
sm-crypto.js 使用详解:
生成密钥对
加密数据
解密数据
服务端客户端加解密交互时需要注意以下两点:
cipherMode要保持一致:都是C1C3C2或者C1C2C3模式。C1临时公钥格式处理:是否需要手动添加或截取开头的04前缀。publicKey格式确认:是否包含 X.509 格式封装,如果各端公钥格式要求不一样,则需要手动转换。sm2需要的数据格式,看是否需要进行转换。