还原 SM2 压缩公钥的几种方法

写这篇文章的起因是朋友让我帮忙解决一个与 SM2 算法加密相关的问题。由于我对 SM2 算法并不熟悉,因此在解决问题的过程中走了很多弯路,花了很多时间去了解 SM2 算法以及如何通过代码还原压缩公钥。随着越来越多的系统采用国密算法,我们在与其对接时难免会遇到类似的问题。然而,关于这方面的资料在网上相对较少。因此,趁周末有空,我决定将还原压缩公钥的方法记录下来,希望对你有所帮助。

在介绍几种还原 SM2 压缩公钥的方法之前,让我们先了解一下什么是 SM2 压缩公钥。

文章会持续修订,转载请注明来源地址:https://her-cat.com/posts/2023/06/19/several-ways-to-decompress-sm2-compressed-public-key/

什么是 SM2 压缩公钥?

在 SM2 算法中,公钥的大小为 64 字节,算上前缀 04 的话就是 65 字节。公钥由椭圆曲线上的坐标点(x, y)组成,即每个坐标点都是 32 字节的大数。为了节省存储空间,通常会对公钥进行压缩后使用,也就是压缩公钥。

压缩公钥分别由前缀和坐标点 x 一共 33 字节组成,当坐标点 y 是偶数时,使用 02 作为前缀,否则使用 03 作为前缀。使用 16 进制字符串表示时,字符串长度为 66 个字符。

还原压缩公钥的原理:先通过压缩公钥的前缀,确定坐标点 y 是奇数还是偶数,然后根据椭圆曲线的公式计算得到完整的公钥。

没看懂?没关系,下面我介绍几种还原压缩公钥的方法,让你不需要知道原理也能还原压缩公钥。

第一种方法:使用在线工具

下面是我找到的两个比较好用的在线工具:

第一个网站,只要将压缩公钥粘贴到输入框,点击「还原公钥」按钮就可以得到完整的公钥,该工具输出的结果还需要去除其中的空格才能使用。

第二个网站,除了支持压缩公钥以外,还支持 HEX、PEM 格式的公钥,先将公钥粘贴到输入框,网站会自动将其转换成 PEM 格式(sm2p256v1),然后借助下面这段代码就能得到完整的公钥。

$pemStr = 'PEM 格式的公钥';
$pemStr = str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', PHP_EOL], '', $pemStr);
$uncompressedPublicKey = substr(bin2hex(base64_decode($pemStr)), -128);
echo "未压缩公钥:" . $uncompressedPublicKey;

第二种方法:使用 Java 的 Bouncy Castle 类库

虽然在线工具非常便捷,并且也能够达到我们想要的目的,但还是缺乏一些安全性,因为我们不清楚这些在线工具是否会收集信息,所以最好还是使用本地运行的代码来还原压缩公钥。然后我开始在网上搜索还原压缩公钥的相关资料,但找了很久都没有找到。于是我修改了搜索词,最后,我在某个使用 Java 基于 Bouncy Castle 封装 SM2 工具类的文章中找到了一些思路,也就有了 Java 版还原压缩公钥的代码。

import cn.hutool.core.util.HexUtil;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;

public class Main {
    public static void main(String[] args) {
        String compressedPublicKey = "02 或 03 开头的压缩公钥";
        // 获取一条SM2曲线参数
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
        // 构造ECC算法参数,曲线方程、椭圆曲线G点、大整数N
        ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
        //提取公钥点
        ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(compressedPublicKey));
        // 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥, 04的时候,可以去掉前面的04
        ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);
        String uncompressedPublicKey = HexUtil.encodeHexStr(publicKeyParameters.getQ().getEncoded(false));
        System.out.println("未压缩公钥:" + uncompressedPublicKey);
    }
}

第三种方法:使用 PHP 的 lpilp/guomi 包

实际上,一开始就只有上面 Java 版本的代码,在准备写这篇文章的时候,突然想到 PHP 应该也能实现才对,然后我又去研究了下怎么在 PHP 中实现还原压缩公钥。

在 PHP 中对于国密算法相关的操作,一般都是使用 lpilp/guomi 这个包,它实现了 SM2、SM3、SM4 国密算法,其中 SM2 算法是基于 mdanter/ecc 这个包实现的。虽然 lpilp/guomi 支持 SM2 算法相关的操作,但是对外提供的方法都只支持未压缩公钥,对于压缩公钥只能我们自己想办法。

于是,我开始研究它对外提供的这几个方法,最后在 RtSm2::verifySignOutKey 方法中找到了一些蛛丝马迹。

public function verifySignOutKey( $document, $sign, $publickeyFile, $userId = null ) {  
	...
    // Parse signature  
    $sigSerializer = new DerSignatureSerializer();  
    $sig = $sigSerializer->parse( $sigData );  
  
    // Parse public key  
    $keyData = file_get_contents( $publickeyFile );  
    $derSerializer = new DerPublicKeySerializer( $adapter );  
    $pemSerializer = new PemPublicKeySerializer( $derSerializer );  
    $key = $pemSerializer->parse( $keyData );  
  
    $pubKeyX = $this->decHex( $key->getPoint()->getX() );  
    $pubKeyY = $this->decHex( $key->getPoint()->getY() );  
    $hash = $this->_doS3Hash( $document, $pubKeyX, $pubKeyY, $generator, $userId );  
    $signer = new Sm2Signer( $adapter );  
  
    return $signer->verify( $key, $sig, $hash );  
}

上面这段代码中的 $pubKeyY 不就是 SM2 算法中公钥的另一个坐标点 y 的值吗?将 $pubKeyX 和 $pubKeyY 拼接在一起就可以得到完整的公钥。经过一顿调试并将上面的代码进行简化后,就有了 PHP 版本的实现。

use Mdanter\Ecc\Serializer\Point\CompressedPointSerializer;
use Mdanter\Ecc\Serializer\Point\UncompressedPointSerializer;
use Rtgm\ecc\RtEccFactory;

$adapter = RtEccFactory::getAdapter();
$curve = RtEccFactory::getSmCurves()->curveSm2();

$compressedPublicKey = '02 或 03 开头的压缩公钥';

$compressedPointSerializer = new CompressedPointSerializer($adapter);
$point = $compressedPointSerializer->unserialize($curve, $key);

$uncompressedPointSerializer = new UncompressedPointSerializer();
$uncompressedPublicKey = $uncompressedPointSerializer->serialize($point);

echo "未压缩公钥:" . $uncompressedPublicKey;

总结

在本文中,我向你介绍了三种还原压缩公钥的方法。首先是使用在线工具,它们可以直接将压缩公钥转换为完整的公钥,使用起来比较方便,但缺乏了安全性。其次是使用 Java 的 Bouncy Castle 类库,通过编写代码来还原压缩公钥,保证了安全性和隐私性。最后是使用 PHP 的 lpilp/guomi 包,其效果与 Java 版本的一致。在实际使用中,你可以根据自己的需求来选择合适的方法。

posted @ 2023-06-30 10:14  她和她的猫_her-cat  阅读(2454)  评论(1编辑  收藏  举报