如何使用 OpenSSL:哈希值、數字簽名等
本系列的第一篇文章通過 OpenSSL 庫和命令行實用程序介紹了哈希、加密/解密、數字簽名和數字證書。這第二篇文章將對細節進行深入探討。讓我們從計算中無處不在的哈希開始,並考慮是什麼使哈希函數具備密碼學意義。
密碼學哈希
OpenSSL 源代碼的下載頁面包含了一個帶有最新版本的表格。每個版本都有兩個 哈希值 :160 位 SHA1 和 256 位 SHA256。這些值可以用來驗證下載的文件是否與存儲庫中的原始文件相匹配:下載者在本地重新計算下載文件的哈希值,然後將結果與原始文件進行比較。現代系統有計算這種哈希值的實用程序。例如,Linux 有 md5sum
和 sha256sum
。OpenSSL 本身也提供了類似的命令行實用程序。
哈希值被用於計算的許多領域。例如,比特幣區塊鏈使用 SHA256 哈希值作為區塊標識符。挖比特幣就是生成一個低於指定閾值的 SHA256 哈希值,也就是至少有 N 個前導零的哈希值。(N 的值可以上升或下降,這取決於特定時間的挖礦生產力)。作為一個興趣點,如今的礦機是為並行生成 SHA256 哈希值而設計的硬體集群。在 2018 年的一個高峰期,全球的比特幣礦工每秒產生約 7500 萬個 太哈希值 —— 這真是一個不可思議的數字。
網路協議也使用哈希值(在這裡通常叫做「 校驗和 」)來支持消息的完整性;也就是說,保證收到的消息與發送的消息是一樣的。消息發送者計算消息的校驗和,並將結果與消息一起發送。當消息到達時,接收方重新計算校驗和。如果發送的校驗和與重新計算的校驗和不一致,那麼消息在傳輸過程中可能出現了一些問題,或者發送的校驗和出現了問題,或者兩者都出現了問題。在這種情況下,應該重新發送消息和它的校驗和,或者至少應該觸發一個錯誤情況。(如 UDP 這樣的低級網路協議不會理會校驗和。)
哈希的其他例子大家都很熟悉。比如一個網站,要求用戶用密碼進行驗證,用戶在瀏覽器中輸入密碼,然後,他們通過 HTTPS 連接到伺服器,密碼從瀏覽器加密發送到伺服器。一旦密碼到達伺服器,就會被解密,然後進行資料庫表的查詢。
在這個查詢表中應該存儲什麼?存儲密碼本身是有風險的。風險要小得多的方式是存儲一個由密碼生成的哈希值,也許在計算哈希值之前「加一些 鹽 (額外的位)改善口味」。你的密碼可能會被發送到 Web 伺服器上,但網站可以向你保證,密碼不會存儲在那裡。
哈希值還出現在安全的各個領域。例如, 基於哈希值的消息認證碼 (HMAC)使用一個哈希值和一個秘密的 加密密鑰 來認證通過網路發送的消息。HMAC 碼輕量級且易於在程序中使用,在 Web 服務中很受歡迎。一個 X509 數字證書包括一個稱為 指紋 的哈希值,它可以方便證書驗證。一個存放於內存中的 可信存儲 可以實現為一個以這種指紋為鍵的查找表 —— 作為一個支持恆定查找時間的 哈希映射 。來自傳入的證書的指紋可以與可信存儲中的密鑰進行比較,以確定是否匹配。
密碼學哈希函數 應該具有什麼特殊屬性?它應該是 單向 的,這意味著很難被逆轉。一個加密哈希函數應該是比較容易計算的,但是計算它的反函數(將哈希值映射回輸入位串的函數)在計算上應該是困難的。下面是一個描述,用 chf
作為加密哈希函數,我的密碼 foobar
作為樣本輸入。
+---+
foobar—>|chf|—>hash value ## 簡單直接
+--–+
相比之下,逆向操作是不可行的:
+-----------+
hash value—>|chf inverse|—>foobar ## 棘手困難
+-----------+
例如,回憶一下 SHA256 哈希函數。對於一個任意長度為 N > 0 的輸入位串,這個函數會生成一個 256 位的固定長度的哈希值;因此,這個哈希值甚至不會反映出輸入位串的長度 N,更不用說字元串中每個位的值了。順便說一下,SHA256 不容易受到 長度擴展攻擊 。唯一有效的逆向工程方法是通過蠻力搜索將計算出的 SHA256 哈希值逆向返回到輸入位串,這意味著需要嘗試所有可能的輸入位串,直到找到與目標哈希值匹配的位串。這樣的搜索在 SHA256 這樣一個完善的加密哈希函數上是不可行的。
現在,最後一個回顧的知識點是 有序 。加密哈希值是統計學上的唯一,而不是無條件的唯一,這意味著兩個不同的輸入位串產生相同的哈希值是不太可能的,但也不是不可能的 —— 這稱之為 碰撞 。生日問題提供了一個很好的反直覺的碰撞例子。對各種哈希演算法的 抗碰撞性 有著廣泛的研究。例如,MD5(128 位哈希值)在大約 2 21 次哈希之後,抗碰撞能力就會崩潰。對於 SHA1(160 位哈希值),大約在 2 61 次哈希後開始崩潰。
對於 SHA256 的抗碰撞能力的剖析,目前還沒有一個很好的估計。這個事實並不奇怪。SHA256 有 2 256 個不同的哈希值範圍,這個數字的十進位表示法有 78 位之多!那麼,SHA256 哈希會不會發生碰撞呢?當然可能,但可能性極小。
在下面的命令行示例中,有兩個輸入文件被用作位串源:hashIn1.txt
和 hashIn2.txt
。第一個文件包含 abc
,第二個文件包含 1a2b3c
。
為了便於閱讀,這些文件包含的是文本,但也可以使用二進位文件代替。
在命令行(百分號 %
是提示符)使用 Linux sha256sum
實用程序對這兩個文件進行處理產生以下哈希值(十六進位):
% sha256sum hashIn1.txt
9e83e05bbf9b5db17ac0deec3b7ce6cba983f6dc50531c7a919f28d5fb3696c3 hashIn1.txt
% sha256sum hashIn2.txt
3eaac518777682bf4e8840dd012c0b104c2e16009083877675f00e995906ed13 hashIn2.txt
OpenSSL 哈希對應的結果與預期相同:
% openssl dgst -sha256 hashIn1.txt
SHA256(hashIn1.txt)= 9e83e05bbf9b5db17ac0deec3b7ce6cba983f6dc50531c7a919f28d5fb3696c3
% openssl dgst -sha256 hashIn2.txt
SHA256(hashIn2.txt)= 3eaac518777682bf4e8840dd012c0b104c2e16009083877675f00e995906ed13
這種對密碼學哈希函數的研究,為我們仔細研究數字簽名及其與密鑰對的關係奠定了基礎。
數字簽名
顧名思義, 數字簽字 可以附在文件或其他一些電子 工件 (如程序)上,以證明其真實性。因此,這種簽名類似於紙質文件上的手寫簽名。驗證數字簽名就是要確認兩件事:第一,被擔保的工件在簽名被附上後沒有改變,因為它部分是基於文件的加密學哈希值。第二,簽名屬於一個人(例如 Alice),只有她才能獲得一對密鑰中的私鑰。順便說一下,對代碼(源碼或編譯後的代碼)進行數字簽名已經成為程序員的普遍做法。
讓我們來了解一下數字簽名是如何創建的。如前所述,沒有公鑰和私鑰對就沒有數字簽名。當使用 OpenSSL 創建這些密鑰時,有兩個獨立的命令:一個是創建私鑰,另一個是從私鑰中提取匹配的公鑰。這些密鑰對用 base64 編碼,在這個過程中可以指定它們的大小。
私鑰 由數值組成,其中兩個數值(一個 模數 和一個 指數 )組成了公鑰。雖然私鑰文件包含了 公鑰 ,但提取出來的公鑰並不會透露相應私鑰的值。
因此,生成的帶有私鑰的文件包含了完整的密鑰對。將公鑰提取到自己的文件中是很實用的,因為這兩把鑰匙有不同的用途,而這種提取方式也將私鑰可能被意外公開的危險降到最低。
接下來,這對密鑰的私鑰被用來生成目標工件(如電子郵件)的哈希值,從而創建簽名。在另一端,接收者的系統使用這對密鑰的公鑰來驗證附在工件上的簽名。
現在舉個例子。首先,用 OpenSSL 生成一個 2048 位的 RSA 密鑰對:
openssl genpkey -out privkey.pem -algorithm rsa 2048
在這個例子中,我們可以捨去 -algorithm rsa
標誌,因為 genpkey
默認為 RSA 類型。文件的名稱(privkey.pem
)是任意的,但是 隱私增強郵件 (PEM)擴展名 .pem
是默認 PEM 格式的慣用擴展名。(如果需要的話,OpenSSL 有命令可以在各種格式之間進行轉換。)如果需要更大的密鑰大小(例如 4096),那麼最後一個參數 2048
可以改成 4096
。這些大小總是二的冪。
下面是產生的 privkey.pem
文件的一個片斷,它是 base64 編碼的:
-----BEGIN PRIVATE KEY--MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBANnlAh4jSKgcNj/Z
JF4J4WdhkljP2R+TXVGuKVRtPkGAiLWE4BDbgsyKVLfs2EdjKL1U+/qtfhYsqhkK
...
-----END PRIVATE KEY--
接下來的命令就會從私鑰中提取出這對密鑰的公鑰:
openssl rsa -in privkey.pem -outform PEM -pubout -out pubkey.pem
由此產生的 pubkey.pem
文件很小,可以在這裡完整地顯示出來:
-----BEGIN PUBLIC KEY--MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZ5QIeI0ioHDY/2SReCeFnYZJY
z9kfk11RrilUbT5BgIi1hOAQ24LMilS37NhHYyi9VPv6rX4WLKoZCmkeYaWk/TR5
4nbH1E/AkniwRoXpeh5VncwWMuMsL5qPWGY8fuuTE27GhwqBiKQGBOmU+MYlZonO
O0xnAKpAvysMy7G7qQIDAQAB
-----END PUBLIC KEY--
現在,有了密鑰對,數字簽名就很容易了 —— 在本例中,源文件 client.c
是要簽名的工件:
openssl dgst -sha256 -sign privkey.pem -out sign.sha256 client.c
client.c
源文件的摘要是 SHA256,私鑰在前面創建的 privkey.pem
文件中。由此產生的二進位簽名文件是 sign.sha256
,這是一個任意的名字。要得到這個文件的可讀版本(比如 base64),後續命令是:
openssl enc -base64 -in sign.sha256 -out sign.sha256.base64
文件 sign.sha256.base64
現在包含如下內容:
h+e+3UPx++KKSlWKIk34fQ1g91XKHOGFRmjc0ZHPEyyjP6/lJ05SfjpAJxAPm075
VNfFwysvqRGmL0jkp/TTdwnDTwt756Ej4X3OwAVeYM7i5DCcjVsQf5+h7JycHKlM
o/Jd3kUIWUkZ8+Lk0ZwzNzhKJu6LM5KWtL+MhJ2DpVc=
或者,可執行文件 client
也可以被簽名,由此產生的 base64 編碼簽名將如預期的不同:
VMVImPgVLKHxVBapJ8DgLNJUKb98GbXgehRPD8o0ImADhLqlEKVy0HKRm/51m9IX
xRAN7DoL4Q3uuVmWWi749Vampong/uT5qjgVNTnRt9jON112fzchgEoMb8CHNsCT
XIMdyaPtnJZdLALw6rwMM55MoLamSc6M/MV1OrJnk/g=
這一過程的最後一步是用公鑰驗證數字簽名。作為驗證的一個重要步驟,應重新計算用於簽署工件(在本例中,是可執行的 client
程序)的哈希值,因為驗證過程應表明工件在簽署後是否發生了變化。
有兩個 OpenSSL 命令用於這個目的。第一條命令是對 base64 簽名進行解碼。
openssl enc -base64 -d -in sign.sha256.base64 -out sign.sha256
第二條是核實簽名:
openssl dgst -sha256 -verify pubkey.pem -signature sign.sha256 client
第二條命令的輸出,應該是這樣的:
Verified OK
為了了解驗證失敗時的情況,一個簡短但有用的練習是將最後一個 OpenSSL 命令中的可執行的 client
文件替換為源文件 client.c
,然後嘗試驗證。另一個練習是改變 client
程序,無論多麼輕微,然後再試一次。
數字證書
數字證書 彙集了到目前為止所分析的各個部分:哈希值、密鑰對、數字簽名和加密/解密。生產級證書的第一步是創建一個 證書籤名請求 (CSR),然後將其發送給 證書頒發機構 (CA)。在 OpenSSL 的例子中,要做到這一點,請運行:
openssl req -out myserver.csr -new -newkey rsa:4096 -nodes -keyout myserverkey.pem
這個例子生成了一個 CSR 文檔,並將該文檔存儲在文件 myserver.csr
(base64 文本)中。這裡的目的是:CSR 文檔要求 CA 保證與指定域名相關聯的身份,域名也就是 CA 所說的 通用名 (CN)。
儘管可以使用現有的密鑰對,但這個命令也會生成一個新的密鑰對。請注意,在諸如 myserver.csr
和 myserverkey.pem
等名稱中使用 server
暗示了數字證書的典型用途:作為與 www.google.com 等域名相關的 Web 伺服器的身份擔保。
然而,無論數字證書如何使用,同樣使用這個命令都會創建一個 CSR。它還會啟動一個問題/回答的互動式會話,提示有關域名的相關信息,以便與請求者的數字證書相連接。這個互動式會話可以通過在命令中提供基本的信息,用反斜杠來續行一步完成。-subj
標誌提供了所需的信息。
% openssl req -new
-newkey rsa:2048 -nodes -keyout privkeyDC.pem
-out myserver.csr
-subj "/C=US/ST=Illinois/L=Chicago/O=Faulty Consulting/OU=IT/CN=myserver.com"
產生的 CSR 文件在發送給 CA 之前可以進行檢查和驗證。這個過程可以創建具有所需格式(如 X509)、簽名、有效期等的數字證書。
openssl req -text -in myserver.csr -noout -verify
這是輸出的一個片斷:
verify OK
Certificate Request:
Data:
Version: 0 (0x0)
Subject: C=US, ST=Illinois, L=Chicago, O=Faulty Consulting, OU=IT, CN=myserver.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:ba:36:fb:57:17:65:bc:40:30:96:1b:6e:de:73:
…
Exponent: 65537 (0x10001)
Attributes:
a0:00
Signature Algorithm: sha256WithRSAEncryption
…
自簽證書
在開發 HTTPS 網站的過程中,手頭有一個不用經過 CA 流程的數字證書是很方便的。在 HTTPS 握手的認證階段, 自簽證書 就能滿足要求,儘管任何現代瀏覽器都會警告說這樣的證書毫無價值。繼續這個例子,自簽證書的 OpenSSL 命令(有效期為一年,使用 RSA 公鑰)如下:
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:4096 -keyout myserver.pem -out myserver.crt
下面的 OpenSSL 命令呈現了生成的證書的可讀版本:
openssl x509 -in myserver.crt -text -noout
這是自簽證書的部分輸出:
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 13951598013130016090 (0xc19e087965a9055a)
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, ST=Illinois, L=Chicago, O=Faulty Consulting, OU=IT, CN=myserver.com
Validity
Not Before: Apr 11 17:22:18 2019 GMT
Not After : Apr 10 17:22:18 2020 GMT
Subject: C=US, ST=Illinois, L=Chicago, O=Faulty Consulting, OU=IT, CN=myserver.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Modulus:
00:ba:36:fb:57:17:65:bc:40:30:96:1b:6e:de:73:
...
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
3A:32:EF:3D:EB:DF:65:E5:A8:96:D7:D7:16:2C:1B:29:AF:46:C4:91
X509v3 Authority Key Identifier:
keyid:3A:32:EF:3D:EB:DF:65:E5:A8:96:D7:D7:16:2C:1B:29:AF:46:C4:91
X509v3 Basic Constraints:
CA:TRUE
Signature Algorithm: sha256WithRSAEncryption
3a:eb:8d:09:53:3b:5c:2e:48:ed:14:ce:f9:20:01:4e:90:c9:
...
如前所述,RSA 私鑰包含的值是用來生成公鑰的。但是,給定的公鑰不會泄露匹配的私鑰。關於底層數學理論的介紹,見 https://simple.wikipedia.org/wiki/RSA_algorithm。
數字證書與用於生成該證書的密鑰對之間存在著重要的對應關係,即使證書只是自簽的:
- 數字證書包含構成公鑰的指數和模數值。這些值是最初生成的 PEM 文件中密鑰對的一部分,在本例中,是文件
myserver.pem
。 - 指數 幾乎總是 65,537(如本例中),所以可以忽略。
- 密鑰對的 模數 應該與數字證書的模數相匹配。
模數是一個很大的值,為了便於閱讀,可以進行哈希處理。下面是兩個 OpenSSL 命令,它們檢查相同的模數,從而確認數字證書是基於 PEM 文件中的密鑰對。
% openssl x509 -noout -modulus -in myserver.crt | openssl sha1 ## 證書中的模數
(stdin)= 364d21d5e53a59d482395b1885aa2c3a5d2e3769
% openssl rsa -noout -modulus -in myserver.pem | openssl sha1 ## 密鑰中的模數
(stdin)= 364d21d5e53a59d482395b1885aa2c3a5d2e3769
所產生的哈希值匹配,從而確認數字證書是基於指定的密鑰對。
回到密鑰分發問題上
讓我們回到第一部分末尾提出的一個問題:client
程序和 Google Web 伺服器之間的 TLS 握手。握手協議有很多種,即使是用在 client
例子中的 Diffie-Hellman 版本也有不同的方式。儘管如此,client
例子遵循了一個共同的模式。
首先,在 TLS 握手過程中,client
程序和 Web 伺服器就 加密套件 達成一致,其中包括要使用的演算法。在本例中,該套件是 ECDHE-RSA-AES128-GCM-SHA256
。
現在值得關注的兩個要素是 RSA 密鑰對演算法和 AES128 塊密碼,用於在握手成功的情況下對消息進行加密和解密。關於加密/解密,這個過程有兩種流派: 對稱 和 非對稱 。在對稱流派中,加密和解密使用的是相同的密鑰,這首先就引出了 密鑰分發問題 。如何將密鑰安全地分發給雙方?在非對稱流派中,一個密鑰用於加密(在這種情況下,是 RSA 公鑰),但另一個密鑰用於解密(在這種情況下,是來自同一對密鑰的 RSA 私鑰)。
client
程序擁有來認證證書的 Google Web 伺服器的公鑰,而 Web 伺服器擁有來自同一對密鑰的私鑰。因此,client
程序可以向 Web 伺服器發送加密信息,而 Web 伺服器可以單獨對該通信進行解密。
在 TLS 的情況下,對稱方式有兩個顯著的優勢:
- 在
client
程序與 Google Web 伺服器之間的互動中,認證是單向的。Google Web 伺服器向client
程序發送三張證書,但client
程序並沒有向 Web 伺服器發送證書,因此,Web 伺服器沒有來自客戶端的公鑰,無法加密發給客戶端的消息。 - 使用 AES128 的對稱加密/解密比使用 RSA 密鑰的非對稱加密/解密快了近千倍。
TLS 握手將兩種加密/解密方式巧妙地結合在一起。在握手過程中,client
程序會生成隨機位,即所謂的 預主密 (PMS)。然後,client
程序用伺服器的公鑰對 PMS 進行加密,並將加密後的 PMS 發送給伺服器,伺服器再用 RSA 密鑰對的私鑰對 PMS 信息進行解密:
+-------------------+ encrypted PMS +--------------------+
client PMS--->|server』s public key|--------------->|server』s private key|--->server PMS
+-------------------+ +--------------------+
在這個過程結束時,client
程序和 Google Web 伺服器現在擁有相同的 PMS 位。每一方都使用這些位生成一個 主密碼 ,並立即生成一個稱為 會話密鑰 的對稱加密/解密密鑰。現在有兩個不同但等價的會話密鑰,連接的每一方都有一個。在 client
的例子中,會話密鑰是 AES128 類的。一旦在 client
程序和 Google Web 伺服器兩邊生成了會話密鑰,每一邊的會話密鑰就會對雙方的對話進行保密。如果任何一方(例如,client
程序)或另一方(在這種情況下,Google Web 伺服器)要求重新開始握手,握手協議(如 Diffie-Hellman)允許整個 PMS 過程重複進行。
總結
在命令行上說明的 OpenSSL 操作也可以通過底層庫的 API 完成。這兩篇文章重點使用了這個實用程序,以保持例子的簡短,並專註於加密主題。如果你對安全問題感興趣,OpenSSL 是一個很好的開始地方,並值得深入研究。
via: https://opensource.com/article/19/6/cryptography-basics-openssl-part-2
作者:Marty Kalin 選題:lujun9972 譯者:wxy 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive