liziyu 发布的文章

对称加密: 在加密和解密过程中使用相同的密钥, 或是两个可以简单地相互推算的密钥的加密算法.
非对称加密: 也称为公开加密, 它需要一个密钥对, 一个是公钥, 一个是私钥, 一个负责加密, 一个负责解密.
对称加密在性能上要优于非对称加密, 但是安全性低于非对称加密.

PHP 7.1 之后的对称加密和非对称加密都需要借助 openssl 扩展实现. mcrypt 库已经被移除.

对称加密函数

openssl_get_cipher_methods() : 返回 openssl 支持的所有加密方式.
openssl_encrypt($data, $method, $key, $options = 0, $iv = '') : 以指定方式 method 和密钥 key 加密 data, 返回 false 或加密后的数据.

data : 明文
method : 加密算法
key : 密钥
options :

0 : 自动对明文进行 padding, 返回的数据经过 base64 编码.
1 : OPENSSL_RAW_DATA,自动对明文进行 padding, 但返回的结果未经过 base64 编码.
2 : OPENSSL_ZERO_PADDING,对明文进行 0 填充, 返回的结果经过 base64 编码. 但是, openssl 不推荐 0 填充的方式,即使选择此项也不会自动进行 padding, 仍需手动 padding.

iv : 非空的初始化向量, 不使用此项会抛出一个警告. 如果未进行手动填充, 则返回加密失败.
openssl_decrypt($data, $method, $key, $options = 0, $iv = '') : 解密数据.
openssl_cipher_iv_length($method) : 获取 method 要求的初始化向量的长度.
openssl_random_pseudo_bytes($length) : 生成指定长度的伪随机字符串.
hash_mac($method, $data, $key, $raw_out) : 生成带有密钥的哈希值.

method : 加密算法
data : 明文
key : 密钥
raw_output :
TRUE : 输出原始二进制数据
FALSE : 输出长度固定的小写 16 进制字符串

对称加密,主流的对称加密方式有 DES, AES. 这两种加密方式都属于分组加密, 先将明文分成多个等长的模块 ( block ), 然后进行加密。

DES 加密

DES 加密的密钥长度为 64 bit, 实际应用中有效使用的是 56 位, 剩余 8 位作为奇偶校验位. 如果密钥长度不足 8 个字节, 将会使用 \0 补充到 8 个字节. 如密钥为 "12345", 其加密后的密文与密钥 "12345\0\0\0" 加密后的密文相同. 明文按 64 bit ( UTF-8 下为 8 个字节长度 ) 进行分组, 每 64 位分成一组 ( 最后一组不足 64 位的需要填充数据 ), 分组后的明文组和密钥按位替代或交换的方法形成密文组。

<?php

class DES {

    private $method = 'DES-CBC';
    private $key = 'www.phpernote.com';

    public function __construct($key = '') {
        // 密钥长度不能超过64bit(UTF-8下为8个字符长度),超过64bit不会影响程序运行,但有效使用的部分只有64bit,多余部分无效,可通过openssl_error_string()  查看错误提示
        $key && $this->key = $key;
    }

    public function encrypt($plaintext) {
        // 生成加密所需的初始化向量, 加密时缺失iv会抛出一个警告
        $ivlen = openssl_cipher_iv_length($this->method);
        $iv = openssl_random_pseudo_bytes($ivlen);

        // 按64bit一组填充明文
        //$plaintext = $this->padding($plaintext);
        // 加密数据. 如果options参数为0, 则不再需要上述的填充操作. 如果options参数为1, 也不需要上述的填充操作, 但是返回的密文未经过base64编码. 如果options参数为2, 虽然PHP说明是自动0填充, 但实际未进行填充, 必须需要上述的填充操作进行手动填充. 上述手动填充的结果和options为0和1是自动填充的结果相同.
        $ciphertext = openssl_encrypt($plaintext, $this->method, $this->key, 1, $iv);
        // 生成hash
        $hash = hash_hmac('sha256', $ciphertext, $this->key, false);

        return base64_encode($iv . $hash . $ciphertext);

    }

    public function decrypt($ciphertext) {
        $ciphertext = base64_decode($ciphertext);
        // 从密文中获取iv
        $ivlen = openssl_cipher_iv_length($this->method);
        $iv = substr($ciphertext, 0, $ivlen);
        // 从密文中获取hash
        $hash = substr($ciphertext, $ivlen, 64);
        // 获取原始密文
        $ciphertext = substr($ciphertext, $ivlen + 64);
        // hash校验
        if (hash_equals($hash, hash_hmac('sha256', $ciphertext, $this->key, false))) {
            // 解密数据
            $plaintext = openssl_decrypt($ciphertext, $this->method, $this->key, 1, $iv) ?? false;
            // 去除填充数据. 加密时进行了填充才需要去填充
            //$plaintext = $plaintext ? $this->unpadding($plaintext) : false;

            return $plaintext;
        }

        return '解密失败';
    }

    // 按64bit一组填充数据
    private function padding($plaintext) {
        $padding = 8 - (strlen($plaintext) % 8);
        $chr = chr($padding);

        return $plaintext . str_repeat($chr, $padding);
    }

    private function unpadding($ciphertext) {
        $chr = substr($ciphertext, -1);
        $padding = ord($chr);

        if ($padding > strlen($ciphertext)) {
            return false;
        }
        if (strspn($ciphertext, $chr, -1 * $padding, $padding) !== $padding) {
            return false;
        }

        return substr($ciphertext, 0, -1 * $padding);
    }
}

$aes = new DES('yhm');
$string = $aes->encrypt('www.phpernote.com');
echo '明文:www.phpernote.com', '<br />';
echo '密文:', $string, '<br />';
//密文:9I5V7XirG0ZiMWQ0MmJlZWY0MWI5ZjNkNzE3YmIwZDRiYTlkOWI3NTc2ZjIyMzc5MmEzOTkxYzIyNzY1MzU3NjljMWFlZjIztd1jrvlOS6tDUaMphQBqusOVjBnhgUdV
//注意这里的密文每次输出都不一样
echo '解密:', $aes->decrypt($string);
//解密:www.phpernote.com

AES 加密

AES 加密的分组长度是 128 位, 即每个分组为 16 个字节 ( 每个字节 8 位 ). 密钥的长度根据加密方式的不同可以是 128 位, 192 位, 256 位. 与 DES 加密一样. 密钥长度超过指定长度时, 超出部分无效. 密钥长度不足时, 会自动以\0补充到指定长度.

AES 密钥长度 ( 位 ) 分组长度 ( 位 )
AES-128 128 128
AES-192 192 128
AES-256 256 128

<?php

class AES {
    private $key = 'www.phpernote.com';
    private $method = 'aes-128-cbc';

    public function __construct($key = '') {
        extension_loaded('openssl') or die('未启用 OPENSSL 扩展');
        $key && $this->key = $key;
    }

    public function encrypt($plaintext) {
        if (!in_array($this->method, openssl_get_cipher_methods())) {
            die('不支持该加密算法!');
        }
        // options为1, 不需要手动填充
        //$plaintext = $this->padding($plaintext);
        // 获取加密算法要求的初始化向量的长度
        $ivlen = openssl_cipher_iv_length($this->method);
        // 生成对应长度的初始化向量. aes-128模式下iv长度是16个字节, 也可以自由指定.
        $iv = openssl_random_pseudo_bytes($ivlen);
        // 加密数据
        $ciphertext = openssl_encrypt($plaintext, $this->method, $this->key, 1, $iv);
        $hmac = hash_hmac('sha256', $ciphertext, $this->key, false);

        return base64_encode($iv . $hmac . $ciphertext);
    }

    public function decrypt($ciphertext) {
        $ciphertext = base64_decode($ciphertext);
        $ivlen = openssl_cipher_iv_length($this->method);
        $iv = substr($ciphertext, 0, $ivlen);
        $hmac = substr($ciphertext, $ivlen, 64);
        $ciphertext = substr($ciphertext, $ivlen + 64);
        $verifyHmac = hash_hmac('sha256', $ciphertext, $this->key, false);
        if (hash_equals($hmac, $verifyHmac)) {
            $plaintext = openssl_decrypt($ciphertext, $this->method, $this->key, 1, $iv) ?? false;

            return $plaintext;
        } else {
            die('数据被修改!');
        }
    }
}

$aes = new AES('yhm');
$string = $aes->encrypt('www.phpernote.com');
echo '明文:www.phpernote.com', '<br />';
echo '密文:', $string, '<br />';
//密文:XJmLnBp3L8tOUYswd7+s3jEzNDZlMjMxNTkwMjNiMDI0MmU2ZWMzNTE4MDk5ZTZkYzc4ODlhMWQ1NWM1YjUzNDNlNjcxMjk1MzI0M2M1ODe5M9dAO4mNXj1rHsypG+u9G8OQBMMJMg8fkmkflvC43g==
//注意这里的密文每次输出都不一样
echo '解密:', $aes->decrypt($string);
//解密:www.phpernote.com


非对称加密函数

$res = openssl_pkey_new([array $config]) : 生成一个新的私钥和公钥对. 通过配置数组, 可以微调密钥的生成.

$config的参数如下:

digest_alg : 摘要或签名哈希算法.
private_key_bits : 指定生成的私钥的长度.
private_key_type : 指定生成私钥的算法. 默认 OPENSSL_KEYTYPE_RSA, 可指定 OPENSSL_KEYTYPE_DSA, OPENSSL_KEYTYPE_DH, OPENSSL_KEYTYPE_RSA, OPENSSL_KEYTYPE_EC.
config : 自定义 openssl.conf 文件的路径.
。。。

openssl_pkey_free($res) : 释放有 openssl_pkey_new() 创建的私钥.
openssl_get_md_methods() : 获取可用的摘要算法.
openssl_pkey_export_to_file($res, $outfilename) : 将 ASCII 格式 ( PEM 编码 ) 的密钥导出到文件中. 使用相对路径时, 是相对服务器目录, 而非当前所在目录.
openssl_pkey_export($res, &$out) : 提取 PEM 格式私钥字符串.
openssl_pkey_get_details($res) : 返回包含密钥详情的数组.
openssl_get_privatekey($key) : 获取私钥. key 是一个 PEM 格式的文件或一个 PEM 格式的私钥.
openssl_get_publickey($certificate) : 获取公钥. certificate 是一个 X.509 证书资源或一个 PEM 格式的文件或一个 PEM 格式的公钥.
openssl_private_encrypt($data, &$crypted, $privKey [, $padding = OPENSSL_PKCS1_PADDING]) : 使用私钥加密数据, 并保存到 crypted . 其中填充模式为 OPENSSL_PKCS1_PADDING 时, 如果明文长度不够, 加密时会在明文中随机填充数据. 为 OPENSSL_NO_PADDING 时, 如果明文长度不够, 会在明文的头部填充 0 .
openssl_public_decrypt($crypted, &$decrypted, $pubKey [, $padding]) : 使用公钥解密数据, 并保存到 decrypted .
openssl_public_encrypt($data, &$crypted, $pubKey [, $padding]) : 使用公钥加密数据, 并保存到 crypted .
openssl_private_decrypt($crypted, &$decrypted, $privKey [, $padding]) : 使用私钥解密数据, 并保存到 decrypted .

非对称加密

RSA 也是一种分组加密方式, 但明文的分组长度根据选择的填充方式的不同而不同。

<?php

class RSA {

    private $private_key; // 私钥
    private $public_key; // 公钥
    private $private_res; // 私钥资源
    private $public_res; // 公钥资源

    public function __construct() {
        extension_loaded('openssl') or die('未加载 openssl');
        // 生成新的公钥和私钥对资源
        $config = [
            'digest_alg' => 'sha256',
            'private_key_bits' => 1204,
            'private_key_type' => OPENSSL_KEYTYPE_RSA
        ];
        $res = openssl_pkey_new($config);
        if (!$res) {
            die('生成密钥对失败');
        }

        // 获取公钥, 生成公钥资源
        $this->public_key = openssl_pkey_get_details($res)['key'];
        $this->public_res = openssl_pkey_get_public($this->public_key);

        // 获取私钥, 生成私钥资源
        openssl_pkey_export($res, $this->private_key);
        $this->private_res = openssl_pkey_get_private($this->private_key);

        openssl_free_key($res);
    }

    // 加密
    public function encrypt($plaintext) {
        $ciphertext = null;
        openssl_public_encrypt($plaintext, $ciphertext, $this->public_res);
        return $ciphertext;
    }

    // 解密
    public function decrypt($ciphertext) {
        $plaintext = null;
        openssl_private_decrypt($ciphertext, $plaintext, $this->private_res);
        return $plaintext;
    }
}

$aes = new RSA('yhm');
$string = $aes->encrypt('www.phpernote.com');
echo '明文:www.phpernote.com', '<br />';
echo '密文:', $string, '<br />';
//密文:密文:O�rZ�v��5 �j��]��9�S��p��(};�ۢf���UejL]��d,� ��v�{ �4'Nq��zr!�iQ��I鷺�T�7 '�����8�$��� �u��������AT����K�U|��� V��߻��܄I���0°���)
//注意这里的密文每次输出都不一样,而且还都是乱码,O(∩_∩)O
echo '解密:', $aes->decrypt($string);
//解密:www.phpernote.com


在传输重要信息时, 一般会采用对称加密和非对称加密相结合的方式, 而非使用单一加密方式. 一般先通过 AES 加密数据, 然后通过 RSA 加密 AES 密钥, 然后将加密后的密钥和数据一起发送. 接收方接收到数据后, 先解密 AES 密钥, 然后使用解密后的密钥解密数据。

为了学习方便,转进来的,原文地址:https://www.phpernote.com/php-function/1538.html

什么是对称加密?

用一个密钥,对明文进行加密,同理,同这把密钥,也可以对密文进行解密。

什么是非对称加密?

有一对密钥对,使用公钥和私钥进行加密解密可以使用私钥加密,公钥进行解密,同理,也可以使用公钥加密,私钥进行解密。

什么是摘要?

用md5(或者其它单向加密算法),对内容进行加密出来的字符串,就叫做摘要。

什么是签名?

用私钥对摘要进行加密,加密出来签字串,就叫做签名。

什么是验签?

用非对称密钥对中的公钥,对签名进行解密操作,解密出来的摘要和原来的对比,就叫做验签。

什么是数字证书?

含有公钥、摘要算法、指纹、签名算法、唯一序列号的特殊文件,就是数字证书。

公钥:网站(服务器)的公钥
颁发者:CA(证书认证机构)
有效期:证书的使用期限
摘要算法:指定的摘要算法,用来计算证书的摘要
指纹:也就是证书的摘要,保证证书的完整性
签名算法:用于生成签名,确保证书是由CA签发
序列号:证书的唯一标识

数字证书是如何产生的?

将网站的公钥,颁发者,有效期,摘要算法 ,哈希算法写入证书。CA 根据证书中的指定的哈希算法,计算出整个证书的摘要,即digest。CA根据签名算法以及上一步计算出来的摘要,CA用自己的私钥对摘要进行加密,生成CA的签名,即signature最后把摘要,签名以及证书的基本信息,一起发布,就得到了网站的证书。

下面以小明与小花的安全通信为例,进行问答模拟,如下:

问题一:小明和小花安全的通信,怎么做?

答:通过加密

问题二:通过哪种加密方式通信,更高效?

答:对称加密因为,单向加密,没办法解密,不行非对称加密,太慢,也不行只有对称加密,速度快

问题三:采用对称加密,密钥S 怎么安全传输?

答:小花使用小明的公钥,对密钥S进行加密,传给小明小明用自己的私钥解密

问题四:小明如何安全的把自己的公钥传输给小花?

答:使用数字证书具体就是 小明向CA申请一个自己的数字证书,把自己的公钥放在证书中小明将数字证书发送给小花。

问题五:小花如何验证数字证书的真实性?

答:小花用操作系统内置的CA的数字证书,拿到CA的公钥,用CA的公钥,对数字证书进行验签

验签通过,说明数字证书是真的。

本文摘自:https://www.helloworld.net/p/0011879971 感谢作者的辛苦付出!

ASN.1 - 数据结构描述语言

引用自Wiki

ASN.1 is a standard interface description language for defining data structures that can be serialized and deserialized in a cross-platform way.

也就是说ASN.1是一种用来定义数据结构的接口描述语言,它不是二进制,也不是文件格式,看下面的例子你就会明白了:

FooQuestion ::= SEQUENCE {
    trackingNumber INTEGER,
    question       IA5String
}

这段代码定义了FooQuestion的数据结构,下面是FooQuestion这个数据接口的某个具体的数据:

myQuestion FooQuestion ::= SEQUENCE {
    trackingNumber     5,
    question           "Anybody there?"
}

ASN.1用在很多地方比如下面要讲的X.509PKCS group of cryptography standards

文件编码格式

DER编码格式

引用自Wiki

ASN.1 is closely associated with a set of encoding rules that specify how to represent a data structure as a series of bytes

意思是ASN.1有一套关联的编码规则,这些编码规则用来规定如何用二进制来表示数据结构,DER是其中一种。

把上面的FooQuestion的例子用DER编码则是(16进制):

30 13 02 01 05 16 0e 41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f

翻译过来就是:

30 — type tag indicating SEQUENCE
13 — length in octets of value that follows
  02 — type tag indicating INTEGER
  01 — length in octets of value that follows
    05 — value (5)
  16 — type tag indicating IA5String 
     (IA5 means the full 7-bit ISO 646 set, including variants, 
      but is generally US-ASCII)
  0e — length in octets of value that follows
    41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f — value ("Anybody there?")

看到这里你应该对DER编码格式有一个比较好的认识了。

PEM编码格式

引用自Wiki

Privacy-Enhanced Mail (PEM) is a de facto file format for storing and sending cryptographic keys, certificates, and other data, based on a set of 1993 IETF standards defining “privacy-enhanced mail.”

PEM是一个用来存储和发送密码学key、证书和其他数据的文件格式的事实标准。许多使用ASN.1的密码学标准(比如X.509PKCS)都使用DER编码,而DER编码的内容是二进制的,不适合与邮件传输(早期Email不能发送附件),因此使用PEM把二进制内容转换成ASCII码。文件内容的格式像下面这样:

-----BEGIN label-----
BASE64Encoded
-----END label-----

label用来区分内容到底是什么类型,下面会讲。

和PEM相关的RFC有很多,与本文内容相关的则是RFC7468,这里面规定了很多label,不过要注意不是所有label都会有对应的RFC或Specification,这些label只是一种约定俗成。

PEM实际上就是把DER编码的文件的二进制内容用base64编码一下,然后加上-----BEGIN label-----这样的头和-----END label-----这样的尾,中间则是DER文件的Base64编码。

我们可以通过下面的方法验证这个结论,先生成一个RSA Private Key,编码格式是PEM格式:

openssl genrsa -out key.pem

查看一下文件内容,可以看到label是RSA PRIVATE KEY

----BEGIN RSA PRIVATE KEY-----
BASE64Encoded
-----END RSA PRIVATE KEY-----

然后我们把PEM格式转换成DER格式:

openssl rsa -in key.pem -outform der -out key.der

如果你这个时候看一下文件内容会发现都是二进制。然后我们把DER文件的内容Base64一下,会看到内容和PEM文件一样(忽略头尾和换行):

base64 -i key.der -o key.der.base64

证书、密码学Key格式

上面讲到的PEM是对证书、密码学Key文件的一种编码方式,下面举例这些证书、密码学Key文件格式:

X.509证书

引用自Wiki

In cryptography, X.509 is a standard defining the format of public key certificates. X.509 certificates are used in many Internet protocols, including TLS/SSL, which is the basis for HTTPS, the secure protocol for browsing the web.

X.509是一个Public Key Certificates的格式标准,TLS/SSL使用它,TLS/SSL是HTTPS的基础所以HTTPS也使用它。而所谓Public Key Certificates又被称为Digital CertificateIdentity Certificate

An X.509 certificate contains a public key and an identity (a hostname, or an organization, or an individual), and is either signed by a certificate authority or self-signed.

一个X.509 Certificate包含一个Public Key和一个身份信息,它要么是被CA签发的要么是自签发的。

下面这种张图就是一个X.509 Certificate:

事实上X.509 Certificate这个名词通常指代的是IETF的PKIX Certificate和CRL Profile,见RFC5280。所以当你看到PKIX Certificate字样的时候可以认为就是X.509 Certificate。

PKCS系列

引用自Wiki

In cryptography, PKCS stands for “Public Key Cryptography Standards”

前面提到的X.509是定义Public Key Certificates的格式的标准,看上去和PKCS有点像,但实际上不同,PKCS是Public Key密码学标准。此外Public-Key Cryptography虽然名字看上去只涉及Public Key,实际上也涉及Priviate Key,因此PKCS也涉及Private Key。

PKCS一共有15个标准编号从1到15,这里只挑讲PKCS #1、PKCS #8、PKCS #12。

PKCS #1

PKCS #1,RSA Cryptography Standard,定义了RSA Public Key和Private Key数学属性和格式,详见RFC8017

PKCS #8

PKCS #8,Private-Key Information Syntax Standard,用于加密或非加密地存储Private Certificate Keypairs(不限于RSA),详见RFC5858

PKCS #12

PKCS #12定义了通常用来存储Private Keys和Public Key Certificates(例如前面提到的X.509)的文件格式,使用基于密码的对称密钥进行保护。注意上述Private Keys和Public Key Certificates是复数形式,这意味着PKCS #12文件实际上是一个Keystore,PKCS #12文件可以被用做Java Key Store(JKS),详见RFC7292

openssl pkcs12 -export \
  -in <cert> \
  -inkey <private-key> \
  -name my-cert \
  -caname my-ca-root \
  -CAfile <ca-cert> \
  -chain
  -out <pkcs-file>

PKCS #12一般不导出PEM编码格式。

PEM格式速查

当你不知道你的PEM文件内容是什么格式的可以根据下面查询。

X.509 Certificate

RFC7468 - Textual Encoding of Certificates

-----BEGIN CERTIFICATE-----
BASE64Encoded
-----END CERTIFICATE-----

X.509 Certificate Subject Public Key Info

RFC7468 - Textual Encoding of Subject Public Key Info

-----BEGIN PUBLIC KEY-----
BASE64Encoded
-----END PUBLIC KEY-----

PKCS #1 Private Key

没有RFC或权威Specification,该格式有时候被称为traditional format、SSLeay format(见SO

-----BEGIN RSA PRIVATE KEY-----
BASE64Encoded
-----END RSA PRIVATE KEY-----

PKCS #1 Public Key

同上没有RFC或权威Specification

-----BEGIN RSA PUBLIC KEY-----
BASE64Encoded
-----END RSA PUBLIC KEY-----

PKCS #8 Unencrypted Private Key

RFC7468 - One Asymmetric Key and the Textual Encoding of PKCS #8 Private Key Info

-----BEGIN PRIVATE KEY-----
BASE64Encoded
-----END PRIVATE KEY-----

PKCS #8 Encrypted Private Key

RFC7468 - Textual Encoding of PKCS #8 Encrypted Private Key Info

-----BEGIN ENCRYPTED PRIVATE KEY-----
BASE64Encoded
-----END ENCRYPTED PRIVATE KEY-----

Private Key操作命令

生成

生成PKCS #1格式的RSA Private Key

openssl genrsa -out private-key.p1.pem 2048

转换

PKCS #1 -> Unencrypted PKCS #8

openssl pkcs8 -topk8 -in private-key.p1.pem -out private-key.p8.pem -nocrypt

PKCS #1 -> Encrypted PKCS #8

openssl pkcs8 -topk8 -in private-key.p1.pem -out private-key.p8.pem

过程中会让你输入密码,你至少得输入4位,所以PKCS #8相比PKCS #1更安全。

PKCS #8 -> PKCS #1

openssl rsa -in private-key.p8.pem -out private-key.p1.pem

如果这个PKCS #8是加密的,那么你得输入密码。

PKCS #8 Unencrypted -> PKCS #8 Encrypted

openssl pkcs8 -topk8 -in private-key.p8.nocrypt.pem -out private-key.p8.crypt.pem

过程中会让你输入密码,你至少得输入4位。

PKCS #8 Encrypted -> PKCS #8 Unencrypted

openssl pkcs8 -topk8 -in private-key.p8.crypt.pem -out private-key.p8.nocrypt.pem -nocrypt

过程中会要求你输入Private Key密码。

Public Key操作命令

从PKCS #1/#8提取

提取指的是从Private Key中提取Public Key,openssl rsa同时支持PKCS #1和PKCS #8的RSA Private Key,唯一的区别是如果PKCS #8是加密的,会要求你输入密码。

提取X.509格式RSA Public Key

openssl rsa -in private-key.pem -pubout -out public-key.x509.pem

提取PKCS #1格式RSA Public Key

openssl rsa -in private-key.pem -out public-key.p1.pem -RSAPublicKey_out

从X.509证书提取

openssl x509 -in cert.pem -pubkey -noout > public-key.x509.pem

转换

X.509 RSA Public Key -> PKCS #1 RSA Public Key

openssl rsa -pubin -in public-key.x509.pem -RSAPublicKey_out -out public-key.p1.pem

PKCS #1 RSA Public Key -> X.509 RSA Public Key

openssl rsa -RSAPublicKey_in -in public-key.p1.pem -pubout -out public-key.x509.pem

参考资料

https://chanjarster.github.io/post/x509-pkcs-file-formats/

步骤一:

先将自动更新之类的取消掉。

步骤二:

执行下面两条命令:

defaults write com.apple.systempreferences AttentionPrefBundleIDs 0


Killall Dock

就完成了。

PHP扩展gmp被加载两次,报错如下:

PHP Warning: Module "gmp" is already loaded in Unknown on line 0

解决办法:

Not all PHP modules are loaded as separate modules and are part of the
PHP base binary. In this case the module is loaded without a separate
load statement/configuration. If in addition a separate configuration
file exists, e.g. /etc/php7/fpm/conf.d/gmp.ini, the web server tries
to load the module twice which can cause these kind of messages.
//打开.ini配置文件,将下面这行注销即可;
extension=gmp.so

原因:

是因为.ini文件底部又加载了一次,如下:

extension =  /www/server/php/80/lib/php/extensions/no-debug-non-zts-20200930/gmp.so

与G银行对接时用到的

<?php
/**
 * Describe:
 * Author: liziyu
 * Date: 2022/10/31 11:10 AM
 */

namespace App\encrypt;

class MD5WithRSA
{
    /**
     * 利用约定数据和私钥生成数字签名
     * @param $data 待签数据
     * @return String 返回签名
     */
    public function sign($data='')
    {
        if (empty($data))
        {
            return false;
        }
        $private_key="MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgE==";
        $private_key = chunk_split($private_key, 64, "\n");
        $private_key = "-----BEGIN PRIVATE KEY-----\n$private_key-----END PRIVATE KEY-----";

        if (empty($private_key))
        {
            echo "私钥为空";
            return false;
        }
        // 生成密钥资源id
        $private_key_resource_id = openssl_get_privatekey($private_key);
        if (empty($private_key_resource_id))
        {
            echo "私钥资源id为空";
            return false;
        }

        $verify = openssl_sign($data, $signature, $private_key_resource_id, OPENSSL_ALGO_MD5);
        openssl_free_key($private_key_resource_id);
        // 二次Base64编码
        return base64_encode(base64_encode($signature));
    }

    /**
     * 利用公钥和数字签名以及约定数据验证合法性
     * @param string $data 待验证数据
     * @param string $signature 数字签名
     * @return  -1 验证错误;0 验证失败;1 验证成功
     */
    public function isValid($data='', $signature='')
    {
        if (empty($data) || empty($signature))
        {
            return false;
        }

        $public_key ="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB";
        $public_key = chunk_split($public_key, 64, "\n");
        $public_key = "-----BEGIN PUBLIC KEY-----\n$public_key-----END PUBLIC KEY-----";

        if (empty($public_key))
        {
            echo "公钥为空";
            return false;
        }
        // 生成密钥资源id
        $public_key_resource_id = openssl_get_publickey($public_key);
        if (empty($public_key_resource_id))
        {
            echo "公钥资源id为空";
            return false;
        }

        // 对签名进行二次Base64解码
        $signature=base64_decode(base64_decode($signature));
        $ret = openssl_verify($data, $signature, $public_key_resource_id, OPENSSL_ALGO_MD5);
        openssl_pkey_free($public_key_resource_id);
        return $ret;
    }
}

步骤一:

先检测本机是否有gmp扩展在mamp下,命令如下:

/Applications/MAMP/bin/php/php8.0.8/bin/php -i | grep gmp

如果没有此扩展,此时什么也不会出现,如果已经成功安装了扩展,将会出现如下所示的提示:

PWD => /Applications/MAMP/bin/php/php8.0.8/include/php/ext/gmp
$_SERVER['PWD'] =>
/Applications/MAMP/bin/php/php8.0.8/include/php/ext/gmp $_ENV['PWD']
=> /Applications/MAMP/bin/php/php8.0.8/include/php/ext/gmp

步骤二:

php官网下载其源码包php-8.0.8.tar.gz
https://www.php.net/releases/

注意:一定要与你的现在版本一致辞,必须,必须,必须!

可以下载到任意目录,下载完成以后,解压。找到目录.
111.png

步骤三:

将目录/Users/liziyu/Downloads/php-8.0.8/ext/,将gmp目录整拷贝到/Applications/MAMP/bin/php/php8.0.8/include/php/ext目录下,如图:
222.png

步骤四:

同时进入该目录下:

cd /Applications/MAMP/bin/php/php8.0.8/include/php/ext/gmp

执行命令:

/Applications/MAMP/bin/php/php8.0.8/bin/phpize

然后执行:

./configure --with-php-config=/Applications/MAMP/bin/php/php8.0.8/bin/php-config

再执行:

make

最后:

make install

3333.png

当出现如下图所示的提示时说明已经安装成功了。
此时重启php,然后执行

php -m 

成功的话会出现如下图的提示:
444.png

如果还没有此提示的话,则进入php的配置文件ini去修改,增加下面一行代码:

extension=gmp.so

再重启php查看。即可

本文参考:https://mycodde.blogspot.com/2020/01/install-php-gmp-extension-in-mamp-2020.html

<?php

namespace process;

use GuzzleHttp\Client;
use Workerman\Crontab\Crontab;

class TaskSocial
{
    private $domain = '';
    public function onWorkerStart()
    {
        // 异步内容审核 每分钟执行一次
        new Crontab('0 */1 * * * *', function () {
            $client = new Client();
            $response = $client->get($this->domain . '/api_v5/crontab_api/crontab_audit');
            echo "异步内容审核 " . date('Y-m-d H:i:s') . "\n";
            echo $response->getBody() . "\n";
            //清除所有过期的心跳 统计等级 每分钟一次
            $response = $client->get($this->domain . '/api/crontab_api/service_crontab');
            echo "清除所有过期的心跳 统计等级 " . date('Y-m-d H:i:s') . "\n";
            echo $response->getBody() . "\n";
        });

        // 每5秒执行一次 自动打招呼
        new Crontab('*/5 * * * * *', function () {
            $client = new Client();
            $response = $client->get($this->domain . '/api/crontab_api/service_see_hi_crontab');
            echo "自动打招呼 " . date('Y-m-d H:i:s') . "\n";
            echo $response->getBody() . "\n";
        });

        // 财务记录 代理数据统计 每日统计一次
        new Crontab('50 0 * * *', function () {
            $client = new Client();
            $response = $client->get(($this->domain . '/api_v5/crontab_api/crontab_finance'));
            echo "财务记录 " . date('Y-m-d H:i:s') . "\n";
            echo $response->getBody() . "\n";
            $response = $client->get(($this->domain . '/api/crontab_api/add_channel_users'));
            echo "代理数据统计 " . date('Y-m-d H:i:s') . "\n";
            echo $response->getBody() . "\n";
            $response = $client->get(($this->domain . '/api/crontab_api/business_day'));
            echo "获取昨天的营业-每日凌晨统计 " . date('Y-m-d H:i:s') . "\n";
            echo $response->getBody() . "\n";
            // 聊天记录
            $response = $client->get(($this->domain . '/api_v5/crontab_api/chat_log'));
            echo "聊天记录保存文件 " . date('Y-m-d H:i:s') . "\n";
            echo $response->getBody() . "\n";
        });
    }
}

https://www.workerman.net/q/9516

HTML

<form class="layui-form" method="">
<input type="hidden" name="venue_info_id" value="1">
<div class="layui-row" style="padding-top: 40px">
    <div class="layui-form-item text-right">
       <button class="layui-btn" type="button" lay-filter="doCreateEvent" lay-submit>
          <i class="layui-icon">&#xe605;</i>提交保存
       </button>
     </div>
</div>
</form>


JS

form.on('submit(doCreateEvent)', function(data){
    layer.confirm('确定添加预约?', {
        skin: 'layui-layer-admin',
        shade: .1
    }, function (i) {
        layer.close(i);
        var loadIndex = layer.load(2);
        $.post("/api/xxx", data.field, function (res) {
            layer.close(loadIndex);
            if (200 === res.code) {
                layer.msg(res.msg, {icon: 1});
                setTimeout(function (){
                    parent.location.reload();
                }, 2500)
            } else {
                layer.msg(res.msg, {icon: 2});
            }
        });
    });
});



这几天和业务方有个签名验签的的需求,对方使用Java对业务数据进行签名,我方使用PHP验签,使用SHA256withRSA算法签名验签,PHP和Java单独签名和验签都没问题,但是由Java签名的数据请求到PHP端时一直验签不通过。
分别对比了Java和PHP生成的待验签字符串和生成的签名,都是一致的,同时也确认过私钥和公钥是一对,但是一到PHP的验签方法openssl_verify()时就是通不过,是在不知道原因出在哪。请求大神们帮忙看看是什么原因,万分感激。

PHP代码

class LubanPay{
    /**
     * 字符串签名
     * @param string $resource 经过urlencode处理过的字符串
     * @return string
     */
    public function SignStrMessage(string $resource):string {
        $privateKey = openssl_get_privatekey($this->GetPrivateKey());
        $res        = openssl_sign($resource, $signature, $privateKey,OPENSSL_ALGO_SHA256);
        openssl_free_key($privateKey);
        if ($res) {
            return base64_encode($signature);
        }else {
            throw new \Exception("String Sign Failed",10004);
        }
    }
    /**
     * 字符串验签
     * @param string $resource
     * @param string $signature
     * @return bool
     */
    public function VerifyStrMessage(string $resource,string $signature,string $mch_public_key):bool {
        $signature  = base64_decode($signature);
        $publicKey  = openssl_get_publickey($this->GetPublicKey($mch_public_key));
        $res        = openssl_verify($resource, $signature, $publicKey,OPENSSL_ALGO_SHA256);
        openssl_free_key($publicKey);
        return $res===1?true:false;
    }

    /**
     * 获取平台私钥
     * @return string
     */
    private function GetPrivateKey():string {
        $privateKey     = Config::$luban_private_key;
        $privateKey     = chunk_split($privateKey, 64, "\n");
        $privateKey = "-----BEGIN RSA PRIVATE KEY-----\n$privateKey-----END RSA PRIVATE KEY-----\n";
        return $privateKey;
    }

    /**
     * 获取商户公钥
     * @param string $type
     * @return string
     */
    private function GetPublicKey($mer_public_key):string{
        $publicKey               = chunk_split($mer_public_key, 64, "\n");
        $publicKey = "-----BEGIN PUBLIC KEY-----\n$publicKey-----END PUBLIC KEY-----\n";
        return $publicKey;
    }

}

Java代码

public static String private_key = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC4RPxDH2GdsIcyDq5ApUfLU0+Gst6aJZwjoqvVxxGSRTlKx10VZHHZ7goqrNSNWVK+WPbz6vCItSgHb1t3eRMkwPEaGcstvcf7krEGOH6CWYXe74OTbFet9Oq1Jdzz7UHNNeRyHO/JnNbVh8kPSqIL2368g+lII0u7zP4OWGw+qht75VY0vKI3dQdK2jFidkRHtEmV0+ui+9RVEtIlURYxkaAHWoUNWov8505cAc+pmKrUYOJf7Q50t+AlClIgngzF2yqT5HAKKYbQ9R6CgKvB9JcAXL0nXi2bExjOQsQa3SalLXxjeNKpjJKtytUAEVaehtA5PCi0VV1jZSGKpTxlAgMBAAECggEAGIJsf00UQdIyGVFkkgqp4vyAzmzKOPyZqQ/BBV1GFAuLFEwyMF882XzU81orp2VjIRhaOJVeSwC1g0+nfdun1TKonw0hPkNI70hSrX4kLZhUuxNmj9xQST4TXebcXcGICBCMAzWgG1P2K061Sohlx2f5kn+FLugq8Z7Rh/zw4OCrFeDZ0YJZKpNk1JwBp3arbSphlbw3ZzTF6g7kukfilXuWNnoqDf341H5rVUY7k4cbej8x6pc6Zrnla33VM5m37+5bXnGnE1Sx7PjVgn+2ZADIhTn8e94bLZacJofrF1kCcScO1TRH4eaqlyhiXsjMy0Cm4OxCY+Uydj3NtOFvHQKBgQD5Nsr83bfAuHCA3dEaclQoBbAD1Qiz3rQPvHh2Zt7WIzKJ4NWVnypp22eCYTgft/X7cXXF7D2yokfOCc9DMfLVOOZzcRSwifsakPWXWKAtcNZZW+1ffgObq4I4tNO9Llb6hIN3pcxf7yBtrMib9C/cqwUAQM3k5bsFTZgjCZI5qwKBgQC9SXsJel+FDFlT4jfda/9fEx8uSOP4jflSn83CLI7UQ9/utR5ldJbllwWwrS5k6nqgi5K9AzOhxxfQ3m6FGJRDzqwyfK05uvuM1CqRMpe5FyQKTpPodWJ3+8oIcluRNaWwuAp+FNWPiGSNnhksS7i54jJAOh9nIdB1aSseq9vyLwKBgFJENinW/wuNVwYTMy2pxAIaLop1TpQh1grDyng7aSADKnG9WIQ1sIiVNswhT6eY0IiaYaheXdeUHmPzdQnXeTPNvrUpBQ1p3wxcAdZeGTIm53tED03QiVxf93LErojqvSehisx6XMbmZywNN4PTzeDoS5RT0CPZei07+hbG2BBVAoGAW6HszAPPper6e18xyCD1+SKan59trO+d2N+/jdZgNmW9TCOl2Vt9iRt5B7Ruly/juUCYAqRAJHrrDpP/ULM7Yy/zsGUmvqHEEMLM8IlbZaDMM6kidRAOYSMlBL3Hkh40Xb5aZfrT/635b40vhoAJpwLXbLw2Y4i9D3mgBDMSQMUCgYBLkj6pnTvJtgR7xCAKaVoERYXmsDjP/gIhSrmnqfO8EZDheVoh+XbN5o3D27aBmWiWTwgJoMuF5GxEL2tSbQBvkT6smTrVBDF8Q8+tgBvP2CV0gwto+Spv5b9h7dNWk5UI0ZOWnlF1aI37UJBKmyp2kYCoDwnA1vtFpXncmaNHxw==";

public static void main(String[] args) throws UnsupportedEncodingException {
    test();
}


public static void test() throws UnsupportedEncodingException {
//构造待签名字符串  
    String string = "content=hello" + "&mch_id=1&method=pay&nonce_str=1&time_stamp=1";
    System.out.println("拼接字符串:" + string);
//加密字符串  
    String str = sign(string.getBytes("utf-8"));
    System.out.println("字符串加密:" + str);
    String authorization = "mch_id=1&method=pay&nonce_str=1&sign=" + URLEncoder.encode(str, "UTF-8") + "&time_stamp=1";
    System.out.println("请求头:" + authorization);
    System.out.println("请求结果:" + DemoCall.doPost("pay/api/v1", "hello", authorization));
}


/**
 * 将字节数组内信息使用签名进行加密
 *
 * @param message
 * @return
 */
public static String sign(byte[] message) {
    Signature sign;
    try {
        sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(getPrivateKey());
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (SignatureException e) {
        e.printStackTrace();
    } catch (InvalidKeyException e) {
        e.printStackTrace();
    }
    return null;
}

/**
 * 获取私钥。
 *
 * @param filename 私钥文件路径 (required)
 * @return 私钥对象
 */
public static PrivateKey getPrivateKey() {
    String content = private_key;
    try {
        String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s+", "");
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePrivate(
                new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException("当前Java环境不支持RSA", e);
    } catch (InvalidKeySpecException e) {
        throw new RuntimeException("无效的密钥格式");
    }
}

转载:https://segmentfault.com/q/1010000022260275