<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>SM2 - 她和她的猫</title>
    <link>https://her-cat.com/tags/sm2/</link>
    <description>SM2的文章列表 - 她和她的猫</description>
    <image>
      <title>她和她的猫</title>
      <url>https://her-cat.com/assets/favorite.jpeg</url>
      <link>https://her-cat.com/assets/favorite.jpeg</link>
    </image>
    <generator>Hugo -- 0.148.1</generator>
    <language>zh</language>
    <lastBuildDate>Mon, 19 Jun 2023 01:09:06 +0800</lastBuildDate>
    <atom:link href="https://her-cat.com/tags/sm2/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>还原 SM2 压缩公钥的几种方法</title>
      <link>https://her-cat.com/posts/2023/06/19/several-ways-to-decompress-sm2-compressed-public-key/</link>
      <pubDate>Mon, 19 Jun 2023 01:09:06 +0800</pubDate>
      <guid>https://her-cat.com/posts/2023/06/19/several-ways-to-decompress-sm2-compressed-public-key/</guid>
      <description>在 SM2 算法中，公钥的大小为 64 字节，算上前缀 04 的话就是 65 字节。公钥由椭圆曲线上的坐标点（x, y）组成，即每个坐标点都是 32 字节的大数。为了节省存储空间，通常会对公钥进行压缩后使用，也就是压缩公钥。</description>
      <content:encoded><![CDATA[<p>写这篇文章的起因是朋友让我帮忙解决一个与 SM2 算法加密相关的问题。由于我对 SM2 算法并不熟悉，因此在解决问题的过程中走了很多弯路，花了很多时间去了解 SM2 算法以及如何通过代码还原压缩公钥。随着越来越多的系统采用国密算法，我们在对接的时候难免会遇到类似的问题，网上关于这方面的资料也比较少，因此趁周末有空，将我发现的几种还原压缩公钥的方法记录下来，希望对你有所帮助。</p>
<p>在介绍几种还原 SM2 压缩公钥的方法之前，让我们先了解一下什么是 SM2 压缩公钥。</p>
<h2 id="什么是-sm2-压缩公钥">什么是 SM2 压缩公钥？</h2>
<p>在 SM2 算法中，公钥的大小为 64 字节，算上前缀 04 的话就是 65 字节。公钥由椭圆曲线上的坐标点（x, y）组成，即每个坐标点都是 32 字节的大数。为了节省存储空间，通常会对公钥进行压缩后使用，也就是压缩公钥。</p>
<p>压缩公钥分别由前缀和坐标点 x 一共 33 字节组成，当坐标点 y 是偶数时，使用 02 作为前缀，否则使用 03 作为前缀。使用 16 进制字符串表示时，字符串长度为 66 个字符。</p>
<p>还原压缩公钥的原理：先通过压缩公钥的前缀，确定坐标点 y 是奇数还是偶数，然后根据椭圆曲线的公式计算得到完整的公钥。</p>
<p>没看懂？没关系，下面我介绍几种还原压缩公钥的方法，让你不需要知道原理也能还原压缩公钥。</p>
<h2 id="第一种方法使用在线工具">第一种方法：使用在线工具</h2>
<p>下面是我找到的两个比较好用的在线工具：</p>
<ul>
<li><a href="https://const.net.cn/tool/sm2/sm2-pubkey-decompress/">https://const.net.cn/tool/sm2/sm2-pubkey-decompress/</a></li>
<li><a href="https://the-x.cn/zh-cn/cryptography/Sm2.aspx">https://the-x.cn/zh-cn/cryptography/Sm2.aspx</a></li>
</ul>
<p>第一个网站，只要将压缩公钥粘贴到输入框，点击「还原公钥」按钮就可以得到完整的公钥，该工具输出的结果还需要去除其中的空格才能使用。</p>
<p>第二个网站，除了支持压缩公钥以外，还支持 HEX、PEM 格式的公钥，先将公钥粘贴到输入框，网站会自动将其转换成 PEM 格式（sm2p256v1），然后借助下面这段代码就能得到完整的公钥。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nv">$pemStr</span> <span class="o">=</span> <span class="s1">&#39;PEM 格式的公钥&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$pemStr</span> <span class="o">=</span> <span class="nx">str_replace</span><span class="p">([</span><span class="s1">&#39;-----BEGIN PUBLIC KEY-----&#39;</span><span class="p">,</span> <span class="s1">&#39;-----END PUBLIC KEY-----&#39;</span><span class="p">,</span> <span class="nx">PHP_EOL</span><span class="p">],</span> <span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="nv">$pemStr</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nv">$uncompressedPublicKey</span> <span class="o">=</span> <span class="nx">substr</span><span class="p">(</span><span class="nx">bin2hex</span><span class="p">(</span><span class="nx">base64_decode</span><span class="p">(</span><span class="nv">$pemStr</span><span class="p">)),</span> <span class="o">-</span><span class="mi">128</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">echo</span> <span class="s2">&#34;未压缩公钥：&#34;</span> <span class="o">.</span> <span class="nv">$uncompressedPublicKey</span><span class="p">;</span>
</span></span></code></pre></div><h2 id="第二种方法使用-java-的-bouncy-castle-类库">第二种方法：使用 Java 的 Bouncy Castle 类库</h2>
<p>虽然在线工具非常便捷，并且也能够达到我们想要的目的，但还是缺乏一些安全性，因为我们不清楚这些在线工具是否会收集信息，所以最好还是使用本地运行的代码来还原压缩公钥。然后我开始在网上搜索还原压缩公钥的相关资料，但找了很久都没有找到。于是我修改了搜索词，最后，我在某个使用 Java 基于 Bouncy Castle 封装 SM2 工具类的文章中找到了一些思路，也就有了 Java 版还原压缩公钥的代码。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="nn">cn.hutool.core.util.HexUtil</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">org.bouncycastle.asn1.gm.GMNamedCurves</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">org.bouncycastle.asn1.x9.X9ECParameters</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">org.bouncycastle.crypto.params.ECDomainParameters</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">org.bouncycastle.crypto.params.ECPublicKeyParameters</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">org.bouncycastle.math.ec.ECPoint</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">org.bouncycastle.util.encoders.Hex</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">Main</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kd">static</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">main</span><span class="p">(</span><span class="n">String</span><span class="o">[]</span><span class="w"> </span><span class="n">args</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">String</span><span class="w"> </span><span class="n">compressedPublicKey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;02 或 03 开头的压缩公钥&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 获取一条SM2曲线参数</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">X9ECParameters</span><span class="w"> </span><span class="n">sm2ECParameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">GMNamedCurves</span><span class="p">.</span><span class="na">getByName</span><span class="p">(</span><span class="s">&#34;sm2p256v1&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 构造ECC算法参数，曲线方程、椭圆曲线G点、大整数N</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">ECDomainParameters</span><span class="w"> </span><span class="n">domainParameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ECDomainParameters</span><span class="p">(</span><span class="n">sm2ECParameters</span><span class="p">.</span><span class="na">getCurve</span><span class="p">(),</span><span class="w"> </span><span class="n">sm2ECParameters</span><span class="p">.</span><span class="na">getG</span><span class="p">(),</span><span class="w"> </span><span class="n">sm2ECParameters</span><span class="p">.</span><span class="na">getN</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 提取公钥点</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">ECPoint</span><span class="w"> </span><span class="n">pukPoint</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sm2ECParameters</span><span class="p">.</span><span class="na">getCurve</span><span class="p">().</span><span class="na">decodePoint</span><span class="p">(</span><span class="n">Hex</span><span class="p">.</span><span class="na">decode</span><span class="p">(</span><span class="n">compressedPublicKey</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// 公钥前面的02或者03表示是压缩公钥，04表示未压缩公钥, 04的时候，可以去掉前面的04</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">ECPublicKeyParameters</span><span class="w"> </span><span class="n">publicKeyParameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ECPublicKeyParameters</span><span class="p">(</span><span class="n">pukPoint</span><span class="p">,</span><span class="w"> </span><span class="n">domainParameters</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">String</span><span class="w"> </span><span class="n">uncompressedPublicKey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">HexUtil</span><span class="p">.</span><span class="na">encodeHexStr</span><span class="p">(</span><span class="n">publicKeyParameters</span><span class="p">.</span><span class="na">getQ</span><span class="p">().</span><span class="na">getEncoded</span><span class="p">(</span><span class="kc">false</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;未压缩公钥：&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">uncompressedPublicKey</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h2 id="第三种方法使用-php-的-lpilpguomi-包">第三种方法：使用 PHP 的 lpilp/guomi 包</h2>
<p>实际上，一开始就只有上面 Java 版本的代码，在准备写这篇文章的时候，突然想到 PHP 应该也能实现才对，然后我又去研究了下怎么在 PHP 中实现还原压缩公钥。</p>
<p>在 PHP 中对于国密算法相关的操作，一般都是使用 <a href="https://github.com/lpilp/phpsm2sm3sm4">lpilp/guomi</a> 这个包，它实现了 SM2、SM3、SM4 国密算法，其中 SM2 算法是基于 <a href="https://github.com/phpecc/phpecc">mdanter/ecc</a> 这个包实现的。虽然 lpilp/guomi 支持 SM2 算法相关的操作，但是对外提供的方法都只支持未压缩公钥，对于压缩公钥只能我们自己想办法。</p>
<p>于是，我开始研究它对外提供的这几个方法，最后在 RtSm2::verifySignOutKey 方法中找到了一些蛛丝马迹。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">public</span> <span class="k">function</span> <span class="nf">verifySignOutKey</span><span class="p">(</span> <span class="nv">$document</span><span class="p">,</span> <span class="nv">$sign</span><span class="p">,</span> <span class="nv">$publickeyFile</span><span class="p">,</span> <span class="nv">$userId</span> <span class="o">=</span> <span class="k">null</span> <span class="p">)</span> <span class="p">{</span>  
</span></span><span class="line"><span class="cl">	<span class="o">...</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Parse signature  
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nv">$sigSerializer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">DerSignatureSerializer</span><span class="p">();</span>  
</span></span><span class="line"><span class="cl">    <span class="nv">$sig</span> <span class="o">=</span> <span class="nv">$sigSerializer</span><span class="o">-&gt;</span><span class="na">parse</span><span class="p">(</span> <span class="nv">$sigData</span> <span class="p">);</span>  
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="c1">// Parse public key  
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nv">$keyData</span> <span class="o">=</span> <span class="nx">file_get_contents</span><span class="p">(</span> <span class="nv">$publickeyFile</span> <span class="p">);</span>  
</span></span><span class="line"><span class="cl">    <span class="nv">$derSerializer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">DerPublicKeySerializer</span><span class="p">(</span> <span class="nv">$adapter</span> <span class="p">);</span>  
</span></span><span class="line"><span class="cl">    <span class="nv">$pemSerializer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PemPublicKeySerializer</span><span class="p">(</span> <span class="nv">$derSerializer</span> <span class="p">);</span>  
</span></span><span class="line"><span class="cl">    <span class="nv">$key</span> <span class="o">=</span> <span class="nv">$pemSerializer</span><span class="o">-&gt;</span><span class="na">parse</span><span class="p">(</span> <span class="nv">$keyData</span> <span class="p">);</span>  
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="nv">$pubKeyX</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decHex</span><span class="p">(</span> <span class="nv">$key</span><span class="o">-&gt;</span><span class="na">getPoint</span><span class="p">()</span><span class="o">-&gt;</span><span class="na">getX</span><span class="p">()</span> <span class="p">);</span>  
</span></span><span class="line"><span class="cl">    <span class="nv">$pubKeyY</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decHex</span><span class="p">(</span> <span class="nv">$key</span><span class="o">-&gt;</span><span class="na">getPoint</span><span class="p">()</span><span class="o">-&gt;</span><span class="na">getY</span><span class="p">()</span> <span class="p">);</span>  
</span></span><span class="line"><span class="cl">    <span class="nv">$hash</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_doS3Hash</span><span class="p">(</span> <span class="nv">$document</span><span class="p">,</span> <span class="nv">$pubKeyX</span><span class="p">,</span> <span class="nv">$pubKeyY</span><span class="p">,</span> <span class="nv">$generator</span><span class="p">,</span> <span class="nv">$userId</span> <span class="p">);</span>  
</span></span><span class="line"><span class="cl">    <span class="nv">$signer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Sm2Signer</span><span class="p">(</span> <span class="nv">$adapter</span> <span class="p">);</span>  
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nv">$signer</span><span class="o">-&gt;</span><span class="na">verify</span><span class="p">(</span> <span class="nv">$key</span><span class="p">,</span> <span class="nv">$sig</span><span class="p">,</span> <span class="nv">$hash</span> <span class="p">);</span>  
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>上面这段代码中的 $pubKeyY 不就是 SM2 算法中公钥的另一个坐标点 y 的值吗？将 $pubKeyX 和 $pubKeyY 拼接在一起就可以得到完整的公钥。经过一顿调试并将上面的代码进行简化后，就有了 PHP 版本的实现。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">Mdanter\Ecc\Serializer\Point\CompressedPointSerializer</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">Mdanter\Ecc\Serializer\Point\UncompressedPointSerializer</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">Rtgm\ecc\RtEccFactory</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$adapter</span> <span class="o">=</span> <span class="nx">RtEccFactory</span><span class="o">::</span><span class="na">getAdapter</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nv">$curve</span> <span class="o">=</span> <span class="nx">RtEccFactory</span><span class="o">::</span><span class="na">getSmCurves</span><span class="p">()</span><span class="o">-&gt;</span><span class="na">curveSm2</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$compressedPublicKey</span> <span class="o">=</span> <span class="s1">&#39;02 或 03 开头的压缩公钥&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$compressedPointSerializer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">CompressedPointSerializer</span><span class="p">(</span><span class="nv">$adapter</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nv">$point</span> <span class="o">=</span> <span class="nv">$compressedPointSerializer</span><span class="o">-&gt;</span><span class="na">unserialize</span><span class="p">(</span><span class="nv">$curve</span><span class="p">,</span> <span class="nv">$key</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$uncompressedPointSerializer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">UncompressedPointSerializer</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nv">$uncompressedPublicKey</span> <span class="o">=</span> <span class="nv">$uncompressedPointSerializer</span><span class="o">-&gt;</span><span class="na">serialize</span><span class="p">(</span><span class="nv">$point</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">echo</span> <span class="s2">&#34;未压缩公钥：&#34;</span> <span class="o">.</span> <span class="nv">$uncompressedPublicKey</span><span class="p">;</span>
</span></span></code></pre></div><h2 id="总结">总结</h2>
<p>在本文中，我向你介绍了三种还原压缩公钥的方法。首先是使用在线工具，它们可以直接将压缩公钥转换为完整的公钥，使用起来比较方便，但缺乏了安全性。其次是使用 Java 的 Bouncy Castle 类库，通过编写代码来还原压缩公钥，保证了安全性和隐私性。最后是使用 PHP 的 lpilp/guomi 包，其效果与 Java 版本的一致。在实际使用中，你可以根据自己的需求来选择合适的方法。</p>
]]></content:encoded>
    </item><follow_challenge>
      <feedId>58021783493571598</feedId>
      <userId>56882619875632128</userId>
    </follow_challenge>
  </channel>
</rss>
