OpenSSL 入門:密碼學基礎知識

想要入門密碼學的基礎知識,尤其是有關 OpenSSL 的入門知識嗎?繼續閱讀。-- Marty Kalin(作者)


本文是使用 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:

<code>/* compilation: gcc -o client client.c -lssl -lcrypto */
#include <stdio.h>
#include <stdlib.h>
#include <openssl> /* BasicInput/Output streams */
#include <openssl> /* errors */
#include <openssl> /* core library */
#define BuffSize 1024

void 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;
}/<openssl>/<openssl>/<openssl>/<stdlib.h>/<stdio.h>/<code>

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

<code>gcc -o client client.c -lssl -lcrypto/<code>

該程序嘗試打開與網站 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_CTX 和 BIO,然後程序在 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)頒發。

數字證書中包含各種信息(例如,激活日期和失效日期以及所有者的域名),也包括髮行者的身份和數字簽名(這是加密過的加密哈希值)。證書還具有未加密的哈希值,用作其標識指紋。

哈希值來自將任意數量的二進制位映射到固定長度的摘要。這些位代表什麼(會計報告、小說或數字電影)無關緊要。例如, 消息摘要版本 5(Message Digest version 5)(MD5)哈希算法將任意長度的輸入位映射到 128 位哈希值,而 SHA1( 安全哈希算法版本 1(Secure 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 的公鑰加密的郵件將可以用她的私鑰解密(假設是她自己的私鑰),如下所示:

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

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

現在,第二個示例,請對文檔簽名以證明其真實性。簽名算法使用密鑰對中的私鑰來處理要簽名的文檔的加密哈希:

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

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

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

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

在 client 程序中,除了數字證書以外,這些安全性都沒有明確展示。下一篇文章使用使用 OpenSSL 實用程序和庫函數的示例填充更多詳細的信息。

命令行的 OpenSSL

同時,讓我們看一下 OpenSSL 命令行實用程序:特別是在 TLS 握手期間檢查來自 Web 服務器的證書的實用程序。調用 OpenSSL 實用程序可以使用 openssl 命令,然後添加參數和標誌的組合以指定所需的操作。

看看以下命令:

<code>openssl list-cipher-algorithms/<code>

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

<code>AES-128-CBC ## Advanced Encryption Standard, Cipher Block Chaining
AES-128-CBC-HMAC-SHA1 ## Hash-based Message Authentication Code with SHA1 hashes
AES-128-CBC-HMAC-SHA256 ## ditto, but SHA256 rather than SHA1
.../<code>

下一條命令使用參數 s_client 將打開到 www.google.com 的安全連接,並在屏幕上顯示有關此連接的所有信息:

<code>openssl s_client -connect www.google.com:443 -showcerts/<code>

端口號 443 是 Web 服務器用於接收 HTTPS(而不是 HTTP 連接)的標準端口號。(對於 HTTP,標準端口為 80)Web 地址 www.google.com:443 也出現在 client 程序的代碼中。如果嘗試連接成功,則將顯示來自 Google 的三個數字證書以及有關安全會話、正在使用的加密算法套件以及相關項目的信息。例如,這是開頭的部分輸出,它聲明證書鏈即將到來。證書的編碼為 base64:

<code>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/dDD1isujANBgkqhkiG9w0BAQsFADBU
MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMSUw
.../<code>

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

輸出以有關 TLS 會話的摘要信息結尾,包括加密算法套件的詳細信息:

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

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 搭配使用。SHA256( 256 位安全哈希算法(Secure Hash Algorithm 256 bits))是我們正在使用的加密哈希算法。生成的哈希值的大小為 256 位,儘管使用 SHA 甚至可以更大。

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

總結

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

via: https://opensource.com/article/19/6/cryptography-basics-openssl-part-1

作者: Marty Kalin 選題: lujun9972 譯者: wxy 校對: wxy

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