php签名以及验签

860 阅读2分钟

1、OpenSSL工具生成密钥

##生成 private.pem 私钥
mkdir temp_key && cd temp_key && openssl genrsa -out private.pem 1024
  • 3.通过密钥生成cer证书
##生成 public.cer 公钥
openssl req -new -x509 -key private.pem -out public.cer -days 3650 -subj /CN=topbooking.natapp1.cc

执行此命令可能报以下错误:

vagrant@homestead:~/code/top/public/ticket_key$ openssl req -new -x509 -key private.pem -out public.cer -days 3650 -subj /CN=topbooking.natapp1.cc
Can't open /usr/local/ssl/openssl.cnf for reading, No such file or directory
140674040239360:error:02001002:system library:fopen:No such file or directory:crypto/bio/bss_file.c:69:fopen('/usr/local/ssl/openssl.cnf','r')
140674040239360:error:2006D080:BIO routines:BIO_new_file:no such file:crypto/bio/bss_file.c:76:
vagrant@homestead:~/code/top/public/ticket_key$ ll /usr/local/ssl/
ls: cannot access '/usr/local/ssl/': No such file or directory

通过下面的方式解决

# 查找本地的配置文件
find / -name openssl.cnf 2>/dev/null

# Ubuntu 文件一般在 /etc/ssl/openssl.cnf
# 建立一个链接
sudo mkdir /usr/local/ssl
sudo ln -s /etc/ssl/openssl.cnf /usr/local/ssl/openssl.cnf
# 重新执行
openssl req -new -x509 -key private.pem -out public.cer -days 3650 -subj /CN=topbooking.natapp1.cc
  • 4.通过pem密钥和cer证书生成pfx文件
## 会需要设置密码,比如密码设置为111111,生成文件 openssl.pfx
openssl pkcs12 -export -out openssl.pfx -inkey private.pem -in public.cer
  • 5.通过pem密钥生成pem公钥给数据交互方比如长隆配置自己的公钥
#生成公钥 app_public_key.pem
openssl rsa -in private.pem -pubout -out app_public_key.pem 
  • 6.将生成的公钥 app_public_key.pem 发给数据交互方

2、数据签名

<?php

namespace App\Services\ChimeLong;

use Illuminate\Support\Str;

/**
 * RSA加密
 * Class RSACryptoService
 * @package App\Services\MPay
 */
class RSACryptoService
{
    private $_mer_no;
    private $_aes_key;
    private $_ver_no;
    private $_privateKey;
    public  $_publicKey;

    /**
     * 参数初始化
     * RSACryptoService constructor.
     */
    public function __construct()
    {
        if (app()->environment('production')) {//正式环境
            $this->_mer_no   = config('api_token.chimeLong.prod.mer_no');
            $this->_aes_key  = substr(md5(config('api_token.chimeLong.prod.key')), 8, 16);
            $this->_ver_no   = config('api_token.chimeLong.prod.ver_no');
            $privatePemPath  = public_path(config('api_token.chimeLong.prod.privatePemPath'));
            $publicPemPath   = public_path(config('api_token.chimeLong.prod.publicPemPath'));
            $publicPemPathCL = public_path(config('api_token.chimeLong.prod.publicPemPathCL'));
        } else {//测试环境
            $this->_mer_no   = config('api_token.chimeLong.test.mer_no');
            $this->_aes_key  = substr(md5(config('api_token.chimeLong.test.key')), 8, 16);
            $this->_ver_no   = config('api_token.chimeLong.test.ver_no');
            $privatePemPath  = public_path(config('api_token.chimeLong.test.privatePemPath'));
            $publicPemPath   = public_path(config('api_token.chimeLong.test.publicPemPath'));
            $publicPemPathCL = public_path(config('api_token.chimeLong.test.publicPemPathCL'));
        }
        $this->get_private_key($privatePemPath);//请求的数据用己方的私钥签名,长隆用我们的公钥验签接收请求数据
        $this->get_public_key($publicPemPathCL);//接收的响应数据用长隆的公钥验签
    }

    /**
     * 获取私钥
     * @param $privatePemPath
     */
    private function get_private_key($privatePemPath)
    {
        $this->_privateKey = openssl_pkey_get_private(file_get_contents($privatePemPath));
    }

    /**
     * 从证书中解析公钥,以供使用
     * @param $publicPemPath
     */
    private function get_public_key($publicPemPath)
    {
        $this->_publicKey = openssl_pkey_get_public(file_get_contents($publicPemPath));
    }

    /**
     * step 1
     * 生成签名 使用各自语言对应的 SHA256WithRSA签名函数利用 发送方 私钥对待签
     * 名字符串进行签名,并进行 Base64编码。
     * @param $data
     * @return string
     * @throws \Exception
     */
    public function rsa_sign($data)
    {
        $signData  = $this->_mer_no . $data['method'] . $data['uuid'] . $data['timestamp'] . $this->_ver_no . json_encode($data['body']);
        $signature = '';
        $res       = openssl_sign($signData, $signature, $this->_privateKey, OPENSSL_ALGO_SHA256);
        openssl_free_key($this->_privateKey);
        if ($res) {
            return base64_encode($signature);
        } else {
            throw new \Exception("请求数据签名错误", 10004);
        }
    }

    /**
     * step 2
     * 传输数据 加密 AES算法 ECB方式加密 ,密钥长度 128位,块长 128位。
     * @param $data
     * @return string
     * @throws \Exception
     */
    public function aes_sign($data)
    {
        if (!key_exists('timestamp', $data)) {
            $data['timestamp'] = now()->toDateTimeString();
        }
        if (!key_exists('uuid', $data)) {
            $data['uuid'] = (string)Str::uuid();
        }
        if (!key_exists('body', $data)) {
            $data['body'] = [];
        }
        $sign        = $this->rsa_sign($data);
        $encryptData = [
            "method"    => $data['method'],
            "sign"      => $sign,
            "nonce_str" => $data['uuid'],
            "timestamp" => $data['timestamp'],
            "ver_no"    => $this->_ver_no,
            "body"      => $data['body'],
        ];
        return $this->aes_encrypt(json_encode($encryptData));
    }

    /**
     * @param string $resource
     * @param string $signature
     * @return bool
     * @todo :待完善,暂无用
     *       字符串验签方式一
     */
    public function verifyResData(string $resource, string $signature): bool
    {
        $signature = base64_decode($signature);
        $res       = openssl_verify($resource, $signature, $this->_publicKey, OPENSSL_ALGO_SHA256);
        //openssl_free_key($this->_publicKey);
        return $res === 1;
    }

    /**
     * @param $response
     * @return int
     * @todo :待完善,暂无用
     *       验证签名方式二
     */
    public function check_sign($response)
    {
        $json      = json_decode($response, false);
        $signature = base64_decode($json->sign);
        unset($json->sign);
        $data = $json->code . $json->msg . $json->nonce_str . $json->body;
        $res = openssl_verify($data, $signature, $this->_publicKey, OPENSSL_ALGO_SHA256);
        //dd($data, $signature, $res);
        openssl_free_key($this->_publicKey);
        return $res === 1;
    }

    /**
     * 解密
     * @param $str
     * @return false|string
     */
    public function aes_decrypt($str)
    {
        return openssl_decrypt(base64_decode($str), "AES-128-ECB", $this->_aes_key, OPENSSL_RAW_DATA);
    }

    /**
     * 加密字符串
     * @param $str
     * @return string
     */
    public function aes_encrypt($str)
    {
        return base64_encode(openssl_encrypt($str, "AES-128-ECB", $this->_aes_key, OPENSSL_RAW_DATA));
    }

    /**
     * 接收到的数据经过解密、反系列化以及签名验证后返回希望接收的数据
     * @param string $resource
     * @return mixed
     * @throws \Exception
     */
    public function handleRes(string $resource)
    {
        // step1: aes解密
        $decryptRes = $this->aes_decrypt($resource);
        // step2: json反系列化
        $resData = json_decode($decryptRes, true);
        // step3: 验签 #响应签名字符串 code+msg+nonce_str+body
        $body      = $resData['body'];
        $signData  = $resData['code'] . $resData['msg'] . $resData['nonce_str'] . $body;
        $signature = $resData['sign'];
        if ($this->verifyResData($signData, $signature) || $this->check_sign($decryptRes)) {//验签成功
            return json_decode($body, true);
        } else {
            throw new \Exception("接收数据签名错误", 10004);
        }
    }
}