上篇文章,我們了解了關於對稱和非對稱加密的一些相關的理論知識,也學習了使用 OpenSSL 來進行對稱加密的操作。今天,我們就更進一步,學習 OpenSSL 中的非對稱加密是如何實現的。
生成私鑰通過之前的學習,我們知道非對稱加密是分別需要一個公鑰和一個私鑰的。我們就先來生成一個私鑰,也就是存放在我們這一端一個密鑰。請記住,在任何時候,私鑰都是不能給別人的哦!
$config = array(
"private_key_bits" => 4096, // 指定應該使用多少位來生成私鑰
);
$res = openssl_pkey_new($config); // 根據配置信息生成私鑰
openssl_pkey_export($res, $privateKey); // 將一個密鑰的可輸出表示轉換為字符串
var_dump($privateKey);
// BEGIN PRIVATE KEY
// MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDFMLW+9t3fNX4C
// YBuV0ILSyPAdSYVXtE4CLv32OvNk9yQZgF2nL/ZuIbBGRcYo2Hf5B31doGrAFDGu
// NoTR+WA7CBjKROFr/+yValsMFIeiKNtttWMkmBciysDJoEoyd6wjDD+kcHQdoJVo
// ……
// END PRIVATE KEY非常簡單的一個函數 openssl_pkey_new() ,它接收一個參數,這個參數是可配置項並且是可選參數。生成的結果是一個私鑰句柄,不是我們能直接讀取的內容,所以我們再使用 openssl_pkey_export() 來提取可輸出的字符串。
注釋中的內容就是我們生成的私鑰信息了,私鑰信息一般會相對多些,所以省略了後面的內容。
抽取公鑰接下來就是生成公鑰了,其實,公鑰是從私鑰中抽取出來的。所以我們使用進行加解密的時候,都可以使用私鑰或者公鑰互相操作。
$publicKey = openssl_pkey_get_details($res); // 抽取公鑰信息
var_dump($publicKey);
// array(4) {
// ["bits"]=>
// int(4096)
// ["key"]=>
// string(800) "BEGIN PUBLIC KEY
// MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtOIImDdS0W0vAr5Ra1+E
// hR2AJwQQwxntYKgTku8EmJRBX2vU+x8th8W8SnoGiVM/sOItG0HIe4Egf1UxoZHt
// gI6r+jpAp7JbTN0sD/VTPDE09F21+hFGjIVBqrkcLPjuEbf7+tjmgAx8cG8WLGId
// G8Hsub70kRANKJe1bCXIBUggRFk0sQGllxA/hxiG5wANqHTrdpJgJba+ahSi2+4H
// UWnyCV1O3AaPyz6a12HNUsG4Eio/tWv/hOB9POt6nAqwPHuIbhp56i5bv1ijMJZM
// jwRen5f/kwdZ01Ig2fi0uBoTR2y/EEaus7xBYpF/gGzZ/uM7cNUXcDyG5YluM/4R
// MEv4msPMVGB72izItED+C6Cqftxl98iBFRDc+PISFbRSgOU/HsuBhKkM5SYzyi3I
// Ypaej25++qLPqcA+EDr3JNDhNZ0GOhofCRtPq4dsr7iLLLRnZ0TnhIYe9wAbmO49
// uthABNBkM54bG+omOfY4Bkn5n39CKpELbhIiXgOd+lA684XUS/2Aw3Dvelc9Gbag
// oIFvb/wljPYsd0Zmd64CXBpTWbfwXC8K4vCKvFLjytcz2Yp4T6fVjbLT5RA6u8su
// E0WwE4QTFNKhnM5OvfiMN+NMc3Y/esVfcin3eyvotdz4N6Tt45dkybkf6aQE3Scg
// E/JBLIEEA+gjGTveY4cNUiECAwEAAQ==
// END PUBLIC KEY
// "
// ["rsa"]=>
// ……
$publicKey = $publicKey['key'];使用 openssl_pkey_get_details() 抽取出來的內容包含很多內容。不過我們所需要的最主要的內容就是 key 下面的這個公鑰。
大家再回過頭來好好看一下公鑰和私鑰的內容,是不是和我們去申請的 HTTPS 證書中的公私鑰內容長得一樣,而且也和我們自己在系統中使用 openssl 命令行生成的本機的密鑰證書一樣。它們本身就是一樣的東西啦,只是在不同的場景應用的不同而已。HTTPS 證書除了非對稱加密的密鑰之外,還包含有 CA 信息,如果 CA 不通過,瀏覽器也會認為證書是無效的,因此,我們使用自己生成的證書來充當 HTTPS 證書是不可以的。而本身生成的一般會用在 SSH 免密登錄上,或者是 GitHub 的免密代碼倉庫操作上。
加密解密數據好了,公鑰和私鑰都生成完成了,那麼我們就要進行最重要的加密和解密操作了。
$data = '測試非對稱加密';
// 公鑰加密數據
openssl_public_encrypt($data, $encrypted, $publicKey);
var_dump($encrypted);
// string(512) "��E��2��~��\d����q�O�=(��Y���3L����0�,�J����s�V��V߬G~'�20���@��6�d�����#Z]�.��<Z��8G�����-ʝ�M�0](2��+$�*����\e�7ҕʴ��|SUw�#rFb�8"�s4K�B�Y�'�\S���~!<�"���!U��S(���S ��?e�֜r��/���c��L�YL�'ŖE*S��[�J�"�n��`(ʿoF$�|kC�*j_y�E�D�O����H5���6�t�TY����b5l^)�`�v�>�1��a��r�̹�D��������@�S�>�t|���匓�z~K�,���y��Gܬ��
// yXZ�L#��c `rj睅,nX���@{7�:�qy�ʲnv�o§�@�@,�n&���I�~ǧ�z6���oe!8,T�����;җ�6�J@A��f����S]��!����2�b��+Oګ��o�<�
// ����-�+et��})�KG��$���,�Z|�"
// 私鑰解密數據
openssl_private_decrypt($encrypted, $decrypted, $privateKey);
var_dump($decrypted);
// string(21) "測試非對稱加密"在這裡,我們使用的就是最標準的公鑰加密,私鑰解密來進行的測試。其實反過來也是可以的,OpenSSL 分別都為我們提供了公鑰的加解密和私鑰的加解密函數。
就像上篇文章的圖示那樣,對方獲得我們的公鑰,然後加密數據傳輸過來,我們通過自己的私鑰解密數據獲得原文。而我方也可以獲得對方的公鑰,並將返回的數據加密後傳輸給對方,然後對方使用自己的私鑰進行解密獲得我們傳遞給它的原文數據。
而 HTTPS 是通過 CA 頒發的證書來獲取公鑰的,瀏覽器通過公鑰加密請求數據傳輸給伺服器,伺服器也是通過相同的原理來向瀏覽器客戶端發送密文數據。因此,在數據傳輸過程中,使用 HTTPS 的傳輸會更加地安全,即使被截獲了,對方也沒有證書提供的密鑰來進行解密。這就是現在所有 App 和 小程序 應用都要求使用 HTTPS 的原因,當然,我們如果做網站開發也最好使用 HTTPS ,就連百度對 HTTPS 的收錄也有相應的調整。
籤名及驗證接下來我們再接觸一個籤名的概念。當兩端進行通信時,我們怎麼知道當前傳輸過來的數據一定是對端發送過來的的呢,中間有沒有黑客進行了篡改呢?這個就可以通過籤名機制來進行驗證。
// 利用私鑰生成籤名
openssl_sign($data, $signature, $privateKey, OPENSSL_ALGO_SHA256);
var_dump($signature);
// ��<�~�k�٭N����M�тq��;��h�dŬ�Ğ�m�3�nj��/i���U�_�E2z���>B�N�WM!TN�c�F�/��,5�|���%��c~O)"
// �� >��)y�fn��q��}
// �`
// �z��{��_D�s`�����|y�,g>R�D��%�
// �gͯ0�@Λ|��|z}���bZI2,����~Q_���I�LW~���G&���f�|eq�s�D���L���bC'D��~8:�Z����\�9]C�Kd~F96�S� 0��y>�(T��S}��1�謃T
// �!��!!�Lj�<�ǺfM�o7�3��������� 8ZR<Vya4����V��Wט����L�QZbv��7?�v`}��?v ǿ�0`�OF��F��@�$b�PBI�o\�v���D���"
// 公鑰驗證籤名
$r = openssl_verify($data, $signature, $publicKey, OPENSSL_ALGO_SHA256);
var_dump($r);
// int(1)我們通過 openssl_sign() 來生成一個對原始數據的私鑰籤名,然後就可以使用 openssl_verify() 通過公鑰驗證數據籤名是否一致。
在使用的時候,發送方通過自己的私鑰生成籤名,由於籤名內容是亂碼的,我們可以將它 base64_encode() 一下,然後連同加密數據一起傳遞給接收方。然後接收方使用公鑰並根據籤名內容來驗證原文數據是否被篡改過。
// 發送方籤名
$resquestSign = base64_encode($signature);
// 假設通過網絡請求發送了數據
// ……
// 接收到獲得籤名及原始數據
// $signature = $_POST['sign'];
// openssl_private_decrypt($_POST['data'], $data, $privateKey);
$responseSign = base64_decode($signature);
// 驗證數據有沒有被篡改
$r = openssl_verify($data, $signature, $publicKey, OPENSSL_ALGO_SHA256);
var_dump($r);
// int(1)
// 假設被篡改
$data = '我被修改了';
$r = openssl_verify($data, $signature, $publicKey, OPENSSL_ALGO_SHA256);
var_dump($r);
// int(0)
總結今天的內容是不是感覺比對稱加密複雜了許多。特別新引入的籤名的這個概念,其實很多證書相關的內容都會和數據籤名有關係。也就是說,看似簡單的一個 HTTPS ,其實瀏覽器和服務端的 openssl 幫我們做了很多事情,遠不止你去 CA 申請一套證書然後在 Nginx 配好那麼簡單。那麼,接下來,我們將要學習的就是生成證書相關的內容了,系好安全帶,車還要繼續飆。
測試代碼:
https://github.com/zhangyue0503/dev-blog/blob/master/php/202007/source/PHP%E7%9A%84OpenSSL%E5%8A%A0%E5%AF%86%E6%89%A9%E5%B1%95%E5%AD%A6%E4%B9%A0%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E9%9D%9E%E5%AF%B9%E7%A7%B0%E5%8A%A0%E5%AF%86.php
參考文檔:
https://www.php.net/manual/zh/function.openssl-pkey-new.php
https://www.php.net/manual/zh/function.openssl-pkey-get-details.php
https://www.php.net/manual/zh/function.openssl-pkey-export.php
https://www.php.net/manual/zh/function.openssl-public-encrypt.php
https://www.php.net/manual/zh/function.openssl-private-decrypt.php
https://www.php.net/manual/zh/function.openssl-sign.php
https://www.php.net/manual/zh/function.openssl-verify.php