最近在对接民生银行的电子账户接口,按照民生的要求,调用接口需要涉及 SM2
国密算法及 SSL
双向认证。目前银行端提供的只有 JAVA
版的 SDK
,把 PHP
作为开发语言的我们表示很受伤。本文就针对涉及的两个点进行说明,简单汇总下 PHP
语言中的解决方案。
1、SM2
国密算法
针对接口请求参数的加密加签及响应数据的解密验签操作,民生银行的要求如下:
对数据进行PKCS#7
带原文签名,并将签名结果加密成CMS
格式的数字信封。SM2
算法,签名所采用的HASH
算法为SM3
(带Z
值),加密所采用的算法为SM4_CBC
,SM2
加密格式为老国密标准C1||C2||C3
。
所谓的数字信封,是一种综合利用了对称加密技术和非对称加密技术两者的优点进行信息安全传输的一种技术。数字信封既发挥了对称加密算法速度快、安全性好的优点,又发挥了非对称加密算法密钥管理方便的优点。
之前对接的平安银行电子账户接口中,对接口的参数安全处理,也是类似上面数字信封的概念。但是采用的对称加密算法为 AES
( AES-256-CBC
),采用的非对称加密算法为 RSA2
(2048
位秘钥,签名采用 SHA256
算法)。对于 AES
及 RSA
算法的实现,在 PHP
中还是非常方便的,使用 OPENSSL
相关函数即可。
至于民生要求的 SM2
国密相关算法,其实在 PHP
中目前没有太好的方案。经过各种搜索,我找到了这个看起来似乎是目前最优的解决方案: GMSSL
( http://gmssl.org/docs/php-api.html
)。
OpenSSL v1.1.1
新特性: 开始支持国密SM2/SM3/SM4加密算法(仅支持算法,未支持国密套件)
由于未对OpenSSL
的的国密相关算法进行详细研究,暂不说明。
按照 GMSSL
的官方说明,首先进行编译安装,服务器系统是 CentOS
,基本很顺利就能安装完成。然后是重新编译 PHP
,按官方文档,需要用 GMSSL
中的文件替换 PHP
中的 ext/openssl
,然后再进行编译安装。整个编译过程报错很多,通过各种搜索和尝试,终于解决了。在这里不得不提一句,互联网上 GMSSL
与 PHP
相关的资料真的很少,基本找不到可以参考的有价值信息,除了官网的那个 PHP API
的简单说明。
经过各种尝试,由于对 PKCS#7
的数字信封的格式要求实在不熟悉,另外参考了银行 SDK
中的 JAVA
代码的封装,仍然无解。出于时间的考虑,暂时放弃。后续有时间了还是要详细研究一下,我想更多是因为自己对这块的基础知识不足导致的。
由于上面的原因,最终我很不情愿的决定采用 PHP
调用 JAVA
代码的方式,通过民生的 SDK
来实现加密加签和解密验签操作。目前 PHP
调用 JAVA
有两种现有的解决方案,都是作者很多年前开发的,很久未更新了(也完全没有更新的必要)。一种是 PHP-Java-Bridge
( http://php-java-bridge.sourceforge.net/pjb/
),另外一种是 LAJP
( http://code.google.com/p/lajp/
)。因为民生的 Demo
中提供的是 LAJP
的方案,我进行了测试可以通过,另外就是这个相对于 PHP-Java-Bridge
更加的轻量级,因此就采用了这个方案,对代码进行重构后整合进现有的项目中。
2、CURL
SSL
双向认证
目前民生银行提供的接口对接方案有两种,即专线模式和互联网模式。专线模式的话就只需要对请求参数及响应数据做加密加签及解密验签即可,互联网模式的话还要额外进行 SSL
双向绑定。由于我们是采用互联网模式,因此就不得不来解决这个问题。
对于双向认证,其实民生也提供有 JAVA
的 SDK
可以使用,但是这是针对于 JAVA
项目而言的,如果想通过 LAJP
的方式进行调用,还需要参考 SDK
来封装代码(因为需要改造成静态方法,需要重新对民生提供的加密解密的 JAR
包进行解包、改造、编译、打包),据说大部分跟他们对接的使用 PHP
语言的商户采用的是这种方案。其实我是很反感引入这种所谓的中间件,因为这会导致项目的复杂性增加,同时影响运行稳定性。因此,我决定采用 PHP
的 CURL
来自己处理 SSL
双向认证问题。
实现 CURL
的 SSL
双向认证其实很简单,只需要在封装的方法中增加如下几行代码即可:
// 开启调试模式
curl_setopt($handle, CURLOPT_VERBOSE, '1');
// 证书格式
curl_setopt($handle, CURLOPT_SSLCERTTYPE, 'PEM');
// 证书路径
curl_setopt($handle, CURLOPT_SSLCERT, '/xxx/cert.pem');
// 证书密码|无密码则可不设置
curl_setopt($handle, CURLOPT_SSLCERTPASSWD, '123456');
// 私钥格式
curl_setopt($handle, CURLOPT_SSLKEYTYPE, 'PEM');
// 私钥路径
curl_setopt($handle, CURLOPT_SSLKEY, '/xxx/key.pem');
需要说明的是,民生银行提供的有用于 JAVA-SDK
来实现 SSL
双向认证的 JKS
文件,我从 JKS
中导出了秘钥 key.pem
及 cert.pem
。
可以使用此工具KeyStore Explorer
(http://keystore-explorer.org/
)来处理JKS
文件。
以上 key.pem
的格式如下:
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIE6jAcBgoqhkiG9w0BDAEDMA4ECP8UGEp4FY8HAgIExwSCBMiV/TafMh5acKTb
[省略N行]...
e+rRu8LkDBaKWNpLPZ8=
-----END ENCRYPTED PRIVATE KEY-----
以上 cert.pem
的格式如下:
-----BEGIN CERTIFICATE-----
MIIEyzCCA7OgAwIBAgIFMAGEFlEwDQYJKoZIhvcNAQEFBQAwKzELMAkGA1UEBhMC
[省略N行]...
ljiaZ6PKnf4kh/gcXVkzzoKAFVB/CNMCoo9Tg0y2qw==
-----END CERTIFICATE-----
可以通过以下命令去除私钥的密码:
openssl rsa -in key.pem -out key-without-pwd.pem
如果一切正常的话,以上配置就能实现 PHP
的 CURL SSL
双向认证。
但是,事与愿违,估计测试后 CURL
会报以下类似的错误:
unable to load client cert: -8018 (SEC_ERROR_UNKNOWN_PKCS11_ERROR)
这是因为 CentOS
自带的 CURL
版本为 NSS
而非 OPENSSL
,可以通过以下命令查看:
[root@PHPHa ~]# curl -V
curl 7.29.0 (x86_64-redhat-linux-gnu) libcurl/7.29.0 NSS/3.36 zlib/1.2.7 libidn/1.28 libssh2/1.4.3
从上面可以看到,当前系统的 CURL
版本为 NSS/3.36
,因此我们需要重新编译 CURL
。
# 下载
[root@PHPHa ~]# wget http://curl.haxx.se/download/curl-7.35.0.tar.gz
# 解压
[root@PHPHa ~]# tar -zxf curl-7.35.0.tar.gz
[root@PHPHa ~]# cd curl-7.35.0
# 编译安装
[root@PHPHa curl-7.35.0]# ./configure --prefix=/usr/local/curl/ --without-nss --with-ssl
[root@PHPHa curl-7.35.0]# make && make install
# 替换命令
[root@PHPHa ~]# mv /usr/bin/curl /usr/bin/cur.bak
[root@PHPHa ~]# ln -s /usr/local/curl/bin/curl /usr/bin/curl
如果正常的话,再次查看 CURL
版本,就可以看到已经变更为 OpenSSL/1.0.2k
。
[root@PHPHa ~]# curl -V
curl 7.35.0 (x86_64-redhat-linux-gnu) libcurl/7.35.0 OpenSSL/1.0.2k zlib/1.2.7 libidn/1.28
以上测试可以编译成功的相关系统/软件版本如下:
CentOS-7.2-x64
curl-7.35.0
openssl-1.0.2k
但在另外一台服务器上就编译报错,最后更换了版本顺利通过,相关系统/软件版本如下:
CentOS-7.6-x64
curl-7.50.0
openssl-1.1.1
系统自带的为 openssl-1.0.2k
,因此需要先编译升级 openssl
。配置时注意需要加上 shared
参数,以生成动态库。
# 下载
[root@PHPHa ~]# wget https://www.openssl.org/source/old/1.1.1/openssl-1.1.1.tar.gz
# 解压
[root@PHPHa ~]# tar -zxf openssl-1.1.1.tar.gz
[root@PHPHa ~]# cd openssl-1.1.1
# 编译安装| `shared` 参数指定生成动态库
[root@PHPHa openssl-1.1.1]# ./config --prefix=/usr/local/openssl shared zlib
[root@PHPHa openssl-1.1.1]# make && make install
# 设置环境变量
[root@PHPHa ~]# vim /etc/profile
# 添加下面这一行
export PKG_CONFIG_PATH=/usr/local/openssl/lib/pkgconfig
# 使环境变量生效
[root@PHPHa ~]# source /etc/profile
# 替换命令
[root@PHPHa ~]# mv /bin/openssl /bin/openssl.bak
[root@PHPHa ~]# ln -s /usr/local/openssl/bin/openssl /bin/openssl
升级完成后,我们可以查看下当前的版本,正常情况应该如下:
[root@PHPHa ~]# openssl version
OpenSSL 1.1.1 11 Sep 2018
如果异常,可能会报如下的错误:
/usr/local/openssl/bin/openssl: error while loading shared libraries: libssl.so.1.1:
cannot open shared object file: No such file or directory
这是因为找不到对应的链接库,我们只需要创建个软连接即可:
[root@PHPHa ~]# ln -s /usr/local/openssl/lib/libssl.so.1.1 /usr/lib64/libssl.so.1.1
[root@PHPHa ~]# ln -s /usr/local/openssl/lib/libcrypto.so.1.1 /usr/lib64/libcrypto.so.1.1
openssl
升级成功后,按照上文重新编译安装 curl-7.50.0
即可。
紧接着,我们需要重新编译 PHP
,指定编译时对应的 CURL
安装路径。
# 查看当前编译配置
[root@PHPHa ~]# php -i | grep configure
Configure Command => './configure' '--prefix=/usr/local/php' '--with-config-file-path=/usr/local/php/etc' '--with-config-file-scan-dir=/usr/local/php/conf.d' '--enable-fpm' '--with-fpm-user=www' '--with-fpm-group=www' '--enable-mysqlnd' '--with-mysqli=mysqlnd' '--with-pdo-mysql=mysqlnd' '--with-iconv-dir' '--with-freetype-dir=/usr/local/freetype' '--with-jpeg-dir' '--with-png-dir' '--with-zlib' '--with-libxml-dir=/usr' '--enable-xml' '--disable-rpath' '--enable-bcmath' '--enable-shmop' '--enable-sysvsem' '--enable-inline-optimization' '--enable-mbregex' '--enable-mbstring' '--enable-intl' '--with-mcrypt' '--enable-ftp' '--with-gd' '--enable-gd-native-ttf' '--with-openssl' '--with-curl' '--with-mhash' '--enable-pcntl' '--enable-sockets' '--with-xmlrpc' '--enable-zip' '--enable-soap' '--with-gettext' '--disable-fileinfo' '--enable-opcache' '--with-xsl'
# 切换目录
[root@PHPHa ~]# cd php-7.2.x
# 重新编译
[root@PHPHa php-7.2.x]# ./configure --prefix=/usr/local/php --with-config-file-path=/usr/local/php/etc --with-config-file-scan-dir=/usr/local/php/conf.d --enable-fpm --with-fpm-user=www --with-fpm-group=www --enable-mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-iconv-dir --with-freetype-dir=/usr/local/freetype --with-jpeg-dir --with-png-dir --with-zlib --with-libxml-dir=/usr --enable-xml --disable-rpath --enable-bcmath --enable-shmop --enable-sysvsem --enable-inline-optimization --enable-mbregex --enable-mbstring --enable-intl --with-mcrypt --enable-ftp --with-gd --enable-gd-native-ttf --with-openssl --with-curl=/usr/local/curl --with-mhash --enable-pcntl --enable-sockets --with-xmlrpc --enable-zip --enable-soap --with-gettext --disable-fileinfo --enable-opcache --with-xsl
[root@Homike php-7.2.x]# make && make install
编译成功后,可以通过以下命令查看对应的 SSL
扩展版本。
[root@PHPHa ~]# php -i | grep 'SSL Version'
SSL Version => OpenSSL/1.0.2k
如果以上通过指定CURL
安装目录重新编译的方式不行的话,那么也可以编译时去掉--with-curl
,然后单独编译CURL
扩展,并将生成的curl.so
添加到配置文件php.ini
中即可。
到此为止,算是实现了 PHP
版本的 CURL SSL
双向认证。至于通过 GMSSL
扩展来实现 PKCS#7
数字信封的方法,如有了解的朋友欢迎通过本博客 关于 页面中的联系邮箱进行沟通。