使用PHP国密实战,打通SM2/SM3/SM4全流程

AI摘要
本文是一篇面向PHP开发者的国密算法实战指南,属于【知识分享】。文章介绍了国密算法(SM2/SM3/SM4)的背景与合规要求,分析了PHP生态中国密开发的挑战,并提供了具体的技术选型方案。核心内容为使用PHP GMP扩展配合`lpilp/guomi` Composer包实现国密加解密的完整代码示例,包括密钥解析、SM2解密、SM3签名验证及SM4加解密,并附有上线检查清单。

作为一个PHP开发者,面对国密(SM2/SM3/SM4)这个“硬骨头”,第一反应是:PHP生态有成熟的解决方案吗?

经历了从迷茫到实现的全过程,我最终用 PHP GMP扩展 + lpilp/guomi Composer包 优雅地解决了所有国密加解密需求。如果你也在为国密开发头疼,这篇文章就是为你准备的实战指南。

国密是什么?为什么突然火了?

国密算法是中国自主研发的密码标准:
SM2:椭圆曲线公钥密码算法(替代RSA)
SM3:密码杂凑算法(类似SHA-256)
SM4:分组密码算法(类似AES)
随着网络安全法的实施和信创(信息技术应用创新)产业的推进,金融、政务、医疗等关键领域都在加速国密改造。

PHP国密开发的三大挑战

生态薄弱:相比Java/C++,PHP国密生态相对匮乏
性能瓶颈:纯PHP实现的国密算法性能堪忧
兼容性问题:与现有系统的平滑过渡

技术选型之路

尝试方案一:纯PHP扩展(放弃)

编译安装php-gmssl等扩展,过程繁琐且版本兼容性差。

尝试方案二:调用外部服务(不适合)

通过API调用密码机服务,增加网络开销和单点故障风险。

最终方案:GMP扩展 + lpilp/guomi(完美解决)
# 1. 安装GMP扩展(大多数环境已内置)
sudo apt-get install php-gmp  # Ubuntu/Debian
# 或编辑php.ini启用:extension=gmp

# 2. Composer一键安装国密库
composer require lpilp/guomi

实战代码:三分钟搞定国密加解密

Sm2PrivateKeyParserUtils.php

<?php
namespace App\Utils;

/**
 * 纯 PHP 解析 PKCS#8 PEM 提取 SM2 私钥 d 值
 * 不依赖 OpenSSL 命令行,只需要 PHP 的数学扩展
 */
class Sm2PrivateKeyParserUtils
{
    /**
     * 从 PKCS#8 PEM 中提取 SM2 私钥 d 值(16进制)
     */
    public static function extractPrivateKeyFromPkcs8(string $pem): string
    {
        // 1. 解码 PEM 格式
        $der = self::pemToDer($pem);

        // 2. 解析 ASN.1 DER 结构
        $data = self::parseAsn1Der($der);

        // 3. 提取私钥数据
        return self::extractSm2PrivateKey($data);
    }

    /**
     * PEM 转换为 DER
     */
    private static function pemToDer(string $pem): string
    {
        // 移除 PEM 头尾和空白字符
        $pem = preg_replace('/-----(BEGIN|END)[\w\s]+-----/', '', $pem);
        $pem = preg_replace('/\s+/', '', $pem);

        // Base64 解码
        $der = base64_decode($pem);
        if ($der === false) {
            throw new \RuntimeException('Base64 解码失败');
        }

        return $der;
    }

    /**
     * 简单的 ASN.1 DER 解析器
     */
    private static function parseAsn1Der(string $der): array
    {
        $offset = 0;
        return self::parseAsn1Value($der, $offset);
    }

    /**
     * 解析单个 ASN.1 值
     */
    private static function parseAsn1Value(string $der, int &$offset)
    {
        if ($offset >= strlen($der)) {
            throw new \RuntimeException('ASN.1 解析越界');
        }

        $tag = ord($der[$offset++]);
        $length = self::parseAsn1Length($der, $offset);

        $value = substr($der, $offset, $length);
        $offset += $length;

        // 检查是否是序列
        if (($tag & 0x1F) === 0x10) {
            return self::parseAsn1Sequence($value);
        }

        // 检查是否是八位组串
        if (($tag & 0x1F) === 0x04) {
            return $value;
        }

        // 检查是否是整数
        if (($tag & 0x1F) === 0x02) {
            return $value;
        }

        return $value;
    }

    /**
     * 解析 ASN.1 长度字段
     */
    private static function parseAsn1Length(string $der, int &$offset): int
    {
        $length = ord($der[$offset++]);

        if ($length & 0x80) {
            $numBytes = $length & 0x7F;
            $length = 0;
            for ($i = 0; $i < $numBytes; $i++) {
                $length = ($length << 8) | ord($der[$offset++]);
            }
        }

        return $length;
    }

    /**
     * 解析 ASN.1 序列
     */
    private static function parseAsn1Sequence(string $data): array
    {
        $result = [];
        $offset = 0;

        while ($offset < strlen($data)) {
            try {
                $result[] = self::parseAsn1Value($data, $offset);
            } catch (\Exception $e) {
                break;
            }
        }

        return $result;
    }

    /**
     * 从解析的数据中提取 SM2 私钥
     */
    private static function extractSm2PrivateKey(array $data): string
    {
        // PKCS#8 结构通常是:
        // Sequence[
        //   Integer(version = 0),
        //   Sequence[OID, params],
        //   OctetString(privateKey)
        // ]

        if (count($data) < 3) {
            throw new \RuntimeException('无效的 PKCS#8 结构');
        }

        // 私钥在第三个元素(八位组串)中
        $privateKeyOctet = $data[2];

        if (!is_string($privateKeyOctet)) {
            throw new \RuntimeException('私钥数据不是字符串');
        }

        // EC 私钥的结构通常是:
        // Sequence[
        //   Integer(version = 1),
        //   OctetString(privateKey d)
        //   [optional parameters]
        // ]
        $privateKeyData = self::parseAsn1Der($privateKeyOctet);

        if (count($privateKeyData) < 2) {
            throw new \RuntimeException('无效的 EC 私钥结构');
        }

        // 私钥 d 值在第二个元素中
        $dValue = $privateKeyData[1];

        if (!is_string($dValue)) {
            throw new \RuntimeException('私钥 d 值不是字符串');
        }

        // 转换为16进制
        $hex = bin2hex($dValue);

        // 清理前导零并填充到64字符
        $hex = ltrim($hex, '0');
        $hex = str_pad($hex, 64, '0', STR_PAD_LEFT);

        return substr($hex, -64);
    }
}

gmssl.php

<?php

/**
 * 国密算法测试用例
 * 使用 lpilp/guomi 扩展包测试 SM2/SM3/SM4 算法
 *
 */

require_once __DIR__ . '/../vendor/autoload.php';

use Rtgm\sm\RtSm2;
use Rtgm\sm\RtSm3;
use Rtgm\sm\RtSm4;
use App\Utils\Sm2PrivateKeyParserUtils;

class GmsslTest
{
    // SM2 解密私钥(PEM 格式)
    private const SM2_PRIVATE_KEY = <<<EOT
-----BEGIN PRIVATE KEY-----
你的解密私钥
-----END PRIVATE KEY-----
EOT;

    private const SLVAS_SYS_SM4_KEY = '你的sys key';
    private const SLVAS_BUS_SM4_KEY = '你的bus key'; 

    private const SUPPLIER_CODE = ' '; // 供应商编码

    /**
     * 解密 body 数据
     * @param string $encryptedBody 加密的 body(BCD 格式十六进制字符串或 base64)
     * @return array|false 解密后的数据
     */
    public function sm2decrypt(string $encryptedBody)
    {
        try {
            $sm2 = new RtSm2('hex'); // 'hex' or 'base64' 主要影响签名输出格式,对加解密无所谓

            $cipherHex = $encryptedBody; // Hutool encryptBcd() 的返回值(一般就是 hex 字符串)
            $privateKeyHex = Sm2PrivateKeyParserUtils::extractPrivateKeyFromPkcs8(self::SM2_PRIVATE_KEY);

            $bodyData = $sm2->doDecrypt($cipherHex, $privateKeyHex, true, C1C3C2);
            return $bodyData;

        } catch (\Exception $e) {
            echo "<pre>";
            print_r([
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'error' => $e->getMessage(),
            ]);
            echo "</pre>";
            exit;
        }
    }

    /**
     * sm3 验证签名
     */
    public function verifySign(string $inspectionSerialNo, string $sign)
    {
        try {
            $received = strtolower(trim($sign));
            if ($received === '') {
                return false;
            }

            $sm3 = new RtSm3();

            // 约定:将 inspectionSerialNo(或按约定拼接后的字符串)进行 SM3,得到 64 位 hex
            // 由于目前“拼接规则”未完全明确,这里做两种候选兼容:
            // 1) inspectionSerialNo
            // 2) SUPPLIER_CODE + inspectionSerialNo
            $candidates = [
                $inspectionSerialNo,
                self::SUPPLIER_CODE . $inspectionSerialNo,
            ];

            foreach ($candidates as $payload) {
                $computed = strtolower($sm3->digest($payload));
                if (hash_equals($computed, $received)) {
                    return true;
                }
            }


            return false;
        } catch (\Exception $e) {

        }
    }
     // busSm4Key 加密
     public function sm4EncryptBus($string)
     {
         $sm4 = new RtSm4(self::SLVAS_BUS_SM4_KEY);
         return $sm4->encrypt($string, 'sm4-ecb', '', 'base64');
     }

     // busSm4Key 解密
     public function sm4DecryptBus($string)
     {
         $sm4 = new RtSm4(self::SLVAS_BUS_SM4_KEY);
         return $sm4->decrypt($string, 'sm4-ecb', '', 'base64');
     }

     // sysSm4Key 加密
     public function sm4EncryptSys($string)
     {
         $sm4 = new RtSm4(self::SLVAS_SYS_SM4_KEY);
         return $sm4->encrypt($string, 'sm4-ecb', '', 'base64');
     }

     // sysSm4Key 解密
     public function sm4DecryptSys($string)
     {
         $sm4 = new RtSm4(self::SLVAS_SYS_SM4_KEY);
         return $sm4->decrypt($string, 'sm4-ecb', '', 'base64');
     }
}
error_reporting(E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR);

echo " ==================sm2 解密============================== <br/>";
$encryptedData = '你的加密字符串';
echo "sm2密文: ".$encryptedData."<br>";
$bodyData = (new GmsslTest())->sm2decrypt($encryptedData);
echo "sm2 解密后的数据:<br>";
echo "<pre>";
print_r($bodyData);
echo "</pre>";
echo "<br/>";
echo " ==================sm3 签名验证============================== <br/>";
echo "inspectionSerialNo:RZXB-1769147224728-000001 <br>";
$inspectionSerialNo = 'RZXB-1769147224728-000001';
$sign = '6a5b67ea4123b54d85c8857f168cdccf8b47c9ea8b4fe849f3c33838f82b2a01';
echo "签名: sign:6a5b67ea4123b54d85c8857f168cdccf8b47c9ea8b4fe849f3c33838f82b2a01<br>";
$sign = (new GmsslTest())->verifySign($inspectionSerialNo, $sign);
echo "签名验证结果:<br>";
var_dump($sign);
echo "<br/>";
echo "<hr/>";
echo " ==================sm4  加密解密============================== <br/>";
$encryptedData = (new GmsslTest())->sm4EncryptBus('hello');
echo "使用 busSm4Key 加密后的数据: ".$encryptedData."<br>";
$decryptedData = (new GmsslTest())->sm4DecryptBus($encryptedData);
echo "使用 busSm4Key 解密后的数据: ".$decryptedData."<br>";
echo "<hr/>";
$encryptedData = (new GmsslTest())->sm4EncryptSys('hello');
echo "使用 sysSm4Key 加密后的数据: ".$encryptedData."<br>";
$decryptedData = (new GmsslTest())->sm4DecryptSys($encryptedData);
echo "使用 sysSm4Key 解密后的数据: ".$decryptedData."<br>";

exit;

上线 Checklist

  • GMP扩展已安装并启用
  • 生产环境密钥妥善保管(≠提交到Git)
  • 与上下游系统完成国密联调
  • 性能压力测试通过
  • 制定密钥轮换方案

本文代码已在PHP8.4环境测试通过,欢迎留言交流经验!

本作品采用《CC 协议》,转载必须注明作者和本文链接
Dcat-Admin (plus版)是汇聚Filament,Laravel-admin , Dcat-admin 优点于一身的基于Laravel + Bootstrap 的极速开发框架
本帖由系统于 2周前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 1
yangweijie

国密 我开发过很多项目,为了方便大家调试 开发了gm-gui 工具

file

最近又接了一个 bcprov-jdk15on的, 问 AI 调通了,里面公钥pem 提取也扩展了 Sm2PublicKeyParserUtil 的类。本身 lpilp/guomi 有一个 MyAsn1 类的。

2周前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
Dcat-plus Admin @ 速码邦
文章
46
粉丝
64
喜欢
237
收藏
196
排名:330
访问:2.9 万
私信
所有博文
社区赞助商