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);

此例子可以跑通,但是有以下几个问题:

  1. 生成的公钥和私钥都非常长,远大于上文说的私钥长度为固定32字节,公钥长度为64字节
  2. 没有给出通过制定的公钥或者私钥来进行单独的验签和签名

官方提供如下例子,用于演示椭圆曲线生成秘钥

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)

这个例子完全跑不通,变量名都是错的
存在如下问题:

  1. 没有指出如何设置椭圆曲线及秘钥对生成
  2. 没有给出通过制定的公钥或者私钥来进行单独的验签和签名
  3. 生成的签名长度不为64字节
  4. 在提供的验证的链接里,这个例子验签不过。验证链接

解决方案:

hutool其实已经引用了国密库,并写好了推荐的椭圆曲线参数,代码如下:

推荐椭圆曲线参数默认sm2也是采用此参数生成。分析源码发现,真正对sm2算法操作的为类cn.hutool.crypto.asymmetric.SM2,此类默认配置如下
sm2默认配置此配置不满足业内要求,具体如下:

  1. 模式需要为C1C2C3
  2. 生成的签名为明文(具体转换类为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);
    }