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>*/@Testpublic 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>*/@Testpublic 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>*/@Testpublic 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);}