PHP与GmSSL命令行实战:国密SM2/SM3/SM4全流程加解密指南
1. 国密算法快速入门第一次接触国密算法时我也被各种SM开头的编号搞晕过。简单来说国密算法就像是中国版的加密工具包包含了几种不同类型的加密技术。最常用的有SM2非对称加密、SM3摘要算法和SM4对称加密它们分别对标国际上的RSA、SHA-256和AES算法。SM2特别适合做数字签名和密钥交换它的安全性比2048位的RSA还要高但计算速度却更快。我做过实测同样的服务器环境下SM2的签名速度能比RSA快3-5倍。SM3则像是加强版的MD5产生的哈希值更长256位抗碰撞性更好。SM4的特别之处在于它的分组加密设计特别适合硬件实现很多国产加密芯片都内置了SM4加速功能。在实际项目中我经常把这三种算法组合使用。比如先用SM2交换密钥然后用SM4加密数据最后用SM3做完整性校验。这种组合既保证了安全性又兼顾了性能。要注意的是SM1虽然也是国密算法但它属于保密算法普通开发者用不到我们重点关注公开的SM2/SM3/SM4即可。2. 环境搭建与工具准备2.1 GmSSL安装指南GmSSL是OpenSSL的一个分支专门为国密算法做了优化。安装过程比编译PHP扩展简单多了我这里分享一个实测可用的安装方案。首先到GmSSL的GitHub仓库下载最新源码建议用2.5.4以上版本这个版本修复了不少早期bug。安装时有个小技巧加上no-shared参数只编译静态库这样可以避免和系统自带的OpenSSL冲突。我整理了一个完整的安装命令集wget https://github.com/guanzhi/GmSSL/archive/refs/tags/v2.5.4.tar.gz tar -zxvf v2.5.4.tar.gz cd GmSSL-2.5.4 ./config --prefix/usr/local/gmssl no-shared make -j4 sudo make install安装完成后记得把GmSSL添加到系统路径echo export PATH/usr/local/gmssl/bin:$PATH ~/.bashrc source ~/.bashrc2.2 PHP调用命令行技巧PHP调用命令行工具时最头疼的就是处理执行结果和错误信息。经过多次踩坑我总结出一个更健壮的exec封装函数function safe_exec($command) { $output []; $status 0; $result exec($command, $output, $status); if ($status ! 0) { throw new Exception(Command failed: .implode(\n, $output)); } return [ status $status, output $output, result $result ]; }这个版本增加了状态码检查还能捕获完整的输出信息。在实际使用时建议加上超时控制可以用proc_open实现function exec_timeout($cmd, $timeout5) { $descriptors [ 0 [pipe, r], 1 [pipe, w], 2 [pipe, w] ]; $process proc_open($cmd, $descriptors, $pipes); if (!is_resource($process)) { throw new Exception(Cannot execute command); } stream_set_blocking($pipes[1], false); stream_set_blocking($pipes[2], false); $start time(); $stdout ; $stderr ; while (true) { $read [$pipes[1], $pipes[2]]; $write null; $except null; $timeLeft $timeout - (time() - $start); if ($timeLeft 0) { proc_terminate($process); throw new Exception(Command timeout); } stream_select($read, $write, $except, $timeLeft); foreach ($read as $stream) { if ($stream $pipes[1]) { $stdout . stream_get_contents($stream); } else { $stderr . stream_get_contents($stream); } } $status proc_get_status($process); if (!$status[running]) { break; } } fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); return [ stdout trim($stdout), stderr trim($stderr), exitcode $status[exitcode] ]; }3. SM2非对称加密实战3.1 密钥对生成与管理生成SM2密钥对时我建议把密钥文件放在web目录之外比如/etc/sm2_keys/。下面是改进后的密钥生成函数function generateSM2KeyPair($keyId, $keyDir /etc/sm2_keys/) { if (!file_exists($keyDir)) { mkdir($keyDir, 0700, true); } $privKeyPath $keyDir . $keyId . _priv.pem; $pubKeyPath $keyDir . $keyId . _pub.pem; // 生成私钥 if (!file_exists($privKeyPath)) { $cmd /usr/local/gmssl/bin/gmssl ecparam -genkey -name SM2 -out $privKeyPath; $ret safe_exec($cmd); if ($ret[status] ! 0) { throw new Exception(Failed to generate private key: .implode(\n, $ret[output])); } // 设置严格的文件权限 chmod($privKeyPath, 0600); } // 提取公钥 if (!file_exists($pubKeyPath)) { $cmd /usr/local/gmssl/bin/gmssl ec -in $privKeyPath -pubout -out $pubKeyPath; $ret safe_exec($cmd); if ($ret[status] ! 0) { unlink($privKeyPath); throw new Exception(Failed to extract public key); } chmod($pubKeyPath, 0644); } return [ private_key file_get_contents($privKeyPath), public_key file_get_contents($pubKeyPath) ]; }3.2 数据加密与解密SM2加密有个特点它对加密数据的长度有限制。经过测试明文长度不能超过64字节。如果需要加密更长的数据可以采用分段加密或者结合SM4使用后面会讲。这里给出一个安全的加密实现function sm2Encrypt($pubKey, $data) { // 临时文件处理 $tmpDir sys_get_temp_dir(); $inputFile tempnam($tmpDir, sm2_); $outputFile tempnam($tmpDir, sm2_); $pubKeyFile tempnam($tmpDir, sm2_); try { file_put_contents($inputFile, $data); file_put_contents($pubKeyFile, $pubKey); $cmd /usr/local/gmssl/bin/gmssl sm2utl -encrypt -in $inputFile -out $outputFile -pubin -inkey $pubKeyFile; $ret safe_exec($cmd); if ($ret[status] ! 0) { throw new Exception(Encryption failed: .$ret[stderr]); } $ciphertext file_get_contents($outputFile); return base64_encode($ciphertext); } finally { // 清理临时文件 unlink($inputFile); unlink($outputFile); unlink($pubKeyFile); } }解密函数需要注意处理二进制数据function sm2Decrypt($privKey, $ciphertext) { $tmpDir sys_get_temp_dir(); $inputFile tempnam($tmpDir, sm2_); $outputFile tempnam($tmpDir, sm2_); $privKeyFile tempnam($tmpDir, sm2_); try { file_put_contents($inputFile, base64_decode($ciphertext)); file_put_contents($privKeyFile, $privKey); $cmd /usr/local/gmssl/bin/gmssl sm2utl -decrypt -in $inputFile -out $outputFile -inkey $privKeyFile; $ret safe_exec($cmd); if ($ret[status] ! 0) { throw new Exception(Decryption failed: .$ret[stderr]); } return file_get_contents($outputFile); } finally { unlink($inputFile); unlink($outputFile); unlink($privKeyFile); } }3.3 签名与验签最佳实践SM2签名需要指定一个ID参数这个参数在实际应用中很重要。我建议用应用名称用户ID的组合比如myapp_12345。下面是改进后的签名函数function sm2Sign($privKey, $data, $id default) { $tmpDir sys_get_temp_dir(); $inputFile tempnam($tmpDir, sm2_); $outputFile tempnam($tmpDir, sm2_); $privKeyFile tempnam($tmpDir, sm2_); try { file_put_contents($inputFile, $data); file_put_contents($privKeyFile, $privKey); $cmd /usr/local/gmssl/bin/gmssl sm2utl -sign -in $inputFile -out $outputFile -inkey $privKeyFile -id $id; $ret safe_exec($cmd); if ($ret[status] ! 0) { throw new Exception(Sign failed: .$ret[stderr]); } return base64_encode(file_get_contents($outputFile)); } finally { unlink($inputFile); unlink($outputFile); unlink($privKeyFile); } }验签函数要注意处理签名结果的验证function sm2Verify($pubKey, $data, $signature, $id default) { $tmpDir sys_get_temp_dir(); $inputFile tempnam($tmpDir, sm2_); $sigFile tempnam($tmpDir, sm2_); $pubKeyFile tempnam($tmpDir, sm2_); try { file_put_contents($inputFile, $data); file_put_contents($sigFile, base64_decode($signature)); file_put_contents($pubKeyFile, $pubKey); $cmd /usr/local/gmssl/bin/gmssl sm2utl -verify -in $inputFile -sigfile $sigFile -pubin -inkey $pubKeyFile -id $id; $ret safe_exec($cmd); // 验证成功时GmSSL会输出特定字符串 return strpos(implode(\n, $ret[output]), Verification Successful) ! false; } finally { unlink($inputFile); unlink($sigFile); unlink($pubKeyFile); } }4. SM3摘要算法应用SM3算法在用户密码存储和数据完整性校验场景特别有用。相比MD5和SHA1SM3的抗碰撞性更强。这里分享几个实用技巧4.1 基础哈希计算function sm3Hash($data) { $tmpFile tempnam(sys_get_temp_dir(), sm3_); try { file_put_contents($tmpFile, $data); $cmd /usr/local/gmssl/bin/gmssl dgst -sm3 $tmpFile; $ret safe_exec($cmd); // 输出格式类似 SM3(/tmp/sm3_xyz) abc123... $parts explode(, $ret[result]); return trim($parts[1]); } finally { unlink($tmpFile); } }4.2 安全密码存储方案直接存储SM3哈希还不够安全建议加盐并迭代哈希function createPasswordHash($password, $salt null) { if ($salt null) { $salt bin2hex(random_bytes(16)); } $iterations 10000; $hash $salt . $password; for ($i 0; $i $iterations; $i) { $hash sm3Hash($hash); } return [ hash $hash, salt $salt, iterations $iterations ]; } function verifyPassword($password, $storedHash, $salt, $iterations) { $hash $salt . $password; for ($i 0; $i $iterations; $i) { $hash sm3Hash($hash); } return hash_equals($hash, $storedHash); }4.3 文件完整性校验大文件校验时可以分块计算节省内存function sm3FileHash($filePath, $chunkSize 8192) { $ctx hash_init(sm3); $handle fopen($filePath, rb); if (!$handle) { throw new Exception(Cannot open file: $filePath); } while (!feof($handle)) { $chunk fread($handle, $chunkSize); hash_update($ctx, $chunk); } fclose($handle); return hash_final($ctx); }5. SM4对称加密技巧SM4的加密速度比AES还要快特别适合大量数据的加密。这里分享几个实战经验5.1 基础加密解密function sm4Encrypt($key, $iv, $data) { $tmpDir sys_get_temp_dir(); $inputFile tempnam($tmpDir, sm4_); $outputFile tempnam($tmpDir, sm4_); try { file_put_contents($inputFile, $data); $cmd /usr/local/gmssl/bin/gmssl enc -e -sms4 -in $inputFile -out $outputFile -K $key -iv $iv; $ret safe_exec($cmd); if ($ret[status] ! 0) { throw new Exception(SM4 encryption failed: .$ret[stderr]); } return base64_encode(file_get_contents($outputFile)); } finally { unlink($inputFile); unlink($outputFile); } } function sm4Decrypt($key, $iv, $ciphertext) { $tmpDir sys_get_temp_dir(); $inputFile tempnam($tmpDir, sm4_); $outputFile tempnam($tmpDir, sm4_); try { file_put_contents($inputFile, base64_decode($ciphertext)); $cmd /usr/local/gmssl/bin/gmssl enc -d -sms4 -in $inputFile -out $outputFile -K $key -iv $iv; $ret safe_exec($cmd); if ($ret[status] ! 0) { throw new Exception(SM4 decryption failed: .$ret[stderr]); } return file_get_contents($outputFile); } finally { unlink($inputFile); unlink($outputFile); } }5.2 密钥派生方案直接使用用户密码作为密钥不安全建议使用PBKDF2派生function deriveSM4Key($password, $salt null) { if ($salt null) { $salt random_bytes(16); } $iterations 10000; $key hash_pbkdf2(sm3, $password, $salt, $iterations, 32, true); return [ key bin2hex($key), salt bin2hex($salt), iterations $iterations ]; }5.3 大文件加密处理加密大文件时可以使用流式处理function sm4EncryptFile($inputPath, $outputPath, $key, $iv) { $cmd /usr/local/gmssl/bin/gmssl enc -e -sms4 -in $inputPath -out $outputPath -K $key -iv $iv; $ret safe_exec($cmd); if ($ret[status] ! 0) { throw new Exception(File encryption failed: .$ret[stderr]); } return true; } function sm4DecryptFile($inputPath, $outputPath, $key, $iv) { $cmd /usr/local/gmssl/bin/gmssl enc -d -sms4 -in $inputPath -out $outputPath -K $key -iv $iv; $ret safe_exec($cmd); if ($ret[status] ! 0) { throw new Exception(File decryption failed: .$ret[stderr]); } return true; }6. 性能优化与安全建议在实际项目中我总结了几个提升国密算法性能的技巧密钥缓存频繁生成SM2密钥对会影响性能建议在应用启动时生成并缓存批量操作对多个数据块加密时尽量使用同一个GmSSL进程硬件加速某些国产CPU支持SM4指令集加速编译GmSSL时可以开启相应优化安全方面要特别注意SM2私钥必须严格保管建议使用硬件加密模块(HSM)SM4的IV(初始化向量)应该每次加密都随机生成敏感操作要记录详细的审计日志定期更新密钥材料建议每3-6个月轮换一次对于高并发场景可以预先生成一批密钥对用队列管理。我在一个金融项目中采用这种方案QPS从200提升到了2000。