使用 OpenSSL 构建安全应用程序





5.00/5 (4投票s)
本文介绍如何在 C 应用程序中访问 OpenSSL。
1. 简介
随着分布式计算日益普遍,程序员理解实现分布式计算的机制变得越来越重要。许多开发者了解源文件、头文件和 makefile,但相对较少的人熟悉数字证书、公钥/私钥文件和证书签名请求。
如今,安全分布式计算的主流方法是 SSL/TLS(安全套接层/传输层安全)。实现 SSL/TLS 的最流行的开源工具集是 OpenSSL。本文旨在解释 OpenSSL 的功能,并展示如何在代码中访问其能力。
2. SSL 和 TLS 简史
早在 1994 年,互联网尚处于起步阶段,Netscape 是当时世界上最受欢迎的浏览器。尽管 Netscape 具有惊人的功能,但它存在两个主要的安全问题:
- 缺乏机密性 - 客户端和服务器之间的消息未加密,因此窃听者可以读取传输的内容。
- 缺乏身份验证 - 在客户端/服务器通信期间,客户端无法确定服务器的身份,服务器也无法确定客户端的身份。
这些问题使得在线交易充满风险。为解决这些问题,Netscape 创建了安全套接层(SSL),用于加密浏览器和服务器之间发送的消息。SSL 1.0 和 SSL 2.0 版本被发现不安全,但 SSL 3.0 一直使用到 2014 年。
1999 年,Certicom 的工程师改进了 SSL 3.0 的加密技术,并将其新协议命名为传输层安全(Transport Layer Security),即 TLS。TLS 1.0 和 1.1 版本已被弃用,但 1.2 版本(2008 年发布)和 1.3 版本(2018 年发布)仍被广泛使用。TLS 1.3 更安全,但 TLS 1.2 更受欢迎,因为 TLS 1.3 的许多改动会破坏现有系统。
现代安全通信几乎完全依赖于 TLS。尽管如此,人们通常仍将该机制称为 SSL/TLS 或简称为 SSL。为符合通用习惯,本文将该协议称为 SSL。
3. 公钥基础设施(PKI)概述
机密性和身份验证是安全通信的两个重要组成部分。为确保其可用性,互联网依赖于公钥基础设施(PKI)。本质上,OpenSSL 库的目标是使开发者能够与 PKI 交互。因此,在介绍该库之前,我想先对 PKI 是什么以及其工作原理进行一个高层次的概述。
3.1. 通过加密实现机密性
为防止窃听,发送方必须能够以一种方式转换消息,使得接收方(且仅限接收方)能够反向转换该消息。对于数字消息,这种转换(称为加密)涉及数学运算。这些运算接受两个输入(消息和一个称为密钥的数字),并生成消息的转换版本(密文)。
一个重要的问题随之而来:接收方如何从密文中恢复(或解密)原始消息?解密必须对接收方尽可能简单,对窃听者尽可能困难。如果发送方和接收方都预先知道密钥,他们可以使用对称密钥加密方法,如高级加密标准(AES)。
但如果接收方没有发送方的密钥怎么办?他或她如何解密消息?这个问题自计算时代曙光初现以来就一直困扰着研究人员,我们目前最好的解决方案是使用两个密钥:一个公钥用于加密,一个私钥用于解密。如果接收方有一个公钥和一个私钥,发送方可以通过一个三步过程安全地传输消息:
- 接收方将其公钥公开给所有人。
- 发送方使用接收方的公钥加密消息,并将其发送给接收方。
- 接收方使用其私钥解密消息。
为使此过程有效,加密操作必须在一个方向上容易执行,而在反向上难以执行。这确保了密文只能用私钥解密,而绝不能用公钥解密。其底层细节超出了本文的范围,但 TLS 1.2 依赖于椭圆曲线迪菲-赫尔曼临时(ECDHE)方法,该方法利用了椭圆曲线的特性。
3.2. 使用证书进行身份验证
在二十世纪,电话簿有用于个人列表的白页和用于商业列表的黄页。当一家公司在黄页上购买列表时,它必须验证其身份。因此,当电话簿印出一家公司的电话号码时,你可以相当确定拨打该号码会连接到正确的公司。
SSL 使用证书而非电话簿列表来验证实体。最流行的证书格式由国际电信联盟(ITU)的 X.509 标准确立,每个 X.509 证书都提供以下信息:
- 主题 - 包括实体的名称、国家和 DNS 名称。
- 公钥 - 用于加密发往该实体消息的值。
- 公钥算法 - 加密消息时使用的算法。
此外,每个 X.509 证书都必须有一个签名,该签名由证书的数据经某个实体的私钥加密而成。用私钥加密证书的过程称为签署证书,在许多情况下,一个实体的证书由另一个实体签署。但如果一个实体签署自己的证书,该证书则被称为自签名证书。
签名者越可靠,你就越能信任该实体。每个操作系统都维护一个它认为可靠的实体列表,这些实体被称为根证书颁发机构(root CAs)。在 Windows 上,你可以通过运行证书管理器(certmgr.exe)来查看根 CA 列表。下图展示了其外观。
在 Linux 上,每个根 CA 在 /etc/ssl/certs 文件夹中都有一个对应的文件。安装新的 CA 需要三个步骤:
- 获取 CA 的证书文件。其后缀通常为 *.crt 或 *.pem。
- 将文件移动到 /usr/local/share/ca-certificates 目录。
- 执行命令
sudo update-ca-certificates
来更新证书存储。
如果一个证书由一个不在受信任 CA 列表中的实体签署,但该实体的证书是由一个信誉良好的 CA 签署的,那么该证书仍然可以被认为是可信的。通过这种方式,证书可以形成一个链条,最终追溯到一个由根 CA 自签名的受信任证书。
3.3. 隐私增强邮件(PEM)文件
在几乎所有情况下,OpenSSL 都将公钥、私钥和证书存储在根据隐私增强邮件(PEM)格式结构的文本文件中。一个 PEM 文件可能包含多个密钥和/或证书,列表中的每个元素都有三个部分:
- 页眉 - 包含被短划线包围的
BEGIN <LABEL>
,其中<LABEL>
的可能值为PRIVATE KEY
、PUBLIC KEY
和CERTIFICATE
。 - 数据 - 以 Base64 格式编码的二进制数据,其中每六位表示为集合 A-Z, a-z, 0-9, +, 和 - 中的一个字符。
- 页脚 - 包含被短划线包围的
END <LABEL>
,其中<LABEL>
与页眉中的值相同。
例如,如果你生成一个私钥并将其存储到 PEM 文件中,该文件的内容可能如下所示:
-----BEGIN PRIVATE KEY-----
BgkqhkiG9w0BAQEF...
-----END PRIVATE KEY-----
PEM 文件没有官方后缀,在某些情况下,*.pem 用于所有类型的文件。然而,许多应用程序采用以下约定:
- *.key - 包含私钥
- *.pem - 包含公钥
- *.crt/*.cert - 包含已签名的证书
- *.csr - 证书签名请求
最后一种文件类型是请求 CA 生成并签署一个证书。例如,如果你想让 DigiCert 提供一个已签名的证书,你会发送一个包含你的证书数据(组织名称、公钥、DNS 名称等)的 CSR。
4. 从命令行使用 OpenSSL
在开始编程之前,最好先熟悉 OpenSSL 实用工具,该工具在许多 Linux 和 macOS 计算机上默认安装。对于 Windows 用户,可以通过安装 Git Bash 并在 Git Bash 命令行中执行 openssl
来访问它。
OpenSSL 命令的一般格式如下:
<code>openssl <command> <options> <arguments></code>
例如,你可以通过执行命令 openssl version
来显示 OpenSSL 的版本。许多选项以短划线开头,你可以通过命令 openssl list -public-key-algorithms
获取加密算法列表。
OpenSSL 实用工具提供了大量的命令。表 1 列出了其中的十个,并对每个命令进行了描述。
命令 | 描述 |
---|---|
req | 生成证书和证书请求 |
x509 | 签署或显示 X.509 证书 |
verify | 验证证书链 |
genrsa | 生成 RSA 私钥 |
rsa | 从私钥生成 RSA 公钥 |
enc | 对称密钥加密和解密 |
dgst | 执行摘要操作 |
rand | 生成伪随机数 |
prime | 生成素数 |
passwd | 计算密码哈希值 |
要探索 OpenSSL 实用工具提供的所有功能,需要一本书的篇幅。因此,本节仅介绍表中的前两个条目:openssl req
和 openssl x509
。
4.1. openssl req 命令
openssl req
命令可以根据请求创建证书,也可以创建带有新密钥的证书。该命令接受多种选项,包括以下内容:
- -in filename - 标识包含输入请求的文件
- -out filename - 标识用于存放命令输出的文件
- -x509 - 生成证书而非证书请求
- -days num - 证书的有效天数
- -new - 创建一个新的证书请求
- -newkey - 生成一个新的私钥
- -noenc - 新的私钥不应被加密
- -keyout filename - 标识用于存储新私钥的文件
为演示其用法,以下命令从 input.pem 中的私钥生成一个证书签名请求(CSR)并存入 request.csr:
openssl req -out request.csr -key input.pem -new
以下命令在 newkey.pem 中创建一个未加密的私钥,并用它创建一个有效期为一年的自签名证书。结果存储在 newcert.crt 中。
openssl req -x509 -sha256 -noenc -days 365 -newkey rsa:4096 -keyout newkey.pem -out newcert.crt
在此命令中,-newkey
后面跟着 rsa:4096
。这告诉 OpenSSL 生成的私钥应基于 RSA-4096 加密算法。
4.2. openssl x509 命令
openssl x509
命令可以对 X.509 证书执行多种操作,包括签名和显示。它接受一个私钥文件(使用 -in
选项)并产生各种形式的输出。
-x509toreq
选项告诉命令创建一个证书请求。以下命令从一个现有证书(input.crt)创建一个请求(request.csr):
openssl x509 -x509toreq -in input.crt -out request.csr -key sign.pem
如果你只想显示有关证书的信息,-noout
选项可以阻止生成输出文件。以下代码以文本形式打印名为 input.crt 的证书内容:
openssl x509 -in input.crt -noout -text
在此命令中,-text
选项指定应打印证书的所有信息。如果你只对证书的特定字段感兴趣,-serial
打印序列号,-subject
打印主题信息,而 -dates
打印证书有效期的开始和结束日期。
5. 使用 OpenSSL 编程
现在你已经了解了 OpenSSL 的全部内容,可以开始编码了。OpenSSL 库是用 C 语言编写的,所以没有类或对象的概念。相反,其 API 由执行诸如创建数据结构、验证证书和与服务器建立通信等操作的函数组成。
大多数函数名以以下两个标识符之一开头:
BIO_
- 函数执行基本输入/输出(BIO)通信SSL_
- 函数使用 SSL 保护通信
本节将探讨 BIO_
函数及其相关数据结构,然后探讨 SSL_
函数及其数据结构。本节最后将展示一个应用程序,该程序向 www.google.com 发送一个 HTTPS 连接请求并打印响应。
5.1 基本输入/输出(BIO)函数
本文讨论的第一组函数可以用来建立基本连接。它们的名称以 BIO_
开头,表 2 列出了其中的二十三个。
函数 | 描述 |
---|---|
BIO_new_connect(const char *name) | 创建一个新的 BIO 结构 |
BIO_new_ssl_connect(SSL_CTX *ctx) | 创建一个带 SSL 的新 BIO 结构 |
BIO_new_socket(int sock, int flag) | 创建一个带套接字的新 BIO 结构 |
BIO_get_ssl(BIO *b, SSL **sslp) | 返回 SSL 结构 |
BIO_set_ssl(BIO *b, SSL *ssl, long c) | 设置 SSL 结构 |
BIO_get_conn_hostname(BIO *b) | 返回主机名 |
BIO_set_conn_hostname(BIO *b, char *host) | 设置主机名 |
BIO_get_conn_address(BIO *b) | 返回地址 |
BIO_set_conn_address(BIO *b, BIO_ADDR* addr) | 设置地址 |
BIO_get_conn_port(BIO *b) | 返回通信端口 |
BIO_set_conn_port(BIO *b, char *port) | 设置通信端口 |
BIO_do_connect(BIO *b) | 建立连接 |
BIO_do_connect_retry(BIO *bio, int t, int ms) | 尝试建立连接 |
BIO_do_handshake(BIO *b) | 尝试建立握手 |
BIO_do_accept(BIO *b) | 接受传入的套接字通信 |
BIO_read(BIO *b, void *buff, int len) | 读取 len 字节,存入 buff |
BIO_gets(BIO *b, char *buff, int len) | 读取以 null 结尾的字符串 |
BIO_get_line(BIO *b, char *buff, int len) | 读取一行文本,存入 buff |
BIO_write(BIO *b, const void *buff, int len) | 从 buff 写入 len 字节 |
BIO_puts(BIO *b, const char *buff) | 写入以 null 结尾的字符串 |
BIO_flush(BIO *b) | 写入剩余的缓冲数据 |
BIO_free(BIO *b) | 释放单个 BIO 结构 |
BIO_free_all(BIO *b) | 释放所有 BIO 结构 |
这些函数的核心数据结构是 BIO
结构,它存储与连接相关的信息。前三个函数返回一个新的 BIO
,其中 BIO_new_ssl_connect
函数尤为重要,因为它返回一个表示使用 SSL 保护的连接的 BIO
。该函数接受一个 SSL_CTX
结构,该结构表示一个 SSL 上下文。我稍后将在本文中讨论这个上下文。
当使用 SSL 上下文创建 BIO
结构时,它将包含一个存储 SSL 配置信息的 SSL
结构。这可以通过 BIO_get_ssl
访问和通过 BIO_set_ssl
设置。应用程序经常访问此结构,通过调用 SSL_set_mode
来设置安全模式,我们稍后会讨论这个函数。
在 BIO
可用于连接到远程系统之前,它需要该系统的信息。系统的 IP 地址可以通过 BIO_set_conn_address
给出,通信端口可以通过 BIO_set_conn_port
给出。应用程序经常调用 BIO_set_conn_hostname,该函数接受系统的 DNS 名称和端口。例如,以下代码指定远程系统是 www.google.com,所需端口是 443:
BIO_set_conn_hostname(bio, "www.google.com:443");
一旦确定了远程系统,BIO_do_connect
函数将尝试建立连接。如果尝试成功,该函数返回 1;如果尝试失败,则返回值小于或等于 0。对于重复尝试,BIO_do_connect_retry
接受一个超时周期和每次尝试之间应间隔的毫秒数。
连接建立后,BIO_read
可用于从远程系统读取数据,BIO_write
可用于写入数据。对于以 null 结尾的字符串,可以改用 BIO_gets
和 BIO_puts
。BIO_read
返回可读取的数据量,如果此值小于或等于 0,则表示没有更多数据可用。
表中的最后两个函数用于释放资源。BIO_free
释放单个 BIO
结构,BIO_free_all
释放一个 BIO
结构链。
5.2 安全套接层(SSL)函数
OpenSSL 库提供了几个函数,用于对使用前面讨论的 BIO
函数创建的连接强制执行 SSL 安全。表 3 列出了其中的十个。
函数 | 描述 |
---|---|
SSL_library_init() | 初始化 SSL 库的操作 |
SSL_load_error_strings() | 加载用于显示错误的文本 |
SSL_CTX_new(const SSL_METHOD *method) | 为 SSL 处理创建一个新的上下文 |
SSL_set_mode(SSL *ssl, long mode) | 设置 SSL 处理模式 |
SSL_clear_mode(SSL *ssl, long mode) | 清除 SSL 处理模式 |
SSL_CTX_load_verify_file(SSL_CTX *ctx, const char *file) | 设置包含用于验证的 CA 证书的文件 用于验证 |
SSL_CTX_load_verify_dir(SSL_CTX *ctx, const char *path) | 设置包含 CA 证书的目录 用于验证 |
SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *file, const char *path) | 设置包含用于验证的 CA 证书的文件和目录 用于验证的证书 |
SSL_get_verify_result(const SSL *ssl) | 获取证书验证结果 |
SSL_CTX_free(SSL_CTX *ctx) | 释放 SSL 上下文 |
前两个函数可以初始化处理环境。SSL_library_init
加载用于 SSL 处理的算法,SSL_load_error_strings
加载发生错误时要显示的文本。这两个函数通常在调用任何其他 OpenSSL 函数之前被调用。
SSL 上下文提供了 OpenSSL 的处理环境,在代码中,它由一个 SSL_CTX
结构表示。要创建这个结构,应用程序调用 SSL_CTX_new
,并传入一个标识通信协议的参数。如果该参数设置为 TLS_client_method
的返回值,协议将在建立通信时确定。
前面我提到,BIO
结构包含一个存储配置数据的 SSL
结构。SSL_set_mode
函数可以通过传入 SSL
结构来配置 SSL 的行为。该函数接受多个值之一或它们的“或”组合。其中的五个值是:
SSL_MODE_ENABLE_PARTIAL_WRITE
- 允许以块的形式写入数据SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER
- 允许在写入数据时更改缓冲区位置SSL_MODE_AUTO_RETRY
- 即使初次失败,读/写操作也会继续尝试SSL_MODE_RELEASE_BUFFERS
- 当读/写缓冲区不再使用时释放内存SSL_MODE_ASYNC
- 启用异步处理
接下来的一组函数可以验证所连接实体的证书。第一步是识别本地系统上的根 CA,这可以通过调用 SSL_load_verify_file
、SSL_load_verify_dir
或 SSL_load_verify_locations
来完成。之后,应用程序可以通过调用 SSL_get_verify_result
来检查验证结果。
5.3 示例应用程序 - 连接到谷歌
本文的示例代码包含一个名为 client.c 的源文件。该程序向 google.com 发送一个 HTTPS 请求并打印其响应。如果你查看代码,会发现它执行了八个步骤:
- 初始化 SSL 库并加载错误字符串。
- 创建 SSL 上下文(
SSL_CTX
)。 - 使用 SSL 上下文创建
BIO
结构。 - 将 SSL 模式设置为
SSL_MODE_AUTO_RETRY
。 - 设置主机名和端口,并尝试建立连接。
- 向 google.com 提交一个 GET 请求。
- 读取并打印响应。
- 释放资源。
以下清单展示了执行这八个步骤的代码:
int main() {
/* Step 1: Initialize SSL */
SSL_library_init();
SSL_load_error_strings();
/* Step 2: Create the SSL context */
SSL_CTX* ctx = SSL_CTX_new(TLS_client_method());
if (!ctx) {
perror("Error creating SSL_CTX");
ERR_print_errors_fp(stderr);
exit(-1);
}
/* Step 3: Create BIO structure */
BIO* bio = BIO_new_ssl_connect(ctx);
if (!bio) {
perror("Error creating BIO");
ERR_print_errors_fp(stderr);
exit(-1);
}
/* Step 4: Set the SSL mode */
SSL* ssl = NULL;
BIO_get_ssl(bio, &ssl);
SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
/* Step 5: Attempt connection */
BIO_set_conn_hostname(bio, "www.google.com:443");
if (BIO_do_connect(bio) <= 0) {
perror("Error connecting to server");
ERR_print_errors_fp(stderr);
SSL_CTX_free(ctx);
BIO_free_all(bio);
exit(-1);
}
/* Step 6: Submit GET request */
BIO_puts(bio, "GET / HTTP/1.1\r\nHost: www.google.com \r\nConnection: close\r\n\r\n");
/* Step 7: Print response when available */
char response[1024];
while(1) {
memset(response, '\0', 1024);
if (BIO_read(bio, response, 1024) <= 0)
break;
puts(response);
}
/* Step 8: Deallocate resources */
SSL_CTX_free(ctx);
BIO_free_all(bio);
return 0;
}
如果出现错误情况,应用程序会调用 ERR_print_errors_fp
将错误输出到标准错误流。这提供了有关产生错误的 SSL 状态的底层信息。
为了接收并打印谷歌的响应,应用程序执行一个无限循环。每次迭代都会清除响应缓冲区并调用 BIO_read
。如果 BIO_read
返回的值大于零,接收到的文本将被打印到标准输出。如果该值小于或等于零,循环终止,应用程序通过调用 SSL_CTX_free
和 BIO_free_all
来释放资源。
如果你的开发系统上安装了 gcc,你可以使用以下命令编译代码:
gcc -o client client.c -lssl -lcrypto
如图所示,开发系统需要安装 OpenSSL 库和 OpenSSL 加密库。
历史
本文最初提交于 2024年9月15日。