【国密算法】SM2椭圆曲线公钥密码算法原理、数据加密、签名、验签、解密(详解与应用)

在这里插入图片描述


更多相关内容可查看

私钥签名,公钥验签

国密算法查询地址

国家标准全文公开系统: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签名公式

在数字签名过程中,用户使用私钥对消息进行签名。具体步骤如下:

  1. 计算消息的哈希值 (e)。
  2. 生成一个随机数 (k),计算点 (Q = k \cdot G),得到 (Q) 的 (x) 坐标 (x1)。
  3. 计算签名值 (r = (e + x1) \bmod n)((n) 为椭圆曲线的阶)。
  4. 计算 (s = (1 + d)^{-1} \cdot (k - r \cdot d) \bmod n)。
  5. 签名结果为 ((r, s))。

SM2验签公式

验证签名时,接收方使用发送方的公钥 (P) 来验证签名的有效性。具体步骤如下:

  1. 计算消息的哈希值 (e)。
  2. 计算 (t = (r + s) \bmod n)。
  3. 计算点 (R = s \cdot G + t \cdot P),得到 (R) 的 (x) 坐标 (x1)。
  4. 验证 (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算法的对比

  1. 数学基础
  • SM2:基于椭圆曲线离散对数问题(ECDLP)。椭圆曲线是一种特定类型的代数曲线,在有限域上定义。SM2 利用椭圆曲线上点的运算特性来构建密码系统,离散对数问题在椭圆曲线环境下被认为是一个困难问题,为密码系统提供了安全性基础
  • RSA:基于大整数分解问题(IFP)。RSA 算法依赖于将两个大素数相乘容易,但将乘积分解回原来的两个素数非常困难这一特性。通过对大整数进行特定的运算来实现加密、解密和签名等功能
  1. 密钥长度
  • SM2:常用的密钥长度192 ~ 256 位。虽然密钥长度相对较短,但基于椭圆曲线的数学特性,能够提供与较长密钥长度的其他算法相当的安全性。
  • RSA:在目前的安全要求下,通常使用 1024 位、2048 位甚至更长的密钥长度。目前普遍认为1024位算法不再安全
  1. 安全性
  • SM2:由于椭圆曲线离散对数问题的困难性,在相同的安全强度下,SM2 算法的密钥长度可以比 RSA 短很多。较短的密钥长度不仅减少了存储和传输的开销,同时也降低了因密钥管理不当而带来的风险。而且,SM2 算法是我国自主设计的密码算法,不存在国外算法可能存在的后门等安全隐患,具有更高的自主性和可控性
  • RSA:其安全性依赖于大整数分解的难度。随着计算技术的发展,尤其是量子计算技术的潜在威胁,大整数分解问题可能在未来变得相对容易解决。一旦量子计算机能够有效地进行大整数分解,RSA 算法的安全性将受到严重挑战。
  1. 性能
  • 加密和解密速度
    • SM2:在加密和解密操作时,由于其基于椭圆曲线的运算,计算量相对较小,尤其是在处理短消息时,速度较快。但对于长消息,需要对消息进行分段处理,可能会增加一定的处理时间。
    • RSA:加密和解密操作涉及到大整数的乘法和幂运算,计算量较大,速度相对较慢。特别是在处理长消息时,RSA 的性能瓶颈更加明显。
  • 签名和验证速度
    • SM2:签名和验证过程相对高效,签名长度较短,验证速度也较快
    • RSA签名和验证操作需要进行复杂的大整数运算,签名长度较长,验证速度相对较慢
  1. 应用场景
  • SM2:广泛应用于我国的金融、政务、物联网等领域。在金融领域,用于网上银行、电子支付等场景的身份认证和数据加密;在政务领域,保障电子公文传输、政务系统登录等的安全性;在物联网领域,由于其密钥长度短、性能高的特点,适合资源受限的物联网设备之间的通信安全。
  • RSA:在国际上广泛应用于各种网络安全场景,如 SSL/TLS 协议、数字证书等。在互联网早期,RSA 由于其通用性和广泛的支持,成为了许多安全应用的首选算法。但随着对安全性和性能要求的提高,在一些特定场景下,逐渐被更高效、安全的算法所替代。
  1. 兼容性
  • SM2:作为我国自主的密码算法,在国内得到了广泛的支持和推广。但在国际上,由于其相对较新且应用范围相对较窄,与一些传统的国际系统和软件的兼容性可能存在一定问题。
  • RSA:经过多年的发展和应用,已经成为国际上广泛认可和支持的标准算法。几乎所有的操作系统、浏览器、服务器等都支持 RSA 算法,具有良好的兼容性。
  1. 标准和规范
  • SM2:由我国国家密码管理局发布标准,在国内有完善的标准体系和规范。随着我国对自主可控密码技术的推广,SM2 的相关标准和规范也在不断完善和发展。
  • RSA:有一系列国际标准和规范,如 PKCS 系列标准等。这些标准在全球范围内被广泛遵循,确保了 RSA 算法在不同系统和应用之间的互操作性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

来一杯龙舌兰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值