使用国密(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值,验签失败,就会抛异常,所以在业务中捕获一下异常就可以判断是否验签成功
更多推荐
所有评论(0)