OpenSSL 入門:密碼學基礎知識

2020-01-23 14:23:00

想要入門密碼學的基礎知識,尤其是有關 OpenSSL 的入門知識嗎?繼續閱讀。

本文是使用 OpenSSL 的密碼學基礎知識的兩篇文章中的第一篇,OpenSSL 是在 Linux 和其他系統上流行的生產級庫和工具包。(要安裝 OpenSSL 的最新版本,請參閱這裡。)OpenSSL 實用程式可在命令列使用,程式也可以呼叫 OpenSSL 庫中的函數。本文的範例程式使用的是 C 語言,即 OpenSSL 庫的源語言。

本系列的兩篇文章涵蓋了加密雜湊、數位簽章、加密和解密以及數位憑證。你可以從我的網站的 ZIP 檔案中找到這些程式碼和命令列範例。

讓我們首先回顧一下 OpenSSL 名稱中的 SSL。

OpenSSL 簡史

安全通訊端層Secure Socket Layer(SSL)是 Netscape 在 1995 年發布的一種加密協定。該協定層可以位於 HTTP 之上,從而為 HTTPS 提供了 S:安全secure。SSL 協定提供了各種安全服務,其中包括兩項在 HTTPS 中至關重要的服務:

  • 對等身份驗證Peer authentication(也稱為相互質詢):連線的每一邊都對另一邊的身份進行身份驗證。如果 Alice 和 Bob 要通過 SSL 交換訊息,則每個人首先驗證彼此的身份。
  • 機密性Confidentiality:傳送者在通過通道傳送訊息之前先對其進行加密。然後,接收者解密每個接收到的訊息。此過程可保護網路對話。即使竊聽者 Eve 截獲了從 Alice 到 Bob 的加密訊息(即中間人攻擊),Eve 會發現他無法在計算上解密此訊息。

反過來,這兩個關鍵 SSL 服務與其他不太受關注的服務相關聯。例如,SSL 支援訊息完整性,從而確保接收到的訊息與傳送的訊息相同。此功能是通過雜湊函數實現的,雜湊函數也隨 OpenSSL 工具箱一起提供。

SSL 有多個版本(例如 SSLv2 和 SSLv3),並且在 1999 年出現了一個基於 SSLv3 的類似協定傳輸層安全性Transport Layer Security(TLS)。TLSv1 和 SSLv3 相似,但不足以相互配合工作。不過,通常將 SSL/TLS 稱為同一協定。例如,即使正在使用的是 TLS(而非 SSL),OpenSSL 函數也經常在名稱中包含 SSL。此外,呼叫 OpenSSL 命令列實用程式以 openssl 開始。

除了 man 頁面之外,OpenSSL 的文件是零零散散的,鑑於 OpenSSL 工具包很大,這些頁面很難以查詢使用。命令列和程式碼範例可以將主要主題集中起來。讓我們從一個熟悉的範例開始(使用 HTTPS 存取網站),然後使用該範例來選出我們感興趣的加密部分進行講述。

一個 HTTPS 用戶端

此處顯示的 client 程式通過 HTTPS 連線到 Google:

/* compilation: gcc -o client client.c -lssl -lcrypto */#include <stdio.h>#include <stdlib.h>#include <openssl/bio.h> /* BasicInput/Output streams */#include <openssl/err.h> /* errors */#include <openssl/ssl.h> /* core library */#define BuffSize 1024void report_and_exit(const char* msg) {  perror(msg);  ERR_print_errors_fp(stderr);  exit(-1);}void init_ssl() {  SSL_load_error_strings();  SSL_library_init();}void cleanup(SSL_CTX* ctx, BIO* bio) {  SSL_CTX_free(ctx);  BIO_free_all(bio);}void secure_connect(const char* hostname) {  char name[BuffSize];  char request[BuffSize];  char response[BuffSize];  const SSL_METHOD* method = TLSv1_2_client_method();  if (NULL == method) report_and_exit("TLSv1_2_client_method...");  SSL_CTX* ctx = SSL_CTX_new(method);  if (NULL == ctx) report_and_exit("SSL_CTX_new...");  BIO* bio = BIO_new_ssl_connect(ctx);  if (NULL == bio) report_and_exit("BIO_new_ssl_connect...");  SSL* ssl = NULL;  /* 鏈路 bio 通道,SSL 對談和伺服器端點 */  sprintf(name, "%s:%s", hostname, "https");  BIO_get_ssl(bio, &ssl); /* 對談 */  SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); /* 魯棒性 */  BIO_set_conn_hostname(bio, name); /* 準備連線 */  /* 嘗試連線 */  if (BIO_do_connect(bio) <= 0) {    cleanup(ctx, bio);    report_and_exit("BIO_do_connect...");  }  /* 驗證信任庫,檢查證書 */  if (!SSL_CTX_load_verify_locations(ctx,                                      "/etc/ssl/certs/ca-certificates.crt", /* 信任庫 */                                      "/etc/ssl/certs/")) /* 其它信任庫 */    report_and_exit("SSL_CTX_load_verify_locations...");  long verify_flag = SSL_get_verify_result(ssl);  if (verify_flag != X509_V_OK)    fprintf(stderr,            "##### Certificate verification error (%i) but continuing...\n",            (int) verify_flag);  /* 獲取主頁作為範例資料 */  sprintf(request,          "GET / HTTP/1.1\x0D\x0AHost: %s\x0D\x0A\x43onnection: Close\x0D\x0A\x0D\x0A",          hostname);  BIO_puts(bio, request);  /* 從伺服器讀取 HTTP 響應並列印到輸出 */  while (1) {    memset(response, '\0', sizeof(response));    int n = BIO_read(bio, response, BuffSize);    if (n <= 0) break; /* 0 代表流結束,< 0 代表有錯誤 */  puts(response);  }  cleanup(ctx, bio);}int main() {  init_ssl();  const char* hostname = "www.google.com:443";  fprintf(stderr, "Trying an HTTPS connection to %s...\n", hostname);  secure_connect(hostname);return 0;}

可以從命令列編譯和執行該程式(請注意 -lssl-lcrypto 中的小寫字母 L):

gcc -o client client.c -lssl -lcrypto

該程式嘗試開啟與網站 www.google.com 的安全連線。在與 Google Web 伺服器的 TLS 握手過程中,client 程式會收到一個或多個數位憑證,該程式會嘗試對其進行驗證(但在我的系統上失敗了)。儘管如此,client 程式仍繼續通過安全通道獲取 Google 主頁。該程式取決於前面提到的安全工件,儘管在上述程式碼中只著重突出了數位憑證。但其它工件仍在幕後發揮作用,稍後將對它們進行詳細說明。

通常,開啟 HTTP(非安全)通道的 C 或 C++ 的用戶端程式將使用諸如檔案描述符網路通訊端之類的結構,它們是兩個進程(例如,這個 client 程式和 Google Web 伺服器)之間連線的端點。另一方面,檔案描述符是一個非負整數值,用於在程式中標識該程式開啟的任何檔案類的結構。這樣的程式還將使用一種結構來指定有關 Web 伺服器地址的詳細資訊。

這些相對較低階別的結構不會出現在用戶端程式中,因為 OpenSSL 庫會將通訊端基礎設施和地址規範等封裝在更高層面的安全結構中。其結果是一個簡單的 API。下面首先看一下 client 程式範例中的安全性詳細資訊。

  • 該程式首先載入相關的 OpenSSL 庫,我的函數 init_ssl 中對 OpenSSL 進行了兩次呼叫:

    SSL_load_error_strings();SSL_library_init(); 
  • 下一個初始化步驟嘗試獲取安全上下文,這是建立和維護通往 Web 伺服器的安全通道所需的資訊框架。如對 OpenSSL 庫函數的呼叫所示,在範例中使用了 TLS 1.2:

    const SSL_METHOD* method = TLSv1_2_client_method(); /* TLS 1.2 */

    如果呼叫成功,則將 method 指標被傳遞給庫函數,該函數建立型別為 SSL_CTX 的上下文:

    SSL_CTX* ctx = SSL_CTX_new(method);

    client 程式會檢查每個關鍵的庫呼叫的錯誤,如果其中一個呼叫失敗,則程式終止。

  • 現在還有另外兩個 OpenSSL 工件也在發揮作用:SSL 型別的安全對談,從頭到尾管理安全連線;以及型別為 BIO(基本輸入/輸出Basic Input/Output)的安全流,用於與 Web 伺服器進行通訊。BIO 流是通過以下呼叫生成的:

    BIO* bio = BIO_new_ssl_connect(ctx);

    請注意,這個最重要的上下文是其引數。BIO 型別是 C 語言中 FILE 型別的 OpenSSL 封裝器。此封裝器可保護 client 程式與 Google 的網路伺服器之間的輸入和輸出流的安全。

  • 有了 SSL_CTXBIO,然後程式在 SSL 對談中將它們組合在一起。三個庫呼叫可以完成工作:

    BIO_get_ssl(bio, &ssl); /* 對談 */SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); /* 魯棒性 */BIO_set_conn_hostname(bio, name); /* 準備連線 */

    安全連線本身是通過以下呼叫建立的:

    BIO_do_connect(bio);

    如果最後一個呼叫不成功,則 client 程式終止;否則,該連線已準備就緒,可以支援 client 程式與 Google Web 伺服器之間的機密對話。

在與 Web 伺服器握手期間,client 程式會接收一個或多個數位憑證,以認證伺服器的身份。但是,client 程式不會傳送自己的證書,這意味著這個身份驗證是單向的。(Web 伺服器通常設定為需要用戶端證書)儘管對 Web 伺服器證書的驗證失敗,但 client 程式仍通過了連線到 Web 伺服器的安全通道繼續獲取 Google 主頁。

為什麼驗證 Google 證書的嘗試會失敗?典型的 OpenSSL 安裝目錄為 /etc/ssl/certs,其中包含 ca-certificates.crt 檔案。該目錄和檔案包含著 OpenSSL 自帶的數位憑證,以此構成信任庫truststore。可以根據需要更新信任庫,尤其是可以包括新信任的證書,並刪除不再受信任的證書。

client 程式從 Google Web 伺服器收到了三個證書,但是我的計算機上的 OpenSSL 信任庫並不包含完全匹配的證書。如目前所寫,client 程式不會通過例如驗證 Google 證書上的數位簽章(一個用來證明該證書的簽名)來解決此問題。如果該簽名是受信任的,則包含該簽名的證書也應受信任。儘管如此,client 程式仍繼續獲取頁面,然後列印出 Google 的主頁。下一節將更詳細地介紹這些。

用戶端程式中隱藏的安全性

讓我們從用戶端範例中可見的安全工件(數位憑證)開始,然後考慮其他安全工件如何與之相關。數位憑證的主要格式標準是 X509,生產級的證書由諸如 Verisign證書頒發機構Certificate Authority(CA)頒發。

數位憑證中包含各種資訊(例如,啟用日期和失效日期以及所有者的域名),也包括發行者的身份和數位簽章(這是加密過的加密雜湊值)。證書還具有未加密的雜湊值,用作其標識指紋

雜湊值來自將任意數量的二進位制位對映到固定長度的摘要。這些位代表什麼(會計報告、小說或數位電影)無關緊要。例如,訊息摘要版本 5Message Digest version 5(MD5)雜湊演算法將任意長度的輸入位對映到 128 位雜湊值,而 SHA1(安全雜湊演算法版本 1Secure Hash Algorithm version 1)演算法將輸入位對映到 160 位雜湊值。不同的輸入位會導致不同的(實際上在統計學上是唯一的)雜湊值。下一篇文章將會進行更詳細的介紹,並著重介紹什麼使雜湊函數具有加密功能。

數位憑證的型別有所不同(例如根證書、中間證書和最終實體證書),並形成了反映這些證書型別的層次結構。顧名思義,證書位於層次結構的頂部,其下的證書繼承了根證書所具有的信任。OpenSSL 庫和大多數現代程式語言都具有 X509 資料型別以及處理此類證書的函數。來自 Google 的證書具有 X509 格式,client 程式會檢查該證書是否為 X509_V_OK

X509 證書基於公共金鑰基礎結構public-key infrastructure(PKI),其中包括的演算法(RSA 是占主導地位的演算法)用於生成金鑰對:公共金鑰及其配對的私有金鑰。公鑰是一種身份:Amazon 的公鑰對其進行標識,而我的公鑰對我進行標識。私鑰應由其所有者負責保密。

成對出現的金鑰具有標準用途。可以使用公鑰對訊息進行加密,然後可以使用同一個金鑰對中的私鑰對訊息進行解密。私鑰也可以用於對文件或其他電子工件(例如程式或電子郵件)進行簽名,然後可以使用該對金鑰中的公鑰來驗證簽名。以下兩個範例補充了一些細節。

在第一個範例中,Alice 將她的公鑰分發給全世界,包括 Bob。然後,Bob 用 Alice 的公鑰加密郵件,然後將加密的郵件傳送給 Alice。用 Alice 的公鑰加密的郵件將可以用她的私鑰解密(假設是她自己的私鑰),如下所示:

             +------------------+ encrypted msg  +-------------------+Bob's msg--->|Alice's public key|--------------->|Alice's private key|---> Bob's msg             +------------------+                +-------------------+

理論上可以在沒有 Alice 的私鑰的情況下解密訊息,但在實際情況中,如果使用像 RSA 這樣的加密金鑰對系統,則在計算上做不到。

現在,第二個範例,請對文件簽名以證明其真實性。簽名演算法使用金鑰對中的私鑰來處理要簽名的文件的加密雜湊:

                    +-------------------+Hash of document--->|Alice's private key|--->Alice's digital signature of the document                    +-------------------+

假設 Alice 以數位方式簽署了傳送給 Bob 的合同。然後,Bob 可以使用 Alice 金鑰對中的公鑰來驗證簽名:

                                             +------------------+Alice's digital signature of the document--->|Alice's public key|--->verified or not                                             +------------------+

假若沒有 Alice 的私鑰,就無法輕鬆偽造 Alice 的簽名:因此,Alice 有必要保密她的私鑰。

client 程式中,除了數位憑證以外,這些安全性都沒有明確展示。下一篇文章使用使用 OpenSSL 實用程式和庫函數的範例填充更多詳細的資訊。

命令列的 OpenSSL

同時,讓我們看一下 OpenSSL 命令列實用程式:特別是在 TLS 握手期間檢查來自 Web 伺服器的證書的實用程式。呼叫 OpenSSL 實用程式可以使用 openssl 命令,然後新增引數和標誌的組合以指定所需的操作。

看看以下命令:

openssl list-cipher-algorithms

該輸出是組成加密演算法套件cipher suite的相關演算法的列表。下面是列表的開頭,加了澄清首字母縮寫詞的註釋:

AES-128-CBC ## Advanced Encryption Standard, Cipher Block ChainingAES-128-CBC-HMAC-SHA1 ## Hash-based Message Authentication Code with SHA1 hashesAES-128-CBC-HMAC-SHA256 ## ditto, but SHA256 rather than SHA1...

下一條命令使用引數 s_client 將開啟到 www.google.com 的安全連線,並在螢幕上顯示有關此連線的所有資訊:

openssl s_client -connect www.google.com:443 -showcerts

埠號 443 是 Web 伺服器用於接收 HTTPS(而不是 HTTP 連線)的標準埠號。(對於 HTTP,標準埠為 80)Web 地址 www.google.com:443 也出現在 client 程式的程式碼中。如果嘗試連線成功,則將顯示來自 Google 的三個數位憑證以及有關安全對談、正在使用的加密演算法套件以及相關專案的資訊。例如,這是開頭的部分輸出,它宣告證書鏈即將到來。證書的編碼為 base64:

Certificate chain 0 s:/C=US/ST=California/L=Mountain View/O=Google LLC/CN=www.google.com i:/C=US/O=Google Trust Services/CN=Google Internet Authority G3-----BEGIN CERTIFICATE-----MIIEijCCA3KgAwIBAgIQdCea9tmy/T6rK/dDD1isujANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMSUw...

諸如 Google 之類的主要網站通常會傳送多個證書進行身份驗證。

輸出以有關 TLS 對談的摘要資訊結尾,包括加密演算法套件的詳細資訊:

SSL-Session:    Protocol : TLSv1.2    Cipher : ECDHE-RSA-AES128-GCM-SHA256    Session-ID: A2BBF0E4991E6BBBC318774EEE37CFCB23095CC7640FFC752448D07C7F438573...

client 程式中使用了協定 TLS 1.2,Session-ID 唯一地標識了 openssl 實用程式和 Google Web 伺服器之間的連線。Cipher 條目可以按以下方式進行解析:

  • ECDHE橢圓曲線 Diffie-Hellman(臨時)Elliptic Curve Diffie Hellman Ephemeral)是一種用於管理 TLS 握手的高效的有效演算法。尤其是,ECDHE 通過確保連線雙方(例如,client 程式和 Google Web 伺服器)使用相同的加密/解密金鑰(稱為對談金鑰)來解決“金鑰分發問題”。後續文章會深入探討該細節。
  • RSA(Rivest Shamir Adleman)是主要的公共金鑰密碼系統,並以 1970 年代末首次描述了該系統的三位學者的名字命名。這個正在使用的金鑰對是使用 RSA 演算法生成的。
  • AES128高階加密標準Advanced Encryption Standard)是一種塊式加密演算法block cipher,用於加密和解密位塊blocks of bits。(另一種演算法是流式加密演算法stream cipher,它一次加密和解密一個位。)這個加密演算法是對稱加密演算法,因為使用同一個金鑰進行加密和解密,這首先引起了金鑰分發問題。AES 支援 128(此處使用)、192 和 256 位的金鑰大小:金鑰越大,安全性越好。

    通常,像 AES 這樣的對稱加密系統的金鑰大小要小於像 RSA 這樣的非對稱(基於金鑰對)系統的金鑰大小。例如,1024 位 RSA 金鑰相對較小,而 256 位金鑰則當前是 AES 最大的金鑰。

  • GCM伽羅瓦計數器模式Galois Counter Mode)處理在安全對話期間重複應用的加密演算法(在這種情況下為 AES128)。AES128 塊的大小僅為 128 位,安全對話很可能包含從一側到另一側的多個 AES128 塊。GCM 非常有效,通常與 AES128 搭配使用。

  • SHA256256 位安全雜湊演算法Secure Hash Algorithm 256 bits)是我們正在使用的加密雜湊演算法。生成的雜湊值的大小為 256 位,儘管使用 SHA 甚至可以更大。

加密演算法套件正在不斷發展中。例如,不久前,Google 使用 RC4 流加密演算法(RSA 的 Ron Rivest 後來開發的 Ron’s Cipher 版本 4)。 RC4 現在有已知的漏洞,這大概部分導致了 Google 轉換為 AES128。

總結

我們通過安全的 C Web 用戶端和各種命令列範例對 OpenSSL 做了首次了解,使一些需要進一步闡明的主題脫穎而出。下一篇文章會詳細介紹,從加密雜湊開始,到對數位憑證如何應對金鑰分發挑戰為結束的更全面討論。