hutool国密sm2算法使用, 正确的秘钥生成签名及验签,签名为64字节
hutool工具类:
在糊涂提供的国密算法,需要通过椭圆曲线生成秘钥,且当前业内私钥长度为固定32字节,公用固定长度为64字节。在参考hutool官方文档中的国密算法的例子,发现生成的秘钥非常长,远大于32字节和64字节,生成的签名长度也不是64字节。
问题描述:
官方提供如下例子,用于演示签名和验签
String content = "我是Hanley.";
KeyPair pair = SecureUtil.generateKeyPair("SM2");
final SM2 sm2 = new SM2(pair.getPrivate(), pair.getPublic());
byte[] sign = sm2.sign(content.getBytes());
// true
boolean verify = sm2.verify(content.getBytes(), sign);
此例子可以跑通,但是有以下几个问题:
- 生成的公钥和私钥都非常长,远大于上文说的
私钥长度为固定32字节,公钥长度为64字节
。 - 没有给出通过制定的公钥或者私钥来进行单独的验签和签名
官方提供如下例子,用于演示椭圆曲线生成秘钥
String privateKeyHex = "FAB8BBE670FAE338C9E9382B9FB6485225C11A3ECB84C938F10F20A93B6215F0";
String x = "9EF573019D9A03B16B0BE44FC8A5B4E8E098F56034C97B312282DD0B4810AFC3";
String y = "CC759673ED0FC9B9DC7E6FA38F0E2B121E02654BF37EA6B63FAF2A0D6013EADF";
// 数据和ID此处使用16进制表示
String dataHex = "434477813974bf58f94bcf760833c2b40f77a5fc360485b0b9ed1bd9682edb45";
String idHex = "31323334353637383132333435363738";
final SM2 sm2 = new SM2(privateKeyHex, x, y);
final String sign = sm2.signHex(data, id);
// true
boolean verify = sm2.verifyHex(data, sign)
这个例子完全跑不通,变量名都是错的
存在如下问题:
- 没有指出如何设置椭圆曲线及秘钥对生成
- 没有给出通过制定的公钥或者私钥来进行单独的验签和签名
- 生成的
签名长度不为64字节
- 在提供的验证的链接里,这个例子验签不过。验证链接
解决方案:
hutool其实已经引用了国密库,并写好了推荐的椭圆曲线参数,代码如下:
默认sm2也是采用此参数生成。分析源码发现,真正对sm2算法操作的为类cn.hutool.crypto.asymmetric.SM2
,此类默认配置如下
此配置不满足业内要求,具体如下:
- 模式需要为C1C2C3
- 生成的签名为明文(具体转换类为
org.bouncycastle.crypto.signers.PlainDSAEncoding
)
明白了其真正干活的类,则直接用sm2这个类
1.引入依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.66</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.10</version>
</dependency>
2.创建秘钥对
/**
* 创建sm2测试
* <i scr="https://i.goto327.top/CryptTools/SM2.aspx?tdsourcetag=s_pctim_aiomsg">秘钥验证</i>
*/
@Test
public void createSm2KeyTest() {
//需要加密的明文
String text = "我是一段测试aaaa";
//创建sm2 对象
SM2 sm2 = SmUtil.sm2();
//这里会自动生成对应的随机秘钥对 , 注意! 这里一定要强转,才能得到对应有效的秘钥信息
byte[] privateKey = BCUtil.encodeECPrivateKey(sm2.getPrivateKey());
//这里公钥不压缩 公钥的第一个字节用于表示是否压缩 可以不要
byte[] publicKey = ((BCECPublicKey) sm2.getPublicKey()).getQ().getEncoded(false);
//这里得到的 压缩后的公钥 ((BCECPublicKey) sm2.getPublicKey()).getQ().getEncoded(true);
// byte[] publicKeyEc = BCUtil.encodeECPublicKey(sm2.getPublicKey());
//打印当前的公私秘钥
System.out.println("私钥: " + HexUtil.encodeHexStr(privateKey));
System.out.println("公钥: " + HexUtil.encodeHexStr(publicKey));
//得到明文对应的字节数组
byte[] dateBytes = text.getBytes();
System.out.println("数据: " + HexUtil.encodeHexStr(dateBytes));
//这里需要手动设置,sm2 对象的默认值与我们期望的不一致
sm2.setMode(SM2Engine.Mode.C1C2C3);
sm2.setEncoding(new PlainDSAEncoding());
//计算签名
byte[] sign = sm2.sign(dateBytes, null);
System.out.println("签名: " + HexUtil.encodeHexStr(sign));
// 校验 验签
boolean verify = sm2.verify(dateBytes, sign);
System.out.println(verify);
}
此方法会创建满足要求的公私钥对,且生成的签名也为64字节,用上面的验证链接,验证也为正常。生成如下打印信息
私钥: 1ebf8b341c695ee456fd1a41b82645724bc25d79935437d30e7e4b0a554baa5e
公钥: 04db9629dd33ba568e9507add5df6587a0998361a03d3321948b448c653c2c1b7056434884ab6f3d1c529501f166a336e86f045cea10dffe58aa82ea13d7253763
数据: e68891e698afe4b880e6aeb5e6b58be8af9561616161
签名: 7f4434d553e20a63ae56b762b210608b1fa1117a2dd04f3abe9007a7545968161bd1e51c8686d11bee55b1c5ea571899db98417389bc89693f0b392eba4da1e4
true
注意!!
公钥生成为65个字节,其中第一个字节表示压缩用的,可以删除,即为64字节。在验证链接中,页面上输入的公钥X为公钥的前32字节,页面上输入的公钥Y为公钥的后32字节
。如果不知道怎么填,可以把私钥输入,点击页面上的通过私钥生成公钥也是一样的。
3.通过指定的私钥进行签名
当实际开发中,我们是生成了一对公私钥就会保存起来,当需要签名的时候也只是用私钥对明文数据进行签名。具体代码如下:
/**
* 指定私钥签名测试
* <i scr="https://i.goto327.top/CryptTools/SM2.aspx?tdsourcetag=s_pctim_aiomsg">秘钥验证</i>
*/
@Test
public void signTest() {
//指定的私钥
String privateKeyHex = "1ebf8b341c695ee456fd1a41b82645724bc25d79935437d30e7e4b0a554baa5e";
//需要加密的明文,得到明文对应的字节数组
byte[] dataBytes = "我是一段测试aaaa".getBytes();
ECPrivateKeyParameters privateKeyParameters = BCUtil.toSm2Params(privateKeyHex);
//创建sm2 对象
SM2 sm2 = new SM2(privateKeyParameters, null);
//这里需要手动设置,sm2 对象的默认值与我们期望的不一致 , 使用明文编码
sm2.usePlainEncoding();
sm2.setMode(SM2Engine.Mode.C1C2C3);
byte[] sign = sm2.sign(dataBytes, null);
System.out.println("数据: " + HexUtil.encodeHexStr(dataBytes));
System.out.println("签名: " + HexUtil.encodeHexStr(sign));
}
4.通过指定的公钥对数据进行验签
/**
* 指定私钥签名测试
* <i scr="https://i.goto327.top/CryptTools/SM2.aspx?tdsourcetag=s_pctim_aiomsg">秘钥验证</i>
*/
@Test
public void verifyTest() {
//指定的公钥
String publicKeyHex = "04db9629dd33ba568e9507add5df6587a0998361a03d3321948b448c653c2c1b7056434884ab6f3d1c529501f166a336e86f045cea10dffe58aa82ea13d7253763";
//需要加密的明文,得到明文对应的字节数组
byte[] dataBytes = "我是一段测试aaaa".getBytes();
//签名值
String signHex = "2881346e038d2ed706ccdd025f2b1dafa7377d5cf090134b98756fafe084dddbcdba0ab00b5348ed48025195af3f1dda29e819bb66aa9d4d088050ff148482a1";
//这里需要根据公钥的长度进行加工
if (publicKeyHex.length() == 130) {
//这里需要去掉开始第一个字节 第一个字节表示标记
publicKeyHex = publicKeyHex.substring(2);
}
String xhex = publicKeyHex.substring(0, 64);
String yhex = publicKeyHex.substring(64, 128);
ECPublicKeyParameters ecPublicKeyParameters = BCUtil.toSm2Params(xhex, yhex);
//创建sm2 对象
SM2 sm2 = new SM2(null, ecPublicKeyParameters);
//这里需要手动设置,sm2 对象的默认值与我们期望的不一致 , 使用明文编码
sm2.usePlainEncoding();
sm2.setMode(SM2Engine.Mode.C1C2C3);
boolean verify = sm2.verify(dataBytes, HexUtil.decodeHex(signHex));
System.out.println("数据: " + HexUtil.encodeHexStr(dataBytes));
System.out.println("验签结果: " + verify);
}