65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 OpenSSL 构建安全应用程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2024年9月15日

CPOL

15分钟阅读

viewsIcon

3102

downloadIcon

84

本文介绍如何在 C 应用程序中访问 OpenSSL。

1. 简介

随着分布式计算日益普遍,程序员理解实现分布式计算的机制变得越来越重要。许多开发者了解源文件、头文件和 makefile,但相对较少的人熟悉数字证书、公钥/私钥文件和证书签名请求。

如今,安全分布式计算的主流方法是 SSL/TLS(安全套接层/传输层安全)。实现 SSL/TLS 的最流行的开源工具集是 OpenSSL。本文旨在解释 OpenSSL 的功能,并展示如何在代码中访问其能力。

2. SSL 和 TLS 简史

早在 1994 年,互联网尚处于起步阶段,Netscape 是当时世界上最受欢迎的浏览器。尽管 Netscape 具有惊人的功能,但它存在两个主要的安全问题:

  1. 缺乏机密性 - 客户端和服务器之间的消息未加密,因此窃听者可以读取传输的内容。
  2. 缺乏身份验证 - 在客户端/服务器通信期间,客户端无法确定服务器的身份,服务器也无法确定客户端的身份。

这些问题使得在线交易充满风险。为解决这些问题,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)。

但如果接收方没有发送方的密钥怎么办?他或她如何解密消息?这个问题自计算时代曙光初现以来就一直困扰着研究人员,我们目前最好的解决方案是使用两个密钥:一个公钥用于加密,一个私钥用于解密。如果接收方有一个公钥和一个私钥,发送方可以通过一个三步过程安全地传输消息:

  1. 接收方将其公钥公开给所有人。
  2. 发送方使用接收方的公钥加密消息,并将其发送给接收方。
  3. 接收方使用其私钥解密消息。

为使此过程有效,加密操作必须在一个方向上容易执行,而在反向上难以执行。这确保了密文只能用私钥解密,而绝不能用公钥解密。其底层细节超出了本文的范围,但 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 需要三个步骤:

  1. 获取 CA 的证书文件。其后缀通常为 *.crt 或 *.pem。
  2. 将文件移动到 /usr/local/share/ca-certificates 目录。
  3. 执行命令 sudo update-ca-certificates 来更新证书存储。

如果一个证书由一个不在受信任 CA 列表中的实体签署,但该实体的证书是由一个信誉良好的 CA 签署的,那么该证书仍然可以被认为是可信的。通过这种方式,证书可以形成一个链条,最终追溯到一个由根 CA 自签名的受信任证书。

3.3. 隐私增强邮件(PEM)文件

在几乎所有情况下,OpenSSL 都将公钥、私钥和证书存储在根据隐私增强邮件(PEM)格式结构的文本文件中。一个 PEM 文件可能包含多个密钥和/或证书,列表中的每个元素都有三个部分:

  • 页眉 - 包含被短划线包围的 BEGIN <LABEL>,其中 <LABEL> 的可能值为 PRIVATE KEYPUBLIC KEYCERTIFICATE
  • 数据 - 以 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 列出了其中的十个,并对每个命令进行了描述。

表 1:OpenSSL 命令(节选)
命令 描述
req 生成证书和证书请求
x509 签署或显示 X.509 证书
verify 验证证书链
genrsa 生成 RSA 私钥
rsa 从私钥生成 RSA 公钥
enc 对称密钥加密和解密
dgst 执行摘要操作
rand 生成伪随机数
prime 生成素数
passwd 计算密码哈希值

要探索 OpenSSL 实用工具提供的所有功能,需要一本书的篇幅。因此,本节仅介绍表中的前两个条目:openssl reqopenssl 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 列出了其中的二十三个。

表 2:OpenSSL 库的基本 I/O (BIO) 函数(节选)
函数 描述
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_getsBIO_putsBIO_read 返回可读取的数据量,如果此值小于或等于 0,则表示没有更多数据可用。

表中的最后两个函数用于释放资源。BIO_free 释放单个 BIO 结构,BIO_free_all 释放一个 BIO 结构链。

5.2 安全套接层(SSL)函数

OpenSSL 库提供了几个函数,用于对使用前面讨论的 BIO 函数创建的连接强制执行 SSL 安全。表 3 列出了其中的十个。

表 3:OpenSSL 库的 SSL 函数(节选)
函数 描述
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_fileSSL_load_verify_dirSSL_load_verify_locations 来完成。之后,应用程序可以通过调用 SSL_get_verify_result 来检查验证结果。

5.3 示例应用程序 - 连接到谷歌

本文的示例代码包含一个名为 client.c 的源文件。该程序向 google.com 发送一个 HTTPS 请求并打印其响应。如果你查看代码,会发现它执行了八个步骤:

  1. 初始化 SSL 库并加载错误字符串。
  2. 创建 SSL 上下文(SSL_CTX)。
  3. 使用 SSL 上下文创建 BIO 结构。
  4. 将 SSL 模式设置为 SSL_MODE_AUTO_RETRY
  5. 设置主机名和端口,并尝试建立连接。
  6. 向 google.com 提交一个 GET 请求。
  7. 读取并打印响应。
  8. 释放资源。

以下清单展示了执行这八个步骤的代码:

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_freeBIO_free_all 来释放资源。

如果你的开发系统上安装了 gcc,你可以使用以下命令编译代码:

gcc -o client client.c -lssl -lcrypto

如图所示,开发系统需要安装 OpenSSL 库和 OpenSSL 加密库。

历史

本文最初提交于 2024年9月15日。

© . All rights reserved.