本文是关于使用 OpenSSL(一个在 Linux 和其他系统上流行的生产级库和工具包)的密码学基础知识的两篇文章中的第一篇。(要安装最新版本的 OpenSSL,请参阅此处。)OpenSSL 实用程序可在命令行中使用,程序可以从 OpenSSL 库调用函数。本文的示例程序是用 C 语言编写的,C 语言是 OpenSSL 库的源代码语言。
本系列的两篇文章共同涵盖了加密哈希、数字签名、加密和解密以及数字证书。您可以从 我的网站 上的 ZIP 文件中找到代码和命令行示例。
让我们从回顾 OpenSSL 名称中的 SSL 开始。
简要历史
安全套接字层 (SSL) 是一种加密协议,由 Netscape 于 1995 年发布。此协议层可以位于 HTTP 之上,从而为 HTTPS 中的安全提供 S。SSL 协议提供各种安全服务,包括 HTTPS 中最重要的两项
- 对等身份验证(又名相互质询):连接的每一方都验证另一方的身份。如果 Alice 和 Bob 要通过 SSL 交换消息,则每个人首先验证对方的身份。
- 保密性:发送者在通过通道发送消息之前对其进行加密。接收者然后解密每个接收到的消息。此过程保护网络对话。即使窃听者 Eve 拦截了从 Alice 发送到 Bob 的加密消息(中间人攻击),Eve 也发现解密此消息在计算上是不可行的。
反过来,这两个关键的 SSL 服务与不太受关注的其他服务相关联。例如,SSL 支持消息完整性,这确保接收到的消息与发送的消息相同。此功能通过哈希函数实现,哈希函数也随 OpenSSL 工具包一起提供。
SSL 有版本(例如,SSLv2 和 SSLv3),并且在 1999 年,传输层安全 (TLS) 作为一种基于 SSLv3 的类似协议出现。TLSv1 和 SSLv3 相似,但不足以协同工作。尽管如此,通常将 SSL/TLS 称为一个协议。例如,即使实际使用的是 TLS 而不是 SSL,OpenSSL 函数的名称中也经常带有 SSL。此外,调用 OpenSSL 命令行实用程序以术语 openssl 开头。
OpenSSL 的文档除了 man 页面外,其他方面都很零散,考虑到 OpenSSL 工具包有多大,man 页面变得笨拙。命令行和代码示例是将主要主题集中在一起的一种方式。让我们从一个熟悉的示例(使用 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 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;
/* link bio channel, SSL session, and server endpoint */
sprintf(name, "%s:%s", hostname, "https");
BIO_get_ssl(bio, &ssl); /* session */
SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); /* robustness */
BIO_set_conn_hostname(bio, name); /* prepare to connect */
/* try to connect */
if (BIO_do_connect(bio) <= 0) {
cleanup(ctx, bio);
report_and_exit("BIO_do_connect...");
}
/* verify truststore, check cert */
if (!SSL_CTX_load_verify_locations(ctx,
"/etc/ssl/certs/ca-certificates.crt", /* truststore */
"/etc/ssl/certs/")) /* more truststore */
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);
/* now fetch the homepage as sample data */
sprintf(request,
"GET / HTTP/1.1\x0D\x0AHost: %s\x0D\x0A\x43onnection: Close\x0D\x0A\x0D\x0A",
hostname);
BIO_puts(bio, request);
/* read HTTP response from server and print to stdout */
while (1) {
memset(response, '\0', sizeof(response));
int n = BIO_read(bio, response, BuffSize);
if (n <= 0) break; /* 0 is end-of-stream, < 0 is an error */
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 首页。此程序依赖于前面提到的安全工件,尽管只有数字证书在代码中突出显示。其他工件仍然在幕后,并在后面详细说明。
通常,用 C 或 C++ 编写的客户端程序如果打开 HTTP(非安全)通道,则会使用诸如文件描述符之类的构造,用于网络套接字,网络套接字是两个进程(例如,客户端程序和 Google Web 服务器)之间连接的端点。反过来,文件描述符是一个非负整数值,用于在程序中标识程序打开的任何类似文件的构造。这样的程序还会使用一个结构来指定有关 Web 服务器地址的详细信息。
在客户端程序中没有出现这些相对低级的构造,因为 OpenSSL 库将套接字基础设施和地址规范包装在高级安全构造中。结果是一个简单的 API。以下是示例 client 程序中安全细节的初步了解。
-
程序首先加载相关的 OpenSSL 库,我的函数 init_ssl 对 OpenSSL 进行两次调用
SSL_library_init(); SSL_load_error_strings();
-
下一个初始化步骤尝试获取安全上下文,这是建立和维护与 Web 服务器的安全通道所需的信息框架。示例中使用 TLS 1.2,如对 OpenSSL 库函数的此调用所示
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(基本输入/输出)类型的安全流,它用于与 Web 服务器通信。BIO 流是通过此调用生成的
BIO* bio = BIO_new_ssl_connect(ctx);
请注意,最重要的上下文是参数。BIO 类型是 C 中 FILE 类型的 OpenSSL 包装器。此包装器保护 client 程序和 Google 的 Web 服务器之间的输入和输出流。
-
有了 SSL_CTX 和 BIO,程序随后将它们链接到 SSL 会话中。三个库调用完成此工作
BIO_get_ssl(bio, &ssl); /* 获取 TLS 会话 */
SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); /* 为了稳健性 */
BIO_set_conn_hostname(bio, name); /* 准备连接到 Google */
安全连接本身是通过此调用建立的
BIO_do_connect(bio);
如果最后一次调用不成功,则 client 程序终止;否则,连接已准备好支持 client 程序和 Google Web 服务器之间的保密对话。
在与 Web 服务器握手期间,client 程序接收一个或多个数字证书,这些证书验证服务器的身份。但是,client 程序不发送自己的证书,这意味着身份验证是单向的。(Web 服务器通常配置为不期望客户端证书。)尽管 Web 服务器的证书验证失败,但 client 程序仍然继续通过安全通道获取 Google 首页到 Web 服务器。
为什么验证 Google 证书的尝试会失败?典型的 OpenSSL 安装具有目录 /etc/ssl/certs,其中包含 ca-certificates.crt 文件。目录和文件共同包含 OpenSSL 开箱即用信任的数字证书,因此构成信任库。可以根据需要更新信任库,特别是包括新信任的证书和删除不再信任的证书。
客户端程序从 Google Web 服务器接收三个证书,但我的机器上的 OpenSSL 信任库不包含完全匹配项。正如目前编写的那样,client 程序不会通过例如验证 Google 证书上的数字签名(为证书担保的签名)来跟进此事。如果该签名被信任,那么包含该签名的证书也应该被信任。尽管如此,客户端程序继续获取然后打印 Google 的首页。下一节将更详细地介绍。
客户端程序中隐藏的安全部分
让我们从客户端示例中可见的安全工件(数字证书)开始,并考虑其他安全工件如何与之相关。数字证书的主要布局标准是 X509,生产级证书由证书颁发机构 (CA)(例如 Verisign)颁发。
数字证书包含各种信息(例如,激活日期和到期日期,以及所有者的域名),包括颁发者的身份和数字签名,这是一个加密的密码哈希值。证书还有一个未加密的哈希值,用作其标识指纹。
哈希值是通过将任意数量的位映射到固定长度的摘要而得出的。位代表什么(会计报告、小说,或者可能是数字电影)是无关紧要的。例如,消息摘要版本 5 (MD5) 哈希算法将任意长度的输入位映射到 128 位哈希值,而 SHA1(安全哈希算法版本 1)算法将输入位映射到 160 位值。不同的输入位导致不同的(实际上是统计上唯一的)哈希值。下一篇文章将更详细地介绍,并重点介绍是什么使哈希函数成为加密的。
数字证书的类型(例如,根证书、中间证书和最终实体证书)和形式各不相同,并形成一个反映这些类型的层次结构。顾名思义,根证书位于层次结构的顶部,其下的证书继承根证书拥有的任何信任。OpenSSL 库和大多数现代编程语言都具有 X509 类型以及处理此类证书的函数。来自 Google 的证书具有 X509 格式,client 程序检查此证书是否为 X509_V_OK。
X509 证书基于公钥基础设施 (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
输出是构成密码套件的相关算法的列表。以下是列表的开头,带有注释以澄清首字母缩写词
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
...
下一个命令,使用参数 s_client,打开与 www.google.com 的安全连接,并打印有关此连接的屏幕信息
openssl s_client -connect www.google.com:443 -showcerts
端口号 443 是 Web 服务器用于接收 HTTPS 而不是 HTTP 连接的标准端口。(对于 HTTP,标准端口是 80。)网络地址 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/dDD1isujANBgkqhkiG9w0BAQsFADBU
MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMSUw
...
像 Google 这样的大型网站通常会发送多个证书进行身份验证。
输出以有关 TLS 会话的摘要信息结束,包括有关密码套件的详细信息
SSL-Session:
Protocol : TLSv1.2
Cipher : ECDHE-RSA-AES128-GCM-SHA256
Session-ID: A2BBF0E4991E6BBBC318774EEE37CFCB23095CC7640FFC752448D07C7F438573
...
协议 TLS 1.2 在 client 程序中使用,Session-ID 唯一标识 openssl 实用程序和 Google Web 服务器之间的连接。Cipher 条目可以按如下方式解析
-
ECDHE(椭圆曲线 Diffie Hellman 短暂)是一种有效且高效的算法,用于管理 TLS 握手。特别是,ECDHE 通过确保连接中的双方(例如,客户端程序和 Google Web 服务器)使用相同的加密/解密密钥(称为会话密钥)来解决密钥分发问题。后续文章将深入探讨细节。
-
RSA(Rivest Shamir Adleman)是主要的公钥密码系统,以 1970 年代后期首次描述该系统的三位学者命名。正在使用的密钥对是使用 RSA 算法生成的。
-
AES128(高级加密标准)是一种分组密码,可加密和解密位块。(另一种选择是流密码,它一次加密和解密一位。)密码是对称的,因为使用相同的密钥进行加密和解密,这首先提出了密钥分发问题。AES 支持 128(此处使用)、192 和 256 位的密钥大小:密钥越大,保护越好。
通常,对称密码系统(如 AES)的密钥大小小于非对称(基于密钥对)系统(如 RSA)的密钥大小。例如,1024 位 RSA 密钥相对较小,而 256 位密钥目前是 AES 的最大密钥。
-
GCM(伽罗瓦计数器模式)处理在安全对话期间密码(在本例中为 AES128)的重复应用。AES128 块的大小仅为 128 位,安全对话很可能由从一方到另一方的多个 AES128 块组成。GCM 效率高,通常与 AES128 配对使用。
-
SHA256(安全哈希算法 256 位)是正在使用的密码哈希算法。生成的哈希值大小为 256 位,尽管 SHA 可以实现更大的值。
密码套件不断发展。例如,不久前,Google 使用了 RC4 流密码(RSA 的 Ron Rivest 之后的 Ron 密码版本 4)。RC4 现在已知存在漏洞,这在一定程度上解释了 Google 切换到 AES128 的原因。
总结
通过安全的 C Web 客户端和各种命令行示例,对 OpenSSL 的首次了解已经突出了许多需要进一步澄清的主题。下一篇文章将深入探讨细节,从密码哈希开始,到更全面地讨论数字证书如何解决密钥分发挑战结束。
2 条评论