Linux中國

如何使用 OpenSSL:哈希值、數字簽名等

本系列的第一篇文章通過 OpenSSL 庫和命令行實用程序介紹了哈希、加密/解密、數字簽名和數字證書。這第二篇文章將對細節進行深入探討。讓我們從計算中無處不在的哈希開始,並考慮是什麼使哈希函數具備密碼學意義

密碼學哈希

OpenSSL 源代碼的下載頁面包含了一個帶有最新版本的表格。每個版本都有兩個 哈希值 hash :160 位 SHA1 和 256 位 SHA256。這些值可以用來驗證下載的文件是否與存儲庫中的原始文件相匹配:下載者在本地重新計算下載文件的哈希值,然後將結果與原始文件進行比較。現代系統有計算這種哈希值的實用程序。例如,Linux 有 md5sumsha256sumOpenSSL 本身也提供了類似的命令行實用程序。

哈希值被用於計算的許多領域。例如,比特幣區塊鏈使用 SHA256 哈希值作為區塊標識符。挖比特幣就是生成一個低於指定閾值的 SHA256 哈希值,也就是至少有 N 個前導零的哈希值。(N 的值可以上升或下降,這取決於特定時間的挖礦生產力)。作為一個興趣點,如今的礦機是為並行生成 SHA256 哈希值而設計的硬體集群。在 2018 年的一個高峰期,全球的比特幣礦工每秒產生約 7500 萬個 太哈希值 terahash —— 這真是一個不可思議的數字。

網路協議也使用哈希值(在這裡通常叫做「 校驗和 checksum 」)來支持消息的完整性;也就是說,保證收到的消息與發送的消息是一樣的。消息發送者計算消息的校驗和,並將結果與消息一起發送。當消息到達時,接收方重新計算校驗和。如果發送的校驗和與重新計算的校驗和不一致,那麼消息在傳輸過程中可能出現了一些問題,或者發送的校驗和出現了問題,或者兩者都出現了問題。在這種情況下,應該重新發送消息和它的校驗和,或者至少應該觸發一個錯誤情況。(如 UDP 這樣的低級網路協議不會理會校驗和。)

哈希的其他例子大家都很熟悉。比如一個網站,要求用戶用密碼進行驗證,用戶在瀏覽器中輸入密碼,然後,他們通過 HTTPS 連接到伺服器,密碼從瀏覽器加密發送到伺服器。一旦密碼到達伺服器,就會被解密,然後進行資料庫表的查詢。

在這個查詢表中應該存儲什麼?存儲密碼本身是有風險的。風險要小得多的方式是存儲一個由密碼生成的哈希值,也許在計算哈希值之前「加一些 salt (額外的位)改善口味」。你的密碼可能會被發送到 Web 伺服器上,但網站可以向你保證,密碼不會存儲在那裡。

哈希值還出現在安全的各個領域。例如, 基於哈希值的消息認證碼 hash-based message authentication code HMAC)使用一個哈希值和一個秘密的 加密密鑰 cryptographic key 來認證通過網路發送的消息。HMAC 碼輕量級且易於在程序中使用,在 Web 服務中很受歡迎。一個 X509 數字證書包括一個稱為 指紋 fingerprint 的哈希值,它可以方便證書驗證。一個存放於內存中的 可信存儲 truststore 可以實現為一個以這種指紋為鍵的查找表 —— 作為一個支持恆定查找時間的 哈希映射 hash map 。來自傳入的證書的指紋可以與可信存儲中的密鑰進行比較,以確定是否匹配。

密碼學哈希函數 cryptographic hash function 應該具有什麼特殊屬性?它應該是 單向 one-way 的,這意味著很難被逆轉。一個加密哈希函數應該是比較容易計算的,但是計算它的反函數(將哈希值映射回輸入位串的函數)在計算上應該是困難的。下面是一個描述,用 chf 作為加密哈希函數,我的密碼 foobar 作為樣本輸入。

        +---+
foobar—>|chf|—>hash value ## 簡單直接
        +--–+

相比之下,逆向操作是不可行的:

            +-----------+
hash value—>|chf inverse|—>foobar ## 棘手困難
            +-----------+

例如,回憶一下 SHA256 哈希函數。對於一個任意長度為 N > 0 的輸入位串,這個函數會生成一個 256 位的固定長度的哈希值;因此,這個哈希值甚至不會反映出輸入位串的長度 N,更不用說字元串中每個位的值了。順便說一下,SHA256 不容易受到 長度擴展攻擊 length extension attack 。唯一有效的逆向工程方法是通過蠻力搜索將計算出的 SHA256 哈希值逆向返回到輸入位串,這意味著需要嘗試所有可能的輸入位串,直到找到與目標哈希值匹配的位串。這樣的搜索在 SHA256 這樣一個完善的加密哈希函數上是不可行的。

現在,最後一個回顧的知識點是 有序 in order 。加密哈希值是統計學上的唯一,而不是無條件的唯一,這意味著兩個不同的輸入位串產生相同的哈希值是不太可能的,但也不是不可能的 —— 這稱之為 碰撞 collision 生日問題提供了一個很好的反直覺的碰撞例子。對各種哈希演算法的 抗碰撞性 collision resistance 有著廣泛的研究。例如,MD5(128 位哈希值)在大約 2 21 次哈希之後,抗碰撞能力就會崩潰。對於 SHA1(160 位哈希值),大約在 2 61 次哈希後開始崩潰。

對於 SHA256 的抗碰撞能力的剖析,目前還沒有一個很好的估計。這個事實並不奇怪。SHA256 有 2 256 個不同的哈希值範圍,這個數字的十進位表示法有 78 位之多!那麼,SHA256 哈希會不會發生碰撞呢?當然可能,但可能性極小。

在下面的命令行示例中,有兩個輸入文件被用作位串源:hashIn1.txthashIn2.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

這種對密碼學哈希函數的研究,為我們仔細研究數字簽名及其與密鑰對的關係奠定了基礎。

數字簽名

顧名思義, 數字簽字 digital signature 可以附在文件或其他一些電子 工件 artifact (如程序)上,以證明其真實性。因此,這種簽名類似於紙質文件上的手寫簽名。驗證數字簽名就是要確認兩件事:第一,被擔保的工件在簽名被附上後沒有改變,因為它部分是基於文件的加密學哈希值。第二,簽名屬於一個人(例如 Alice),只有她才能獲得一對密鑰中的私鑰。順便說一下,對代碼(源碼或編譯後的代碼)進行數字簽名已經成為程序員的普遍做法。

讓我們來了解一下數字簽名是如何創建的。如前所述,沒有公鑰和私鑰對就沒有數字簽名。當使用 OpenSSL 創建這些密鑰時,有兩個獨立的命令:一個是創建私鑰,另一個是從私鑰中提取匹配的公鑰。這些密鑰對用 base64 編碼,在這個過程中可以指定它們的大小。

私鑰 private key 由數值組成,其中兩個數值(一個 模數 modulus 和一個 指數 exponent )組成了公鑰。雖然私鑰文件包含了 公鑰 public key ,但提取出來的公鑰並不會透露相應私鑰的值。

因此,生成的帶有私鑰的文件包含了完整的密鑰對。將公鑰提取到自己的文件中是很實用的,因為這兩把鑰匙有不同的用途,而這種提取方式也將私鑰可能被意外公開的危險降到最低。

接下來,這對密鑰的私鑰被用來生成目標工件(如電子郵件)的哈希值,從而創建簽名。在另一端,接收者的系統使用這對密鑰的公鑰來驗證附在工件上的簽名。

現在舉個例子。首先,用 OpenSSL 生成一個 2048 位的 RSA 密鑰對:

openssl genpkey -out privkey.pem -algorithm rsa 2048

在這個例子中,我們可以捨去 -algorithm rsa 標誌,因為 genpkey 默認為 RSA 類型。文件的名稱(privkey.pem)是任意的,但是 隱私增強郵件 Privacy Enhanced Mail (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 程序,無論多麼輕微,然後再試一次。

數字證書

數字證書 digital certificate 彙集了到目前為止所分析的各個部分:哈希值、密鑰對、數字簽名和加密/解密。生產級證書的第一步是創建一個 證書籤名請求 certificate signing request (CSR),然後將其發送給 證書頒發機構 certificate authority (CA)。在 OpenSSL 的例子中,要做到這一點,請運行:

openssl req -out myserver.csr -new -newkey rsa:4096 -nodes -keyout myserverkey.pem

這個例子生成了一個 CSR 文檔,並將該文檔存儲在文件 myserver.csr(base64 文本)中。這裡的目的是:CSR 文檔要求 CA 保證與指定域名相關聯的身份,域名也就是 CA 所說的 通用名 common name (CN)。

儘管可以使用現有的密鑰對,但這個命令也會生成一個新的密鑰對。請注意,在諸如 myserver.csrmyserverkey.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 握手的認證階段, 自簽證書 self-signed certificate 就能滿足要求,儘管任何現代瀏覽器都會警告說這樣的證書毫無價值。繼續這個例子,自簽證書的 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
  • 指數 exponent 幾乎總是 65,537(如本例中),所以可以忽略。
  • 密鑰對的 模數 modulus 應該與數字證書的模數相匹配。

模數是一個很大的值,為了便於閱讀,可以進行哈希處理。下面是兩個 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 伺服器就 加密套件 cipher suite 達成一致,其中包括要使用的演算法。在本例中,該套件是 ECDHE-RSA-AES128-GCM-SHA256

現在值得關注的兩個要素是 RSA 密鑰對演算法和 AES128 塊密碼,用於在握手成功的情況下對消息進行加密和解密。關於加密/解密,這個過程有兩種流派: 對稱 symmetric 非對稱 asymmetric 。在對稱流派中,加密和解密使用的是相同的密鑰,這首先就引出了 密鑰分發問題 key distribution problem 。如何將密鑰安全地分發給雙方?在非對稱流派中,一個密鑰用於加密(在這種情況下,是 RSA 公鑰),但另一個密鑰用於解密(在這種情況下,是來自同一對密鑰的 RSA 私鑰)。

client 程序擁有來認證證書的 Google Web 伺服器的公鑰,而 Web 伺服器擁有來自同一對密鑰的私鑰。因此,client 程序可以向 Web 伺服器發送加密信息,而 Web 伺服器可以單獨對該通信進行解密。

在 TLS 的情況下,對稱方式有兩個顯著的優勢:

  • client 程序與 Google Web 伺服器之間的互動中,認證是單向的。Google Web 伺服器向 client 程序發送三張證書,但 client 程序並沒有向 Web 伺服器發送證書,因此,Web 伺服器沒有來自客戶端的公鑰,無法加密發給客戶端的消息。
  • 使用 AES128 的對稱加密/解密比使用 RSA 密鑰的非對稱加密/解密快了近千倍

TLS 握手將兩種加密/解密方式巧妙地結合在一起。在握手過程中,client 程序會生成隨機位,即所謂的 預主密 pre-master secret (PMS)。然後,client 程序用伺服器的公鑰對 PMS 進行加密,並將加密後的 PMS 發送給伺服器,伺服器再用 RSA 密鑰對的私鑰對 PMS 信息進行解密:

              +-------------------+ encrypted PMS  +--------------------+
client PMS--->|server』s public key|--------------->|server』s private key|--->server PMS
              +-------------------+                +--------------------+

在這個過程結束時,client 程序和 Google Web 伺服器現在擁有相同的 PMS 位。每一方都使用這些位生成一個 主密碼 master secret ,並立即生成一個稱為 會話密鑰 session key 的對稱加密/解密密鑰。現在有兩個不同但等價的會話密鑰,連接的每一方都有一個。在 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

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國