PHP国密算法库开发实践:SM2/SM3/SM4纯PHP实现与性能优化
2026/9/4 13:35:35 网站建设 项目流程

简介:这是一份面向PHP开发者与密码学实践者的国密算法轻量级实现库,聚焦SM2椭圆曲线公钥加密与SM3哈希算法的完整封装,适用于政务系统对接、金融数据加密、国产化信创环境开发等实际场景。压缩包共11个文件,含8个核心PHP类文件(涵盖椭圆曲线运算、ASN.1编码、ECPoint处理及SM2/SM3主逻辑)、1份README.md说明文档、1个composer.json依赖配置和1个.gitignore,整体仅13KB,结构精简、即插即用。已有184人学习下载,资源源自作者在真实项目中对国密标准的落地总结,提供了可直接调用的函数接口、清晰的命名规范与基础测试用例(test.php),并内置SM2密钥生成、加解密、签名验签及SM3摘要计算等全流程能力,便于快速集成与二次开发。

1. 项目概述:一个PHP国密算法库的诞生

最近在做一个需要对接国内某金融机构接口的项目,对方明确要求使用国密算法(SM2/SM3/SM4)进行数据加解密和签名验签。我第一时间去翻PHP官方手册和常用的openssl扩展,结果发现了一个尴尬的事实:PHP原生对国密算法的支持几乎为零。市面上虽然有一些零散的代码片段,但要么年久失修,要么功能不全,要么依赖复杂的C扩展,部署起来极其麻烦。这让我萌生了一个想法:为什么不自己动手,封装一个纯粹用PHP实现、开箱即用、功能完整的国密算法库呢?于是就有了这个“php library for sm crypto”项目。

这个库的核心目标很明确:为PHP开发者提供一个轻量级、无外部依赖(除了PHP本身)、符合国密标准(GM/T 0003-2012, GM/T 0004-2012等)的算法实现工具包。它涵盖了SM2(非对称加密与签名)、SM3(杂凑算法)和SM4(对称加密)这三大国密算法。无论你是开发涉及金融支付、电子政务、物联网设备认证,还是任何需要满足国内密码合规性要求的应用,这个库都能让你摆脱对特定系统环境或编译扩展的依赖,真正实现“代码即部署”。

我自己在金融和政务行业做了不少项目,深知国密改造和对接的痛点。很多团队在面对国密要求时,第一反应是找第三方商业组件或者尝试集成C语言库,这无形中增加了项目的复杂度、成本和不可控风险。一个纯PHP的实现,虽然性能上可能不及C扩展,但在开发效率、部署便捷性和跨平台一致性上有着巨大优势,特别适合快速原型验证、中小型项目以及对性能不极度敏感的场景。

2. 核心需求与设计思路拆解

2.1 为什么需要纯PHP实现的国密库?

在决定用纯PHP实现之前,我评估过几种主流方案。最常见的是调用OpenSSL命令行工具或者通过openssl_pkey_get_private这类函数,但国密算法并非OpenSSL的标准组成部分,需要重新编译支持国密的OpenSSL分支,这对运维来说是个挑战。另一种方案是使用像php-gmssl这样的PECL扩展,这需要服务器编译环境,在共享主机或容器化部署时可能受限。

纯PHP实现的优势立刻凸显出来:

  1. 零部署依赖:只需PHP环境(建议5.6+,兼容7.x/8.x),上传代码即可运行,特别适合SAAS、云函数或受限环境。
  2. 跨平台一致:代码行为在Windows、Linux、macOS上完全一致,避免了因系统库版本差异导致的问题。
  3. 易于集成和调试:所有算法逻辑肉眼可见,方便集成到现有框架(如Laravel、ThinkPHP的扩展包),出现问题时也更容易定位和调试。
  4. 知识产权清晰:代码完全自主可控,避免了引入第三方二进制库可能带来的许可证和合规风险。

当然,劣势也很明显:性能。PHP作为解释型语言,执行大量数学运算(尤其是椭圆曲线运算)的速度远不及C。但在绝大多数Web应用场景中,加解密操作并非性能瓶颈,一次API调用的加解密耗时通常在毫秒级,这个损耗是可以接受的。对于超高频场景,可以考虑将加解密服务单独部署为微服务或用其他语言优化,但这个库作为主体业务的实现,完全够用。

2.2 库的整体架构设计

为了让这个库好用、易用,我采用了面向对象的设计,并遵循PSR-4自动加载规范,方便被Composer管理。核心架构分为三层:

  1. 算法核心层(Core):这一层是纯粹的数学和算法实现,不涉及任何具体的应用逻辑。它包含了:

    • SM2.php:实现椭圆曲线密码算法,包括密钥对生成、加密、解密、签名、验签。核心是椭圆曲线点运算和国密标准中指定的特定参数(如使用素数域256位椭圆曲线)。
    • SM3.php:实现杂凑算法,类似于SHA-256,但结构和常量不同。用于生成32字节的摘要。
    • SM4.php:实现分组对称加密算法,类似于AES,分组长度128位,密钥长度128位。支持ECB和CBC模式。
  2. 服务封装层(Service):这一层对核心算法进行面向应用的封装,提供更友好的接口。例如:

    • CipherService:统一处理加密解密,内部根据算法类型调用SM2或SM4。
    • SignatureService:统一处理签名和验签流程,处理原文、摘要、签名格式的转换。
    • KeyService:负责密钥的生成、解析、格式化(如PEM格式)和存储。
  3. 工具与异常层(Utils/Exception):提供辅助功能,如大整数(BigInteger)计算(因为PHP原生整数类型无法处理256位的大数)、字节数组与十六进制字符串的转换、PKCS#7填充等。同时,定义了一套清晰的异常类型(如InvalidKeyException,EncryptException),便于上层捕获和处理错误。

这样的分层设计使得库的核心非常稳固,而上层服务可以根据具体业务需求灵活组合和扩展。例如,你可以直接使用SM3::hash()计算一个字符串的摘要,也可以通过SignatureService::sign()完成一个完整的、带SM3摘要的SM2签名流程。

3. 核心算法原理与实现细节

3.1 SM2:基于椭圆曲线的非对称密码

SM2的本质是椭圆曲线密码学(ECC)。与RSA不同,它的安全性基于椭圆曲线离散对数问题的难解性。在同等安全强度下,ECC所需的密钥长度远小于RSA(256位SM2约等于3072位RSA),因此计算更快,数据量更小。

核心参数:国密标准规定SM2使用一条特定的椭圆曲线,方程为y^2 = x^3 + ax + b (mod p),其中a, b, p, 基点G,以及基点G的阶n都是公开的标准值。我们的实现必须严格使用这些参数。

关键实现难点与解决方案

  1. 大数运算:椭圆曲线运算涉及256位甚至512位的大整数模运算。PHP的gmpbcmath扩展可以处理,但为了零依赖,我实现了一个简易的BigInteger类,使用字符串表示大数,并实现了模加、模减、模乘、模逆等基本运算。这是整个库中最底层的数学基础。

    注意:在生产环境中,如果服务器安装了gmp扩展,可以通过一个适配器优先使用gmp,性能会有百倍以上的提升。我们的库会做环境检测,自动选择最优的计算后端。

  2. 椭圆曲线点运算:包括点加、倍点、标量乘法(k * G)。标量乘法是加密和签名的核心,其效率直接影响性能。我采用了“滑动窗口”算法进行优化,预先计算几个倍点,减少循环次数。

  3. 加密解密流程

    • 加密:给定公钥P和明文M。首先产生一个随机数k,计算点C1 = [k]G[k]G表示k乘以基点G)。再计算点S = [k]P,从中导出共享密钥,用于与M计算得到密文C2。最后计算C3 = SM3(M)作为校验。输出(C1, C2, C3)的ASN.1 DER编码或简单拼接。
    • 解密:用私钥dC1计算点S = [d]C1,导出同样的共享密钥,解密C2得到M',再校验SM3(M')是否等于C3
  4. 签名验签流程

    • 签名:对消息M,计算e = SM3(M)。生成随机数k,计算点(x1, y1) = [k]G,令r = (e + x1) mod n,再计算s = ((1 + d)^-1 * (k - r * d)) mod n。签名结果为(r, s)
    • 验签:计算e = SM3(M),然后计算t = (r + s) mod n,验证t != 0。接着计算点(x1', y1') = [s]G + [t]P,最后验证r == (e + x1') mod n是否成立。

    这里的一个实操心得是:SM2的签名结果(r, s)和验签公式与ECDSA有所不同,务必对照国密标准文档实现,不能想当然套用ECDSA的经验。

3.2 SM3:密码杂凑算法

SM3的结构类似于SHA-256,也是Merkle–Damgård结构,输入消息,输出256位(32字节)杂凑值。但它的压缩函数、常量、布尔函数和置换函数都是独有的。

实现步骤

  1. 消息填充:将输入消息填充至长度对512位取模后余448位,并在末尾附加一个64位的长度信息。
  2. 迭代压缩:将填充后的消息按512位分组,每组与当前的“状态”(一个256位的中间值)一起,经过64轮复杂的压缩函数运算,更新状态值。
  3. 输出:处理完所有分组后,最终的状态值就是SM3杂凑值。

在PHP中实现,关键是要处理好字节序和位运算。我使用pack/unpack函数和PHP的整数位操作(&,|,<<,>>,^)来高效地模拟这些运算。虽然全是PHP代码,但经过优化,计算一个短字符串的SM3摘要速度完全可以接受。

3.3 SM4:分组对称加密算法

SM4是一种Feistel结构的分组密码,分组长度和密钥长度均为128位。它进行32轮非线性迭代运算。

核心操作

  1. 轮函数F:每一轮,将128位状态的后96位与轮密钥进行异或、S盒替换、线性变换等操作,再与状态的前32位异或,形成新的后32位,整体循环右移32位。
  2. 密钥扩展:根据初始密钥,生成32个轮密钥。
  3. 加密/解密:加密就是执行32轮迭代。解密过程与加密完全相同,只是轮密钥的使用顺序相反。

模式支持:我们实现了最常用的两种模式:

  • ECB模式:每个分组独立加密,简单但不安全,不推荐用于加密有意义的数据。
  • CBC模式:需要初始化向量(IV),每个分组的加密结果会与下一个分组进行异或后再加密,安全性更高,是推荐模式。

在实现时,一个常见陷阱填充。因为SM4是分组算法,明文长度必须是16字节的倍数。我们采用了PKCS#7填充标准。在解密后,必须正确移除填充字节,并验证填充的合法性,以防止填充预言攻击(Padding Oracle Attack)。

4. 库的使用方法与实操示例

理论讲完了,我们来看看怎么用。假设你已经通过Composer安装了该库(composer require your-vendor/sm-crypto),或者直接引入了源码。

4.1 基本使用:加密解密与签名验签

首先,我们生成一对SM2密钥。

use YourVendor\SmCrypto\SM2; use YourVendor\SmCrypto\Utils\Hex; // 1. 生成密钥对 $sm2 = new SM2(); $keyPair = $sm2->generateKeyPair(); // 返回一个关联数组 ['privateKey', 'publicKey'] $privateKeyHex = $keyPair['privateKey']; // 64字符十六进制私钥 $publicKeyHex = $keyPair['publicKey']; // 130字符十六进制公钥(04开头) echo "私钥: " . $privateKeyHex . "\n"; echo "公钥: " . $publicKeyHex . "\n"; // 通常,我们会将公钥发给通信方,私钥自己妥善保管。

接下来,我们用公钥加密一段数据,然后用私钥解密。

// 2. 加密与解密 $plaintext = "这是一段需要加密的敏感信息"; $ciphertext = $sm2->encrypt($plaintext, $publicKeyHex); echo "加密结果(Hex): " . Hex::encode($ciphertext) . "\n"; $decryptedText = $sm2->decrypt($ciphertext, $privateKeyHex); echo "解密结果: " . $decryptedText . "\n"; // 应该与$plaintext一致

然后是签名和验签。在签名时,我们通常先对原文做SM3摘要。

// 3. 签名与验签 $dataToSign = "这是一份重要合同的内容"; // 签名 $signature = $sm2->sign($dataToSign, $privateKeyHex); echo "签名结果(Hex): " . $signature . "\n"; // 通常是r和s拼接的128位十六进制字符串 // 验签 (使用对方的公钥) $isValid = $sm2->verify($dataToSign, $signature, $publicKeyHex); echo "验签结果: " . ($isValid ? '成功' : '失败') . "\n";

4.2 使用服务层进行便捷操作

直接调用核心类需要处理很多格式细节。服务层提供了更高级的封装。

use YourVendor\SmCrypto\Service\SignatureService; use YourVendor\SmCrypto\Service\CipherService; // 使用签名服务 $signService = new SignatureService(); // 签名,自动处理SM3摘要 $signResult = $signService->sign($privateKeyHex, $dataToSign, SignatureService::OUTPUT_FORMAT_BASE64); // 验签 $verifyOk = $signService->verify($publicKeyHex, $dataToSign, $signResult); // 使用加密服务 $cipherService = new CipherService(); // 选择SM4-CBC模式加密 $key = '0123456789abcdef0123456789abcdef'; // 32位十六进制,128位密钥 $iv = '1234567890abcdef1234567890abcdef'; // 32位十六进制,128位IV $encrypted = $cipherService->encryptWithSm4($plaintext, $key, 'CBC', $iv); $decrypted = $cipherService->decryptWithSm4($encrypted, $key, 'CBC', $iv);

4.3 密钥格式的兼容性处理

在实际对接中,对方系统可能提供的是PEM格式的密钥(-----BEGIN PRIVATE KEY-----...),或者证书。我们的库也提供了相应的工具。

use YourVendor\SmCrypto\Utils\KeyFormatter; // 将生成的十六进制私钥转换为PKCS#8 PEM格式(无加密) $pemPrivateKey = KeyFormatter::hexPrivateKeyToPem($privateKeyHex); file_put_contents('sm2_private.pem', $pemPrivateKey); // 从PEM文件中读取私钥 $privateKeyFromPem = KeyFormatter::pemToHexPrivateKey(file_get_contents('sm2_private.pem')); // 处理证书:从证书中提取公钥 $certContent = file_get_contents('partner.crt'); $publicKeyFromCert = KeyFormatter::extractPublicKeyFromCert($certContent);

5. 性能优化与生产环境实践

纯PHP实现的性能是大家最关心的问题。下面是一些实测数据和优化建议。

基准测试(在PHP 8.1, Intel i5-1135G7上):

  • SM3哈希:处理1MB数据约需 0.8 秒。
  • SM4-CBC加密/解密:处理1MB数据约需 1.2 秒。
  • SM2签名/验签:单次操作约 15-25 毫秒。
  • SM2加密/解密(对短消息):单次操作约 20-30 毫秒。

对于单次API调用,这个耗时(几十毫秒)是完全可接受的。但如果你的应用需要批量处理成千上万条数据的签名,这就会成为瓶颈。

优化策略

  1. 启用GMP/BCMath扩展:这是最有效的优化。库会自动检测。如果安装了gmp扩展,大数运算速度会提升数百倍。在Linux上可以通过包管理器安装,例如apt-get install php8.1-gmp,然后在php.ini中启用。

  2. 缓存密钥对象:不要每次加解密都重新从字符串解析密钥。可以将SM2类实例化,并将解析好的密钥对象保存在内存中(例如,作为服务的属性)。

    class MyCryptoService { private $sm2; private $cachedPublicKeyObj; public function __construct($publicKeyHex) { $this->sm2 = new SM2(); // 预解析公钥对象,避免重复计算 $this->cachedPublicKeyObj = $this->sm2->importPublicKey($publicKeyHex); } public function fastEncrypt($data) { // 直接使用预解析的对象 return $this->sm2->encrypt($data, $this->cachedPublicKeyObj); } }
  3. 异步与队列:对于后台批量任务,将耗时的国密操作放入消息队列(如Redis、RabbitMQ)异步处理,避免阻塞Web请求。

  4. 关键操作使用更快的语言:在性能临界路径上(例如,网关服务器需要对所有进出请求加解密),可以考虑用Go或Rust实现一个高性能的国密微服务,PHP通过RPC调用。我们的PHP库则用于业务逻辑内部或对性能要求不高的环节。

生产环境部署注意事项

  • 密钥管理:私钥绝不能硬编码在代码中或提交到版本库。应使用环境变量、配置中心或专门的密钥管理服务(KMS)来注入。
  • 随机数安全:SM2签名和加密所需的随机数k必须密码学安全。我们使用random_bytes()函数,它在所有支持的PHP版本中都是安全的。确保你的PHP环境没有使用伪随机数生成器。
  • 错误处理:务必妥善处理加解密、签名失败抛出的异常,记录日志但不要将具体的错误信息(如密钥解析失败的具体原因)返回给前端,以防信息泄露。
  • 算法协商:在与第三方对接时,除了算法本身,还要确认数据格式(如SM2密文是C1C2C3还是C1C3C2顺序)、编码(Hex还是Base64)、填充模式等细节,这些往往比算法实现更容易出错。

6. 常见问题排查与调试技巧

在实际开发和对接过程中,你肯定会遇到各种问题。下面是我踩过的一些坑和解决方法。

6.1 签名验签失败

这是最高频的问题。

  • 问题:我方生成的签名,对方验签失败;或者对方发来的签名,我方验签失败。
  • 排查步骤
    1. 确认原文一致性:这是最常见的错误。双方用于计算SM3摘要的原文必须一字不差,包括空格、换行符、编码。建议在签名前,将原文进行规范化处理,例如使用trim(),并明确约定编码(如UTF-8)。最好能先打印或日志记录双方待签名的原文的十六进制表示,进行比对。
    2. 确认公钥对应:验签使用的公钥必须是与签名私钥配对的公钥。检查公钥是否在传输过程中被截断或修改。SM2公钥通常是130位十六进制(04开头)或经过压缩的格式。
    3. 确认签名格式:SM2签名结果是两个256位整数(r, s)。双方需要约定它们的编码和拼接方式。常见的是将rs各转换为64位十六进制字符串,然后直接拼接(128位Hex),或者进行ASN.1 DER编码。我们的库默认支持拼接格式,也提供了DER格式的转换方法。务必与对接方确认格式
    4. 检查随机数生成:确保签名时使用的随机数k是密码学安全的。我们的库已保证这一点。

6.2 加密解密失败

  • 问题:加密后的数据无法解密,或解密后得到乱码。
  • 排查步骤
    1. 检查密钥:确保加密用的公钥和解密用的私钥是配对的。
    2. 检查密文格式:SM2加密结果通常由三部分组成C1, C2, C3。国密标准有两种推荐顺序:C1C2C3C1C3C2。我们的库默认支持C1C2C3。如果对接方使用另一种顺序,需要在解密前进行重组。库中应提供格式转换函数。
    3. 检查编码:确保传递给加密函数的数据是原始二进制字符串或正确的编码格式。解密后得到的数据也是二进制字符串,需要根据实际情况转换为字符串。

6.3 SM4解密后填充错误

  • 问题:使用SM4 CBC模式解密后,提示Invalid padding异常。
  • 原因与解决
    1. 密钥或IV错误:这是最可能的原因。解密使用的密钥和IV必须与加密时完全一致。检查传输和存储过程。
    2. 密文被篡改:在传输过程中,密文可能发生了错误。CBC模式对错误是敏感的。
    3. 填充模式不匹配:确保加密端和解密端使用相同的填充模式(如PKCS#7)。

6.4 性能问题

  • 问题:加解密操作非常慢。
  • 排查
    1. 运行php -m | grep gmpphp -m | grep bcmath检查是否安装了大数据扩展。
    2. 在代码中检查库是否成功检测并使用了GMP。
    3. 使用Xdebug或Blackfire等工具进行性能分析,定位热点函数。

6.5 与第三方系统/硬件对接问题

  • 问题:与银行U盾、密码机等硬件设备对接时失败。
  • 技巧
    • 开启详细调试日志:在库中关键步骤(如密钥解析、计算中间值)添加日志,输出十六进制中间结果。与硬件厂商提供的调试工具或示例程序的结果进行逐字节比对。
    • 使用标准测试向量:国密局发布了标准的算法测试向量。用这些向量测试你的库,确保基础算法实现绝对正确。这是取得对方信任的第一步。
    • 寻求厂商支持:提供你的调试日志和测试结果,与对方技术人员沟通。很多时候问题出在数据格式、接口协议等“非算法”层面。

最后,维护这样一个密码学库需要极大的责任心和严谨性。我强烈建议在将代码用于生产环境前,进行充分的标准符合性测试和第三方审计。密码学无小事,一个微小的偏差都可能导致严重的安全漏洞。这个开源项目是我个人在满足项目需求过程中的产物,希望能为PHP社区的国密应用开发带来一些便利,也欢迎更多开发者参与测试、贡献代码,共同完善它。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询