Write the Code. Change the World.

8月 28

国密 sm2,sm3,sm3 在 php 中使用。国密 sm2,sm3,sm3 在 laravel 中使用。

Github

先准备 php docker 镜像

FROM xxxx

# 其他代码

WORKDIR /www

# 编译工具链(独立成层,成功后即缓存)
RUN set -eux; \
    apt-get update; \
    apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config; \
    rm -rf /var/lib/apt/lists/*

# GmSSL底层库:源码编译安装(独立成层)
RUN set -eux; \
    cd /tmp; \
    git clone https://github.com/guanzhi/GmSSL --depth=1; \
    cd GmSSL; \
    mkdir build && cd build; \
    cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local/gmssl; \
    make -j$(nproc); \
    make install; \
    echo "/usr/local/gmssl/lib" > /etc/ld.so.conf.d/gmssl.conf; \
    ldconfig; \
    rm -rf /tmp/GmSSL

# GmSSL-PHP国密扩展:编译安装并启用(独立成层)
RUN set -eux; \
    cd /tmp; \
    git clone https://github.com/GmSSL/GmSSL-PHP.git --depth=1; \
    cd GmSSL-PHP; \
    phpize; \
    ./configure --with-gmssl=/usr/local/gmssl; \
    make -j$(nproc); \
    make install; \
    docker-php-ext-enable gmssl; \
    rm -rf /tmp/GmSSL-PHP

# 创建软链接,让系统可以直接找到 gmssl
RUN ln -sf /usr/local/gmssl/bin/gmssl /usr/local/bin/gmssl

# 验证安装
RUN gmssl version

环境准备好了,就可以在 laravel 项目中使用了。

使用

  1. 生成 sm4 需要的 key,iv 以及 sm2 需要的公钥私钥口令这些
#!/bin/bash

# 用来生成 GM SM4、SM2 的密钥(在已安装 GmSSL 的 Linux / docker 容器中执行)
# 使用方法: ./gmssl.sh
# 可选环境变量:
#   GMSSL_BIN  gmssl 命令路径(默认从 PATH 查找)
#   SM2_PASS   SM2 口令私钥的密码(默认随机生成24位hex,生产环境务必自行修改保管)

set -e  # 遇到错误立即退出

# ========== 配置区域 ==========
# gmssl 命令(默认安装到 /usr/local/bin/gmssl,从 PATH 查找即可)
GMSSL_BIN="${GMSSL_BIN:-gmssl}"

# 密钥存储目录(位于 storage/app 下,.env 中填相对路径即可)
KEY_DIR="./storage/app/gmssl"

# SM2 口令私钥的密码:未指定时随机生成
SM2_PASS="${SM2_PASS:-$(openssl rand -hex 12)}"

# ========== 开始生成 ==========

# 0. 检查 gmssl 命令是否可用
if ! command -v "$GMSSL_BIN" >/dev/null 2>&1; then
    echo "错误: 找不到 gmssl 命令,请确认容器内已安装 GmSSL,或通过 GMSSL_BIN 环境变量指定路径"
    exit 1
fi

# 1. 创建密钥目录
echo "创建密钥目录: $KEY_DIR"
mkdir -p "$KEY_DIR"

# 2. 生成 SM4 密钥和 IV (16字节 -> 32位十六进制)
echo "生成 SM4 密钥和 IV..."
GM_SM4_KEY=$(openssl rand -hex 16)
GM_SM4_IV=$(openssl rand -hex 16)

# 3. 生成 SM2 密钥对(口令加密私钥,公钥不加密)
echo "生成 SM2 密钥对..."
"$GMSSL_BIN" sm2keygen \
    -pass "$SM2_PASS" \
    -out "$KEY_DIR/sm2_private.pem" \
    -pubout "$KEY_DIR/sm2_public.pem"

# 4. 验证文件是否生成成功
if [ ! -f "$KEY_DIR/sm2_private.pem" ] || [ ! -f "$KEY_DIR/sm2_public.pem" ]; then
    echo "错误: SM2 密钥文件生成失败"
    exit 1
fi

# 5. 设置文件权限(安全:私钥仅属主可读写)
chmod 600 "$KEY_DIR/sm2_private.pem"
chmod 644 "$KEY_DIR/sm2_public.pem"

# 6. 输出结果(.env 中填相对于 storage/app 的路径,由 PHP 端 storage_path() 解析,避免依赖运行时工作目录)
echo ""
echo "=========================================="
echo "✅ 密钥生成完成!"
echo "=========================================="
echo ""
echo "📁 密钥文件位置:"
echo "  加密私钥: $KEY_DIR/sm2_private.pem"
echo "  公钥:     $KEY_DIR/sm2_public.pem"
echo ""
echo "📝 请将以下内容添加到 .env 文件:"
echo "----------------------------------------"
echo "GM_SM4_KEY=$GM_SM4_KEY"
echo "GM_SM4_IV=$GM_SM4_IV"
echo "GM_SM2_PRIV_KEY_FILE=gmssl/sm2_private.pem"
echo "GM_SM2_PUB_KEY_FILE=gmssl/sm2_public.pem"
echo "GM_SM2_PRIV_PASS=$SM2_PASS"
echo "----------------------------------------"
echo ""
echo "⚠️  安全提示:"
echo "  1. 请妥善保管 sm2_private.pem 及 GM_SM2_PRIV_PASS 口令,丢失后已加密数据无法恢复"
echo "  2. 不要将密钥文件和 .env 提交到 Git 仓库"
echo "  3. 生产环境请使用更强的口令,并通过密钥管理服务分发"
echo "=========================================="

# 7. 确保密钥目录被 Git 忽略
if [ -f ".gitignore" ]; then
    if ! grep -q "storage/app/gmssl" .gitignore 2>/dev/null; then
        echo "" >> .gitignore
        echo "# GMSSL 密钥文件" >> .gitignore
        echo "/storage/app/gmssl/" >> .gitignore
        echo "已添加 /storage/app/gmssl/ 到 .gitignore"
    fi
fi

echo ""
echo "🎉 完成!"

在容器内,执行 sh ./gmssl.sh 来生成上边说的这些。将上边对应的结果添加的 .env 文件中。记得在 .env.example 中增加对应的配置项。

  1. 增加 config/gm.php,填入下边的内容。
<?php

return [
    // SM4-CBC 密钥与IV:16字节原始密钥的32位hex字符串(由 gmssl.sh 生成)
    'sm4_key' => env('GM_SM4_KEY'),
    'sm4_iv'  => env('GM_SM4_IV'),

    // SM2 密钥文件路径:支持绝对路径;相对路径基于 storage/app 解析
    // 例如填 gmssl/sm2_private.pem 即 storage/app/gmssl/sm2_private.pem
    'sm2_priv_key_file' => env('GM_SM2_PRIV_KEY_FILE'),
    'sm2_pub_key_file'  => env('GM_SM2_PUB_KEY_FILE'),

    // SM2 加密私钥的口令(gmssl sm2keygen -pass 所用密码)
    'sm2_priv_pass' => env('GM_SM2_PRIV_PASS'),

    // SM2 签名/验签使用的签名者ID(与对接方约定一致,国密默认值为 1234567812345678)
    'sm2_id' => env('GM_SM2_ID', '1234567812345678'),
];
  1. 增加工具类
<?php

namespace App\Utils;

use RuntimeException;

/**
 * 国密算法工具类(SM2 / SM3 / SM4)
 *
 * 基于 gmssl PHP 扩展(https://github.com/GmSSL/GmSSL-PHP,依赖 GmSSL C 库 >= 3.1.0)
 *
 * 注意:官方扩展的函数签名与本类早期写法差异较大,以下为官方 API:
 * - gmssl_sm4_cbc_encrypt($key, $iv, $data) / gmssl_sm4_cbc_decrypt($key, $iv, $ciphertext)
 * - gmssl_sm2_sign($keypair, $id, $message),签名固定为 ASN.1 DER 编码
 * - gmssl_sm2_verify($public_key, $id, $message, $signature)
 * - gmssl_sm2_encrypt($public_key, $data) / gmssl_sm2_decrypt($keypair, $ciphertext)
 * - SM2 密钥对象必须通过文件路径加载:
 *   gmssl_sm2_private_key_info_decrypt_from_pem($file, $passphrase)
 *   gmssl_sm2_public_key_info_from_pem($file)
 */
class GmUtils
{
    /** SM4 二进制密钥(16字节),进程内缓存 */
    protected static ?string $sm4Key = null;

    /** SM4 二进制IV(16字节),进程内缓存 */
    protected static ?string $sm4Iv = null;

    /** SM2 私钥对象(96字节,含口令解密结果),进程内缓存 */
    protected static ?string $sm2PrivKey = null;

    /** SM2 公钥对象(96字节),进程内缓存 */
    protected static ?string $sm2PubKey = null;

    /**
     * SM3 摘要
     * @param string $data 原始数据
     * @return string 小写hex摘要(64位)
     */
    public static function sm3(string $data): string
    {
        return bin2hex(gmssl_sm3($data));
    }

    /**
     * SM4‑CBC 加密(带PKCS填充)
     * @param string $plainText 明文
     * @param string|null $keyHex 自定义密钥(32位hex),不传则用 GM_SM4_KEY
     * @param string|null $ivHex 自定义IV(32位hex),不传则用 GM_SM4_IV
     * @return string base64密文(适合http/json传输)
     */
    public static function sm4Encrypt(string $plainText, ?string $keyHex = null, ?string $ivHex = null): string
    {
        [$key, $iv] = self::sm4Keys($keyHex, $ivHex);
        return base64_encode(gmssl_sm4_cbc_encrypt($key, $iv, $plainText));
    }

    /**
     * SM4‑CBC 解密
     * @param string $base64Cipher base64密文
     * @param string|null $keyHex 自定义密钥(32位hex),不传则用 GM_SM4_KEY
     * @param string|null $ivHex 自定义IV(32位hex),不传则用 GM_SM4_IV
     * @return string 原始明文
     */
    public static function sm4Decrypt(string $base64Cipher, ?string $keyHex = null, ?string $ivHex = null): string
    {
        $cipher = base64_decode($base64Cipher, true);
        if ($cipher === false) {
            throw new RuntimeException('SM4解密失败: 密文不是合法的base64');
        }
        [$key, $iv] = self::sm4Keys($keyHex, $ivHex);
        return gmssl_sm4_cbc_decrypt($key, $iv, $cipher);
    }

    /**
     * SM4‑CBC 加密,返回hex密文(与小程序/前端 JS 库如 sm-crypto 对接用)
     */
    public static function sm4EncryptHex(string $plainText, ?string $keyHex = null, ?string $ivHex = null): string
    {
        [$key, $iv] = self::sm4Keys($keyHex, $ivHex);
        return bin2hex(gmssl_sm4_cbc_encrypt($key, $iv, $plainText));
    }

    /**
     * SM4‑CBC 解密,入参为hex密文(与小程序/前端 JS 库如 sm-crypto 对接用)
     */
    public static function sm4DecryptHex(string $hexCipher, ?string $keyHex = null, ?string $ivHex = null): string
    {
        $hex = trim($hexCipher);
        if ($hex === '' || strlen($hex) % 2 !== 0 || !ctype_xdigit($hex)) {
            throw new RuntimeException('SM4解密失败: 密文不是合法的hex字符串');
        }
        [$key, $iv] = self::sm4Keys($keyHex, $ivHex);
        return gmssl_sm4_cbc_decrypt($key, $iv, hex2bin($hex));
    }

    /**
     * SM2签名(SM3withSM2),返回 base64 的 ASN.1 DER 签名
     * @param string $data 待签名原文
     * @param string|null $id 签名者ID,默认取 GM_SM2_ID 配置(1234567812345678)
     * @return string base64签名
     */
    public static function sm2Sign(string $data, ?string $id = null): string
    {
        $sig = gmssl_sm2_sign(self::getSm2PrivKey(), self::sm2Id($id), $data);
        return base64_encode($sig);
    }

    /**
     * SM2验签
     * @param string $data 原文
     * @param string $base64Sign base64编码的DER签名
     * @param string|null $id 签名者ID,需与签名时一致
     * @return bool
     */
    public static function sm2Verify(string $data, string $base64Sign, ?string $id = null): bool
    {
        $sig = base64_decode($base64Sign, true);
        if ($sig === false || $sig === '') {
            return false;
        }
        return gmssl_sm2_verify(self::getSm2PubKey(), self::sm2Id($id), $data, $sig);
    }

    /**
     * SM2公钥加密,返回base64(密文为ASN.1 DER,内部C1C3C2)
     * @param string $plain 明文(SM2加密适用于短数据/密钥材料保护)
     * @return string base64密文
     */
    public static function sm2Encrypt(string $plain): string
    {
        $cipher = gmssl_sm2_encrypt(self::getSm2PubKey(), $plain);
        return base64_encode($cipher);
    }

    /**
     * SM2私钥解密
     * @param string $base64Cipher base64密文
     * @return string 明文
     */
    public static function sm2Decrypt(string $base64Cipher): string
    {
        $cipher = base64_decode($base64Cipher, true);
        if ($cipher === false || $cipher === '') {
            throw new RuntimeException('SM2解密失败: 密文不是合法的base64');
        }
        return gmssl_sm2_decrypt(self::getSm2PrivKey(), $cipher);
    }

    /**
     * SM2解密兼容方法:解密第三方客户端(小程序/前端 sm-crypto 等)传来的裸 C1C3C2 hex 密文
     * 密文布局:[04] || C1(X 32字节 || Y 32字节) || C3(32字节SM3摘要) || C2
     * @param string $hexCipher hex密文,带不带 04 前缀均可,要求客户端使用 C1C3C2 顺序
     * @return string 明文
     */
    public static function sm2DecryptHex(string $hexCipher): string
    {
        $hex = trim($hexCipher);
        if ($hex === '' || strlen($hex) % 2 !== 0 || !ctype_xdigit($hex)) {
            throw new RuntimeException('SM2解密失败: 密文不是合法的hex字符串');
        }
        $bin = hex2bin($hex);
        // 兼容带 04 非压缩点前缀的写法
        if ($bin[0] === "\x04") {
            $bin = substr($bin, 1);
        }
        if (strlen($bin) < 97) {
            throw new RuntimeException('SM2解密失败: 密文长度不足,请确认客户端使用 C1C3C2 顺序');
        }
        $x  = substr($bin, 0, 32);
        $y  = substr($bin, 32, 32);
        $c3 = substr($bin, 64, 32);
        $c2 = substr($bin, 96);

        // gmssl 扩展只接受 ASN.1 DER 密文,需把裸密文组装成:
        // SEQUENCE { xCoordinate INTEGER, yCoordinate INTEGER, hash OCTET STRING, cipherText OCTET STRING }
        $body = self::derInt($x) . self::derInt($y) . self::derOctetString($c3) . self::derOctetString($c2);
        $der  = "\x30" . self::derLength(strlen($body)) . $body;
        return gmssl_sm2_decrypt(self::getSm2PrivKey(), $der);
    }

    /**
     * 导出SM2公钥为非压缩点hex(04||X||Y,130位字符),用于下发给客户端 JS 库(如 sm-crypto)
     * @return string
     */
    public static function sm2PubKeyHex(): string
    {
        $pub = self::getSm2PubKey();
        // 公钥对象为96字节,前64字节即 X||Y,后32字节为零填充的私钥位,不取
        return '04' . bin2hex(substr($pub, 0, 64));
    }

    /**
     * 从SM2私钥导出公钥PEM(公钥丢失时可随时由私钥重新导出,密钥对不变)
     * @param string $targetFile 目标公钥PEM路径
     * @return string 导出的公钥非压缩点hex(04||X||Y,130位)
     */
    public static function exportSm2PubPem(string $targetFile): string
    {
        $keypair = self::getSm2PrivKey();
        if (gmssl_sm2_public_key_info_to_pem($keypair, $targetFile) !== true) {
            throw new RuntimeException('SM2公钥导出失败: 请检查目标路径是否可写');
        }
        // 私钥对象前64字节即公钥点 X||Y,直接推导,无需再读公钥文件
        return '04' . bin2hex(substr($keypair, 0, 64));
    }

    /**
     * 解析 SM4 密钥/IV:未传自定义值时用配置默认值,传了则现场 hex 解码(不走缓存)
     * @return array [二进制key, 二进制iv]
     */
    protected static function sm4Keys(?string $keyHex, ?string $ivHex): array
    {
        $key = $keyHex === null ? self::getSm4Key() : self::hexToBin($keyHex, 'SM4自定义密钥');
        $iv  = $ivHex === null ? self::getSm4Iv() : self::hexToBin($ivHex, 'SM4自定义IV');
        return [$key, $iv];
    }

    /**
     * 获取SM4二进制key(16字节)
     */
    protected static function getSm4Key(): string
    {
        if (self::$sm4Key === null) {
            self::$sm4Key = self::hexToBin((string) config('gm.sm4_key'), 'GM_SM4_KEY');
        }
        return self::$sm4Key;
    }

    /**
     * 获取SM4二进制iv(16字节)
     */
    protected static function getSm4Iv(): string
    {
        if (self::$sm4Iv === null) {
            self::$sm4Iv = self::hexToBin((string) config('gm.sm4_iv'), 'GM_SM4_IV');
        }
        return self::$sm4Iv;
    }

    /**
     * 加载SM2私钥对象(从口令加密的PEM文件解密导入)
     */
    protected static function getSm2PrivKey(): string
    {
        if (self::$sm2PrivKey === null) {
            $file = self::resolveKeyPath((string) config('gm.sm2_priv_key_file'), 'GM_SM2_PRIV_KEY_FILE');
            $pass = (string) config('gm.sm2_priv_pass');
            if ($pass === '') {
                throw new RuntimeException('国密配置错误: 未配置 GM_SM2_PRIV_PASS(SM2私钥口令)');
            }
            self::$sm2PrivKey = gmssl_sm2_private_key_info_decrypt_from_pem($file, $pass);
        }
        return self::$sm2PrivKey;
    }

    /**
     * 加载SM2公钥对象
     */
    protected static function getSm2PubKey(): string
    {
        if (self::$sm2PubKey === null) {
            $file = self::resolveKeyPath((string) config('gm.sm2_pub_key_file'), 'GM_SM2_PUB_KEY_FILE');
            self::$sm2PubKey = gmssl_sm2_public_key_info_from_pem($file);
        }
        return self::$sm2PubKey;
    }

    /**
     * 解析密钥文件路径:绝对路径直接使用,相对路径基于 storage/app 解析
     */
    protected static function resolveKeyPath(string $path, string $envName): string
    {
        $path = trim($path);
        if ($path === '') {
            throw new RuntimeException("国密配置错误: 未配置 {$envName}");
        }
        if (!str_starts_with($path, '/') && !preg_match('/^[A-Za-z]:[\\\\\/]/', $path)) {
            $path = storage_path('app/' . ltrim($path, '/'));
        }
        if (!is_file($path)) {
            throw new RuntimeException("国密配置错误: {$envName} 指向的文件不存在: {$path}");
        }
        return $path;
    }

    /**
     * hex字符串转二进制,并校验长度为16字节
     */
    protected static function hexToBin(string $hex, string $envName): string
    {
        $hex = trim($hex);
        if ($hex === '' || !ctype_xdigit($hex) || strlen($hex) % 2 !== 0) {
            throw new RuntimeException("国密配置错误: {$envName} 必须是合法的hex字符串");
        }
        $bin = hex2bin($hex);
        if (strlen($bin) !== 16) {
            throw new RuntimeException("国密配置错误: {$envName} 必须是16字节(32位hex)");
        }
        return $bin;
    }

    /**
     * SM2签名/验签使用的签名者ID
     */
    protected static function sm2Id(?string $id): string
    {
        if ($id !== null && $id !== '') {
            return $id;
        }
        $configId = (string) config('gm.sm2_id', '');
        return $configId !== '' ? $configId : GMSSL_SM2_DEFAULT_ID;
    }

    /**
     * DER 长度编码
     */
    protected static function derLength(int $len): string
    {
        if ($len < 0x80) {
            return chr($len);
        }
        $bytes = '';
        while ($len > 0) {
            $bytes = chr($len & 0xFF) . $bytes;
            $len >>= 8;
        }
        return chr(0x80 | strlen($bytes)) . $bytes;
    }

    /**
     * DER INTEGER 编码(去前导零,高位为1时补 0x00)
     */
    protected static function derInt(string $bin): string
    {
        $bin = ltrim($bin, "\x00");
        if ($bin === '') {
            $bin = "\x00";
        }
        if (ord($bin[0]) & 0x80) {
            $bin = "\x00" . $bin;
        }
        return "\x02" . self::derLength(strlen($bin)) . $bin;
    }

    /**
     * DER OCTET STRING 编码
     */
    protected static function derOctetString(string $bin): string
    {
        return "\x04" . self::derLength(strlen($bin)) . $bin;
    }
}
  1. 在全局辅助函数 helpers.php 中增加函数。
use App\Utils\GmUtils;

/**
 * SM2签名,返回base64 der签名
 */
function sm2_sign(string $data): string
{
    return GmUtils::sm2Sign($data);
}

/**
 * SM2验签
 */
function sm2_verify(string $data, string $base64Sign): bool
{
    return GmUtils::sm2Verify($data, $base64Sign);
}

/**
 * SM2公钥加密 返回base64密文
 */
function sm2_encrypt(string $plain): string
{
    return GmUtils::sm2Encrypt($plain);
}

/**
 * SM2私钥解密
 */
function sm2_decrypt(string $base64Cipher): string
{
    return GmUtils::sm2Decrypt($base64Cipher);
}

/**
 * SM2解密:客户端(小程序/前端 sm-crypto)传来的裸 C1C3C2 hex 密文
 */
function sm2_decrypt_hex(string $hexCipher): string
{
    return GmUtils::sm2DecryptHex($hexCipher);
}

/**
 * SM3哈希摘要 返回小写hex
 */
function sm3_hash(string $data): string
{
    return GmUtils::sm3($data);
}

/**
 * SM4‑CBC加密 返回base64密文;可选自定义密钥/IV(32位hex),不传用配置默认值
 */
function sm4_encrypt(string $plainText, ?string $keyHex = null, ?string $ivHex = null): string
{
    return GmUtils::sm4Encrypt($plainText, $keyHex, $ivHex);
}

/**
 * SM4‑CBC解密;可选自定义密钥/IV(32位hex),不传用配置默认值
 */
function sm4_decrypt(string $base64Cipher, ?string $keyHex = null, ?string $ivHex = null): string
{
    return GmUtils::sm4Decrypt($base64Cipher, $keyHex, $ivHex);
}

/**
 * SM4‑CBC加密 返回hex密文(与小程序/前端对接用)
 */
function sm4_encrypt_hex(string $plainText, ?string $keyHex = null, ?string $ivHex = null): string
{
    return GmUtils::sm4EncryptHex($plainText, $keyHex, $ivHex);
}

/**
 * SM4‑CBC解密 入参为hex密文(与小程序/前端对接用)
 */
function sm4_decrypt_hex(string $hexCipher, ?string $keyHex = null, ?string $ivHex = null): string
{
    return GmUtils::sm4DecryptHex($hexCipher, $keyHex, $ivHex);
}
  1. 到此,算是都准备好了。进行测试和具体的业务使用了。
# 在容器中,laravel 项目根目录,执行下边的命令,测试一下

php artisan tinker --execute="var_dump(sm4_decrypt(sm4_encrypt('国密测试')), sm2_verify('hello', sm2_sign('hello')), sm2_decrypt(sm2_encrypt('hello')), sm3_hash('abc'));"

# 输出
string(12) "国密测试"
bool(true)
string(5) "hello"
string(64) "66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0"
  1. 其他端,要使用非对称加密 sm2 的时候,可以把公钥给过去,签名者ID 约定好就可以。其他的东西不要给过去。

sm2 只能前端加密,后端加密。没办法反着来。如果想要双向的,只能结合 sm4。

  • 前端随机生成一个 16 字节 SM4 密钥(hex)
  • 用 SM2 公钥加密这个 SM4 密钥,随请求上传(服务端 sm2_decrypt_hex 解开)
  • 之后双方用这个 SM4 密钥走 GM_SM4_KEY/IV 相同模式的 CBC 加解密(服务端已有 sm4_encrypt/sm4_decrypt)

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注