文章目录
更多相关内容可查看
私钥签名,公钥验签
国密算法查询地址
国家标准全文公开系统:https://openstd.samr.gov.cn/bzgk/gb/
查看SM2的对应国家文档
在线预览
SM2背景
SM2算法全称是SM2椭圆曲线公钥密码算法(SM是商用密码的拼音缩写),是一种基于“椭圆曲线”的密码 ECC(Elliptic CurveCryptography)。2016年,SM2成为中国国家密码标准。在商用密码体系中,SM2主要用于替换RSA加密算法。
SM2算法是中国国家密码局
推出的国产化算法,和RSA算法一样,同属于非对称算法体系,而且是椭圆曲线加密(ECC)算法的一种。但与RSA算法不同的是:RSA算法是基于大整数分解数学难题,SM2算法是基于椭圆曲线上点群离散对数难题
SM2椭圆曲线公式
之所以叫椭圆曲线,是因为他的公式很像椭圆曲线周长的公式,而不是他的图形是椭圆
椭圆曲线计算
在密码学中,我们通常在有限域上定义椭圆曲线,这样曲线上的点就构成了一个有限的集合。椭圆曲线的点具有加法和乘法运算规则,这些运算规则构成了 SM2 算法的数学基础
计算公式:A+B取与曲线的交点然后做对称
AB重叠的计算公式(2A运算):2A取与曲线的交点然后做对称
G点:就是AB重合的点就叫做G点
有限域上的椭圆曲线
GF(p):p就是其中的质数,比如p是20,那GF(20)就是0,1,2,…20
计算公式:这0-20的数的集合只要满足图中的公式就是图片中的点
有限域的加法计算
举例计算
SM2加密流程
总流程:Bob生成一对公钥私钥,公钥(Bob)传给 Alice ,Alice拿公钥(Bob)对明文进行加密变为密文,传给Bob,Bob用私钥进行解密
流程拆解:
1. Bob生成密钥对:
通俗点理解就是私钥是一个随机数生成的,命名为d,公钥是用d进行G运算,就是上文提到的G点,去计算2G,3G
2. Alice拿公钥(Bob)对明文进行加密:
通俗理解的算法步骤如下:
- 生成随机数K
- C1=【k】G=(x1,y1):相当于对k做了一个G运算
- S = 【h】Pb :这个Pb就是公钥,相当于对公钥做了一个运算,得出来的值去做一个判断是否是无穷远点
- 【k】Pb = (X2,Y2):对公钥进行计算生成一个坐标点用于下面的计算
- t=KDF(X2 || Y2,klen) :KDF意思就是会生成一个klen长度的比特串,klen的长度就是明文的比特长度
- C2 = M 异或 t :因为M t都是klen,所以C2就是Klen的长度
- C3 = Hash(X2 || M || Y2):相当于对前面取得值做一个Hash运算,但是这个Hash不是国际Hash,因为要做国产化,所以这个Hash运算是SM3的一部分
- C = C1||C3||C2:意思就是最终的密文加密的方式是这C1,C2,C3的值组合而成的密文
3. Bob用私钥进行解密:
通俗理解的算法步骤如下:
- 密文的结构:C = C1||C3||C2
- 拿C1去验证是否是椭圆曲线上的点,不是就报错
- 拿C1计算出S,看S是否是无穷远的点,不是就报错
- 用私钥【Db】进行解密,得到(x2,y2)
- KDF计算算出t
- 拿t跟C2做异或,得到M,M就是明文,但是这里明文是否输出还要进行下面的验证
- 拿到上述的M跟(x2,y2)做一个Hash运算得到u,u跟C3如果不想等就报错,相等就最终输出明文M
SM2签名公式
在数字签名过程中,用户使用私钥对消息进行签名。具体步骤如下:
- 计算消息的哈希值 (e)。
- 生成一个随机数 (k),计算点 (Q = k \cdot G),得到 (Q) 的 (x) 坐标 (x1)。
- 计算签名值 (r = (e + x1) \bmod n)((n) 为椭圆曲线的阶)。
- 计算 (s = (1 + d)^{-1} \cdot (k - r \cdot d) \bmod n)。
- 签名结果为 ((r, s))。
SM2验签公式
验证签名时,接收方使用发送方的公钥 (P) 来验证签名的有效性。具体步骤如下:
- 计算消息的哈希值 (e)。
- 计算 (t = (r + s) \bmod n)。
- 计算点 (R = s \cdot G + t \cdot P),得到 (R) 的 (x) 坐标 (x1)。
- 验证 (r = (e + x1) \bmod n) 是否成立。如果成立,则签名有效;否则,签名无效。
SM2在JAVA中的使用(JDK17)
引入pom依赖
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<!--注意检查是否已经引入lombok依赖,已经引入则不需要此依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
密钥对工具类
package com.hl.sm2demo.util;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
/**
* @ Description 国密公私钥对工具类
*/
@Slf4j
public class KeyUtils {
public static final String PUBLIC_KEY = "publicKey";
public static final String PRIVATE_KEY = "privateKey";
/**
* 生成国密公私钥对
*/
public static Map<String, String> generateSmKey() throws Exception {
KeyPairGenerator keyPairGenerator = null;
SecureRandom secureRandom = new SecureRandom();
ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
keyPairGenerator = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());
keyPairGenerator.initialize(sm2Spec);
keyPairGenerator.initialize(sm2Spec, secureRandom);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
String publicKeyStr = new String(Base64.getEncoder().encode(publicKey.getEncoded()));
String privateKeyStr = new String(Base64.getEncoder().encode(privateKey.getEncoded()));
return Map.of(PUBLIC_KEY, publicKeyStr, PRIVATE_KEY, privateKeyStr);
}
/**
* 将Base64转码的公钥串,转化为公钥对象
*/
public static PublicKey createPublicKey(String publicKey) {
PublicKey publickey = null;
try {
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey));
KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());
publickey = keyFactory.generatePublic(publicKeySpec);
} catch (Exception e) {
log.error("将Base64转码的公钥串,转化为公钥对象异常:{}", e.getMessage(), e);
}
return publickey;
}
/**
* 将Base64转码的私钥串,转化为私钥对象
*/
public static PrivateKey createPrivateKey(String privateKey) {
PrivateKey publickey = null;
try {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey));
KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());
publickey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
} catch (Exception e) {
log.error("将Base64转码的私钥串,转化为私钥对象异常:{}", e.getMessage(), e);
}
return publickey;
}
}
1.生成国密公私钥对:
方法generateSmKey()使用KeyPairGenerator生成一个基于椭圆曲线"sm2p256v1"的KeyPair,这是SM2算法所使用的曲线。
公钥和私钥分别编码为Base64字符串,然后以键值对的形式存储在Map中,键分别为PUBLIC_KEY和PRIVATE_KEY。
注意:实际开发中,我们可能会要求生成的公私钥对是稳定不变的,我们可以先提前使用上面的KeyUtils 工具类方法generateSmKey()生成好公私钥字符串,加密解密需要用到时,再使用其中的转公私钥对象方法createPublicKey()、createPrivateKey()。
2.公钥对象的转换:
方法createPublicKey(String publicKey)接收一个Base64编码的公钥字符串,通过X509EncodedKeySpec解析成公钥对象。这里使用了KeyFactory实例化EC类型的公钥。
3.私钥对象的转换:
方法createPrivateKey(String privateKey)类似地,接收一个Base64编码的私钥字符串,通过PKCS8EncodedKeySpec解析成私钥对象。同样使用KeyFactory实例化EC类型的私钥。
SM2工具类
package com.hl.sm2demo.util;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.gm.GMObjectIdentifiers;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import java.security.*;
/**
* @ Description SM2实现工具类
*/
@Slf4j
public class Sm2Util {
/* 这行代码是在Java中用于向安全系统添加Bouncy Castle安全提供器的。
Bouncy Castle是一个流行的开源加密库,它提供了许多密码学算法和安全协议的实现。
通过调用Security.addProvider并传入BouncyCastleProvider对象,你可以注册Bouncy Castle提供的安全服务和算法到Java的安全框架中。
这样一来,你就可以在你的应用程序中使用Bouncy Castle所提供的加密算法、密钥生成和管理等功能。*/
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* 根据publicKey对原始数据data,使用SM2加密
*/
public static byte[] encrypt(byte[] data, PublicKey publicKey) {
ECPublicKeyParameters localECPublicKeyParameters = getEcPublicKeyParameters(publicKey);
SM2Engine localSM2Engine = new SM2Engine();
localSM2Engine.init(true, new ParametersWithRandom(localECPublicKeyParameters, new SecureRandom()));
byte[] arrayOfByte2;
try {
arrayOfByte2 = localSM2Engine.processBlock(data, 0, data.length);
return arrayOfByte2;
} catch (InvalidCipherTextException e) {
log.error("SM2加密失败:{}", e.getMessage(), e);
return null;
}
}
private static ECPublicKeyParameters getEcPublicKeyParameters(PublicKey publicKey) {
ECPublicKeyParameters localECPublicKeyParameters = null;
if (publicKey instanceof BCECPublicKey localECPublicKey) {
ECParameterSpec localECParameterSpec = localECPublicKey.getParameters();
ECDomainParameters localECDomainParameters = new ECDomainParameters(localECParameterSpec.getCurve(),
localECParameterSpec.getG(), localECParameterSpec.getN());
localECPublicKeyParameters = new ECPublicKeyParameters(localECPublicKey.getQ(), localECDomainParameters);
}
return localECPublicKeyParameters;
}
/**
* 根据privateKey对加密数据encode data,使用SM2解密
*/
public static byte[] decrypt(byte[] encodeData, PrivateKey privateKey) {
SM2Engine localSM2Engine = new SM2Engine();
BCECPrivateKey sm2PriK = (BCECPrivateKey) privateKey;
ECParameterSpec localECParameterSpec = sm2PriK.getParameters();
ECDomainParameters localECDomainParameters = new ECDomainParameters(localECParameterSpec.getCurve(),
localECParameterSpec.getG(), localECParameterSpec.getN());
ECPrivateKeyParameters localECPrivateKeyParameters = new ECPrivateKeyParameters(sm2PriK.getD(),
localECDomainParameters);
localSM2Engine.init(false, localECPrivateKeyParameters);
try {
return localSM2Engine.processBlock(encodeData, 0, encodeData.length);
} catch (InvalidCipherTextException e) {
log.error("SM2解密失败:{}", e.getMessage(), e);
return null;
}
}
/**
* 私钥签名
*/
public static byte[] signByPrivateKey(byte[] data, PrivateKey privateKey) throws Exception {
Signature sig = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), BouncyCastleProvider.PROVIDER_NAME);
sig.initSign(privateKey);
sig.update(data);
return sig.sign();
}
/**
* 公钥验签
*/
public static boolean verifyByPublicKey(byte[] data, PublicKey publicKey, byte[] signature) throws Exception {
Signature sig = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), BouncyCastleProvider.PROVIDER_NAME);
sig.initVerify(publicKey);
sig.update(data);
return sig.verify(signature);
}
}
1.加密:
encrypt方法使用SM2算法对原始数据进行加密。它首先获取ECPublicKeyParameters对象,然后初始化SM2Engine并进行加密处理。如果加密过程中出现InvalidCipherTextException,则记录错误日志并返回null。
2.解密:
decrypt方法根据私钥解密加密数据。同样,初始化SM2Engine,进行解密处理。如果解密过程中出现InvalidCipherTextException,则记录错误日志并返回null。
3.签名:
signByPrivateKey方法使用私钥对数据进行签名。首先,创建一个Signature实例,指定SM2签名算法,初始化签名器,更新数据,然后生成签名。
4.验证:
verifyByPublicKey方法使用公钥验证签名。创建Signature实例,指定SM2签名算法,初始化验证器,更新数据,然后执行验证。返回验证结果,即布尔值表示签名是否有效
案例测试Junit
package com.hl.sm2demo;
import com.hl.sm2demo.util.KeyUtils;
import com.hl.sm2demo.util.Sm2Util;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
import java.util.Map;
@SpringBootTest
class Sm2DemoApplicationTests {
PublicKey publicKey = null;
PrivateKey privateKey = null;
@Test
public void test() throws Exception {
//生成公私钥对
Map<String,String> keys = KeyUtils.generateSmKey();
String testStr = "hello JAVA";
System.out.println("原始字符串:" + testStr);
System.out.println("公钥:" + keys.get(KeyUtils.PUBLIC_KEY));
publicKey = KeyUtils.createPublicKey(keys.get(KeyUtils.PUBLIC_KEY));
System.out.println("私钥:" + keys.get(KeyUtils.PRIVATE_KEY));
privateKey = KeyUtils.createPrivateKey(keys.get(KeyUtils.PRIVATE_KEY));
System.out.println();
//公钥加密
byte[] encrypt = Sm2Util.encrypt(testStr.getBytes(), publicKey);
//加密转base64
String encryptBase64Str = Base64.getEncoder().encodeToString(encrypt);
System.out.println("加密数据:" + encryptBase64Str);
//私钥签名,方便对方收到数据后用公钥验签
byte[] sign = Sm2Util.signByPrivateKey(testStr.getBytes(), privateKey);
System.out.println("数据签名:" + Base64.getEncoder().encodeToString(sign));
//公钥验签,验证通过后再进行数据解密
boolean b = Sm2Util.verifyByPublicKey(testStr.getBytes(), publicKey, sign);
System.out.println("数据验签:" + b);
//私钥解密
byte[] decode = Base64.getDecoder().decode(encryptBase64Str);
byte[] decrypt = Sm2Util.decrypt(decode, privateKey);
assert decrypt != null;
System.out.println("解密数据:" + new String(decrypt));
}
}
测试代码主要逻辑:
密钥准备:首先生成一个公私钥对字符串,再使用工具分别转换为公私钥的java对象;
加密过程:把数据使用公钥加密,把密文转换成base64编码格式,再用私钥签名;
解密过程:拿到加密数据后用公钥验签,以确保密文数据没有被第三方拦截篡改,验证通过后,base64解码,最后使用私钥进行解密,还原数据;
SM2与RSA算法的对比
- 数学基础
- SM2:基于
椭圆曲线离散对数问题
(ECDLP)。椭圆曲线是一种特定类型的代数曲线,在有限域上定义。SM2 利用椭圆曲线上点的运算特性来构建密码系统,离散对数问题在椭圆曲线环境下被认为是一个困难问题,为密码系统提供了安全性基础
。 - RSA:基于
大整数分解问题
(IFP)。RSA 算法依赖于将两个大素数相乘容易,但将乘积分解回原来的两个素数非常困难这一特性。通过对大整数进行特定的运算来实现加密、解密和签名等功能
。
- 密钥长度
- SM2:常用的密钥长度
192 ~ 256 位
。虽然密钥长度相对较短,但基于椭圆曲线的数学特性,能够提供与较长密钥长度的其他算法相当的安全性。 - RSA:在目前的安全要求下,通常使用
1024 位
、2048 位甚至更长的密钥长度。目前普遍认为1024位算法不再安全
。
- 安全性
- SM2:由于椭圆曲线离散对数问题的困难性,在相同的安全强度下,SM2 算法的密钥长度可以比 RSA 短很多。较短的密钥长度不仅减少了存储和传输的开销,同时也降低了因密钥管理不当而带来的风险。而且,SM2 算法是
我国自主设计的密码算法,不存在国外算法可能存在的后门等安全隐患,具有更高的自主性和可控性
。 - RSA:其安全性依赖于大整数分解的难度。随着计算技术的发展,尤其是
量子计算技术的潜在威胁
,大整数分解问题可能在未来变得相对容易解决。一旦量子计算机能够有效地进行大整数分解,RSA 算法的安全性将受到严重挑战。
- 性能
- 加密和解密速度:
- SM2:在加密和解密操作时,由于其基于椭圆曲线的运算,计算量相对较小,尤其是在
处理短消息时,速度较快
。但对于长消息,需要对消息进行分段处理
,可能会增加一定的处理时间。 - RSA:加密和解密操作涉及到
大整数的乘法和幂运算,计算量较大,速度相对较慢
。特别是在处理长消息时,RSA 的性能瓶颈更加明显。
- SM2:在加密和解密操作时,由于其基于椭圆曲线的运算,计算量相对较小,尤其是在
- 签名和验证速度:
- SM2:签名和验证过程相对高效,
签名长度较短,验证速度也较快
。 - RSA:
签名和验证操作需要进行复杂的大整数运算,签名长度较长,验证速度相对较慢
。
- SM2:签名和验证过程相对高效,
- 应用场景
- SM2:广泛应用于我国的
金融、政务、物联网
等领域。在金融领域,用于网上银行、电子支付等场景的身份认证和数据加密;在政务领域,保障电子公文传输、政务系统登录等的安全性;在物联网领域,由于其密钥长度短、性能高的特点,适合资源受限的物联网设备之间的通信安全。 - RSA:在国际上广泛应用于各种网络安全场景,如
SSL/TLS 协议、数字证书
等。在互联网早期,RSA 由于其通用性和广泛的支持,成为了许多安全应用的首选算法。但随着对安全性和性能要求的提高,在一些特定场景下,逐渐被更高效、安全的算法所替代。
- 兼容性
- SM2:作为我国自主的密码算法,在
国内得到了广泛的支持和推广
。但在国际上,由于其相对较新且应用范围相对较窄,与一些传统的国际系统和软件的兼容性可能存在一定问题。 - RSA:经过多年的发展和应用,已经成为
国际上广泛认可和支持的标准算法
。几乎所有的操作系统、浏览器、服务器等都支持 RSA 算法,具有良好的兼容性。
- 标准和规范
- SM2:由我国国家密码管理局发布标准,在
国内有完善的标准体系和规范
。随着我国对自主可控密码技术的推广,SM2 的相关标准和规范也在不断完善和发展。 - RSA:有一系列
国际标准和规
范,如 PKCS 系列标准等。这些标准在全球范围内被广泛遵循,确保了 RSA 算法在不同系统和应用之间的互操作性。