使用国密(SM3WithSM2)对jwt进行签名

环境

jdk8、spring boot 2.3.4、java-jwt 3.11.0、bouncycastle 1.65

背景介绍

在多个系统之间,由于调用链长,使用了jwt token的方式鉴权,然后获取相应的资源,这里用到核心的一点就是jwt的防篡改特性。

以往使用的签名算法大都是HS256(HMAC with SHA-256)、RS256(RSASSA-PKCS1-v1_5 with SHA-256),这次来试试SM3WithSM2签名算法给jwt签名

国密系列简要介绍

国密系列常用的有SM1、SM2、SM3、SM4
SM1 为对称加密。其加密强度与AES相当。该算法不公开,调用该算法时,需要通过加密芯片的接口进行调用。
SM2为非对称加密,基于ECC。该算法已公开。与RSA相比,相同密钥长度下,安全性能更高。计算量小,处理速度快。存储空间占用小 ECC的密钥尺寸和系统参数与RSA、DSA相比要小得多
SM3 消息摘要。可以用MD5作为对比理解。该算法已公开。校验结果为256位。
SM4 无线局域网标准的分组数据算法。对称加密,密钥长度和分组长度均为128位。

参考:
SM2密码算法使用规范
国密算法系列概述

实操

1、首先去git上把开源项目拉下来

https://github.com/ZZMarquis/gmhelper
或直接下载:https://download.csdn.net/download/w57685321/12920144
在github上发现了别人已经实现好了的开源项目,就借鉴借鉴啦,感谢开源项目的分享!

在bouncycastle - 1.57版本之后,加入了对国密SM2、SM3、SM4算法的支持,这个开源项目是个封装或示例
该开源项目具有的功能:
SM2/SM3/SM4算法的简单封装
SM2 X509v3证书的签发
SM2 pfx证书的签发

2、关于曲线参数修改

SM2公钥是SM2曲线上的一个点为Q(x, y),每个分量为256位
如果有修改x或者y参数的需求,那么就在这个SM2Util里面修改这个曲线参数就行了

package org.zz.gmhelper;

public class SM2Util extends GMBaseUtil {
    
    
    //
    /*
     * 以下为SM2推荐曲线参数
     */
    public static final SM2P256V1Curve CURVE = new SM2P256V1Curve();
    public final static BigInteger SM2_ECC_P = CURVE.getQ();
    public final static BigInteger SM2_ECC_A = CURVE.getA().toBigInteger();
    public final static BigInteger SM2_ECC_B = CURVE.getB().toBigInteger();
    public final static BigInteger SM2_ECC_N = CURVE.getOrder();
    public final static BigInteger SM2_ECC_H = CURVE.getCofactor();
    public final static BigInteger SM2_ECC_GX = new BigInteger(
            "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", 16);
    public final static BigInteger SM2_ECC_GY = new BigInteger(
            "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", 16);
    public static final ECPoint G_POINT = CURVE.createPoint(SM2_ECC_GX, SM2_ECC_GY);
    public static final ECDomainParameters DOMAIN_PARAMS = new ECDomainParameters(CURVE, G_POINT,
            SM2_ECC_N, SM2_ECC_H);
    public static final int CURVE_LEN = BCECUtil.getCurveLength(DOMAIN_PARAMS);
}

3、生成证书

首先进入这个类,我选择的是X509规范的证书
org.zz.gmhelper.cert.test.SM2X509CertMakerTest
testMakeCertificate运行这个方法即可生成证书,可以修改SubjectDN、RootCADN这两个标识信息构造(Distinguished Name)的方法

关于DN里字段的含义介绍:https://www.ibm.com/support/knowledgecenter/en/SSFKSJ_7.5.0/com.ibm.mq.sec.doc/q009860_.htm
在这里插入图片描述
这就是生成的证书文件和私钥,这是分开的,如果想要不分开可以使用Pfx、Pkcs12等格式,这个开源项目也提供生成这种类型的方法org.zz.gmhelper.cert.test.SM2PfxMakerTest、SM2Pkcs12MakerTest
在这里插入图片描述
点开证书文件可以发现证书的签名算法变成了SM3WithSM2的oid

关于证书oid标识:

对象标识符 名称 oid
rsaEncryption RSA算法标识 1.2.840.113549.1.1.1
sha1withRSAEncryption SHA1的RSA签名 1.2.840.113549.1.1.5
ECC ECC算法标识 1.2.840.10045.2.1
SM2 SM2算法标识 1.2.156.10197.1.301
SM3WithSM2 SM3的SM2签名 1.2.156.10197.1.501
sha1withSM2 SHA1的SM2签名 1.2.156.10197.1.502
sha256withSM2 SHA256的SM2签名 1.2.156.10197.1.503
sm3withRSAEncryption SM3的RSA签名 1.2.156.10197.1.504
commonName 主体名 2.5.4.3
emailAddress 邮箱 1.2.840.113549.1.9.1
cRLDistributionPoints CRL分发点 2.5.29.31
extKeyUsage 扩展密钥用法 2.5.29.37
subjectAltName 使用者备用名称 2.5.29.17
CP 证书策略 2.5.29.32
clientAuth 客户端认证 1.3.6.1.5.5.7.3.2

4、引入pom

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.65</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
    <version>1.65</version>
</dependency>

引入了jwt和bc库,BouncyCastle是一款开源的密码包,其中包含了大量的密码算法,使用BouncyCastle的目的就是为了扩充算法支持

5、签名验签思路

参考开源项目的org.zz.gmhelper.test.SM2UtilTest类,里面有SM2加密解密,签名验签的方法

根据签名的一般思路: 把需要签名的数据,就是将jwt的header和jwt的payload先base64编码,base64encode(jwt.header) + ‘.’ + base64encode(jwt.payload),然后使用SM3生成它的摘要(tips:如果不生成摘要直接去加密的话,由于加密后密文体积一般都比原文大,特别是非对称加密的情况下,这样很影响性能)

对它的摘要使用SM2算法+私钥进行加密,然后base64编码为可见字符,就得到了我们需要的sign签名值

signature = base64encode(SM2(SM3(base64encode(jwt.header) + ‘.’ + base64encode(jwt.payload)), ‘SECRET_KEY’))

验签: 拿到jwt,用base64解码,再用SM2算法+SM2公钥对signature进行解密,就得到了信息的摘要,然后把信息用相同的算法(SM3)生成摘要与jwt解密后的signature进行对比,一致则验签通过,这样就达到了防篡改的效果

6、编码

有了思路就可以开始编码了,首先我们把开源项目的工具类copy过来
在这里插入图片描述
目录结构就是这样的

首先扩充java-jwt的Algorithm,这些算法它都是调用jce(Java Cryptography Extension) 实现的(我们平常生成AES、DES、MD5等等大都是调用的这个库,还是很强大的)

通过java-jwt的官方git发现它是不支持SM3WithSM2这种签名算法的,那么就自己依葫芦画瓢弄一个

加密算法类com.auth0.jwt.algorithms.Algorithm
支持的加密算法:
JWS Algorithm Description
HS256 HMAC256 HMAC with SHA-256
HS384 HMAC384 HMAC with SHA-384
HS512 HMAC512 HMAC with SHA-512
RS256 RSA256 RSASSA-PKCS1-v1_5 with SHA-256
RS384 RSA384 RSASSA-PKCS1-v1_5 with SHA-384
RS512 RSA512 RSASSA-PKCS1-v1_5 with SHA-512
ES256 ECDSA256 ECDSA with curve P-256 and SHA-256
ES256K ECDSA256 ECDSA with curve secp256k1 and SHA-256
ES384 ECDSA384 ECDSA with curve P-384 and SHA-384
ES512 ECDSA512 ECDSA with curve P-521 and SHA-512

import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.SignatureGenerationException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;

/**
 * 扩充auth0.java-jwt的签名算法
 * SM2是国家密码管理局于2010年12月17日发布的椭圆曲线公钥密码算法
 * 是一种非对称加密算法,证书保存在 /resources/jwt.sm2.cer
 * SM3是中华人民共和国政府采用的一种密码散列函数标准,由国家密码管理局于2010年12月17日发布
 *
 * QA: 为什么使用该系列算法 ===> 支持国产!
 * 基于ECC的SM2证书普遍采用256位密钥长度,加密强度等同于3072位RSA证书,远高于业界普遍采用的2048位RSA证书
 * 测基准试:com.ai.base.tool.JwtTest、com.ai.base.tool.JwtTestSm3WithSm2
 * 对各种算法进行简单的性能测试,SM3WithSM2速度大大快于ECDSA256

 * @see com.auth0.jwt.algorithms.Algorithm
 * 这里使用SM3WithSM2的方式签名、验签,对标SHA256withRSA(RS256)
 * signature = SM2(SM3(base64encode(header) + '.' + base64encode(payload)), 'SECRET_KEY')
 * <p>
 * 签名:用SM3对jwt生成摘要, 再用SM2的私钥对其进行加密(如上面的公式),完成后即生成jwt的signature
 * 验签:拿到jwt,用base64解码,再用SM2算法+SM2公钥对signature进行解密,就得到了信息的摘要,然后把信息用相同的算法(SM3)生成摘要与jwt解密后的signature进行对比,一致则验签通过,这样就达到了防篡改的效果
 *
 * @author Created by zkk on 2020/9/23
 **/
@Slf4j
public class SMAlgorithm extends Algorithm {
    
    

    private final BCECPublicKey publicKey;
    private final BCECPrivateKey privateKey;

    private static final byte JWT_PART_SEPARATOR = (byte) 46;

    protected SMAlgorithm(BCECPublicKey publicKey, BCECPrivateKey privateKey) {
    
    
        super("SM3WithSM2", "SM3WithSM2");
        this.publicKey = publicKey;
        this.privateKey = privateKey;
        if (publicKey == null || privateKey == null) {
    
    
            throw new IllegalArgumentException("The Key Provider cannot be null.");
        }
    }

    @Override
    public void verify(DecodedJWT jwt) throws SignatureVerificationException {
    
    
        byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature());
        byte[] data = combineSignByte(jwt.getHeader().getBytes(), jwt.getPayload().getBytes());
        try {
    
    
            if(!SM2Util.verify(publicKey, data, signatureBytes)) {
    
    
                throw new SignatureVerificationException(this);
            }
        } catch (Exception e) {
    
    
            throw new SignatureVerificationException(this);
        }
    }

    @Override
    @Deprecated
    public byte[] sign(byte[] contentBytes) throws SignatureGenerationException {
    
    
        // 不支持该方法
        throw new RuntimeException("该方法已过时");
    }

    @Override
    public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException {
    
    
        byte[] hash = combineSignByte(headerBytes, payloadBytes);
        byte[] signatureByte;
        try {
    
    
            signatureByte = SM2Util.sign(privateKey, hash);
        } catch (CryptoException e) {
    
    
            throw new SignatureGenerationException(this, e);
        }

        return signatureByte;
    }

    /**
     * 拼接签名部分 header + . + payload
     *
     * @param headerBytes  header
     * @param payloadBytes payload
     * @return bytes
     */
    private byte[] combineSignByte(byte[] headerBytes, byte[] payloadBytes) {
    
    
        // header + payload
        byte[] hash = new byte[headerBytes.length + payloadBytes.length + 1];
        System.arraycopy(headerBytes, 0, hash, 0, headerBytes.length);
        hash[headerBytes.length] = JWT_PART_SEPARATOR;
        System.arraycopy(payloadBytes, 0, hash, headerBytes.length + 1, payloadBytes.length);
        return hash;
    }

    /**
     * builder
     */
    public static class SMAlogrithmBuilder {
    
    
        private BCECPublicKey publicKey;
        private BCECPrivateKey privateKey;

        SMAlogrithmBuilder() {
    
    
        }

        public SMAlgorithm.SMAlogrithmBuilder publicKey(final BCECPublicKey publicKey) {
    
    
            this.publicKey = publicKey;
            return this;
        }

        public SMAlgorithm.SMAlogrithmBuilder privateKey(final BCECPrivateKey privateKey) {
    
    
            this.privateKey = privateKey;
            return this;
        }

        public SMAlgorithm build() {
    
    
            return new SMAlgorithm(this.publicKey, this.privateKey);
        }
    }

    public static SMAlgorithm.SMAlogrithmBuilder builder() {
    
    
        return new SMAlgorithm.SMAlogrithmBuilder();
    }
}

直接调用了SM2Util.这个开源项目提供的工具类签名、验签了
最开始自己写的签名和验签过程,先SM3取摘要然后SM2加密,但是后面发现这个Util提供了这个方法,它是调用的bc框架的org.bouncycastle.crypto.signers.SM2Signer

public class SM2Signer
    implements Signer, ECConstants
{
    
    
	…………
    public SM2Signer()
    {
    
    
        this(StandardDSAEncoding.INSTANCE, new SM3Digest());
    }
    …………
    }

发现它这个签名算法就是用的SM3取的摘要,所以效果是一样的

有了签名算法就可以封装我们的jwt工具类了

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.InputStream;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.util.Objects;

/**
 * 生成jwt的工具类,基于auth0.java-jwt封装
 * 签名算法使用SM3WithSM2
 * payload统一使用Map<String, String>类型
 * @author Created by zkk on 2020/9/22
 **/
@Slf4j
public class JwtHelper {
    
    

    static {
    
    
        Security.addProvider(new BouncyCastleProvider());
        X509Certificate cert;
        try {
    
    
            // 从yml中读取配置
            PropertiesTool propertiesTool = ApplicationContextProvider.getBean(PropertiesTool.class);
            InputStream streamCer = JwtHelper.class.getClassLoader().getResourceAsStream(propertiesTool.getCerFilePath());
            InputStream streamPri = JwtHelper.class.getClassLoader().getResourceAsStream(propertiesTool.getCerPriKeyPath());
            int streamPriLen = Objects.requireNonNull(streamPri).available();

            cert = SM2CertUtil.getX509Certificate(streamCer);

            byte[] priKeyData = new byte[streamPriLen];
            streamPri.read(priKeyData);
            // 从证书中获取公钥,从私钥文件中获取私钥
            publicKey = SM2CertUtil.getBCECPublicKey(cert);
            privateKey = BCECUtil.convertSEC1ToBCECPrivateKey(priKeyData);

        } catch (Exception e) {
    
    
            log.error("JWT工具初始化异常", e);
        }

    }

    /**
     * 设置发行人
     */
    private static final String ISSUER = "zzz";

    /**
     * SM2需要的公钥和私钥
     */
    private static BCECPublicKey publicKey;
    private static BCECPrivateKey privateKey;

    /**
     * 初始化SM3WithSM2算法
     */
    private static final SMAlgorithm ALGORITHM = SMAlgorithm.builder().publicKey(publicKey).privateKey(privateKey).build();

    /**
     * 生成jwt
     * @param claims 携带的payload
     * @return jwt token
     */
    public static String genToken(Map<String, String> claims){
    
    
        try {
    
    
            JWTCreator.Builder builder = JWT.create()
                    .withIssuer(ISSUER);
            claims.forEach(builder::withClaim);
            return builder.sign(ALGORITHM);
        } catch (IllegalArgumentException e) {
    
    
            log.error("jwt生成失败", e);
        }
        return null;
    }

    /**
     * 验签方法
     * @param token jwt token
     * @return jwt payload
     */
    public static Map<String, String> verifyToken(String token) {
    
    
        JWTVerifier verifier = JWT.require(ALGORITHM).withIssuer(ISSUER).build();
        DecodedJWT jwt =  verifier.verify(token);
        Map<String, Claim> map = jwt.getClaims();
        Map<String, String> resultMap = Maps.newHashMap();
        map.forEach((k,v) -> resultMap.put(k, v.asString()));
        return resultMap;
    }
}

ApplicationContextProvider是实现的ApplicationContextAware接口,用于获取bean

通过这个工具类就可以生成和解析jwt了

# jwt需要的证书路径
app:
  jwt:
    certificate:
      filePath: jwt.sm2.cer
      priKeyPath: jwt.sm2.pri

证书文件我直接放在resources目录下的,然后写在yml配置里面

7、单元测试

编码完成后就可以进行愉快的单元测试了

/**
 * @author Created by zkk on 2020/9/24
 **/
@SpringBootTest
class JwtHelperTest {
    
    

    @Test
    void signToken() {
    
    
        HashMap<String, String> map = new HashMap<>();
        map.put("test","test");
        map.put("test4","test");
        map.put("test5","test");
        String token = JwtHelper.genToken(map);
        System.out.println(token);
    }

    @Test
    void verifyToken() {
    
    
        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJTTTNXaXRoU00yIn0.eyJ0ZXN0NCI6InRlc3QiLCJ0ZXN0NSI6InRlc3QiLCJ0ZXN0IjoidGVzdCIsImlzcyI6Inp6eiJ9.MEQCICkcIuJ3cOYCd2wKHOwnt9ZnGcM_6xrNgRy3Bzq905s9AiAc0zzNG4_OhxCCZHMCB9Bg8vSBcLnX5jU1JUS56Hb6fg";
        Map<String, String> map1 = JwtHelper.verifyToken(token);
        System.out.println(map1);
    }
}

在这里插入图片描述
base64解码后发现就是SM3WithSM2的签名算法了
在这里插入图片描述
验签通过,然后获取到payload
在这里插入图片描述
修改sign值,验签失败,就会抛异常,所以在业务中捕获一下异常就可以判断是否验签成功

猜你喜欢

转载自blog.csdn.net/w57685321/article/details/109102706