029 《OpenSSL C++ 开发:深度解析与实战》
🌟🌟🌟本文由Gemini 2.5 Flash Preview 04-17生成,用来辅助学习。🌟🌟🌟
书籍大纲
▮▮ 1. 引言:OpenSSL与C++开发概述
▮▮▮▮ 1.1 什么是OpenSSL?
▮▮▮▮ 1.2 为什么使用OpenSSL进行C++开发?
▮▮▮▮ 1.3 目标读者与本书结构
▮▮▮▮ 1.4 学习OpenSSL前的准备
▮▮ 2. 开发环境搭建与OpenSSL库初探
▮▮▮▮ 2.1 OpenSSL的安装与编译
▮▮▮▮ 2.2 配置C++项目使用OpenSSL
▮▮▮▮ 2.3 OpenSSL库结构概述
▮▮▮▮ 2.4 OpenSSL的初始化与清理
▮▮ 3. 核心加密概念与OpenSSL基础
▮▮▮▮ 3.1 密码学基础回顾
▮▮▮▮ 3.2 OpenSSL中的内存管理
▮▮▮▮ 3.3 OpenSSL的错误处理机制
▮▮▮▮ 3.4 BIO I/O抽象层
▮▮ 4. 对称加密与哈希函数
▮▮▮▮ 4.1 EVP高级加密接口
▮▮▮▮ 4.2 对称加密算法与模式
▮▮▮▮▮▮ 4.2.1 使用EVP接口进行对称加密
▮▮▮▮▮▮ 4.2.2 密钥(Key)与初始化向量(IV)管理
▮▮▮▮ 4.3 哈希函数与消息认证码(HMAC)
▮▮▮▮▮▮ 4.3.1 使用EVP接口进行哈希计算
▮▮▮▮▮▮ 4.3.2 使用EVP接口实现HMAC
▮▮ 5. 非对称加密与密钥管理
▮▮▮▮ 5.1 非对称加密算法原理概述
▮▮▮▮ 5.2 生成非对称密钥对
▮▮▮▮▮▮ 5.2.1 生成RSA密钥对
▮▮▮▮▮▮ 5.2.2 生成ECC密钥对
▮▮▮▮ 5.3 加载和保存密钥对
▮▮▮▮▮▮ 5.3.1 加载私钥
▮▮▮▮▮▮ 5.3.2 加载公钥
▮▮▮▮▮▮ 5.3.3 保存密钥到文件
▮▮▮▮ 5.4 使用密钥对进行数据加解密
▮▮▮▮▮▮ 5.4.1 RSA加解密
▮▮▮▮▮▮ 5.4.2 ECC加解密(通常用于密钥交换)
▮▮ 6. 数字签名与验证
▮▮▮▮ 6.1 数字签名原理
▮▮▮▮ 6.2 使用EVP接口进行签名
▮▮▮▮ 6.3 使用EVP接口进行签名验证
▮▮▮▮ 6.4 RSA签名与验证
▮▮▮▮ 6.5 ECC签名与验证(ECDSA)
▮▮ 7. 证书(X.509)管理
▮▮▮▮ 7.1 X.509证书结构与格式
▮▮▮▮ 7.2 加载和解析证书
▮▮▮▮ 7.3 证书链构建与验证
▮▮▮▮ 7.4 创建自签名证书
▮▮▮▮ 7.5 生成证书签名请求(CSR)
▮▮▮▮ 7.6 证书撤销列表(CRL)与OCSP
▮▮ 8. TLS/SSL协议基础与客户端开发
▮▮▮▮ 8.1 TLS/SSL协议概述
▮▮▮▮ 8.2 TLS/SSL握手过程详解
▮▮▮▮ 8.3 OpenSSL SSL上下文(SSL_CTX)
▮▮▮▮ 8.4 OpenSSL SSL对象(SSL)
▮▮▮▮ 8.5 实现一个基本TLS/SSL客户端
▮▮▮▮ 8.6 客户端证书验证
▮▮ 9. TLS/SSL服务器开发
▮▮▮▮ 9.1 OpenSSL SSL服务器上下文配置
▮▮▮▮ 9.2 接受TLS/SSL连接
▮▮▮▮ 9.3 实现一个基本TLS/SSL服务器
▮▮▮▮ 9.4 客户端证书认证(Mutual Authentication)
▮▮▮▮ 9.5 处理多个客户端连接
▮▮ 10. 高级TLS/SSL特性与实践
▮▮▮▮ 10.1 性能优化
▮▮▮▮ 10.2 会话复用
▮▮▮▮ 10.3 SNI (Server Name Indication)
▮▮▮▮ 10.4 ALPN (Application-Layer Protocol Negotiation)
▮▮▮▮ 10.5 自定义证书验证
▮▮▮▮ 10.6 TLS/SSL安全加固
▮▮ 11. OpenSSL线程安全
▮▮▮▮ 11.1 OpenSSL与线程
▮▮▮▮ 11.2 配置线程回调函数
▮▮▮▮ 11.3 新的线程安全API (OpenSSL 1.1.0+)
▮▮ 12. 实战案例分析
▮▮▮▮ 12.1 安全文件传输工具
▮▮▮▮ 12.2 构建简易HTTPS服务器
▮▮▮▮ 12.3 安全API通信
▮▮▮▮ 12.4 使用OpenSSL处理特定加密标准
▮▮ 13. OpenSSL的演进与未来
▮▮▮▮ 13.1 OpenSSL版本历史与API变化
▮▮▮▮ 13.2 OpenSSL 3.0+ 的新特性
▮▮▮▮ 13.3 OpenSSL的维护与社区
▮▮ 附录A: OpenSSL常用函数参考
▮▮ 附录B: 常见错误码与排查
▮▮ 附录C: 代码示例与说明
▮▮ 附录D: 参考资料与拓展阅读
1. 引言:OpenSSL与C++开发概述
欢迎来到《OpenSSL C++ 开发:深度解析与实战》。在当今高度互联的世界中,信息安全(Information Security)已不再是一个可选的附加项,而是构建任何可靠软件系统的基石。无论是保护敏感数据的存储和传输、验证通信双方的身份,还是确保数据的完整性和不可否认性,密码学(Cryptography)都在其中扮演着核心角色。而OpenSSL,正是密码学和安全协议领域一个功能强大、应用广泛的开源(Open Source)工具包,尤其在构建安全网络通信(Secure Network Communication)方面,它是事实上的标准库之一。
本书旨在为C++开发者提供一条清晰、深入的路径,帮助您掌握如何利用OpenSSL库在C++应用程序中实现各种安全功能。我们将从基础概念出发,逐步深入到具体的API使用、常见应用场景以及高级特性,力求使不同经验水平的读者都能从中获益。
1.1 什么是OpenSSL?
OpenSSL是一个功能齐全的、强大的、商业级(Commercial-grade)的、开源的工具包,实现了安全套接字层(Secure Sockets Layer, SSL)和传输层安全(Transport Layer Security, TLS)协议,并提供了强大的通用密码学库。
它的起源可以追溯到1998年,是SSLeay库的一个分支,由Eric Young和Tim Hudson开发。OpenSSL项目由全球志愿者维护,并广泛应用于各种软件产品和系统中,例如:
⚝ Web服务器(Web Server):如Apache和Nginx,用于实现HTTPS。
⚝ 邮件服务器(Mail Server):用于实现SMTPS, POP3S, IMAPS。
⚝ VPN解决方案:如OpenVPN。
⚝ 各种需要安全通信或数据加密的应用。
OpenSSL的核心功能主要体现在两个库中:
① libcrypto
:这是一个通用密码学库,提供了各种加密(Encryption)、解密(Decryption)、哈希(Hashing)、数字签名(Digital Signature)、伪随机数生成(Pseudo-Random Number Generation, PRNG)等算法的实现。它支持对称加密算法(Symmetric Encryption Algorithms)如AES、DES;非对称加密算法(Asymmetric Encryption Algorithms)如RSA、ECC;哈希函数(Hash Functions)如SHA-256、SHA-3;以及公钥基础设施(Public Key Infrastructure, PKI)相关的操作,如证书(Certificate)的解析和管理。
② libssl
:这个库实现了SSL和TLS协议。它建立在libcrypto
之上,提供了一套用于在网络连接上协商安全参数、进行身份验证和安全数据传输的API。这是实现HTTPS等安全协议的关键。
了解OpenSSL的这些基本组成和功能,是深入学习的前提。
1.2 为什么使用OpenSSL进行C++开发?
在C++项目中使用OpenSSL库具有多方面的优势和必要性:
⚝ 功能强大且全面(Powerful and Comprehensive):OpenSSL提供了业界标准的大量密码学算法和安全协议实现。您不需要从头开始实现复杂的密码学原语或TLS状态机,可以直接利用OpenSSL提供的经过严格测试和广泛应用的实现。
⚝ 跨平台性(Cross-Platform):OpenSSL库可以在几乎所有主流的操作系统(Operating System)上编译和运行,包括Linux, Windows, macOS, BSD, Solaris等。这使得使用OpenSSL开发的C++应用具有良好的跨平台移植性。
⚝ 工业标准和互操作性(Industry Standard and Interoperability):由于OpenSSL的广泛应用,使用它开发的应用程序能够与同样使用OpenSSL或遵循相同标准的其他安全实现进行互操作(Interoperate)。例如,使用OpenSSL实现的TLS客户端可以与使用其他TLS库(如微软Schannel, Java JSSE)实现的服务器进行安全通信。
⚝ 性能(Performance):OpenSSL在实现时考虑了性能优化,包括对常见CPU指令集(Instruction Set)的利用(如AES-NI)和硬件加速(Hardware Acceleration)的支持(通过Engine接口)。对于性能敏感的应用程序,这是一个重要的优势。
⚝ 灵活性(Flexibility):OpenSSL提供了相对底层的API,允许开发者精细控制加密过程、密钥管理、证书验证策略以及TLS握手行为。这对于需要定制安全逻辑的复杂应用非常有价值。
⚝ 解决C++标准库(C++ Standard Library)的不足:C++标准库本身并不提供全面的密码学或网络安全协议实现。虽然有一些第三方C++安全库存在,但OpenSSL以其成熟度、功能丰富性和社区支持成为许多C++项目的首选。
使用OpenSSL的C++开发场景非常广泛,包括但不限于:
⚝ 构建安全的网络服务(Secure Network Services),如自定义的HTTPS服务器或客户端。
⚝ 实现数据加密和解密工具,用于保护文件或数据库中的敏感信息。
⚝ 开发涉及数字签名和验证的应用,如软件更新的完整性检查或电子合同签名。
⚝ 构建安全代理(Secure Proxy)或隧道(Tunneling)程序。
⚝ 开发需要处理X.509证书和PKI的应用。
尽管OpenSSL的C API有时被认为复杂且容易出错(尤其是在内存管理和错误处理方面),但通过C++的RAII(Resource Acquisition Is Initialization)等机制以及良好的封装实践,可以有效地管理这些复杂性,构建出既安全又易于维护的代码。本书将重点介绍如何以现代C++的方式安全有效地使用OpenSSL。
1.3 目标读者与本书结构
本书面向以下三类读者:
① 初学者(Beginners):对C++有一定了解,希望学习如何在C++应用中添加基本的安全功能,如数据加密、哈希计算或建立基本的TLS连接。本书将从OpenSSL的安装、基本概念和常用API开始讲解。
② 中级开发者(Intermediate Developers):已经有一定的C++开发经验,可能接触过OpenSSL,但对某些功能(如证书管理、高级TLS配置)或其API的细节理解不够深入。本书将提供更详细的原理讲解、更复杂的示例和错误处理指导。
③ 专家(Experts):具有丰富的C++和信息安全开发经验,希望深入了解OpenSSL的内部机制、高级特性(如Engine、会话复用、自定义验证)或如何在高性能、多线程环境中使用OpenSSL。本书的部分章节将探讨这些高级主题和优化技巧。
本书的结构设计如下,旨在循序渐进地引导读者掌握OpenSSL的C++开发:
⚝ 第1-3章:介绍OpenSSL的背景、开发环境搭建、库的基本结构以及核心密码学概念和OpenSSL的基础编程模式(内存管理、错误处理)。
⚝ 第4-6章:深入讲解对称加密、哈希函数、非对称加密、数字签名等核心密码学功能在OpenSSL中的实现和使用。
⚝ 第7章:详细介绍X.509证书的结构、管理和验证,这是理解PKI和TLS的基础。
⚝ 第8-10章:重点讲解TLS/SSL协议,包括其工作原理、客户端和服务器端的开发实现,以及高级特性和安全加固。
⚝ 第11章:讨论在多线程C++应用中安全使用OpenSSL的关键问题和解决方案。
⚝ 第12章:通过具体的实战案例,演示如何将前面学到的知识应用于实际场景。
⚝ 第13章:回顾OpenSSL的历史演进,展望其未来发展,并介绍OpenSSL 3.0+ 的新特性。
⚝ 附录:提供常用函数参考、错误码排查、代码示例索引和进一步学习资源。
通过这种结构,读者可以根据自己的需求和背景,选择性地阅读或深入研究感兴趣的章节,也可以按照章节顺序系统地学习OpenSSL的C++开发。
1.4 学习OpenSSL前的准备
为了能够顺利地学习本书内容并进行实践,读者需要具备以下基础知识:
① C++基础:
▮▮▮▮⚝ 熟悉C++的语法、数据类型、控制结构、函数等基本概念。
▮▮▮▮⚝ 理解指针(Pointer)和引用(Reference)的使用。
▮▮▮▮⚝ 对面向对象编程(Object-Oriented Programming, OOP)有基本的了解,如类(Class)、对象(Object)、继承(Inheritance)等。
▮▮▮▮⚝ 对C++的内存管理(Memory Management),特别是堆(Heap)上的动态内存分配(Dynamic Memory Allocation)和释放(Deallocation)有概念。虽然OpenSSL有自己的内存管理机制,但理解C++层面的内存操作对于避免常见的内存错误至关重要。
▮▮▮▮⚝ 了解基本的C++标准库容器(Containers)和算法(Algorithms)会有帮助。
② 操作系统基础:
▮▮▮▮⚝ 了解如何在您使用的操作系统上进行基本的命令行操作。OpenSSL的编译、安装和一些工具的使用通常涉及命令行。
▮▮▮▮⚝ 对文件输入/输出(File I/O)和网络编程(Network Programming)中的套接字(Socket)概念有基本认识,特别是在学习TLS/SSL章节时。
▮▮▮▮⚝ 如果您计划学习线程安全部分,对多线程(Multi-threading)和并发编程(Concurrent Programming)的基本概念(如锁 Lock、互斥量 Mutex)需要有所了解。
③ 安全与密码学概念(可选):
▮▮▮▮⚝ 虽然本书第3章会回顾基本的密码学概念,但如果您事先了解对称加密、非对称加密、哈希函数、数字签名和证书的基本原理,将有助于更快地理解后续章节的内容。
请确保您已具备这些基础知识,这将使您的OpenSSL学习之旅更加顺畅。在开始正式的学习之前,我们将在下一章详细介绍如何搭建OpenSSL的C++开发环境。
2. 开发环境搭建与OpenSSL库初探
本章旨在引导读者成功搭建OpenSSL C++开发环境,这是学习和实践OpenSSL编程的基础。我们将详细介绍如何在常见的操作系统(Linux, Windows, macOS)上获取和配置OpenSSL库,以及如何在C++项目中正确地集成和链接它。此外,我们还将初步探索OpenSSL库的整体结构,并讲解在使用OpenSSL进行开发时至关重要的初始化与清理机制。掌握这些基本步骤,将为后续深入学习OpenSSL的各种功能奠定坚实基础。
2.1 OpenSSL的安装与编译
获取OpenSSL库有两种主要方式:通过系统或第三方的包管理器安装预编译版本,或者从源代码编译安装。对于初学者或仅需使用标准功能的开发者,推荐使用包管理器方式,因为它更便捷。对于需要特定配置、最新版本或进行深度定制的开发者,则需要从源代码编译。
2.1.1 在Linux系统上安装与编译
在大多数Linux发行版上,OpenSSL库都可以通过其自带的包管理器轻松安装。
① 使用包管理器安装(推荐)
▮▮▮▮在基于Debian的系统(如Ubuntu)上:
1
sudo apt update
2
sudo apt install libssl-dev
▮▮▮▮这会安装开发所需的头文件(header files)和库文件(library files)。
▮▮▮▮在基于RPM的系统(如CentOS, Fedora)上:
1
sudo yum install openssl-devel
2
# 或者对于较新的Fedora
3
sudo dnf install openssl-devel
② 从源代码编译安装
▮▮▮▮从OpenSSL官网(https://www.openssl.org/source/)下载最新版本的源代码压缩包(例如:openssl-3.1.0.tar.gz)。
▮▮▮▮解压(extract)文件:
1
tar -zxf openssl-3.1.0.tar.gz
2
cd openssl-3.1.0
▮▮▮▮配置(configure)编译选项:
1
# 常见配置选项:
2
# --prefix=/usr/local/ssl 指定安装路径
3
# shared 构建共享库 (.so)
4
# zlib 启用zlib压缩支持
5
# enable-XXXX 启用特定算法或特性
6
# no-YYYY 禁用特定算法或特性
7
./config --prefix=/usr/local/ssl shared zlib
▮▮▮▮执行编译:
1
make
▮▮▮▮安装(通常需要root权限):
1
sudo make install
▮▮▮▮完成安装后,需要确保系统的链接器(linker)能够找到安装的库文件。如果安装到非标准路径(如/usr/local/ssl
),可能需要更新环境变量 LD_LIBRARY_PATH
或 /etc/ld.so.conf
文件。
2.1.2 在Windows系统上安装与编译
在Windows上安装OpenSSL比Linux稍复杂,通常有两种途径:使用第三方提供的预编译版本,或使用MSVC或MinGW/MSYS2环境自行编译。
① 使用第三方预编译版本(推荐)
▮▮▮▮推荐使用Shining Light Productions提供的Windows版OpenSSL(https://slproweb.com/products/Win32OpenSSL.html)。选择适合你的系统架构(32位或64位)和是否包含开发文件(带"Light"的通常不包含开发文件)。
▮▮▮▮下载安装包(例如:Win64OpenSSL-3_1_0.exe),按照安装向导进行安装。记住所选择的安装路径。安装过程中可以选择是否将OpenSSL DLLs复制到系统目录或OpenSSL bin目录。
② 从源代码编译安装(使用MSVC或MinGW/MSYS2)
▮▮▮▮这通常需要安装Perl解释器(如ActivePerl或Strawberry Perl)和相应的C/C++编译器(如Visual Studio或MinGW-w64)。
▮▮▮▮下载OpenSSL源代码,解压。
▮▮▮▮打开适用的开发环境的命令行(例如:Visual Studio的Developer Command Prompt或MSYS2终端)。
▮▮▮▮进入解压后的OpenSSL源代码目录。
▮▮▮▮执行配置命令。对于MSVC:
1
perl Configure VC-WIN64A --prefix=C:\OpenSSL-Win64
2
# 对于MinGW-w64:
3
# perl Configure mingw64 --prefix=/c/OpenSSL-MinGW64
▮▮▮▮执行编译:
1
nmake # For MSVC
2
# make # For MinGW-w64
▮▮▮▮执行安装:
1
nmake install # For MSVC
2
# make install # For MinGW-w64
▮▮▮▮编译过程可能会遇到依赖问题,需要根据具体环境调整。
2.1.3 在macOS系统上安装与编译
macOS系统内置了OpenSSL库,但其版本可能较旧,且不包含开发所需的头文件。推荐使用Homebrew进行安装。
① 使用Homebrew安装(推荐)
▮▮▮▮如果未安装Homebrew,请先安装它(参见Homebrew官网)。
▮▮▮▮安装OpenSSL:
1
brew install openssl@3
▮▮▮▮Homebrew会将OpenSSL安装到 /usr/local/opt/openssl@3
或类似路径。请注意Homebrew的提示信息,它会告诉你如何设置环境变量以便编译器和链接器找到OpenSSL。通常需要将OpenSSL的include和lib路径添加到编译器和链接器的搜索路径中。
② 从源代码编译安装
▮▮▮▮步骤与Linux类似,但配置时可能需要指定macOS的目标环境(例如:./Configure darwin64-x86_64-cc
)。
2.2 配置C++项目使用OpenSSL
将OpenSSL库集成到C++项目中最重要的一步是正确配置构建系统,使其能够找到OpenSSL的头文件和库文件,并在编译和链接阶段正确引用它们。现代C++项目常使用CMake作为构建系统生成工具。
2.2.1 使用CMake配置项目
CMake是一种跨平台的构建系统生成工具(build system generator)。它可以生成各种构建系统(如Makefile, Visual Studio projects)的文件。OpenSSL通常提供了CMake配置文件,使得查找和使用OpenSSL库变得相对简单。
① 创建CMakeLists.txt文件
▮▮▮▮在项目的根目录下创建 CMakeLists.txt
文件。以下是一个基本示例:
1
cmake_minimum_required(VERSION 3.10)
2
project(MyOpenSSLApp)
3
4
# 查找 OpenSSL 库
5
# COMPONENTS 参数可选,指定需要查找的组件,如 Crypto 和 SSL
6
# QUIET 参数抑制查找失败的警告
7
# REQUIRED 参数如果查找失败则报错并停止配置
8
find_package(OpenSSL REQUIRED)
9
10
# 检查 OpenSSL 是否找到
11
if (OPENSSL_FOUND)
12
message(STATUS "Found OpenSSL, include dir: ${OPENSSL_INCLUDE_DIR}, libraries: ${OPENSSL_LIBRARIES}")
13
else()
14
message(FATAL_ERROR "OpenSSL not found!")
15
endif()
16
17
# 添加可执行文件
18
add_executable(my_app main.cpp)
19
20
# 链接 OpenSSL 库
21
# target_link_libraries() 将 OpenSSL 库链接到目标可执行文件
22
target_link_libraries(my_app PRIVATE ${OPENSSL_LIBRARIES})
23
24
# 添加头文件搜索路径(如果 find_package 没有自动设置)
25
# target_include_directories(my_app PRIVATE ${OPENSSL_INCLUDE_DIR})
▮▮▮▮解释:
▮▮▮▮find_package(OpenSSL REQUIRED)
:这是关键命令,CMake会在预设的路径或环境变量指定的路径中查找OpenSSL的配置文件(通常是 FindOpenSSL.cmake
或 OpenSSLConfig.cmake
)。REQUIRED
参数表示如果找不到OpenSSL,则CMake配置失败。
▮▮▮▮如果找到OpenSSL,CMake会定义一些变量,最常用的是:
▮▮▮▮⚝ OPENSSL_FOUND
:布尔变量,表示是否找到OpenSSL。
▮▮▮▮⚝ OPENSSL_INCLUDE_DIR
或 OPENSSL_INCLUDE_DIRS
:包含OpenSSL头文件的目录。
▮▮▮▮⚝ OPENSSL_LIBRARIES
或 OPENSSL_LIBS
:需要链接的OpenSSL库文件。
▮▮▮▮add_executable(my_app main.cpp)
:定义一个名为 my_app
的可执行目标,源文件是 main.cpp
。
▮▮▮▮target_link_libraries(my_app PRIVATE ${OPENSSL_LIBRARIES})
:将 OPENSSL_LIBRARIES
中指定的库文件链接到 my_app
目标。PRIVATE
关键字表示链接库仅对 my_app
目标本身是必需的。
▮▮▮▮target_include_directories()
:如果 find_package
没有自动设置头文件路径,或者你需要添加额外的OpenSSL相关路径,可以使用此命令。CMake的 FindOpenSSL
模块通常会自动设置。
② 编译项目
▮▮▮▮创建构建目录(推荐在项目根目录外创建):
1
mkdir build
2
cd build
▮▮▮▮运行CMake生成构建系统文件:
1
# 根据你的操作系统和需求选择生成器 (Generator)
2
# Linux/macOS:
3
cmake ..
4
# Windows (Visual Studio):
5
# cmake .. -G "Visual Studio 16 2019" # 选择对应的VS版本
6
# Windows (MinGW Makefiles):
7
# cmake .. -G "MinGW Makefiles"
▮▮▮▮执行编译:
1
make # For Makefiles
2
# cmake --build . # For any generator
2.2.2 使用pkg-config配置项目(仅适用于Unix/Linux/macOS)
pkg-config
是一个命令行工具,用于获取安装在系统中的库的信息,如头文件路径、库文件路径以及所需的链接参数。在Unix/Linux/macOS系统上,这是配置项目使用库的常见方式。
① 检查pkg-config是否安装
1
pkg-config --version
▮▮▮▮如果未安装,请使用包管理器安装(例如:sudo apt install pkg-config
)。
② 使用pkg-config获取OpenSSL信息
1
# 获取头文件路径
2
pkg-config --cflags openssl
3
# 输出类似:-I/usr/include/openssl
4
5
# 获取库文件路径和链接参数
6
pkg-config --libs openssl
7
# 输出类似:-L/usr/lib/x86_64-linux-gnu -lssl -lcrypto
③ 在Makefile或Shell脚本中使用pkg-config
▮▮▮▮你可以将 pkg-config
的输出直接嵌入到编译命令中。
1
# 假设你的源文件是 main.cpp
2
g++ main.cpp $(pkg-config --cflags openssl) $(pkg-config --libs openssl) -o my_app
▮▮▮▮这会将 pkg-config --cflags openssl
的输出作为编译器的选项( -I...
),将 pkg-config --libs openssl
的输出作为链接器的选项( -L... -lssl -lcrypto
)。
2.2.3 手动配置
在没有CMake或pkg-config的情况下,或者在特定IDE(Integrated Development Environment)中,你可能需要手动指定OpenSSL的头文件和库文件路径。
▮▮▮▮在IDE的项目设置中,找到 "Include Directories" 或 "Header Search Paths" 选项,添加OpenSSL头文件所在的目录(例如:/usr/local/ssl/include
)。
▮▮▮▮找到 "Library Directories" 或 "Linker Search Paths" 选项,添加OpenSSL库文件所在的目录(例如:/usr/local/ssl/lib
)。
▮▮▮▮找到 "Linker Input" 或 "Libraries" 选项,添加需要链接的库文件名称(通常是 ssl
和 crypto
,在Windows上可能是 libssl.lib
和 libcrypto.lib
)。
2.3 OpenSSL库结构概述
OpenSSL库主要由两个核心部分组成:libcrypto和libssl。理解它们的职责划分对于有效地使用OpenSSL API(Application Programming Interface)至关重要。
① libcrypto库
▮▮▮▮libcrypto是OpenSSL的密码学核心库,提供了各种密码学算法和工具的实现。
▮▮▮▮它包含了:
▮▮▮▮⚝ 对称加密算法(Symmetric Ciphers),如AES, DES3。
▮▮▮▮⚝ 非对称加密算法(Asymmetric Ciphers),如RSA, ECC。
▮▮▮▮⚝ 哈希函数(Hash Functions),如SHA-256, SHA-3, MD5。
▮▮▮▮⚝ 数字签名算法(Digital Signature Algorithms),如RSA签名, ECDSA。
▮▮▮▮⚝ 密钥生成(Key Generation)和管理功能。
▮▮▮▮⚝ 随机数生成器(Random Number Generator, RNG)。
▮▮▮▮⚝ 证书(X.509 Certificates)和证书签名请求(CSR)的解析和处理功能。
▮▮▮▮⚝ 密码学数据结构(如BIGNUM, ASN.1对象)的处理。
▮▮▮▮⚝ BIO(Basic Input/Output)层,用于方便地处理各种输入/输出源。
▮▮▮▮⚝ EVP(Envelope)接口,提供了一套统一的高级API,用于执行各种密码学操作,强烈推荐使用此接口。
② libssl库
▮▮▮▮libssl库构建在libcrypto之上,实现了TLS(Transport Layer Security)和SSL(Secure Sockets Layer)协议。
▮▮▮▮它提供了:
▮▮▮▮⚝ TLS/SSL协议客户端和服务端功能的实现。
▮▮▮▮⚝ 处理TLS/SSL握手(handshake)过程。
▮▮▮▮⚝ 加密和解密通过TLS/SSL连接传输的数据。
▮▮▮▮⚝ 管理TLS/SSL会话状态。
▮▮▮▮⚝ 证书验证和协商密码套件(cipher suites)。
在实际开发中,你通常会同时链接 libcrypto
和 libssl
库,因为 libssl
依赖于 libcrypto
提供的底层密码学功能。
③ OpenSSL命令行工具(openssl
)
▮▮▮▮OpenSSL还提供了一个强大的命令行工具 openssl
,它可以执行与libcrypto和libssl提供的功能相关的许多任务,例如生成密钥对、创建和验证证书、对文件进行加解密、测试SSL/TLS连接等。这个工具对于理解和测试OpenSSL的功能非常有帮助。
2.4 OpenSSL的初始化与清理
在使用OpenSSL的API之前,特别是涉及到全局状态或算法注册的功能时,通常需要进行初始化。在使用完毕后,为了释放资源、防止内存泄漏并清理全局状态,进行清理操作同样重要。OpenSSL在不同版本中提供了不同的初始化和清理API。
2.4.1 初始化
对于OpenSSL 1.1.0及以上版本,推荐使用 OPENSSL_init_ssl
函数进行统一的初始化。
对于OpenSSL 1.0.2及以下版本,通常需要调用多个函数来初始化。
① OpenSSL 1.1.0+ 初始化(推荐)
▮▮▮▮使用 OPENSSL_init_ssl
函数,它集成了之前版本需要多个函数完成的任务。
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
4
int main() {
5
// 初始化 OpenSSL 库
6
// OPENSSL_INIT_LOAD_SSL_STRINGS 加载 SSL 相关的错误字符串
7
// OPENSSL_INIT_ADD_ALL_CIPHERS 加载所有加密算法
8
// OPENSSL_INIT_ADD_ALL_DIGESTS 加载所有哈希算法
9
// OPENSSL_INIT_LOAD_CRYPTO_STRINGS 加载 crypto 相关的错误字符串
10
if (OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_ADD_ALL_CIPHERS | OPENSSL_INIT_ADD_ALL_DIGESTS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL) != 1) {
11
// 处理初始化失败
12
ERR_print_errors_fp(stderr);
13
return 1;
14
}
15
16
// ... 使用 OpenSSL 功能 ...
17
18
// 清理 (在 OpenSSL 1.1.0+ 中,通常不需要显式调用清理函数,
19
// 但如果需要确保所有资源被释放,可以使用 OPENSSL_cleanup)
20
// OPENSSL_cleanup(); // 谨慎使用,可能影响其他依赖 OpenSSL 的库
21
22
return 0;
23
}
▮▮▮▮OPENSSL_init_ssl
函数的第一个参数是一个标志位,通过按位或(bitwise OR)可以组合多个初始化选项。第二个参数通常设为 NULL
。
② OpenSSL 1.0.2- 初始化(了解)
▮▮▮▮在旧版本中,通常需要调用以下函数:
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
4
int main() {
5
// 初始化 SSL/TLS 库
6
SSL_library_init();
7
// 加载错误字符串
8
SSL_load_error_strings();
9
// 加载所有加密和哈希算法
10
OpenSSL_add_all_algorithms(); // 注意函数名大小写
11
12
// ... 使用 OpenSSL 功能 ...
13
14
// 清理
15
// ERR_free_strings(); // 释放错误字符串内存
16
// EVP_cleanup(); // 释放 EVP 环境(算法)内存
17
// CRYPTO_cleanup_all_ex_data(); // 清理扩展数据 (某些特殊情况可能需要)
18
19
return 0;
20
}
▮▮▮▮尽管这些函数在1.1.0+版本中仍然存在并向后兼容(backward compatible),但它们通常只是调用了 OPENSSL_init_ssl
并设置了相应的标志。推荐优先使用 OPENSSL_init_ssl
。
2.4.2 清理
在应用程序退出前,特别是如果你的应用程序是长期运行的,或者你需要在某个阶段彻底卸载OpenSSL库,进行清理操作是良好的编程习惯。清理可以释放OpenSSL内部占用的内存,防止资源泄漏。
① OpenSSL 1.1.0+ 清理
▮▮▮▮如前所述,在大多数情况下,OpenSSL 1.1.0+ 的清理是自动进行的,或者在进程退出时由操作系统负责。但是,如果你需要显式清理,可以使用 OPENSSL_cleanup()
。
1
// ... 使用 OpenSSL 功能 ...
2
3
// 显式清理 OpenSSL 资源 (谨慎使用)
4
OPENSSL_cleanup();
▮▮▮▮注意: OPENSSL_cleanup()
会释放OpenSSL内部的许多资源,包括注册的算法、错误字符串等。如果在调用此函数后,你的程序或其他链接到OpenSSL的库仍然尝试使用OpenSSL的功能,可能会导致崩溃或其他未定义行为。因此,通常只在应用程序即将完全退出时调用,或者在确定没有其他代码会再使用OpenSSL时调用。
② OpenSSL 1.0.2- 清理(了解)
▮▮▮▮在旧版本中,通常需要调用与初始化对应的清理函数:
1
// ... 使用 OpenSSL 功能 ...
2
3
// 清理
4
ERR_free_strings();
5
EVP_cleanup();
6
CRYPTO_cleanup_all_ex_data(); // 如果使用了需要清理扩展数据的特性
总结: 对于OpenSSL 1.1.0及更新版本,推荐使用 OPENSSL_init_ssl
进行初始化,并且在程序正常退出时通常不需要显式调用清理函数。但如果在初始化后出现错误导致程序流程中断,或者需要提前释放资源,了解并谨慎使用清理函数是必要的。在多线程环境中,初始化只需要进行一次(通常在主线程或程序入口),OpenSSL 1.1.0+ 的初始化和大多数操作已经是线程安全的。旧版本则需要额外配置线程安全回调函数(将在后续章节讨论)。
3. 核心加密概念与OpenSSL基础
本章旨在为读者建立使用OpenSSL进行C++开发所需的密码学基础知识,并深入讲解OpenSSL库特有的核心机制,包括内存管理、错误处理以及BIO(Basic Input/Output)抽象层。理解这些基础对于正确、安全、高效地使用OpenSSL至关重要,无论您是初学者、中级开发者还是希望深入理解OpenSSL的专家,本章都将为您打下坚实的基础。
3.1 密码学基础回顾
在深入OpenSSL的具体API之前,我们首先简要回顾一些核心的密码学概念。这些概念构成了OpenSSL库所实现功能的基础。
⚝ 加密 (Encryption) 和解密 (Decryption)
加密是将可读数据(明文 (plaintext))转换为不可读格式(密文 (ciphertext))的过程,旨在保护数据的机密性 (confidentiality)。解密则是将密文恢复为明文的过程。加密算法依赖于一个密钥 (key)。
⚝ 对称加密 (Symmetric Encryption)
▮▮▮▮⚝ 特点:加密和解密使用同一个密钥。
▮▮▮▮⚝ 优点:速度快,适合大量数据加密。
▮▮▮▮⚝ 缺点:密钥分发 (key distribution) 困难,发送方和接收方必须安全地共享密钥。
▮▮▮▮⚝ 常见算法:AES (Advanced Encryption Standard),DES (Data Encryption Standard)(已不安全,但有时仍见),3DES (Triple DES)。
▮▮▮▮⚝ 工作模式 (Modes of Operation):除了基本的算法外,对称加密还有不同的工作模式,如CBC (Cipher Block Chaining)、GCM (Galois/Counter Mode)、CTR (Counter Mode) 等,这些模式会影响加密过程和安全性特性。
⚝ 非对称加密 (Asymmetric Encryption) / 公钥加密 (Public Key Cryptography)
▮▮▮▮⚝ 特点:使用一对关联的密钥:公钥 (public key) 和私钥 (private key)。通常用公钥加密,用私钥解密;或者用私钥签名,用公钥验证。
▮▮▮▮⚝ 优点:解决了对称加密的密钥分发问题,公钥可以公开,私钥必须保密。可以用于密钥交换和数字签名。
▮▮▮▮⚝ 缺点:速度比对称加密慢得多,不适合直接加密大量数据。
▮▮▮▮⚝ 常见算法:RSA (Rivest–Shamir–Adleman),ECC (Elliptic Curve Cryptography)。
⚝ 哈希函数 (Hash Functions)
▮▮▮▮⚝ 特点:将任意长度的输入数据通过确定性算法转换为固定长度的输出(哈希值 (hash value) 或摘要 (digest))。
▮▮▮▮⚝ 属性:
▮▮▮▮▮▮▮▮⚝ 单向性 (One-way):从哈希值很难逆推出原始输入数据。
▮▮▮▮▮▮▮▮⚝ 抗碰撞性 (Collision Resistance):很难找到两个不同的输入产生相同的哈希值。
▮▮▮▮⚝ 用途:主要用于验证数据的完整性 (integrity)。如果在传输或存储过程中数据被篡改,其哈希值将发生变化。
▮▮▮▮⚝ 常见算法:SHA-256 (Secure Hash Algorithm 256-bit),SHA-3,MD5 (Message Digest 5)(已被证明不安全,应避免在新应用中使用)。
⚝ 数字签名 (Digital Signature)
▮▮▮▮⚝ 特点:结合哈希函数和非对称加密。发送方用其私钥对数据的哈希值进行加密(签名),接收方用发送方的公钥对签名进行解密,得到哈希值,再计算接收到的数据的哈希值,对比两个哈希值是否一致。
▮▮▮▮⚝ 用途:验证数据的完整性 (integrity) 和来源的真实性 (authenticity),并提供不可否认性 (non-repudiation)。
▮▮▮▮⚝ 常见算法:RSA签名,ECDSA (Elliptic Curve Digital Signature Algorithm)。
⚝ 证书 (Certificates, X.509)
▮▮▮▮⚝ 特点:一种数字文档,它绑定了一个实体的身份(如个人、组织、服务器)与其公钥。
▮▮▮▮⚝ 结构:包含实体信息、公钥、颁发者信息(证书机构 (Certification Authority, CA))、有效期、颁发者对整个证书的数字签名等。
▮▮▮▮⚝ 用途:建立公钥的信任链。通过信任CA的公钥,可以信任CA签发的证书中包含的公钥,从而信任该公钥对应的实体。这解决了“如何安全地获取某个实体的公钥”的问题。
OpenSSL库提供了实现上述各种密码学操作的强大功能。接下来,我们将探讨如何在C++环境中使用OpenSSL时必须掌握的一些基础性库机制。
3.2 OpenSSL中的内存管理
使用C++开发人员习惯的RAII (Resource Acquisition Is Initialization) 范式时,OpenSSL基于C语言的API内存管理模型是一个重要的区别点,也是导致常见错误(如内存泄漏、重复释放、使用已释放内存)的根源。OpenSSL对象通常是通过特定的函数创建,并通过另一个特定的函数释放。
① C风格的内存分配与释放
② 大多数OpenSSL内部使用的对象(如 X509
, EVP_PKEY
, SSL_CTX
, SSL
, BIO
等)是通过返回指针的函数(如 X509_new()
, PEM_read_bio_X509()
, SSL_CTX_new()
, BIO_new()
等)创建或加载的。
③ 释放这些对象需要调用其对应的特定释放函数,通常命名规则是 类型_free()
,例如:
▮▮▮▮▮▮▮▮❹ X509_free(X509 *a)
▮▮▮▮▮▮▮▮❺ EVP_PKEY_free(EVP_PKEY *a)
▮▮▮▮▮▮▮▮❻ SSL_CTX_free(SSL_CTX *a)
▮▮▮▮▮▮▮▮❼ SSL_free(SSL *s)
▮▮▮▮▮▮▮▮❽ BIO_free(BIO *a)
⑨ 直接使用C标准库的 free()
函数释放OpenSSL对象是错误的,因为OpenSSL可能使用了自定义的内存分配函数,或者对象内部管理了其他资源。
⑩ 对于一些底层内存块,OpenSSL提供了 CRYPTO_malloc()
, CRYPTO_realloc()
, CRYPTO_free()
等函数。尽管可以使用这些函数,但在C++中通常更倾向于使用标准库的容器和智能指针来管理通用内存。
② 引用计数 (Reference Counting)
▮▮▮▮⚝ 某些OpenSSL对象(如 X509
, EVP_PKEY
)实现了引用计数机制。这意味着多个地方可以“持有”同一个对象的指针。
▮▮▮▮⚝ 当调用 类型_free()
函数时,对象的实际内存不一定立即释放,而是其内部的引用计数会减一。只有当引用计数变为零时,内存才会被释放。
▮▮▮▮⚝ 可以使用 类型_up_ref()
函数(例如 X509_up_ref()
, EVP_PKEY_up_ref()
)来增加对象的引用计数,表示又有一个地方引用了该对象。这在使用同一个对象多次或在不同作用域共享时很有用。
▮▮▮▮⚝ 理解引用计数对于避免过早释放或内存泄漏非常重要。如果在引用计数大于1时调用 _free
,对象不会被删除;如果忘记在所有引用都结束后调用 _free
(或 _up_ref
没有对应的 _free
),就会发生内存泄漏。
③ 在C++中管理OpenSSL内存的建议
鉴于OpenSSL的C风格内存管理,在C++中强烈推荐使用RAII包装器来自动化资源的生命周期管理。
▮▮▮▮⚝ 使用智能指针:对于那些没有引用计数的对象,可以考虑自定义删除器 (deleter) 的 std::unique_ptr
或 std::shared_ptr
。对于支持引用计数的对象,std::shared_ptr
配合自定义删除器是一个不错的选择,或者使用 _up_ref
和 _free
手动管理引用计数(但智能指针更安全)。
▮▮▮▮⚝ 创建包装类:封装OpenSSL对象指针到一个C++类中,在该类的析构函数中调用对应的 _free
函数。
▮▮▮▮⚝ 示例概念(非完整实现):
1
// 概念性的X509 RAII包装器
2
class X509Cert {
3
public:
4
X509Cert(X509* cert = nullptr) : cert_(cert) {}
5
~X509Cert() {
6
if (cert_) {
7
X509_free(cert_); // 在析构函数中调用OpenSSL的释放函数
8
cert_ = nullptr;
9
}
10
}
11
// 禁止拷贝和赋值,除非实现引用计数逻辑
12
X509Cert(const X509Cert&) = delete;
13
X509Cert& operator=(const X509Cert&) = delete;
14
15
// 移动构造和赋值
16
X509Cert(X509Cert&& other) noexcept : cert_(other.cert_) {
17
other.cert_ = nullptr;
18
}
19
X509Cert& operator=(X509Cert&& other) noexcept {
20
if (this != &other) {
21
X509_free(cert_);
22
cert_ = other.cert_;
23
other.cert_ = nullptr;
24
}
25
return *this;
26
}
27
28
// 获取底层指针
29
X509* get() const { return cert_; }
30
31
// 可以在这里添加其他X509相关的成员函数
32
33
private:
34
X509* cert_;
35
};
36
37
// 使用示例:
38
// X509* raw_cert = PEM_read_bio_X509(...);
39
// X509Cert cert(raw_cert); // 自动管理raw_cert的生命周期
40
// // cert对象超出作用域时,其析构函数会自动调用 X509_free(raw_cert);
理解OpenSSL的内存管理模型并采取适当的C++包装策略,是编写健壮、无内存问题的OpenSSL C++代码的关键。
3.3 OpenSSL的错误处理机制
与许多C库一样,OpenSSL函数通常通过返回值来指示成功或失败(例如,返回非NULL指针表示成功,NULL表示失败;返回1表示成功,0或负数表示失败)。然而,要获取更详细的错误信息,不能依赖全局的 errno
或异常,而需要使用OpenSSL特有的错误栈 (Error Stack) 机制。
① 错误栈 (Error Stack)
▮▮▮▮⚝ OpenSSL为每个线程维护一个独立的错误栈。
▮▮▮▮⚝ 当一个OpenSSL函数内部发生错误时,它会将一个或多个错误记录压入当前线程的错误栈。一个错误记录通常包含以下信息:
▮▮▮▮▮▮▮▮❶ 发生错误的库 (Library)(例如,libcrypto
, libssl
)。
▮▮▮▮▮▮▮▮❷ 发生错误的函数 (Function)(例如,SSL_connect
, X509_check_issued
)。
▮▮▮▮▮▮▮▮❸ 错误发生的原因 (Reason)(一个具体的错误码,描述了错误类型)。
▮▮▮▮▮▮▮▮❹ (可选)发生错误的文件名和行号。
▮▮▮▮⚝ 错误栈是先进后出 (LIFO) 的结构,但最常用的错误检索函数 ERR_get_error()
却返回最旧的 (oldest) 错误,并将其从栈中移除。这是为了方便在代码中按顺序处理错误原因。
② 获取和处理错误信息
以下是处理OpenSSL错误的关键函数:
⚝ unsigned long ERR_get_error(void)
:
▮▮▮▮⚝ 功能:从当前线程的错误栈中取出并移除最旧的一个错误码。
▮▮▮▮⚝ 返回值:一个 unsigned long
类型的错误码。如果栈为空,返回0。
▮▮▮▮⚝ 用法:通常在一个循环中使用,直到返回0,以获取栈中的所有错误。
⚝ unsigned long ERR_peek_error(void)
:
▮▮▮▮⚝ 功能:查看(但不移除)当前线程错误栈中最旧的一个错误码。
▮▮▮▮⚝ 返回值:错误码或0。
⚝ unsigned long ERR_peek_last_error(void)
:
▮▮▮▮⚝ 功能:查看(但不移除)当前线程错误栈中最新的一个错误码。
▮▮▮▮⚝ 返回值:错误码或0。
⚝ char *ERR_error_string(unsigned long e, char *buf)
:
▮▮▮▮⚝ 功能:将一个错误码 e
转换为人类可读的错误字符串。
▮▮▮▮⚝ 参数:e
是错误码;buf
是一个可选的缓冲区,如果传入NULL,函数会使用内部静态缓冲区(注意:这在多线程环境下不安全);如果传入非NULL缓冲区,确保其足够大(至少 256
字节,使用 ERR_ERROR_STRING_BUF_LEN
宏)。
▮▮▮▮⚝ 返回值:错误字符串。
⚝ void ERR_error_string_n(unsigned long e, char *buf, size_t len)
:
▮▮▮▮⚝ 功能:ERR_error_string
的线程安全版本,将错误码 e
转换为字符串存入缓冲区 buf
,最大长度为 len
。
⚝ void ERR_print_errors(BIO *bp)
:
▮▮▮▮⚝ 功能:将当前线程错误栈中的所有错误信息格式化后写入指定的BIO对象,并清空错误栈。
▮▮▮▮⚝ 参数:bp
是一个BIO对象,例如文件BIO或内存BIO。
⚝ void ERR_print_errors_fp(FILE *fp)
:
▮▮▮▮⚝ 功能:将当前线程错误栈中的所有错误信息格式化后写入指定的文件指针(如 stderr
),并清空错误栈。这是最常用的简便调试方法。
⚝ void ERR_clear_error(void)
:
▮▮▮▮⚝ 功能:清空当前线程的错误栈,不输出任何信息。在某些情况下(如预期可能失败,但不关心具体原因)可以使用。
③ 错误处理的最佳实践
▮▮▮▮⚝ 检查返回值:始终检查OpenSSL函数的返回值,判断是否成功。
▮▮▮▮⚝ 及时处理错误栈:一旦发现函数调用失败,应立即处理当前线程的错误栈。最简单的方式是调用 ERR_print_errors_fp(stderr)
打印错误。在生产代码中,你可能需要更精细的处理,例如将错误记录到日志系统。
▮▮▮▮⚝ 清空错误栈:处理完错误后,错误栈通常应该被清空,以避免之前的错误信息干扰后续的OpenSSL操作产生的错误。ERR_print_errors
和 ERR_print_errors_fp
会自动清空,而 ERR_get_error
通过循环调用也能达到清空的目的。
▮▮▮▮⚝ 线程安全:错误栈是线程本地的,这简化了多线程环境下的错误处理。但像 ERR_error_string
这样依赖内部静态缓冲区的函数则不是线程安全的,应使用 ERR_error_string_n
或确保在适当的锁保护下使用。
④ 代码示例:获取并打印错误
1
#include <openssl/err.h>
2
#include <openssl/ssl.h> // 包含一些OpenSSL函数声明,虽然这里只用ERR
3
4
void print_openssl_errors(const char* context) {
5
unsigned long err_code;
6
// ERR_get_error() 会取出并移除错误
7
while ((err_code = ERR_get_error()) != 0) {
8
char err_buf[256];
9
// ERR_error_string_n 是线程安全的
10
ERR_error_string_n(err_code, err_buf, sizeof(err_buf));
11
fprintf(stderr, "OpenSSL Error (%s): %lu - %s\n", context, err_code, err_buf);
12
}
13
}
14
15
// 示例函数调用后检查错误
16
void example_function_call() {
17
// 假设调用了一个可能失败的OpenSSL函数
18
// 例如:X509* cert = PEM_read_bio_X509(...);
19
// if (cert == nullptr) {
20
// print_openssl_errors("PEM_read_bio_X509 failed");
21
// }
22
// else {
23
// X509_free(cert);
24
// }
25
26
// 为了演示,我们手动向错误栈添加一个错误
27
// 这在实际开发中通常不需要手动操作,错误由失败的函数添加
28
ERR_put_error(ERR_LIB_X509, X509_F_X509_PUBKEY_CREATE, X509_R_CERT_ALREADY_IN_HASH_TABLE, __FILE__, __LINE__);
29
ERR_put_error(ERR_LIB_SSL, SSL_F_SSL_ACCEPT, SSL_R_CERTIFICATE_VERIFY_FAILED, __FILE__, __LINE__);
30
31
32
fprintf(stderr, "Checking errors after some operation...\n");
33
print_openssl_errors("operation context");
34
35
// 再次检查,此时栈应该已被清空
36
fprintf(stderr, "Checking errors again...\n");
37
print_openssl_errors("second check");
38
}
39
40
int main() {
41
// OpenSSL全局初始化,加载错误字符串
42
SSL_library_init(); // OpenSSL < 1.1.0 使用
43
SSL_load_error_strings(); // OpenSSL < 1.1.0 使用
44
45
// OpenSSL 1.1.0+ 的初始化方式
46
// OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
47
48
49
example_function_call();
50
51
// OpenSSL 清理 (可选,但推荐在应用程序退出前执行)
52
// EVP_cleanup(); // OpenSSL < 1.1.0 使用
53
// ERR_free_strings(); // OpenSSL < 1.1.0 使用
54
// CRYPTO_cleanup_all_ex_data(); // OpenSSL < 1.1.0 使用
55
56
// OpenSSL 1.1.0+ 清理方式 (通常无需显式调用,系统会自动清理)
57
// 不过对于严格的资源管理,也可以考虑是否有对应的清理函数
58
59
return 0;
60
}
上述示例代码演示了如何使用 ERR_get_error
和 ERR_error_string_n
循环获取并打印所有错误。在实际应用中,您需要根据OpenSSL函数的返回值来决定何时调用错误处理函数。
3.4 BIO I/O抽象层
BIO (Basic Input/Output) 是OpenSSL库中一个强大的抽象层,它使得OpenSSL的加密、SSL/TLS等功能可以方便地应用于各种不同的I/O源和目的地,而无需关心底层是文件、内存缓冲区还是网络套接字 (socket)。
① BIO的核心概念与作用
▮▮▮▮⚝ 抽象 I/O:BIO提供了一套统一的读写接口(如 BIO_read()
, BIO_write()
, BIO_gets()
, BIO_puts()
),类似于标准C库的 FILE
指针或类Unix系统的文件描述符,但更灵活。
▮▮▮▮⚝ 链式结构 (Chaining):BIO可以串联起来形成一个处理链。数据通过链中的BIO逐级传递,每一级BIO都可以对数据进行特定的处理(如加密、解密、压缩、添加SSL/TLS协议头等)。例如,一个常见的链可能是:套接字BIO <-> SSL BIO <-> 内存BIO
。数据从套接字读入,经过SSL BIO解密,最终存入内存BIO;写入数据时,数据从内存BIO取出,经过SSL BIO加密,最终写入套接字。
▮▮▮▮⚝ 模块化:BIO将I/O操作与密码学逻辑分离,提高了OpenSSL库的模块化和可重用性。
② 常见的BIO类型 (BIO Types)
OpenSSL提供了多种内建的BIO类型,可以通过 BIO_new()
函数指定创建:
▮▮▮▮⚝ Source/Sink BIOs (源/终点 BIO):这些BIO直接与外部数据源或目的地交互。
▮▮▮▮▮▮▮▮❶ 文件 BIO (File BIO):操作标准C库的 FILE *
文件流。创建函数:BIO_new_fp(FILE *fp, int close_flag)
。
▮▮▮▮▮▮▮▮❷ 内存 BIO (Memory BIO):操作内存中的缓冲区。数据读写实际上是在操作一块内存区域。创建函数:BIO_new(BIO_s_mem())
。可以通过 BIO_ctrl(bio, BIO_CTRL_INFO, 0, &data)
等函数获取缓冲区信息。
▮▮▮▮▮▮▮▮❸ 套接字 BIO (Socket BIO):操作网络套接字。这是实现SSL/TLS网络通信的基础。创建函数:BIO_new_socket(int fd, int close_flag)
。
▮▮▮▮▮▮▮▮❹ 接受连接 BIO (Accept BIO) 和 连接 BIO (Connect BIO):用于创建网络服务器和客户端套接字并进行连接管理。
▮▮▮▮⚝ Filter BIOs (过滤 BIO):这些BIO位于源/终点BIO之上,对流经的数据进行处理。
▮▮▮▮▮▮▮▮❶ SSL BIO:这是最重要的过滤BIO,用于在数据流中实现SSL/TLS协议的加密、解密、握手等。通过 SSL_set_bio()
将一个SSL对象与BIO链关联。
▮▮▮▮▮▮▮▮❷ Buffer BIO:提供缓冲功能。
▮▮▮▮▮▮▮▮❸ Cipher BIO:对数据进行加密/解密(不同于SSL BIO,它只进行对称加密/解密,不涉及TLS/SSL协议)。
▮▮▮▮▮▮▮▮❹ Digest BIO:计算流经数据的哈希值。
③ BIO的关键函数
⚝ BIO *BIO_new(const BIO_METHOD *type)
: 创建指定类型的BIO对象。type
参数通常是像 BIO_s_mem()
, BIO_s_file()
, BIO_s_socket()
这样的工厂函数返回值。
⚝ int BIO_free(BIO *a)
: 释放BIO对象。返回值通常表示成功或失败,但通常不检查。
⚝ int BIO_read(BIO *b, void *buf, int len)
: 从BIO中读取最多 len
字节到 buf
。返回值是实际读取的字节数,0表示EOF或无数据可读(非阻塞BIO),小于0表示错误。
⚝ int BIO_write(BIO *b, const void *buf, int len)
: 向BIO中写入 len
字节的 buf
。返回值是实际写入的字节数,0表示无数据可写(非阻塞BIO),小于0表示错误。
⚝ int BIO_gets(BIO *b, char *buf, int size)
: 从BIO中读取一行数据(直到换行符或EOF),最多读取 size-1
字节,结果存储在 buf
中并以null终止。返回值是读取的字节数,小于0表示错误。
⚝ int BIO_puts(BIO *b, const char *buf)
: 向BIO中写入一个null终止的字符串。返回值是写入的字节数,小于0表示错误。
⚝ long BIO_ctrl(BIO *b, int cmd, long larg, void *parg)
: 执行控制命令,例如获取或设置BIO的特定属性。cmd
是控制命令类型,larg
和 parg
是命令的参数。
⚝ BIO *BIO_push(BIO *b, BIO *append)
: 将 append
BIO推到 b
BIO之上,形成 append -> b
的链。数据将先经过 append
再到 b
。
⚝ BIO *BIO_pop(BIO *b)
: 移除并返回链中 b
之上的那个BIO。
④ 代码示例:使用内存BIO
使用内存BIO可以在不涉及文件或网络的情况下,方便地在内存缓冲区之间进行数据传递或使用OpenSSL的过滤功能(如加密、哈希)。
1
#include <openssl/bio.h>
2
#include <openssl/err.h>
3
#include <string.h> // For strlen
4
5
void use_memory_bio() {
6
BIO *mem_bio = nullptr;
7
const char* test_data = "Hello, OpenSSL BIO!";
8
char read_buf[128];
9
int data_len = strlen(test_data);
10
int read_len;
11
12
// 1. 创建一个内存BIO
13
mem_bio = BIO_new(BIO_s_mem());
14
if (!mem_bio) {
15
fprintf(stderr, "Failed to create memory BIO\n");
16
ERR_print_errors_fp(stderr);
17
return;
18
}
19
20
// 2. 向内存BIO写入数据
21
// BIO_write 返回实际写入的字节数
22
int written = BIO_write(mem_bio, test_data, data_len);
23
if (written != data_len) {
24
fprintf(stderr, "Warning: Did not write all data to memory BIO (%d vs %d)\n", written, data_len);
25
// Note: Memory BIO write typically succeeds unless out of memory
26
} else {
27
fprintf(stdout, "Successfully wrote %d bytes to memory BIO\n", written);
28
}
29
30
// 3. 从内存BIO读取数据
31
// BIO_read 返回实际读取的字节数
32
read_len = BIO_read(mem_bio, read_buf, sizeof(read_buf) - 1); // 留一个字节给null终止符
33
if (read_len > 0) {
34
read_buf[read_len] = '\0'; // 手动添加null终止符
35
fprintf(stdout, "Successfully read %d bytes from memory BIO: '%s'\n", read_len, read_buf);
36
} else if (read_len == 0) {
37
fprintf(stdout, "No data available to read from memory BIO\n");
38
} else { // read_len < 0
39
fprintf(stderr, "Error reading from memory BIO\n");
40
ERR_print_errors_fp(stderr);
41
}
42
43
// 4. 清理BIO对象
44
BIO_free(mem_bio); // 释放内存BIO,同时释放其内部缓冲区
45
fprintf(stdout, "Memory BIO freed\n");
46
}
47
48
int main() {
49
// OpenSSL全局初始化,加载错误字符串
50
SSL_library_init(); // OpenSSL < 1.1.0 使用
51
SSL_load_error_strings(); // OpenSSL < 1.1.0 使用
52
// 或 OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
53
54
55
use_memory_bio();
56
57
// OpenSSL 清理
58
59
return 0;
60
}
理解并熟练运用BIO层,是掌握OpenSSL高级功能(尤其是SSL/TLS网络编程)的基础。它提供了一种灵活的方式来集成加密操作到不同的I/O流中。
通过本章的学习,您应该对OpenSSL库的底层工作方式、如何正确管理资源以及处理错误有了基本的认识。这些基础知识将帮助您在后续章节中更顺利地学习和实践各种具体的密码学应用。
4. 对称加密与哈希函数
本章将带您深入理解信息安全中最基础也是最核心的两个概念:对称加密 (symmetric encryption) 和哈希函数 (hash function)。我们将详细介绍如何使用 OpenSSL 库来执行这些操作,包括常用的算法、工作模式以及 OpenSSL 提供的强大的 EVP 接口。无论是数据的机密性(通过加密)还是数据的完整性与认证(通过哈希和消息认证码),OpenSSL 都提供了丰富的工具和灵活的接口来满足开发需求。本章旨在帮助读者掌握在 C++ 应用程序中实现安全数据处理的关键技术。
4.1 EVP高级加密接口
在 OpenSSL 库中,处理各种加密和哈希操作的方式多种多样。除了直接调用特定算法的低层函数(例如 AES_set_encrypt_key
, SHA256_Init
等),OpenSSL 提供了一套更高级、更通用的接口,这就是 EVP (Envelope) 接口。使用 EVP 接口的主要优势在于它提供了一种算法无关的方式来执行加密、解密、签名、验证、哈希和消息认证码 (HMAC) 等操作。这意味着您可以通过统一的函数调用来使用不同的算法,而无需修改核心的处理逻辑,大大提高了代码的灵活性和可维护性。
EVP 接口通过上下文 (context) 对象来管理操作的状态。对于不同的操作类型,有不同的上下文结构:
⚝ EVP_CIPHER_CTX: 用于对称加密和解密。
⚝ EVP_MD_CTX: 用于哈希计算和数字签名/验证(与 EVP_PKEY 一起使用)。
⚝ EVP_PKEY_CTX: 用于密钥相关的操作,如密钥生成、协商、加密/解密(非对称)、签名/验证(非对称)。
本章主要关注对称加密和哈希,因此我们会重点使用 EVP_CIPHER_CTX
和 EVP_MD_CTX
。
使用 EVP 接口的基本流程通常包括以下几个步骤:
① 创建上下文 (Context Creation): 调用 EVP_CIPHER_CTX_new()
或 EVP_MD_CTX_new()
创建一个上下文对象。
② 初始化 (Initialization): 调用 EVP_CipherInit_ex()
(加密/解密) 或 EVP_DigestInit_ex()
(哈希) 等函数来初始化上下文,指定要使用的算法、密钥、IV(对于加密)、操作模式(加密/解密)。
③ 更新 (Update): 调用 EVP_CipherUpdate()
或 EVP_DigestUpdate()
来处理输入数据。数据可以分块处理,每次调用会处理一部分数据并产生对应的输出(加密)或更新内部状态(哈希)。
④ 结束 (Finalization): 调用 EVP_CipherFinal_ex()
或 EVP_DigestFinal_ex()
来处理剩余的数据块(如果存在)并产生最后的输出(加密的最后一部分或哈希的最终结果)。
⑤ 清理 (Cleanup): 调用 EVP_CIPHER_CTX_free()
或 EVP_MD_CTX_free()
释放上下文对象占用的资源。
采用 EVP 接口的优点包括:
⚝ 算法切换方便: 只需要修改初始化时指定的算法类型,核心处理循环代码无需改动。
⚝ 错误处理统一: EVP 接口的错误处理方式相对一致,易于管理。
⚝ 线程安全 (Thread Safety): EVP 上下文本身是线程安全的,可以在一个线程中使用,但要注意共享密钥、IV等资源时的线程同步问题。
⚝ 支持硬件加速: EVP 接口可以透明地利用 OpenSSL 配置的硬件加速模块 (Engine)。
虽然 EVP 接口增加了抽象层,但对于大多数应用来说,它是推荐的 OpenSSL API 使用方式,因为它提供了更好的灵活性、安全性和向前兼容性。
4.2 对称加密算法与模式
对称加密 (symmetric encryption) 是指加密和解密使用相同密钥(或可以轻易相互推算出的密钥)的加密技术。其主要优点是加解密速度快,适合对大量数据进行加密。常见的对称加密算法包括 AES (Advanced Encryption Standard,高级加密标准) 和已被认为不太安全的 DES (Data Encryption Standard,数据加密标准) 或 3DES (Triple Data Encryption Standard,三重数据加密标准)。OpenSSL 支持大量的对称加密算法及其变种。
对称密码 (symmetric cipher) 通常分为两种类型:
⚝ 分组密码 (Block Cipher): 将明文分成固定大小的数据块(例如 64 位、128 位)进行加密。如果明文长度不是块大小的整数倍,需要进行填充 (padding)。常见的如 AES, DES。
⚝ 流密码 (Stream Cipher): 对明文的每一个比特 (bit) 或字节 (byte) 进行逐位或逐字节加密。无需填充。常见的如 RC4(已不安全),ChaCha20。OpenSSL 的 EVP 接口也支持流密码,但使用模式的概念主要针对分组密码。
对于分组密码,需要通过工作模式 (Working Mode) 来将块加密器应用于任意长度的数据。不同的工作模式提供不同的安全属性和性能特点。常见的工作模式包括:
⚝ ECB (Electronic Codebook,电子密码本模式): 最简单模式,独立加密每个数据块。缺点:对相同的明文块总是产生相同的密文块,不隐藏数据模式,不推荐使用。
⚝ CBC (Cipher Block Chaining,密码分组链接模式): 每个明文块在加密前与前一个密文块进行异或 (XOR) 运算。需要一个初始化向量 (IV) 来处理第一个数据块。优点:隐藏数据模式。缺点:串行加密,不支持并行化,需要填充。
⚝ CTR (Counter,计数器模式): 将块加密器作为一个伪随机数生成器,通过加密一个递增的计数器来生成密钥流,然后将密钥流与明文进行异或。需要一个 IV 或初始计数器。优点:支持并行加密和解密,是流模式,无需填充(可以在加密前计算出所需的密钥流长度)。
⚝ GCM (Galois/Counter Mode,伽罗瓦/计数器模式): 一种认证加密 (Authenticated Encryption) 模式。它结合了 CTR 模式的效率和并行性,并提供了数据完整性验证和认证(通过一个附加的认证标签 Authentication Tag)。是目前广泛推荐使用的模式。需要一个 IV。
在选择对称加密算法和工作模式时,需要考虑安全性、性能、标准化和应用需求。AES 配合 GCM 或 CTR 是当前主流推荐的组合。
4.2.1 使用EVP接口进行对称加密
使用 EVP 接口进行对称加密(或解密)的基本步骤如下。这里以 AES-256-CBC 为例进行演示。
① 创建上下文:
1
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
2
if (!ctx) {
3
// 错误处理
4
}
② 初始化上下文:
1
// key: 32字节 (256 bits) 的密钥
2
// iv: 16字节 (AES块大小) 的初始化向量
3
// enc: 1表示加密,0表示解密
4
5
const EVP_CIPHER* cipher_type = EVP_aes_256_cbc(); // 选择算法和模式
6
int enc = 1; // 加密操作
7
8
if (1 != EVP_CipherInit_ex(ctx, cipher_type, NULL, key, iv, enc)) {
9
// 错误处理,获取错误信息
10
ERR_print_errors_fp(stderr);
11
EVP_CIPHER_CTX_free(ctx);
12
// ...
13
}
EVP_CipherInit_ex
是一个重要的函数。它的参数依次是:上下文、密码类型、Engine(通常为 NULL)、密钥、IV、操作类型(加密/解密)。
③ 处理数据 (Update):
输入数据通常分块处理。
1
unsigned char* plaintext = ...; // 明文数据
2
int plaintext_len = ...; // 明文长度
3
unsigned char* ciphertext = ...; // 用于存放密文的缓冲区,大小需要足够大
4
5
int len; // 用于存储每次update产生的输出长度
6
int ciphertext_len = 0; // 总密文长度
7
8
// 缓冲区大小估算: 对于加密,输出密文长度最大为输入明文长度 + 算法块大小 - 1,再加上最终的填充块大小。
9
// 一个安全的估算方法是: 明文长度 + 算法块大小。对于AES,块大小是16字节。
10
// 对于解密,输出明文长度最大为输入密文长度。
11
12
if (1 != EVP_CipherUpdate(ctx, ciphertext, &len, plaintext, plaintext_len)) {
13
// 错误处理
14
ERR_print_errors_fp(stderr);
15
EVP_CIPHER_CTX_free(ctx);
16
// ...
17
}
18
ciphertext_len += len;
19
20
// 如果数据量大,可以在循环中多次调用 EVP_CipherUpdate
21
// 例如: while(bytes_remaining > 0) { ... EVP_CipherUpdate(...) ... }
EVP_CipherUpdate
的参数依次是:上下文、输出缓冲区、输出长度指针、输入缓冲区、输入长度。它会处理输入的明文块,并将产生的密文输出到输出缓冲区。
④ 结束处理 (Finalize):
处理最后剩余的数据块和填充。
1
int final_len; // 用于存储final产生的输出长度
2
3
// 注意: 对于某些流模式(如CTR),Final函数可能不产生输出或只处理少量状态。
4
// 对于分组模式(如CBC, GCM),Final函数会处理最后的填充块。
5
if (1 != EVP_CipherFinal_ex(ctx, ciphertext + ciphertext_len, &final_len)) {
6
// 错误处理 (解密时,如果填充不正确,Final函数会失败)
7
ERR_print_errors_fp(stderr);
8
EVP_CIPHER_CTX_free(ctx);
9
// ...
10
}
11
ciphertext_len += final_len; // 加上最后一部分密文(或明文)长度
EVP_CipherFinal_ex
的参数类似 EVP_CipherUpdate
,但输入缓冲区是内部管理的。输出缓冲区应指向紧接着 EVP_CipherUpdate
最后一次输出的位置。
⑤ 清理上下文:
1
EVP_CIPHER_CTX_free(ctx);
2
ctx = NULL; // 养成将释放后的指针置为NULL的好习惯
下面是一个完整的对称加密示例框架:
1
#include <openssl/evp.h>
2
#include <openssl/err.h>
3
#include <string.h>
4
#include <stdio.h>
5
6
// 假设已经初始化了OpenSSL库 (例如调用了 OPENSSL_init_crypto())
7
8
int symmetric_encrypt(const unsigned char* plaintext, int plaintext_len,
9
const unsigned char* key, const unsigned char* iv,
10
unsigned char* ciphertext, int* ciphertext_len)
11
{
12
EVP_CIPHER_CTX *ctx = NULL;
13
int len;
14
int total_len = 0;
15
int ret = -1; // 返回状态,-1表示失败
16
17
// 1. Create and initialize the context
18
ctx = EVP_CIPHER_CTX_new();
19
if (!ctx) {
20
ERR_print_errors_fp(stderr);
21
goto cleanup;
22
}
23
24
// 使用AES-256-CBC加密
25
if (1 != EVP_CipherInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv, 1)) {
26
ERR_print_errors_fp(stderr);
27
goto cleanup;
28
}
29
30
// 2. Provide the message to be encrypted, and get the output
31
// OpenSSL可以分块处理数据
32
if (plaintext_len > 0) {
33
if (1 != EVP_CipherUpdate(ctx, ciphertext, &len, plaintext, plaintext_len)) {
34
ERR_print_errors_fp(stderr);
35
goto cleanup;
36
}
37
total_len += len;
38
}
39
40
// 3. Finalize the encryption
41
// 处理最后剩余的数据块和填充
42
if (1 != EVP_CipherFinal_ex(ctx, ciphertext + total_len, &len)) {
43
ERR_print_errors_fp(stderr);
44
goto cleanup;
45
}
46
total_len += len;
47
48
*ciphertext_len = total_len;
49
ret = 0; // 成功
50
51
cleanup:
52
// 4. Clean up
53
if (ctx) {
54
EVP_CIPHER_CTX_free(ctx);
55
}
56
57
return ret;
58
}
59
60
int symmetric_decrypt(const unsigned char* ciphertext, int ciphertext_len,
61
const unsigned char* key, const unsigned char* iv,
62
unsigned char* plaintext, int* plaintext_len)
63
{
64
EVP_CIPHER_CTX *ctx = NULL;
65
int len;
66
int total_len = 0;
67
int ret = -1; // 返回状态,-1表示失败
68
69
// 1. Create and initialize the context
70
ctx = EVP_CIPHER_CTX_new();
71
if (!ctx) {
72
ERR_print_errors_fp(stderr);
73
goto cleanup;
74
}
75
76
// 使用AES-256-CBC解密
77
if (1 != EVP_CipherInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv, 0)) {
78
ERR_print_errors_fp(stderr);
79
goto cleanup;
80
}
81
82
// 2. Provide the message to be decrypted, and get the output
83
if (ciphertext_len > 0) {
84
if (1 != EVP_CipherUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len)) {
85
ERR_print_errors_fp(stderr);
86
goto cleanup;
87
}
88
total_len += len;
89
}
90
91
// 3. Finalize the decryption
92
// 处理最后剩余的数据块,并移除填充
93
if (1 != EVP_CipherFinal_ex(ctx, plaintext + total_len, &len)) {
94
ERR_print_errors_fp(stderr); // 解密失败通常是数据或填充错误
95
goto cleanup;
96
}
97
total_len += len;
98
99
*plaintext_len = total_len;
100
ret = 0; // 成功
101
102
cleanup:
103
// 4. Clean up
104
if (ctx) {
105
EVP_CIPHER_CTX_free(ctx);
106
}
107
108
return ret;
109
}
110
111
/*
112
// 示例用法 (在 main 函数或其他地方调用)
113
int main() {
114
// 假设 OpenSSL 已经初始化
115
// OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
116
117
unsigned char key[32] = {0}; // 实际应用中需要安全生成和管理密钥
118
unsigned char iv[16] = {0}; // 实际应用中需要生成并随密文传输 IV
119
120
// 使用随机函数生成密钥和 IV (更安全的方式)
121
// RAND_bytes(key, sizeof(key));
122
// RAND_bytes(iv, sizeof(iv));
123
124
unsigned char plaintext[] = "This is a secret message.";
125
int plaintext_len = strlen((char*)plaintext);
126
127
// 估算密文缓冲区大小 (足够安全)
128
int max_ciphertext_len = plaintext_len + EVP_aes_256_cbc()->block_size;
129
unsigned char* ciphertext = (unsigned char*)malloc(max_ciphertext_len);
130
if (!ciphertext) {
131
perror("malloc failed");
132
return 1;
133
}
134
int ciphertext_len;
135
136
printf("Original Plaintext: %s\n", plaintext);
137
138
// 加密
139
if (symmetric_encrypt(plaintext, plaintext_len, key, iv, ciphertext, &ciphertext_len) == 0) {
140
printf("Encryption successful. Ciphertext length: %d\n", ciphertext_len);
141
// 通常会打印密文的十六进制表示
142
// for (int i = 0; i < ciphertext_len; ++i) printf("%02x", ciphertext[i]); printf("\n");
143
144
// 解密
145
unsigned char* decryptedtext = (unsigned char*)malloc(ciphertext_len); // 解密输出不会超过密文长度
146
if (!decryptedtext) {
147
perror("malloc failed");
148
free(ciphertext);
149
return 1;
150
}
151
int decryptedtext_len;
152
153
if (symmetric_decrypt(ciphertext, ciphertext_len, key, iv, decryptedtext, &decryptedtext_len) == 0) {
154
decryptedtext[decryptedtext_len] = '\0'; // Null terminate decrypted data
155
printf("Decryption successful. Decrypted text: %s\n", decryptedtext);
156
} else {
157
printf("Decryption failed.\n");
158
}
159
160
free(decryptedtext);
161
} else {
162
printf("Encryption failed.\n");
163
}
164
165
free(ciphertext);
166
167
// 清理 OpenSSL 库
168
// OPENSSL_cleanup(); // 如果在开始时调用了 OPENSSL_init_crypto
169
// 或对于更早的版本调用 CRYPTO_cleanup_all_ex_data(); ERR_free_strings(); EVP_cleanup();
170
171
return 0;
172
}
173
*/
请注意,上面的代码示例框架中,密钥 key
和初始化向量 iv
是硬编码或简单初始化的,这在实际应用中是非常不安全的!它们必须通过安全的方式生成、管理和传输。
4.2.2 密钥(Key)与初始化向量(IV)管理
在对称加密中,密钥 (Key) 是最核心的安全要素。密钥的安全性直接决定了加密数据的安全性。如果密钥泄露,任何人都能够解密密文。因此,密钥必须是保密的,并且长度应足够长以抵抗暴力破解攻击。AES 的密钥长度通常有 128 比特 (bits)、192 比特和 256 比特。OpenSSL 提供的 EVP_aes_128_cbc()
, EVP_aes_192_cbc()
, EVP_aes_256_cbc()
等函数对应不同的密钥长度要求。
初始化向量 (IV),有时也称为 nonce (Number Used Once),是用于在某些工作模式(如 CBC, CTR, GCM)中增加随机性的一个输入。它的主要目的是:
⚝ 即使使用相同的密钥对相同的明文进行多次加密,也能产生不同的密文,从而避免泄露明文模式。
⚝ 在链式模式(如 CBC)中,为第一个数据块提供初始的“随机性输入”。
IV 的要求与密钥不同:
⚝ IV 不需要保密,但必须是唯一的 (unique)——对于同一个密钥,每次加密操作必须使用不同的 IV。
⚝ 对于某些模式(如 CBC, CTR),IV 甚至可以是公开的,通常与密文一起传输。
⚝ 对于某些模式(如 GCM),IV 不仅要唯一,最好是不可预测的 (unpredictable),但 OpenSSL 的 GCM 实现可以接受固定长度的随机 IV。
如何安全地生成密钥和 IV?
① 密钥生成: 密钥必须通过高质量的随机数生成器 (Random Number Generator, RNG) 生成。OpenSSL 提供了密码学安全的伪随机数生成器 (CSPRNG)。最常用的函数是 RAND_bytes()
或 RAND_priv_bytes()
。
1
#include <openssl/rand.h>
2
#include <openssl/err.h>
3
4
// 生成一个32字节的AES-256密钥
5
unsigned char key[32];
6
if (1 != RAND_bytes(key, sizeof(key))) {
7
// 错误处理
8
ERR_print_errors_fp(stderr);
9
// ...
10
}
11
12
// 生成一个16字节的AES IV
13
unsigned char iv[16];
14
if (1 != RAND_bytes(iv, sizeof(iv))) {
15
// 错误处理
16
ERR_print_errors_fp(stderr);
17
// ...
18
}
RAND_bytes()
从 OpenSSL 的内部随机池中获取随机字节,适用于生成公开或可传输的随机数据(如 IV)。RAND_priv_bytes()
也是获取随机字节,但设计用于生成私密数据(如密钥),理论上会有额外的内部保护措施(尽管实际区别可能不大,但使用上推荐区分)。
② 密钥管理: 密钥生成后,如何安全地存储、分发和使用密钥是一个复杂的问题,通常涉及密钥管理系统 (Key Management System, KMS)、硬件安全模块 (Hardware Security Module, HSM) 或安全的密钥文件存储。这超出了本书 OpenSSL API 使用的范畴,但开发者必须意识到其重要性。切勿将密钥硬编码在代码中或以明文形式存储在不安全的位置。
③ IV 传输: 对于需要 IV 的模式,通常的做法是将生成的 IV 附加在密文的前面或通过其他带外方式与密文一起发送给接收方,以便接收方能够使用相同的 IV 进行解密。
例如,发送方:
生成密钥 Key 和 IV -> 使用 Key 和 IV 加密明文 -> 将 IV 和密文组合发送。
接收方:
接收到组合数据 -> 分离出 IV 和密文 -> 使用 Key 和 IV 解密密文。
以下代码片段演示如何生成密钥和 IV:
1
#include <openssl/rand.h>
2
#include <openssl/err.h>
3
#include <stdio.h>
4
5
// 假设 OpenSSL 已初始化
6
7
void generate_aes_key_iv(unsigned char* key, int key_len,
8
unsigned char* iv, int iv_len)
9
{
10
if (!RAND_bytes(key, key_len)) {
11
fprintf(stderr, "Error generating key:\n");
12
ERR_print_errors_fp(stderr);
13
// 处理错误,可能退出或抛异常
14
}
15
16
if (!RAND_bytes(iv, iv_len)) {
17
fprintf(stderr, "Error generating IV:\n");
18
ERR_print_errors_fp(stderr);
19
// 处理错误
20
}
21
}
22
23
/*
24
// 示例用法
25
int main() {
26
// OpenSSL初始化...
27
28
unsigned char aes256_key[32]; // 256 bits = 32 bytes
29
unsigned char aes_iv[16]; // AES block size = 16 bytes
30
31
generate_aes_key_iv(aes256_key, sizeof(aes256_key), aes_iv, sizeof(aes_iv));
32
33
printf("Generated Key (hex): ");
34
for (int i = 0; i < sizeof(aes256_key); ++i) printf("%02x", aes256_key[i]);
35
printf("\n");
36
37
printf("Generated IV (hex): ");
38
for (int i = 0; i < sizeof(aes_iv); ++i) printf("%02x", aes_iv[i]);
39
printf("\n");
40
41
// 实际应用中,使用这些 key 和 iv 进行加密/解密
42
43
// OpenSSL清理...
44
return 0;
45
}
46
*/
4.3 哈希函数与消息认证码(HMAC)
哈希函数 (Hash Function),也称为散列函数,是一种将任意长度的输入数据(消息)映射到固定长度输出(称为哈希值、哈希码、消息摘要 Message Digest 或指纹 fingerprint)的数学函数。密码学哈希函数设计有特定的安全属性:
⚝ 单向性 (One-wayness / Pre-image Resistance): 从哈希值很难计算出原始输入数据。
⚝ 弱碰撞抗性 (Weak Collision Resistance / Second Pre-image Resistance): 给定一个输入数据,很难找到另一个不同的输入数据产生相同的哈希值。
⚝ 强碰撞抗性 (Strong Collision Resistance / Collision Resistance): 很难找到任意两个不同的输入数据产生相同的哈希值。
哈希函数的主要用途是验证数据的完整性 (integrity)。通过计算数据的哈希值,并在数据传输或存储后重新计算其哈希值并与原始哈希值进行比较,可以检测数据是否被篡改。如果两个哈希值不同,则数据已被修改。
OpenSSL 支持多种哈希算法,包括:
⚝ MD5 (Message Digest Algorithm 5): 输出 128 位哈希值。MD5 已被证明存在严重碰撞漏洞,不应再用于需要碰撞抗性的安全场景(如数字签名、证书、软件完整性校验),但仍可用于非安全目的(如校验文件重复性)。
⚝ SHA-1 (Secure Hash Algorithm 1): 输出 160 位哈希值。SHA-1 也已被证明存在碰撞漏洞,不应再用于新的安全应用。
⚝ SHA-2 系列: 包括 SHA-224, SHA-256, SHA-384, SHA-512 等,输出长度分别为 224, 256, 384, 512 位。这些算法目前仍被认为是安全的,SHA-256 和 SHA-512 被广泛使用。
⚝ SHA-3 系列: 也称为 Keccak,是 NIST 2012 年选定的新一代哈希标准,与 SHA-2 独立。包括 SHA3-224, SHA3-256, SHA3-384, SHA3-512。
消息认证码 (Message Authentication Code, MAC) 是一种带密钥的哈希函数。它不仅能验证数据的完整性,还能验证数据的来源 (authenticity),因为只有持有密钥的双方才能计算或验证正确的 MAC 值。
HMAC (Hash-based Message Authentication Code) 是一种常用的 MAC 构造方法,它结合了密钥和密码学哈希函数。HMAC 的安全性依赖于底层哈希函数的安全性以及密钥的保密性。常见的如 HMAC-SHA256, HMAC-SHA512。
4.3.1 使用EVP接口进行哈希计算
使用 EVP 接口计算哈希值(消息摘要)的基本步骤如下。这里以 SHA-256 为例。
① 创建上下文:
1
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
2
if (!ctx) {
3
// 错误处理
4
}
② 初始化上下文:
1
// 指定哈希算法类型
2
const EVP_MD* md_type = EVP_sha256();
3
4
if (1 != EVP_DigestInit_ex(ctx, md_type, NULL)) {
5
// 错误处理
6
ERR_print_errors_fp(stderr);
7
EVP_MD_CTX_free(ctx);
8
// ...
9
}
EVP_DigestInit_ex
的参数依次是:上下文、消息摘要类型、Engine(通常为 NULL)。
③ 处理数据 (Update):
输入数据可以分块处理。
1
unsigned char* data = ...; // 输入数据
2
int data_len = ...; // 数据长度
3
4
if (1 != EVP_DigestUpdate(ctx, data, data_len)) {
5
// 错误处理
6
ERR_print_errors_fp(stderr);
7
EVP_MD_CTX_free(ctx);
8
// ...
9
}
10
11
// 如果数据量大,可以在循环中多次调用 EVP_DigestUpdate
12
// 例如: while(bytes_remaining > 0) { ... EVP_DigestUpdate(...) ... }
EVP_DigestUpdate
的参数依次是:上下文、输入缓冲区、输入长度。它会更新内部状态,不产生输出。
④ 结束处理 (Finalize):
计算并获取最终的哈希值。
1
unsigned char digest[EVP_MAX_MD_SIZE]; // 存储哈希值的缓冲区,大小应至少为 EVP_MAX_MD_SIZE
2
unsigned int digest_len; // 用于存储哈希值的实际长度
3
4
if (1 != EVP_DigestFinal_ex(ctx, digest, &digest_len)) {
5
// 错误处理
6
ERR_print_errors_fp(stderr);
7
EVP_MD_CTX_free(ctx);
8
// ...
9
}
10
// 此时,digest 数组中存放着计算出的哈希值,长度为 digest_len
EVP_DigestFinal_ex
的参数依次是:上下文、输出缓冲区(用于存放哈希值)、输出长度指针。
⑤ 清理上下文:
1
EVP_MD_CTX_free(ctx);
2
ctx = NULL;
下面是一个计算 SHA-256 哈希值的示例框架:
1
#include <openssl/evp.h>
2
#include <openssl/err.h>
3
#include <string.h>
4
#include <stdio.h>
5
6
// 假设已经初始化了OpenSSL库
7
8
int calculate_sha256(const unsigned char* data, int data_len,
9
unsigned char* digest, unsigned int* digest_len)
10
{
11
EVP_MD_CTX *ctx = NULL;
12
int ret = -1; // 返回状态,-1表示失败
13
14
// 1. Create context
15
ctx = EVP_MD_CTX_new();
16
if (!ctx) {
17
ERR_print_errors_fp(stderr);
18
goto cleanup;
19
}
20
21
// 2. Initialize the digest operation
22
// 使用SHA-256算法
23
if (1 != EVP_DigestInit_ex(ctx, EVP_sha256(), NULL)) {
24
ERR_print_errors_fp(stderr);
25
goto cleanup;
26
}
27
28
// 3. Provide the message to be hashed
29
if (data_len > 0) {
30
if (1 != EVP_DigestUpdate(ctx, data, data_len)) {
31
ERR_print_errors_fp(stderr);
32
goto cleanup;
33
}
34
}
35
36
// 4. Finalize the digest operation
37
// 获取最终的哈希值
38
if (1 != EVP_DigestFinal_ex(ctx, digest, digest_len)) {
39
ERR_print_errors_fp(stderr);
40
goto cleanup;
41
}
42
43
ret = 0; // 成功
44
45
cleanup:
46
// 5. Clean up
47
if (ctx) {
48
EVP_MD_CTX_free(ctx);
49
}
50
51
return ret;
52
}
53
54
/*
55
// 示例用法
56
int main() {
57
// OpenSSL初始化...
58
59
unsigned char data[] = "This is a message to be hashed.";
60
int data_len = strlen((char*)data);
61
62
unsigned char digest[EVP_MAX_MD_SIZE]; // 缓冲区大小至少为 EVP_MAX_MD_SIZE
63
unsigned int digest_len; // 实际哈希值长度
64
65
if (calculate_sha256(data, data_len, digest, &digest_len) == 0) {
66
printf("SHA-256 Hash: ");
67
for (int i = 0; i < digest_len; ++i) {
68
printf("%02x", digest[i]);
69
}
70
printf("\n");
71
} else {
72
printf("Hash calculation failed.\n");
73
}
74
75
// OpenSSL清理...
76
return 0;
77
}
78
*/
79
请注意,`EVP_MAX_MD_SIZE` 是一个宏,定义了 OpenSSL 支持的最大消息摘要长度(通常是 SHA-512 的 64 字节)。您也可以根据具体算法获取其摘要长度,例如 `EVP_MD_size(EVP_sha256())` 返回 32。
80
81
*/
4.3.2 使用EVP接口实现HMAC
使用 EVP 接口计算 HMAC 的流程与计算普通哈希类似,但需要一个密钥,并且使用的是不同的初始化函数。EVP 接口通过 EVP_PKEY
结构来表示密钥,即使对于对称密钥(如 HMAC 密钥),也会将其封装在 EVP_PKEY
中。HMAC 的操作通常通过 EVP_DigestSign*
函数族来实现,这组函数也用于非对称签名。
使用 EVP 接口计算 HMAC 的基本步骤:
① 创建并加载/生成密钥:
HMAC 使用对称密钥。您可以从裸字节数组创建 EVP_PKEY
。
1
#include <openssl/evp.h>
2
#include <openssl/err.h>
3
#include <string.h>
4
5
// hmac_key: HMAC使用的密钥字节数组
6
// hmac_key_len: 密钥长度
7
8
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, hmac_key, hmac_key_len);
9
if (!pkey) {
10
// 错误处理
11
ERR_print_errors_fp(stderr);
12
// ...
13
}
EVP_PKEY_new_raw_private_key
用于从原始密钥字节创建一个 EVP_PKEY
对象。第一个参数指定密钥类型 (EVP_PKEY_HMAC
),第二个参数是 Engine(通常为 NULL),第三个和第四个参数是密钥数据和长度。
② 创建上下文:
1
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
2
if (!ctx) {
3
// 错误处理
4
EVP_PKEY_free(pkey); // 记得释放密钥
5
// ...
6
}
③ 初始化上下文 (用于签名/计算MAC):
1
// md_type: 使用的哈希算法类型,如 EVP_sha256()
2
3
if (1 != EVP_DigestSignInit(ctx, NULL, md_type, NULL, pkey)) {
4
// 错误处理
5
ERR_print_errors_fp(stderr);
6
EVP_MD_CTX_free(ctx);
7
EVP_PKEY_free(pkey);
8
// ...
9
}
EVP_DigestSignInit
的参数依次是:上下文、内部 EVP_PKEY_CTX
指针(通常为 NULL,内部会自动创建)、消息摘要类型、Engine(通常为 NULL)、用于签名的密钥 (EVP_PKEY
)。
④ 处理数据 (Update):
1
unsigned char* data = ...; // 输入数据
2
int data_len = ...; // 数据长度
3
4
if (1 != EVP_DigestSignUpdate(ctx, data, data_len)) {
5
// 错误处理
6
ERR_print_errors_fp(stderr);
7
EVP_MD_CTX_free(ctx);
8
EVP_PKEY_free(pkey);
9
// ...
10
}
11
12
// 数据量大可以多次调用 EVP_DigestSignUpdate
EVP_DigestSignUpdate
参数类似 EVP_DigestUpdate
。
⑤ 结束处理 (Finalize):
获取计算出的 HMAC 值。首先调用一次获取需要的输出缓冲区大小,再调用一次获取实际结果。
1
size_t hmac_len; // 用于存储HMAC值的实际长度
2
3
// 第一次调用:获取输出缓冲区大小
4
if (1 != EVP_DigestSignFinal(ctx, NULL, &hmac_len)) {
5
// 错误处理
6
ERR_print_errors_fp(stderr);
7
EVP_MD_CTX_free(ctx);
8
EVP_PKEY_free(pkey);
9
// ...
10
}
11
12
// 分配足够的缓冲区
13
unsigned char* hmac_value = (unsigned char*)malloc(hmac_len);
14
if (!hmac_value) {
15
// 内存分配错误处理
16
EVP_MD_CTX_free(ctx);
17
EVP_PKEY_free(pkey);
18
// ...
19
}
20
21
// 第二次调用:计算并获取HMAC值
22
if (1 != EVP_DigestSignFinal(ctx, hmac_value, &hmac_len)) {
23
// 错误处理
24
ERR_print_errors_fp(stderr);
25
free(hmac_value);
26
EVP_MD_CTX_free(ctx);
27
EVP_PKEY_free(pkey);
28
// ...
29
}
30
31
// 此时 hmac_value 数组中存放着计算出的 HMAC 值,长度为 hmac_len
EVP_DigestSignFinal
的第一个参数是输出缓冲区,第二个参数是输出长度指针。第一次调用时传入 NULL 获取长度,第二次调用时传入缓冲区获取结果。
⑥ 清理:
1
free(hmac_value); // 释放动态分配的缓冲区
2
EVP_MD_CTX_free(ctx);
3
EVP_PKEY_free(pkey); // 释放密钥对象
下面是一个计算 HMAC-SHA256 的示例框架:
1
#include <openssl/evp.h>
2
#include <openssl/err.h>
3
#include <string.h>
4
#include <stdio.h>
5
#include <stdlib.h> // For malloc/free
6
7
// 假设已经初始化了OpenSSL库
8
9
int calculate_hmac_sha256(const unsigned char* data, int data_len,
10
const unsigned char* key, int key_len,
11
unsigned char** hmac_value, size_t* hmac_len)
12
{
13
EVP_PKEY *pkey = NULL;
14
EVP_MD_CTX *ctx = NULL;
15
int ret = -1; // 返回状态,-1表示失败
16
17
*hmac_value = NULL; // 初始化输出指针
18
19
// 1. Create EVP_PKEY from raw key bytes
20
pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, key, key_len);
21
if (!pkey) {
22
ERR_print_errors_fp(stderr);
23
goto cleanup;
24
}
25
26
// 2. Create context
27
ctx = EVP_MD_CTX_new();
28
if (!ctx) {
29
ERR_print_errors_fp(stderr);
30
goto cleanup;
31
}
32
33
// 3. Initialize the HMAC operation
34
if (1 != EVP_DigestSignInit(ctx, NULL, EVP_sha256(), NULL, pkey)) {
35
ERR_print_errors_fp(stderr);
36
goto cleanup;
37
}
38
39
// 4. Provide the message data
40
if (data_len > 0) {
41
if (1 != EVP_DigestSignUpdate(ctx, data, data_len)) {
42
ERR_print_errors_fp(stderr);
43
goto cleanup;
44
}
45
}
46
47
// 5. Finalize the HMAC operation - get size
48
size_t required_len;
49
if (1 != EVP_DigestSignFinal(ctx, NULL, &required_len)) {
50
ERR_print_errors_fp(stderr);
51
goto cleanup;
52
}
53
54
// Allocate output buffer
55
*hmac_value = (unsigned char*)malloc(required_len);
56
if (!*hmac_value) {
57
perror("malloc failed");
58
goto cleanup;
59
}
60
*hmac_len = required_len;
61
62
// 6. Finalize the HMAC operation - get value
63
if (1 != EVP_DigestSignFinal(ctx, *hmac_value, hmac_len)) {
64
ERR_print_errors_fp(stderr);
65
free(*hmac_value); // Free the allocated buffer on error
66
*hmac_value = NULL;
67
goto cleanup;
68
}
69
70
ret = 0; // 成功
71
72
cleanup:
73
// 7. Clean up
74
if (ctx) {
75
EVP_MD_CTX_free(ctx);
76
}
77
if (pkey) {
78
EVP_PKEY_free(pkey); // Remember to free the key object!
79
}
80
81
return ret;
82
}
83
84
// HMAC验证函数示例
85
int verify_hmac_sha256(const unsigned char* data, int data_len,
86
const unsigned char* key, int key_len,
87
const unsigned char* received_hmac, size_t received_hmac_len)
88
{
89
EVP_PKEY *pkey = NULL;
90
EVP_MD_CTX *ctx = NULL;
91
int ret = -1; // 返回状态,-1表示失败
92
93
// 1. Create EVP_PKEY from raw key bytes
94
pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, key, key_len);
95
if (!pkey) {
96
ERR_print_errors_fp(stderr);
97
goto cleanup;
98
}
99
100
// 2. Create context
101
ctx = EVP_MD_CTX_new();
102
if (!ctx) {
103
ERR_print_errors_fp(stderr);
104
goto cleanup;
105
}
106
107
// 3. Initialize the HMAC verification operation
108
// 注意这里使用的是 EVP_DigestVerifyInit
109
if (1 != EVP_DigestVerifyInit(ctx, NULL, EVP_sha256(), NULL, pkey)) {
110
ERR_print_errors_fp(stderr);
111
goto cleanup;
112
}
113
114
// 4. Provide the message data
115
if (data_len > 0) {
116
if (1 != EVP_DigestVerifyUpdate(ctx, data, data_len)) {
117
ERR_print_errors_fp(stderr);
118
goto cleanup;
119
}
120
}
121
122
// 5. Finalize the HMAC verification
123
// 比较计算出的HMAC与接收到的HMAC
124
// EVP_DigestVerifyFinal 成功返回1,失败返回0,错误返回<0
125
int verify_result = EVP_DigestVerifyFinal(ctx, received_hmac, received_hmac_len);
126
127
if (verify_result == 1) {
128
ret = 0; // 验证成功
129
} else if (verify_result == 0) {
130
fprintf(stderr, "HMAC verification failed (data mismatch or wrong key).\n");
131
ret = 1; // 验证失败 (HMAC不匹配)
132
} else {
133
fprintf(stderr, "Error during HMAC verification:\n");
134
ERR_print_errors_fp(stderr);
135
ret = -1; // 验证过程中发生错误
136
}
137
138
cleanup:
139
// 6. Clean up
140
if (ctx) {
141
EVP_MD_CTX_free(ctx);
142
}
143
if (pkey) {
144
EVP_PKEY_free(pkey);
145
}
146
147
return ret; // 0 for success, 1 for verification failure, -1 for error
148
}
149
150
151
/*
152
// 示例用法
153
int main() {
154
// OpenSSL初始化...
155
156
unsigned char data[] = "This is a message for HMAC.";
157
int data_len = strlen((char*)data);
158
159
unsigned char hmac_key[16] = { // 实际应用中需要安全生成和管理密钥
160
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
161
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10
162
};
163
int hmac_key_len = sizeof(hmac_key);
164
165
unsigned char* calculated_hmac = NULL;
166
size_t calculated_hmac_len = 0;
167
168
// 计算HMAC
169
if (calculate_hmac_sha256(data, data_len, hmac_key, hmac_key_len,
170
&calculated_hmac, &calculated_hmac_len) == 0) {
171
printf("Calculated HMAC-SHA256: ");
172
for (int i = 0; i < calculated_hmac_len; ++i) {
173
printf("%02x", calculated_hmac[i]);
174
}
175
printf("\n");
176
177
// 模拟接收到数据和HMAC
178
unsigned char received_data[] = "This is a message for HMAC."; // 与原始数据相同
179
int received_data_len = strlen((char*)received_data);
180
unsigned char* received_hmac = calculated_hmac; // 接收到的HMAC就是刚刚计算出的
181
182
// 验证HMAC
183
int verify_status = verify_hmac_sha256(received_data, received_data_len,
184
hmac_key, hmac_key_len,
185
received_hmac, calculated_hmac_len);
186
187
if (verify_status == 0) {
188
printf("HMAC verification successful. Data integrity and authenticity verified.\n");
189
} else if (verify_status == 1) {
190
printf("HMAC verification failed. Data may have been tampered or wrong key used.\n");
191
} else {
192
printf("HMAC verification encountered an error.\n");
193
}
194
195
free(calculated_hmac); // 释放计算出的HMAC缓冲区
196
197
// 模拟数据被篡改的情况
198
printf("\nAttempting verification with tampered data...\n");
199
unsigned char tampered_data[] = "This is a tampered message!";
200
int tampered_data_len = strlen((char*)tampered_data);
201
202
verify_status = verify_hmac_sha256(tampered_data, tampered_data_len,
203
hmac_key, hmac_key_len,
204
received_hmac, calculated_hmac_len);
205
206
if (verify_status == 0) {
207
printf("HMAC verification successful with tampered data? (This should not happen!)\n");
208
} else if (verify_status == 1) {
209
printf("HMAC verification correctly failed with tampered data.\n");
210
} else {
211
printf("HMAC verification encountered an error with tampered data.\n");
212
}
213
214
215
} else {
216
printf("HMAC calculation failed.\n");
217
}
218
219
// OpenSSL清理...
220
return 0;
221
}
222
*/
需要注意的是,EVP_DigestSignFinal
返回 1 表示成功将计算出的签名/HMAC放入缓冲区,而 EVP_DigestVerifyFinal
返回 1 表示验证成功,返回 0 表示验证失败(计算出的 MAC 与提供的 MAC 不匹配),返回小于 0 的值表示过程中发生错误。在实际应用中,务必区分这三种返回值。
本章我们深入探讨了对称加密和哈希函数的基本原理,并详细演示了如何使用 OpenSSL 的 EVP 接口来实现这些功能。对称加密提供了数据的机密性,而哈希函数和 HMAC 提供了数据的完整性和认证。掌握了这些基础,我们就可以在此基础上构建更复杂的安全应用。下一章将介绍非对称加密,它是实现密钥交换、数字签名和证书体系结构的基础。
5. 非对称加密与密钥管理
本章将带您深入探索非对称加密(Asymmetric Encryption)的奥秘,以及如何在OpenSSL库中有效地进行非对称密钥(Asymmetric Key)的管理和使用。我们将重点讲解RSA和ECC这两种主流的非对称加密算法(Asymmetric Encryption Algorithm),详细阐述如何生成、加载、保存密钥对(Key Pair),并演示如何利用它们进行数据的加解密(Encryption and Decryption)操作。通过本章的学习,您将掌握非对称加密的核心技术,并能在C++开发中灵活运用OpenSSL实现安全可靠的密钥管理和数据保护。
5.1 非对称加密算法原理概述
非对称加密,又称公钥加密(Public Key Cryptography),是一种密钥(Key)管理系统,它使用一对相关的密钥:公钥(Public Key)和私钥(Private Key)。这对密钥具有数学上的关联性,但从公钥推导出私钥在计算上是极其困难的。
① 原理核心:
▮▮▮▮⚝ 公钥:可以公开给任何人。用于加密数据或验证数字签名(Digital Signature)。
▮▮▮▮⚝ 私钥:必须严格保密,只有密钥所有者才能拥有。用于解密数据或创建数字签名。
② 工作流程:
② 数据加密:如果您想向某人发送只有他能阅读的消息,您可以使用他的公钥对消息进行加密。加密后的密文(Ciphertext)只能由他的私钥解密。
③ 数字签名:如果您想证明某个消息确实来源于您,您可以使用您的私钥对消息的哈希值(Hash Value)进行签名。其他人可以使用您的公钥来验证这个签名,从而确认消息的完整性(Integrity)和来源的真实性(Authenticity)。
③ 主要算法:
▮▮▮▮⚝ RSA (Rivest–Shamir–Adleman):基于大整数因子分解的困难性。广泛用于加密和数字签名,但相对于ECC,在相同安全强度下密钥长度更长,计算开销更大。
▮▮▮▮⚝ ECC (Elliptic Curve Cryptography):基于椭圆曲线离散对数的困难性。在提供相同安全强度的同时,可以使用更短的密钥,因此计算速度更快,更适用于资源受限的环境或需要高性能的应用。ECC主要用于密钥交换(Key Exchange,如ECDH)和数字签名(如ECDSA)。
在OpenSSL中,这些非对称算法的功能都集成在了libcrypto
库中。通过OpenSSL提供的API,我们可以方便地进行密钥的生成、管理和使用。
5.2 生成非对称密钥对
生成非对称密钥对是使用非对称加密的第一步。OpenSSL提供了相应的函数来生成RSA和ECC密钥对。
5.2.1 生成RSA密钥对
生成RSA密钥对需要指定密钥的长度(以比特为单位)以及公钥指数(Public Exponent)。常用的公钥指数是65537,因为它是一个质数,且二进制表示只有两个1,有利于提高计算效率。
OpenSSL中生成RSA密钥对的主要函数是 RSA_generate_key_ex
(或其变体)。
1
#include <openssl/rsa.h>
2
#include <openssl/pem.h>
3
#include <openssl/err.h>
4
#include <openssl/bio.h>
5
6
// 示例函数:生成RSA密钥对并保存到BIO
7
RSA* generate_rsa_key_pair(int bits) {
8
RSA* rsa = nullptr;
9
BIGNUM* bne = nullptr;
10
int ret = 0;
11
12
// 1. 创建一个BIGNUM对象作为公钥指数
13
bne = BN_new();
14
if (!bne) {
15
ERR_print_errors_fp(stderr);
16
goto err;
17
}
18
ret = BN_set_word(bne, RSA_F4); // RSA_F4 是 65537
19
20
if (ret != 1) {
21
ERR_print_errors_fp(stderr);
22
goto err;
23
}
24
25
// 2. 生成RSA密钥对
26
rsa = RSA_new();
27
if (!rsa) {
28
ERR_print_errors_fp(stderr);
29
goto err;
30
}
31
32
// 使用 RSA_generate_key_ex (OpenSSL 1.1.0+) 或 RSA_generate_key (较旧版本)
33
#if OPENSSL_VERSION_NUMBER < 0x10100000L // OpenSSL 1.1.0 之前的版本
34
ret = RSA_generate_key(bits, RSA_F4, nullptr, nullptr);
35
if (ret != 1) {
36
ERR_print_errors_fp(stderr);
37
goto err;
38
}
39
// 注意:较旧版本 RSA_generate_key 返回一个新的 RSA* 对象,需要赋值
40
// rsa = generated_rsa_key; // 假设 RSA_generate_key 返回 RSA*
41
// 但为了简化和兼容性,我们使用 RSA_generate_key_ex 的模式,该函数会填充已有的 RSA 结构体
42
// 如果必须使用旧 API,需要重新组织代码逻辑。这里以新 API 为主进行示例。
43
// 或者,如果您确定使用旧版本,可以直接调用 RSA_generate_key(bits, RSA_F4, nullptr, nullptr);
44
// 并在成功时返回生成的 RSA* 对象。
45
// 为了适应更现代的OpenSSL版本,我们优先使用 RSA_generate_key_ex 的模式。
46
// 实际上,在1.1.0+,RSA_generate_key_ex 是推荐使用的。
47
#else // OpenSSL 1.1.0 及以上版本
48
ret = RSA_generate_key_ex(rsa, bits, bne, nullptr);
49
if (ret != 1) {
50
ERR_print_errors_fp(stderr);
51
goto err;
52
}
53
#endif
54
55
// 3. 返回生成的RSA密钥对
56
BN_free(bne);
57
return rsa;
58
59
err:
60
// 错误处理和资源释放
61
RSA_free(rsa);
62
BN_free(bne);
63
return nullptr;
64
}
65
66
// 示例:生成一个2048比特的RSA密钥
67
/*
68
int main() {
69
OpenSSL_add_all_algorithms(); // 初始化算法
70
ERR_load_BIO_strings(); // 加载BIO错误字符串
71
ERR_load_crypto_strings(); // 加载其他加密错误字符串
72
73
int key_bits = 2048;
74
RSA* rsa_key = generate_rsa_key_pair(key_bits);
75
76
if (rsa_key) {
77
printf("RSA密钥对生成成功,位长:%d\n", key_bits);
78
79
// 可以在这里使用 PEM_write_bio_RSAPrivateKey 等函数将密钥保存到文件或内存
80
// 例如:
81
BIO* pri_bio = BIO_new(BIO_s_mem());
82
if (PEM_write_bio_RSAPrivateKey(pri_bio, rsa_key, nullptr, nullptr, 0, nullptr, nullptr)) {
83
printf("私钥已保存到内存BIO\n");
84
// 从BIO读取并打印密钥内容...
85
char* pri_pem;
86
long pri_len = BIO_get_mem_data(pri_bio, &pri_pem);
87
printf("Private Key:\n%s\n", pri_pem);
88
} else {
89
ERR_print_errors_fp(stderr);
90
}
91
BIO_free_all(pri_bio);
92
93
94
BIO* pub_bio = BIO_new(BIO_s_mem());
95
if (PEM_write_bio_RSA_PUBKEY(pub_bio, rsa_key)) { // 注意:导出公钥使用 RSA_PUBKEY
96
printf("公钥已保存到内存BIO\n");
97
char* pub_pem;
98
long pub_len = BIO_get_mem_data(pub_bio, &pub_pem);
99
printf("Public Key:\n%s\n", pub_pem);
100
} else {
101
ERR_print_errors_fp(stderr);
102
}
103
BIO_free_all(pub_bio);
104
105
106
RSA_free(rsa_key); // 释放RSA结构体
107
} else {
108
fprintf(stderr, "RSA密钥对生成失败\n");
109
}
110
111
EVP_cleanup(); // 清理EVP
112
ERR_free_strings(); // 释放错误字符串
113
// CRYPTO_cleanup_all_ex_data(); // 如果需要,清理额外的扩展数据
114
// ERR_remove_state(0); // 在多线程环境下可能需要清理线程本地的错误状态
115
116
return 0;
117
}
118
*/
代码讲解:
① BN_new()
和 BN_set_word()
:用于创建和设置大数(Big Number),这里用来表示公钥指数。RSA_F4
是OpenSSL定义的常量,表示65537。
② RSA_new()
:创建一个空的RSA结构体。
③ RSA_generate_key_ex(rsa, bits, bne, nullptr)
:核心函数,用于生成RSA密钥对。
▮▮▮▮ⓓ 第一个参数 rsa
:一个已创建的RSA*
结构体,生成的密钥将存储在这里。
▮▮▮▮ⓔ 第二个参数 bits
:密钥的位长度,如1024, 2048, 3072, 4096。推荐至少使用2048比特。
▮▮▮▮ⓕ 第三个参数 bne
:公钥指数的大数表示。
▮▮▮▮ⓖ 第四个参数 nullptr
:一个回调函数,用于在生成过程中显示进度信息(如果需要)。
⑧ 成功时函数返回1,失败返回0。失败时可以通过OpenSSL的错误栈获取详细错误信息。
⑨ RSA_free(rsa)
和 BN_free(bne)
:释放分配的内存资源。
5.2.2 生成ECC密钥对
生成ECC密钥对需要选择一个具体的椭圆曲线(Elliptic Curve)。OpenSSL支持多种标准的命名曲线(Named Curve)。每条曲线都有一个对应的数字标识符(NID)。
OpenSSL中生成ECC密钥对的主要函数是 EC_KEY_generate_key
。
1
#include <openssl/ec.h>
2
#include <openssl/obj_mac.h> // 包含各种NID宏定义
3
#include <openssl/pem.h>
4
#include <openssl/err.h>
5
#include <openssl/bio.h>
6
7
// 示例函数:生成ECC密钥对并保存到BIO
8
EC_KEY* generate_ecc_key_pair(int nid) {
9
EC_KEY* ec_key = nullptr;
10
EC_GROUP* ec_group = nullptr;
11
12
// 1. 根据NID获取椭圆曲线组
13
ec_group = EC_GROUP_new_by_nid(nid);
14
if (!ec_group) {
15
ERR_print_errors_fp(stderr);
16
goto err;
17
}
18
19
// 2. 创建一个EC_KEY结构体
20
ec_key = EC_KEY_new();
21
if (!ec_key) {
22
ERR_print_errors_fp(stderr);
23
goto err;
24
}
25
26
// 3. 设置密钥所属的曲线组
27
if (EC_KEY_set_group(ec_key, ec_group) != 1) {
28
ERR_print_errors_fp(stderr);
29
goto err;
30
}
31
32
// 4. 生成ECC密钥对 (私钥和公钥)
33
if (EC_KEY_generate_key(ec_key) != 1) {
34
ERR_print_errors_fp(stderr);
35
goto err;
36
}
37
38
// 5. 返回生成的ECC密钥对
39
EC_GROUP_free(ec_group); // 释放曲线组对象,ec_key 内部持有其引用
40
return ec_key;
41
42
err:
43
// 错误处理和资源释放
44
EC_GROUP_free(ec_group);
45
EC_KEY_free(ec_key);
46
return nullptr;
47
}
48
49
// 示例:生成一个使用secp256k1曲线的ECC密钥
50
/*
51
int main() {
52
OpenSSL_add_all_algorithms(); // 初始化算法
53
ERR_load_BIO_strings(); // 加载BIO错误字符串
54
ERR_load_crypto_strings(); // 加载其他加密错误字符串
55
56
// 选择一条曲线,例如:NID_secp256k1 (比特币等使用), NID_X9_62_prime256v1 (secp256r1) 等
57
int curve_nid = NID_secp256k1; // 或者 NID_X9_62_prime256v1;
58
59
EC_KEY* ecc_key = generate_ecc_key_pair(curve_nid);
60
61
if (ecc_key) {
62
printf("ECC密钥对生成成功,曲线NID:%d (%s)\n", curve_nid, OBJ_nid2sn(curve_nid));
63
64
// 可以在这里使用 PEM_write_bio_ECPrivateKey 等函数将密钥保存
65
// 例如:
66
BIO* pri_bio = BIO_new(BIO_s_mem());
67
if (PEM_write_bio_ECPrivateKey(pri_bio, ecc_key, nullptr, nullptr, 0, nullptr, nullptr)) {
68
printf("私钥已保存到内存BIO\n");
69
// 从BIO读取并打印密钥内容...
70
char* pri_pem;
71
long pri_len = BIO_get_mem_data(pri_bio, &pri_pem);
72
printf("Private Key:\n%s\n", pri_pem);
73
} else {
74
ERR_print_errors_fp(stderr);
75
}
76
BIO_free_all(pri_bio);
77
78
79
BIO* pub_bio = BIO_new(BIO_s_mem());
80
if (PEM_write_bio_EC_PUBKEY(pub_bio, ecc_key)) { // 注意:导出公钥使用 EC_PUBKEY
81
printf("公钥已保存到内存BIO\n");
82
char* pub_pem;
83
long pub_len = BIO_get_mem_data(pub_bio, &pub_pem);
84
printf("Public Key:\n%s\n", pub_pem);
85
} else {
86
ERR_print_errors_fp(stderr);
87
}
88
BIO_free_all(pub_bio);
89
90
91
EC_KEY_free(ecc_key); // 释放ECC结构体
92
} else {
93
fprintf(stderr, "ECC密钥对生成失败\n");
94
}
95
96
EVP_cleanup(); // 清理EVP
97
ERR_free_strings(); // 释放错误字符串
98
// CRYPTO_cleanup_all_ex_data(); // 如果需要,清理额外的扩展数据
99
// ERR_remove_state(0); // 在多线程环境下可能需要清理线程本地的错误状态
100
101
return 0;
102
}
103
*/
代码讲解:
① EC_GROUP_new_by_nid(nid)
:根据NID创建一个椭圆曲线组对象。NID是OpenSSL定义的一系列标识符,可以在openssl/obj_mac.h
中找到,例如NID_secp256k1
, NID_X9_62_prime256v1
(secp256r1)。
② EC_KEY_new()
:创建一个空的EC_KEY
结构体。
③ EC_KEY_set_group(ec_key, ec_group)
:将创建的曲线组设置到EC_KEY
结构体中。
④ EC_KEY_generate_key(ec_key)
:核心函数,在指定的曲线组上生成ECC密钥对(私钥和公钥),并存储在ec_key
中。
⑤ 成功时函数返回1,失败返回0。
⑥ EC_GROUP_free(ec_group)
:释放曲线组对象。注意,EC_KEY
内部会持有曲线组的引用,所以即使释放了ec_group
,只要ec_key
未释放,曲线组的实际内存不会被回收。
⑦ EC_KEY_free(ec_key)
:释放EC_KEY
结构体。
生成密钥后,通常需要将其保存到文件或数据库中,以便后续使用。常见的密钥文件格式是PEM(Privacy-Enhanced Mail)和DER(Distinguished Encoding Rules)。PEM是Base64编码的文本格式,以-----BEGIN...-----
开头,-----END...-----
结尾,方便在不同系统间传输和查看。DER是二进制格式,更紧凑,常用于证书和密钥存储。
5.3 加载和保存密钥对
在实际应用中,我们通常不会在每次需要使用时都重新生成密钥,而是从文件或内存中加载已有的密钥。OpenSSL提供了丰富的函数来加载和保存各种格式的密钥。使用BIO (Basic Input/Output) 是推荐的方式,因为它提供了对不同I/O源(文件、内存、套接字等)的抽象。
本节将演示如何使用BIO来加载和保存PEM/DER格式的RSA和ECC私钥及公钥。
5.3.1 加载私钥
加载私钥是加密、签名等操作的前提。私钥文件通常需要妥善保管,有时会用密码进行加密保护。
OpenSSL加载私钥的函数通常需要指定文件格式(PEM或DER)以及是否需要密码。使用BIO进行加载,主要的函数族是PEM_read_bio_PrivateKey
和d2i_PrivateKey_bio
。
1
#include <openssl/rsa.h>
2
#include <openssl/ec.h>
3
#include <openssl/pem.h>
4
#include <openssl/dsa.h> // DSA私钥加载也类似
5
#include <openssl/err.h>
6
#include <openssl/bio.h>
7
8
// 示例函数:从BIO加载通用的私钥 (RSA, ECC, DSA等)
9
EVP_PKEY* load_private_key_from_bio(BIO* bio, const char* password = nullptr) {
10
EVP_PKEY* pkey = nullptr;
11
12
// PEM格式
13
pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, (void*)password);
14
if (pkey) {
15
printf("成功加载PEM格式私钥\n");
16
return pkey;
17
}
18
19
// 检查是否是因为PEM格式错误,如果不是,尝试DER格式
20
unsigned long err = ERR_peek_last_error();
21
if (ERR_GET_LIB(err) == ERR_LIB_PEM && ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
22
// 如果是因为没有PEM开始行,可能是DER格式,重置BIO,尝试DER
23
if (BIO_reset(bio) != 0) { // 重置BIO的读取位置
24
// DER格式
25
pkey = d2i_PrivateKey_bio(bio, nullptr); // d2i_PrivateKey_bio 可以自动检测类型
26
if (pkey) {
27
printf("成功加载DER格式私钥\n");
28
return pkey;
29
}
30
} else {
31
fprintf(stderr, "警告: 无法重置BIO以尝试加载DER格式\n");
32
}
33
} else {
34
// 其他PEM错误或者不是PEM错误,打印错误栈
35
ERR_print_errors_fp(stderr);
36
}
37
38
39
// 如果以上都失败,打印最后的错误信息
40
ERR_print_errors_fp(stderr);
41
return nullptr;
42
}
43
44
// 示例:从文件加载私钥
45
/*
46
int main() {
47
OpenSSL_add_all_algorithms();
48
ERR_load_BIO_strings();
49
ERR_load_crypto_strings();
50
51
BIO* file_bio = BIO_new_file("private_key.pem", "rb"); // 假设有一个名为private_key.pem的文件
52
if (!file_bio) {
53
ERR_print_errors_fp(stderr);
54
return 1;
55
}
56
57
// 如果私钥文件有密码,将密码作为第二个参数传入 load_private_key_from_bio
58
EVP_PKEY* private_key = load_private_key_from_bio(file_bio, "mypassword"); // 替换为您的密码
59
60
if (private_key) {
61
printf("私钥加载成功!密钥类型:%s\n", OBJ_nid2sn(EVP_PKEY_id(private_key)));
62
// 现在可以使用 private_key 进行加密、签名等操作
63
EVP_PKEY_free(private_key); // 使用完毕后释放
64
} else {
65
fprintf(stderr, "私钥加载失败\n");
66
}
67
68
BIO_free_all(file_bio); // 释放BIO
69
70
EVP_cleanup();
71
ERR_free_strings();
72
73
return 0;
74
}
75
*/
代码讲解:
① EVP_PKEY*
:这是一个通用的密钥结构体,可以表示多种类型的密钥(RSA, ECC, DSA等)。在OpenSSL 1.0.0+版本中,推荐使用EVP_PKEY
来管理密钥,而非直接使用RSA*
或EC_KEY*
,因为这提供了更好的抽象和算法独立性。
② PEM_read_bio_PrivateKey(bio, nullptr, nullptr, (void*)password)
:尝试从PEM格式的BIO中读取私钥。
▮▮▮▮ⓒ 第一个参数 bio
:要读取的BIO对象。
▮▮▮▮ⓓ 第二个参数 nullptr
:一个可选的已存在的EVP_PKEY*
,如果提供,读取的内容会填充到这个结构体中,否则会创建一个新的。通常传入nullptr
让函数创建新的。
▮▮▮▮ⓔ 第三个参数 nullptr
:一个回调函数,用于在读取加密私钥时获取密码。如果私钥未加密或密码直接提供给第四个参数,可以为nullptr
。
▮▮▮▮ⓕ 第四个参数 (void*)password
:如果私钥是加密的,直接提供密码字符串。
⑦ d2i_PrivateKey_bio(bio, nullptr)
:尝试从DER格式的BIO中读取私钥。这个函数通常能够自动识别私钥的类型(RSA, ECC, DSA等)。
⑧ BIO_reset(bio)
:重置BIO的内部指针到开始位置,以便可以重新读取。在尝试了PEM格式失败后,如果怀疑是DER格式,需要重置BIO。
⑨ ERR_peek_last_error()
和 ERR_GET_LIB
, ERR_GET_REASON
:用于检查最近一次错误的库和原因码。这里用来判断PEM读取失败是否是因为PEM_R_NO_START_LINE
,从而确定是否应该尝试DER格式。
⑩ EVP_PKEY_free(pkey)
:释放EVP_PKEY
结构体。
5.3.2 加载公钥
加载公钥用于验证数字签名或加密数据。公钥可以从私钥中提取,也可以从证书中获取,或者单独存储在公钥文件中。
OpenSSL加载公钥的函数通常是PEM_read_bio_PUBKEY
和d2i_PUBKEY_bio
。
1
#include <openssl/rsa.h>
2
#include <openssl/ec.h>
3
#include <openssl/pem.h>
4
#include <openssl/err.h>
5
#include <openssl/bio.h>
6
7
// 示例函数:从BIO加载通用的公钥
8
EVP_PKEY* load_public_key_from_bio(BIO* bio) {
9
EVP_PKEY* pkey = nullptr;
10
11
// PEM格式
12
pkey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr);
13
if (pkey) {
14
printf("成功加载PEM格式公钥\n");
15
return pkey;
16
}
17
18
// 检查是否是因为PEM格式错误,如果不是,尝试DER格式
19
unsigned long err = ERR_peek_last_error();
20
if (ERR_GET_LIB(err) == ERR_LIB_PEM && ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
21
if (BIO_reset(bio) != 0) { // 重置BIO
22
// DER格式
23
pkey = d2i_PUBKEY_bio(bio, nullptr); // d2i_PUBKEY_bio 可以自动检测类型
24
if (pkey) {
25
printf("成功加载DER格式公钥\n");
26
return pkey;
27
}
28
} else {
29
fprintf(stderr, "警告: 无法重置BIO以尝试加载DER格式\n");
30
}
31
} else {
32
// 其他PEM错误或者不是PEM错误,打印错误栈
33
ERR_print_errors_fp(stderr);
34
}
35
36
37
// 如果以上都失败,打印最后的错误信息
38
ERR_print_errors_fp(stderr);
39
return nullptr;
40
}
41
42
// 示例:从文件加载公钥
43
/*
44
int main() {
45
OpenSSL_add_all_algorithms();
46
ERR_load_BIO_strings();
47
ERR_load_crypto_strings();
48
49
BIO* file_bio = BIO_new_file("public_key.pem", "rb"); // 假设有一个名为public_key.pem的文件
50
if (!file_bio) {
51
ERR_print_errors_fp(stderr);
52
return 1;
53
}
54
55
EVP_PKEY* public_key = load_public_key_from_bio(file_bio);
56
57
if (public_key) {
58
printf("公钥加载成功!密钥类型:%s\n", OBJ_nid2sn(EVP_PKEY_id(public_key)));
59
// 现在可以使用 public_key 进行加密或验证签名
60
EVP_PKEY_free(public_key); // 使用完毕后释放
61
} else {
62
fprintf(stderr, "公钥加载失败\n");
63
}
64
65
BIO_free_all(file_bio); // 释放BIO
66
67
EVP_cleanup();
68
ERR_free_strings();
69
70
return 0;
71
}
72
*/
代码讲解:
① PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr)
:从PEM格式的BIO中读取公钥。参数与加载私钥类似,但不需要密码参数。
② d2i_PUBKEY_bio(bio, nullptr)
:从DER格式的BIO中读取公钥。
③ BIO_reset(bio)
和错误检查逻辑与加载私钥类似,用于处理可能的PEM/DER格式。
④ EVP_PKEY_free(pkey)
:释放EVP_PKEY
结构体。
5.3.3 保存密钥到文件
将生成的或加载的密钥保存到持久化存储是密钥管理的重要环节。与加载类似,保存密钥也可以使用BIO,支持PEM和DER格式。
OpenSSL保存密钥的函数族是PEM_write_bio_PrivateKey
, i2d_PrivateKey_bio
, PEM_write_bio_PUBKEY
, i2d_PUBKEY_bio
等。
1
#include <openssl/rsa.h>
2
#include <openssl/ec.h>
3
#include <openssl/pem.h>
4
#include <openssl/err.h>
5
#include <openssl/bio.h>
6
#include <openssl/evp.h>
7
8
// 示例函数:将通用的私钥保存到BIO (PEM或DER)
9
int save_private_key_to_bio(BIO* bio, EVP_PKEY* pkey, int format /* 0 for PEM, 1 for DER */, const EVP_CIPHER* cipher = nullptr, const unsigned char* pass = nullptr, int passlen = 0) {
10
int ret = 0;
11
if (!bio || !pkey) {
12
return 0;
13
}
14
15
if (format == 0) { // 保存为PEM格式
16
// 如果 cipher 不为 nullptr,私钥会被加密
17
ret = PEM_write_bio_PrivateKey(bio, pkey, cipher, pass, passlen, nullptr, nullptr);
18
if (ret == 1) {
19
printf("私钥成功保存为PEM格式\n");
20
} else {
21
fprintf(stderr, "保存私钥为PEM格式失败\n");
22
ERR_print_errors_fp(stderr);
23
}
24
} else if (format == 1) { // 保存为DER格式
25
ret = i2d_PrivateKey_bio(bio, pkey); // i2d_PrivateKey_bio 自动处理密钥类型
26
if (ret > 0) { // DER格式返回写入的字节数
27
printf("私钥成功保存为DER格式 (%d 字节)\n", ret);
28
ret = 1; // 统一返回1表示成功
29
} else {
30
fprintf(stderr, "保存私钥为DER格式失败\n");
31
ERR_print_errors_fp(stderr);
32
ret = 0; // 统一返回0表示失败
33
}
34
} else {
35
fprintf(stderr, "未知的文件格式 %d\n", format);
36
ret = 0;
37
}
38
39
return ret;
40
}
41
42
// 示例函数:将通用的公钥保存到BIO (PEM或DER)
43
int save_public_key_to_bio(BIO* bio, EVP_PKEY* pkey, int format /* 0 for PEM, 1 for DER */) {
44
int ret = 0;
45
if (!bio || !pkey) {
46
return 0;
47
}
48
49
if (format == 0) { // 保存为PEM格式
50
ret = PEM_write_bio_PUBKEY(bio, pkey); // PEM_write_bio_PUBKEY 自动处理密钥类型
51
if (ret == 1) {
52
printf("公钥成功保存为PEM格式\n");
53
} else {
54
fprintf(stderr, "保存公钥为PEM格式失败\n");
55
ERR_print_errors_fp(stderr);
56
}
57
} else if (format == 1) { // 保存为DER格式
58
ret = i2d_PUBKEY_bio(bio, pkey); // i2d_PUBKEY_bio 自动处理密钥类型
59
if (ret > 0) { // DER格式返回写入的字节数
60
printf("公钥成功保存为DER格式 (%d 字节)\n", ret);
61
ret = 1; // 统一返回1表示成功
62
} else {
63
fprintf(stderr, "保存公钥为DER格式失败\n");
64
ERR_print_errors_fp(stderr);
65
ret = 0; // 统一返回0表示失败
66
}
67
} else {
68
fprintf(stderr, "未知的文件格式 %d\n", format);
69
ret = 0;
70
}
71
72
return ret;
73
}
74
75
76
// 示例:生成RSA密钥对并保存到文件
77
/*
78
int main() {
79
OpenSSL_add_all_algorithms();
80
ERR_load_BIO_strings();
81
ERR_load_crypto_strings();
82
83
// 生成一个RSA密钥对
84
int key_bits = 2048;
85
BIGNUM* bne = BN_new();
86
BN_set_word(bne, RSA_F4);
87
RSA* rsa_key = RSA_new();
88
if (RSA_generate_key_ex(rsa_key, key_bits, bne, nullptr) != 1) {
89
ERR_print_errors_fp(stderr);
90
BN_free(bne);
91
RSA_free(rsa_key);
92
return 1;
93
}
94
BN_free(bne);
95
96
// 将 RSA* 转换为 EVP_PKEY*
97
EVP_PKEY* pkey = EVP_PKEY_new();
98
if (!pkey) {
99
ERR_print_errors_fp(stderr);
100
RSA_free(rsa_key);
101
return 1;
102
}
103
if (EVP_PKEY_assign_RSA(pkey, rsa_key) != 1) { // 将 rsa_key 赋值给 pkey, pkey 现在拥有 rsa_key 的所有权
104
ERR_print_errors_fp(stderr);
105
// 注意:EVP_PKEY_assign_RSA 成功后,不应该再单独释放 rsa_key
106
EVP_PKEY_free(pkey); // 释放 pkey 会同时释放 rsa_key
107
return 1;
108
}
109
// rsa_key = nullptr; // 避免后续误用已转移所有权的指针
110
111
// 保存私钥到文件 (PEM格式,不加密)
112
BIO* pri_file_bio_pem = BIO_new_file("rsa_private_key_noenc.pem", "wb");
113
if (pri_file_bio_pem) {
114
save_private_key_to_bio(pri_file_bio_pem, pkey, 0); // 0 for PEM
115
BIO_free_all(pri_file_bio_pem);
116
} else {
117
ERR_print_errors_fp(stderr);
118
}
119
120
// 保存私钥到文件 (PEM格式,使用AES-256-CBC加密,指定密码)
121
BIO* pri_file_bio_enc_pem = BIO_new_file("rsa_private_key_aes256.pem", "wb");
122
if (pri_file_bio_enc_pem) {
123
const char* password = "mysecretpassword";
124
save_private_key_to_bio(pri_file_bio_enc_pem, pkey, 0, EVP_aes_256_cbc(), (const unsigned char*)password, strlen(password));
125
BIO_free_all(pri_file_bio_enc_pem);
126
} else {
127
ERR_print_errors_fp(stderr);
128
}
129
130
131
// 保存公钥到文件 (PEM格式)
132
BIO* pub_file_bio_pem = BIO_new_file("rsa_public_key.pem", "wb");
133
if (pub_file_bio_pem) {
134
save_public_key_to_bio(pub_file_bio_pem, pkey, 0); // 0 for PEM
135
BIO_free_all(pub_file_bio_pem);
136
} else {
137
ERR_print_errors_fp(stderr);
138
}
139
140
// 保存私钥到文件 (DER格式)
141
BIO* pri_file_bio_der = BIO_new_file("rsa_private_key.der", "wb");
142
if (pri_file_bio_der) {
143
save_private_key_to_bio(pri_file_bio_der, pkey, 1); // 1 for DER
144
BIO_free_all(pri_file_bio_der);
145
} else {
146
ERR_print_errors_fp(stderr);
147
}
148
149
// 保存公钥到文件 (DER格式)
150
BIO* pub_file_bio_der = BIO_new_file("rsa_public_key.der", "wb");
151
if (pub_file_bio_der) {
152
save_public_key_to_bio(pub_file_bio_der, pkey, 1); // 1 for DER
153
BIO_free_all(pub_file_bio_der);
154
} else {
155
ERR_print_errors_fp(stderr);
156
}
157
158
159
EVP_PKEY_free(pkey); // 释放 EVP_PKEY (内部包含 RSA* 的释放)
160
161
EVP_cleanup();
162
ERR_free_strings();
163
164
return 0;
165
}
166
*/
代码讲解:
① PEM_write_bio_PrivateKey(bio, pkey, cipher, pass, passlen, nullptr, nullptr)
:将私钥保存为PEM格式。
▮▮▮▮ⓑ 第三个参数 cipher
:如果不是nullptr
,指定用于加密私钥的对称加密算法,如EVP_aes_256_cbc()
。
▮▮▮▮ⓒ 第四个参数 pass
和 第五个参数 passlen
:用于加密私钥的密码和密码长度。
④ i2d_PrivateKey_bio(bio, pkey)
:将私钥保存为DER格式。函数返回写入的字节数,成功时大于0。
⑤ PEM_write_bio_PUBKEY(bio, pkey)
:将公钥保存为PEM格式。
⑥ i2d_PUBKEY_bio(bio, pkey)
:将公钥保存为DER格式。函数返回写入的字节数,成功时大于0。
⑦ 在示例中,首先生成了RSA*
密钥,然后使用EVP_PKEY_assign_RSA(pkey, rsa_key)
将其赋值给一个EVP_PKEY*
对象。这个函数会转移rsa_key
的所有权,意味着后续只需要释放pkey
即可,rsa_key
会被自动释放。这是OpenSSL中管理不同密钥类型的一种常用模式。对于ECC密钥,可以使用EVP_PKEY_assign_EC_KEY(pkey, ec_key)
。
5.4 使用密钥对进行数据加解密
非对称加密的主要用途是用于密钥交换和数字签名。虽然也可以直接用于数据加解密,但由于其计算开销大且对数据长度有限制,通常只用于加密小块数据,如对称加密的密钥(Session Key)。
5.4.1 RSA加解密
RSA可以用于使用公钥加密数据,然后使用私钥解密。或者使用私钥加密(实际上是签名过程的一部分),使用公钥解密(验证签名)。
OpenSSL提供了RSA_public_encrypt
, RSA_private_decrypt
, RSA_private_encrypt
, RSA_public_decrypt
等函数。在使用这些函数时,需要考虑填充(Padding)方式。常见的填充方式有:
⚝ RSA_PKCS1_PADDING
: 最早的填充标准,理论上存在安全风险,但在很多旧协议中仍在使用。
⚝ RSA_PKCS1_OAEP_PADDING
: PKCS#1 v2.0引入,基于OAEP(Optimal Asymmetric Encryption Padding),是目前推荐用于加密的填充方式。
⚝ RSA_NO_PADDING
: 不使用填充,需要输入数据长度精确等于密钥模长减去少量开销。仅用于特殊场景,不推荐直接用于加密任意数据。
1
#include <openssl/rsa.h>
2
#include <openssl/pem.h>
3
#include <openssl/err.h>
4
#include <openssl/bio.h>
5
#include <string.h>
6
7
// 示例函数:使用RSA公钥加密数据
8
// 返回加密后的数据长度,失败返回-1
9
int rsa_public_encrypt(RSA* rsa_pub_key, const unsigned char* in, int in_len, unsigned char* out, int padding) {
10
int out_len = -1;
11
if (!rsa_pub_key || !in || !out || in_len <= 0) {
12
fprintf(stderr, "输入参数无效\n");
13
return -1;
14
}
15
16
// RSA加密的数据长度不能超过 RSA_size(rsa_pub_key) - 填充长度
17
// 对于 PKCS1_OAEP_PADDING,最大加密长度约为 RSA_size(rsa_pub_key) - 42
18
// 对于 PKCS1_PADDING,最大加密长度约为 RSA_size(rsa_pub_key) - 11
19
// 对于 NO_PADDING,输入长度必须精确等于 RSA_size(rsa_pub_key)
20
int rsa_len = RSA_size(rsa_pub_key); // 获取RSA模长对应的字节数
21
22
if (padding == RSA_PKCS1_OAEP_PADDING) {
23
if (in_len > rsa_len - 42) { // 估算值,实际取决于哈希算法等
24
fprintf(stderr, "输入数据过长,PKCS1_OAEP_PADDING 最大支持 %d 字节\n", rsa_len - 42);
25
ERR_print_errors_fp(stderr);
26
return -1;
27
}
28
} else if (padding == RSA_PKCS1_PADDING) {
29
if (in_len > rsa_len - 11) {
30
fprintf(stderr, "输入数据过长,PKCS1_PADDING 最大支持 %d 字节\n", rsa_len - 11);
31
ERR_print_errors_fp(stderr);
32
return -1;
33
}
34
} else if (padding == RSA_NO_PADDING) {
35
if (in_len != rsa_len) {
36
fprintf(stderr, "输入数据长度必须精确等于模长 %d 字节,当前输入 %d 字节\n", rsa_len, in_len);
37
ERR_print_errors_fp(stderr);
38
return -1;
39
}
40
}
41
42
43
out_len = RSA_public_encrypt(in_len, in, out, rsa_pub_key, padding);
44
45
if (out_len == -1) {
46
ERR_print_errors_fp(stderr);
47
}
48
49
return out_len;
50
}
51
52
// 示例函数:使用RSA私钥解密数据
53
// 返回解密后的数据长度,失败返回-1
54
int rsa_private_decrypt(RSA* rsa_pri_key, const unsigned char* in, int in_len, unsigned char* out, int padding) {
55
int out_len = -1;
56
if (!rsa_pri_key || !in || !out || in_len <= 0) {
57
fprintf(stderr, "输入参数无效\n");
58
return -1;
59
}
60
61
int rsa_len = RSA_size(rsa_pri_key); // 获取RSA模长对应的字节数
62
// 解密的输入数据长度必须等于模长
63
if (in_len != rsa_len) {
64
fprintf(stderr, "解密输入数据长度必须等于模长 %d 字节,当前输入 %d 字节\n", rsa_len, in_len);
65
ERR_print_errors_fp(stderr);
66
return -1;
67
}
68
69
70
out_len = RSA_private_decrypt(in_len, in, out, rsa_pri_key, padding);
71
72
if (out_len == -1) {
73
ERR_print_errors_fp(stderr);
74
}
75
76
return out_len;
77
}
78
79
80
// 示例:RSA加解密过程
81
/*
82
int main() {
83
OpenSSL_add_all_algorithms();
84
ERR_load_BIO_strings();
85
ERR_load_crypto_strings();
86
87
// 1. 生成或加载RSA密钥对 (这里简化为直接生成)
88
int key_bits = 2048;
89
BIGNUM* bne = BN_new();
90
BN_set_word(bne, RSA_F4);
91
RSA* rsa_key = RSA_new();
92
if (RSA_generate_key_ex(rsa_key, key_bits, bne, nullptr) != 1) {
93
ERR_print_errors_fp(stderr);
94
BN_free(bne);
95
RSA_free(rsa_key);
96
return 1;
97
}
98
BN_free(bne);
99
100
// 获取公钥和私钥指针
101
// 注意:RSA* 对象同时包含公钥和私钥信息。
102
// 在进行加密/解密时,OpenSSL函数内部会根据操作类型(public_encrypt vs private_decrypt)
103
// 使用相应的密钥分量。所以同一个RSA* 对象即可。
104
// 但为了清晰起见,有时也会倾向于只传递包含必要信息的对象(例如:从证书加载的RSA公钥)。
105
// 这里使用生成的完整密钥对对象。
106
107
const unsigned char plaintext[] = "This is a short message to be encrypted with RSA.";
108
int plaintext_len = strlen((const char*)plaintext);
109
int rsa_len = RSA_size(rsa_key); // 加密输出和解密输入的长度都等于RSA模长
110
111
// 分配加密和解密缓冲区
112
unsigned char* encrypted_data = (unsigned char*)malloc(rsa_len);
113
unsigned char* decrypted_data = (unsigned char*)malloc(rsa_len); // 解密后的最大长度也等于模长
114
115
if (!encrypted_data || !decrypted_data) {
116
fprintf(stderr, "内存分配失败\n");
117
free(encrypted_data);
118
free(decrypted_data);
119
RSA_free(rsa_key);
120
return 1;
121
}
122
123
// 2. 使用公钥加密
124
printf("原文:%s (长度:%d)\n", plaintext, plaintext_len);
125
// 推荐使用 RSA_PKCS1_OAEP_PADDING
126
int encrypted_len = rsa_public_encrypt(rsa_key, plaintext, plaintext_len, encrypted_data, RSA_PKCS1_OAEP_PADDING);
127
128
if (encrypted_len != -1) {
129
printf("加密成功,密文长度:%d\n", encrypted_len);
130
// 密文通常是二进制数据,不适合直接打印
131
// printf("密文:%s\n", encrypted_data); // 不要这样做
132
} else {
133
fprintf(stderr, "RSA公钥加密失败\n");
134
goto cleanup;
135
}
136
137
// 3. 使用私钥解密
138
int decrypted_len = rsa_private_decrypt(rsa_key, encrypted_data, encrypted_len, decrypted_data, RSA_PKCS1_OAEP_PADDING);
139
140
if (decrypted_len != -1) {
141
printf("解密成功,解密后长度:%d\n", decrypted_len);
142
// 解密后的数据可能不是以null结尾的字符串,根据实际情况处理
143
printf("解密后数据:");
144
fwrite(decrypted_data, 1, decrypted_len, stdout);
145
printf("\n");
146
147
// 比较解密后的数据与原文是否一致
148
if (decrypted_len == plaintext_len && memcmp(plaintext, decrypted_data, plaintext_len) == 0) {
149
printf("解密后的数据与原文一致!\n");
150
} else {
151
printf("解密后的数据与原文不一致!\n");
152
}
153
154
} else {
155
fprintf(stderr, "RSA私钥解密失败\n");
156
}
157
158
159
cleanup:
160
// 释放资源
161
free(encrypted_data);
162
free(decrypted_data);
163
RSA_free(rsa_key);
164
165
EVP_cleanup();
166
ERR_free_strings();
167
168
return 0;
169
}
170
*/
代码讲解:
① RSA_size(rsa_key)
:返回RSA密钥模长(Modulus)的字节数。这是加密输出和解密输入的固定长度。
② RSA_public_encrypt(in_len, in, out, rsa_pub_key, padding)
:使用rsa_pub_key
中的公钥部分对输入数据in
进行加密,结果存入out
。
③ RSA_private_decrypt(in_len, in, out, rsa_pri_key, padding)
:使用rsa_pri_key
中的私钥部分对输入数据in
进行解密,结果存入out
。
④ padding
参数指定填充方式。对于加密和解密,使用的填充方式必须一致。RSA_PKCS1_OAEP_PADDING
是推荐用于加密的填充方式。
⑤ 加密函数的返回值是密文的长度,解密函数的返回值是解密后明文的长度。失败返回-1。
⑥ RSA加密的数据长度有限制,不能超过模长减去填充所需的开销。因此,RSA不适合直接加密大文件。
5.4.2 ECC加解密(通常用于密钥交换)
正如前面提到的,ECC通常不直接用于数据加解密,而是主要用于密钥交换(如ECDH)和数字签名(ECDSA)。ECC的数学特性使其更适合于这些用途,尤其是在性能和密钥长度方面优于RSA。
本小节将重点介绍ECC在密钥交换中的应用——ECDH (Elliptic Curve Diffie–Hellman)。ECDH允许通信双方在不安全的信道上协商出一个只有他们知道的共享秘密(Shared Secret),这个秘密可以作为对称加密算法的密钥来加密后续的通信数据。
ECDH的过程大致如下:
① 双方各自生成ECC密钥对(私钥和公钥),使用相同的曲线参数。
② 双方交换各自的ECC公钥。
③ 每一方使用对方的公钥和自己的私钥计算出一个共享秘密。根据椭圆曲线数学的特性,双方计算出的共享秘密是相同的。
OpenSSL中实现ECDH的关键函数是 ECDH_compute_key
。
1
#include <openssl/ec.h>
2
#include <openssl/obj_mac.h>
3
#include <openssl/err.h>
4
#include <openssl/bio.h>
5
#include <string.h>
6
7
// 示例函数:使用ECDH计算共享秘密
8
// 返回共享秘密的长度,失败返回-1
9
int compute_ecdh_shared_secret(EC_KEY* private_key, const EC_KEY* peer_public_key, unsigned char* out, size_t out_len) {
10
if (!private_key || !peer_public_key || !out || out_len <= 0) {
11
fprintf(stderr, "输入参数无效\n");
12
return -1;
13
}
14
15
// 获取对端的公钥点
16
const EC_POINT* peer_pub_point = EC_KEY_get0_public_key(peer_public_key);
17
if (!peer_pub_point) {
18
ERR_print_errors_fp(stderr);
19
return -1;
20
}
21
22
// 获取本端的私钥大数
23
const BIGNUM* private_bn = EC_KEY_get0_private_key(private_key);
24
if (!private_bn) {
25
ERR_print_errors_fp(stderr);
26
return -1;
27
}
28
29
// 获取本端的曲线组
30
const EC_GROUP* group = EC_KEY_get0_group(private_key);
31
if (!group) {
32
ERR_print_errors_fp(stderr);
33
return -1;
34
}
35
36
37
// 计算共享秘密
38
// ECDH_compute_key(out, out_len, peer_pub_point, private_key, nullptr); // 旧 API
39
// ECDH_compute_key 使用本端的私钥和对端的公钥点进行计算
40
int shared_secret_len = ECDH_compute_key(out, out_len, peer_pub_point, private_key, nullptr);
41
42
43
if (shared_secret_len <= 0) {
44
ERR_print_errors_fp(stderr);
45
return -1;
46
}
47
48
return shared_secret_len;
49
}
50
51
// 示例:ECDH密钥交换过程
52
/*
53
int main() {
54
OpenSSL_add_all_algorithms();
55
ERR_load_BIO_strings();
56
ERR_load_crypto_strings();
57
58
// 1. 双方(Alice 和 Bob)使用相同的曲线参数生成各自的ECC密钥对
59
int curve_nid = NID_secp256k1; // 例如使用secp256k1曲线
60
61
EC_KEY* alice_key = generate_ecc_key_pair(curve_nid);
62
EC_KEY* bob_key = generate_ecc_key_pair(curve_nid);
63
64
if (!alice_key || !bob_key) {
65
fprintf(stderr, "生成ECC密钥对失败\n");
66
EC_KEY_free(alice_key);
67
EC_KEY_free(bob_key);
68
return 1;
69
}
70
printf("Alice 和 Bob 各自生成了ECC密钥对\n");
71
72
// 2. 双方交换各自的ECC公钥
73
// 在实际应用中,公钥会通过网络等方式传输。
74
// 这里我们直接从各自的 EC_KEY 结构体中获取公钥部分。
75
// Alice 将 Bob 的公钥点和自己的私钥结合计算共享秘密
76
// Bob 将 Alice 的公钥点和自己的私钥结合计算共享秘密
77
78
// 计算共享秘密的最大可能长度(曲线的字段大小,通常是曲线位数 / 8)
79
int field_size = EC_GROUP_get_degree(EC_KEY_get0_group(alice_key));
80
size_t max_shared_secret_len = (field_size + 7) / 8; // 将比特转换为字节
81
82
unsigned char* alice_shared_secret = (unsigned char*)malloc(max_shared_secret_len);
83
unsigned char* bob_shared_secret = (unsigned char*)malloc(max_shared_secret_len);
84
85
if (!alice_shared_secret || !bob_shared_secret) {
86
fprintf(stderr, "内存分配失败\n");
87
free(alice_shared_secret);
88
free(bob_shared_secret);
89
EC_KEY_free(alice_key);
90
EC_KEY_free(bob_key);
91
return 1;
92
}
93
94
95
// 3. Alice 使用 Bob 的公钥和自己的私钥计算共享秘密
96
printf("Alice 使用 Bob 的公钥和自己的私钥计算共享秘密...\n");
97
int alice_secret_len = compute_ecdh_shared_secret(alice_key, bob_key, alice_shared_secret, max_shared_secret_len);
98
99
if (alice_secret_len <= 0) {
100
fprintf(stderr, "Alice 计算共享秘密失败\n");
101
goto cleanup;
102
}
103
printf("Alice 计算出的共享秘密长度: %d\n", alice_secret_len);
104
105
106
// 4. Bob 使用 Alice 的公钥和自己的私钥计算共享秘密
107
printf("Bob 使用 Alice 的公钥和自己的私钥计算共享秘密...\n");
108
int bob_secret_len = compute_ecdh_shared_secret(bob_key, alice_key, bob_shared_secret, max_shared_secret_len);
109
110
if (bob_secret_len <= 0) {
111
fprintf(stderr, "Bob 计算共享秘密失败\n");
112
goto cleanup;
113
}
114
printf("Bob 计算出的共享秘密长度: %d\n", bob_secret_len);
115
116
117
// 5. 比较双方计算出的共享秘密
118
if (alice_secret_len == bob_secret_len && memcmp(alice_shared_secret, bob_shared_secret, alice_secret_len) == 0) {
119
printf("双方计算出的共享秘密一致!\n");
120
// 这个共享秘密 (alice_shared_secret/bob_shared_secret) 现在可以作为对称加密的密钥使用
121
// 例如,可以使用 SHA256 或其他哈希函数对这个共享秘密进行哈希,作为 AES 密钥
122
printf("共享秘密 (前16字节): ");
123
for (int i = 0; i < (alice_secret_len > 16 ? 16 : alice_secret_len); ++i) {
124
printf("%02x", alice_shared_secret[i]);
125
}
126
printf("...\n");
127
128
} else {
129
fprintf(stderr, "双方计算出的共享秘密不一致!\n");
130
}
131
132
133
cleanup:
134
// 释放资源
135
free(alice_shared_secret);
136
free(bob_shared_secret);
137
EC_KEY_free(alice_key);
138
EC_KEY_free(bob_key);
139
140
EVP_cleanup();
141
ERR_free_strings();
142
143
return 0;
144
}
145
*/
代码讲解:
① EC_KEY_get0_public_key(peer_public_key)
:从对方的ECC密钥结构体中获取公钥点(EC_POINT*
)。
② EC_KEY_get0_private_key(private_key)
:从自己的ECC密钥结构体中获取私钥大数(BIGNUM*
)。尽管ECDH_compute_key
的文档通常只说明需要EC_KEY*
和EC_POINT*
,但在OpenSSL 1.1.0+版本中,ECDH_compute_key
的函数原型是int ECDH_compute_key(void *out, size_t outlen, const EC_POINT *pub_key, EC_KEY *ecdh, void *(*KDF)(const void *in, size_t inlen, void *out, size_t *outlen))
。它需要对方的公钥点(pub_key
)和自己的私钥所在的EC_KEY结构体(ecdh
)。函数会内部使用ecdh
中的私钥。旧版本的API可能有所不同。
③ ECDH_compute_key(out, out_len, peer_pub_point, private_key, nullptr)
:这是核心函数,使用本端的私钥(来自private_key
)和对端的公钥点(peer_pub_point
)计算共享秘密,结果存储在out
缓冲区中,最大长度为out_len
。最后一个参数是一个可选的密钥派生函数(Key Derivation Function, KDF),如果传入nullptr
,则直接输出原始的共享秘密。在实际应用中,通常会使用KDF(如HKDF)来从原始共享秘密派生出用于对称加密的最终密钥,以提高安全性。
④ 函数返回计算出的共享秘密的实际长度,失败返回负数或0(取决于版本)。
⑤ 共享秘密的长度通常与曲线的字段大小相关,可以通过 EC_GROUP_get_degree
获取曲线的位数,然后除以8得到字节数。
通过ECDH协商出的共享秘密可以安全地用于后续的对称加密通信,例如使用AES。这结合了非对称加密在密钥交换中的优势和对称加密在数据加解密中的高性能。
至此,我们已经详细讲解了OpenSSL中非对称密钥的生成、加载、保存以及使用RSA进行加解密和使用ECC进行密钥交换的基本方法。在实际应用中,密钥管理涉及到更复杂的策略,如密钥生命周期、密钥轮换、硬件安全模块(Hardware Security Module, HSM)集成等。OpenSSL也提供了一些功能来支持这些高级场景,但超出本章的基本范畴。
本章为您构建了非对称加密在OpenSSL C++开发中的核心知识框架,后续章节将在此基础上,深入探讨数字签名、证书管理以及TLS/SSL等更高级的应用。
6. 数字签名与验证
本章将带您深入了解数字签名的概念及其在保障数据完整性(Integrity)和来源真实性(Authenticity)方面的作用。我们将详细讲解如何利用OpenSSL库提供的功能,特别是强大的EVP接口,来实现数字签名和验证的全过程。此外,我们还将探讨特定算法(如RSA和ECC/ECDSA)在签名和验证中的具体应用。通过本章的学习,读者将能够熟练地在自己的C++项目中集成数字签名功能,提升应用程序的安全性。
6.1 数字签名原理
数字签名(Digital Signature)是密码学中用于验证数字信息真实性的技术,其功能类似于现实生活中的手写签名。它能确保信息自签署以来未被篡改(完整性),并且证明信息确实是由签署者创建的(来源真实性/不可否认性 Non-repudiation)。
数字签名的核心原理是将哈希函数(Hash Function)和非对称加密(Asymmetric Encryption)结合使用。过程大致如下:
① 签名过程(Signer):
▮▮▮▮ⓑ 签署者(Signer)首先对原始消息(Original Message)计算一个哈希值(Hash Value),也称为消息摘要(Message Digest)。哈希函数保证即使原始消息发生微小改动,计算出的哈希值也会截然不同。常用的哈希算法有SHA-256、SHA-3等。
▮▮▮▮ⓒ 签署者使用自己的私钥(Private Key)对这个哈希值进行加密。
▮▮▮▮ⓓ 加密后的哈希值就是数字签名(Digital Signature)。
▮▮▮▮ⓔ 签署者将原始消息和数字签名一起发送给接收者(Recipient)。
② 验证过程(Recipient):
▮▮▮▮ⓑ 接收者收到消息和数字签名后,首先使用与签署者相同的哈希函数,独立地计算收到的原始消息的哈希值。
▮▮▮▮ⓒ 接收者使用签署者的公钥(Public Key)对收到的数字签名进行解密,得到一个哈希值。由于只有签署者的私钥才能正确加密,而其对应的公钥才能正确解密,这一步验证了签名的来源。
▮▮▮▮ⓓ 接收者将自己计算的哈希值与从数字签名中解密得到的哈希值进行比较。
▮▮▮▮ⓔ 如果两个哈希值完全一致,则验证成功。这表明:
▮▮▮▮▮▮▮▮❻ 消息在传输过程中没有被篡改(完整性),因为如果消息被改动,重新计算的哈希值会不同。
▮▮▮▮▮▮▮▮❼ 签名确实是由持有对应私钥的签署者创建的(来源真实性/不可否认性),因为只有正确的公钥才能解密出与消息对应的正确哈希值。
▮▮▮▮ⓗ 如果两个哈希值不一致,则验证失败,说明消息可能被篡改或签名不是由声称的签署者创建的。
关键点:
⚝ 数字签名是对消息的哈希值进行加密,而不是对整个消息进行加密,这大大提高了效率,尤其对于大型文件。
⚝ 使用的是签署者的私钥进行“加密”(更准确地说是签名算法,对于RSA通常是指数运算),使用其公钥进行“解密”(验证)。这与非对称加密用于保密性的方向是相反的。
OpenSSL提供了多种方式来实现数字签名和验证,其中EVP(Envelope)接口是推荐使用的抽象层,它提供了一致的API来处理不同的算法。
6.2 使用EVP接口进行签名
EVP(Envelope)接口是OpenSSL推荐使用的高级API,它为不同的加密算法、哈希算法和签名算法提供了一个统一的调用界面。使用EVP接口进行数字签名具有更好的灵活性和向前兼容性。
使用EVP接口进行签名的基本步骤如下:
① 创建一个签名上下文(Signature Context),使用EVP_SignInit_ex
函数指定哈希算法和签名算法。
② 使用EVP_SignUpdate
函数将待签名的数据喂给上下文。可以分多次调用此函数处理大量数据。
③ 使用EVP_SignFinal
函数完成签名计算,并获取生成的数字签名。此函数内部会使用私钥对哈希值进行签名。
④ 释放签名上下文和其他相关资源。
下面是一个使用EVP接口进行签名的C++示例代码框架:
1
#include <openssl/evp.h>
2
#include <openssl/pem.h>
3
#include <openssl/err.h>
4
#include <string>
5
#include <vector>
6
#include <iostream>
7
8
// 辅助函数:加载PEM格式的私钥
9
EVP_PKEY* load_private_key(const std::string& key_file_path) {
10
FILE* fp = fopen(key_file_path.c_str(), "r");
11
if (!fp) {
12
std::cerr << "Error opening private key file: " << key_file_path << std::endl;
13
return nullptr;
14
}
15
// 使用PEM_read_PrivateKey而不是PEM_read_bio_PrivateKey简化文件操作
16
EVP_PKEY* pkey = PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr);
17
fclose(fp);
18
if (!pkey) {
19
std::cerr << "Error reading private key file." << std::endl;
20
ERR_print_errors_fp(stderr);
21
}
22
return pkey;
23
}
24
25
// 使用EVP接口对数据进行签名
26
std::vector<unsigned char> sign_data_evp(const unsigned char* data, size_t data_len, EVP_PKEY* private_key, const EVP_MD* md) {
27
EVP_MD_CTX* mdctx = nullptr;
28
std::vector<unsigned char> signature;
29
unsigned int signature_len = 0;
30
31
// 创建签名上下文
32
mdctx = EVP_MD_CTX_new();
33
if (!mdctx) {
34
std::cerr << "Error creating EVP_MD_CTX." << std::endl;
35
goto err;
36
}
37
38
// 初始化签名操作,指定哈希算法和私钥
39
// 对于OpenSSL 1.1.0+, EVP_MD_CTX_init is deprecated, use EVP_MD_CTX_new/free
40
if (EVP_SignInit_ex(mdctx, md, nullptr) != 1) {
41
std::cerr << "Error in EVP_SignInit_ex." << std::endl;
42
ERR_print_errors_fp(stderr);
43
goto err;
44
}
45
46
// 更新签名数据(将数据喂给哈希函数)
47
if (EVP_SignUpdate(mdctx, data, data_len) != 1) {
48
std::cerr << "Error in EVP_SignUpdate." << std::endl;
49
ERR_print_errors_fp(stderr);
50
goto err;
51
}
52
53
// 结束签名操作并获取签名结果
54
// 首先获取签名长度
55
if (EVP_SignFinal(mdctx, nullptr, &signature_len, private_key) != 1) {
56
std::cerr << "Error getting signature length in EVP_SignFinal." << std::endl;
57
ERR_print_errors_fp(stderr);
58
goto err;
59
}
60
61
// 调整signature vector大小并获取最终签名
62
signature.resize(signature_len);
63
if (EVP_SignFinal(mdctx, signature.data(), &signature_len, private_key) != 1) {
64
std::cerr << "Error in EVP_SignFinal." << std::endl;
65
ERR_print_errors_fp(stderr);
66
goto err;
67
}
68
69
signature.resize(signature_len); // 确保vector大小准确
70
71
err:
72
if (mdctx) {
73
EVP_MD_CTX_free(mdctx);
74
}
75
return signature;
76
}
77
78
int main() {
79
// OpenSSL初始化
80
// For OpenSSL 1.1.0+
81
// OPENSSL_init_ssl(0, nullptr); // 如果只用libcrypto,可以省略此行
82
OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS | OPENSSL_INIT_ADD_ALL_CIPHERS | OPENSSL_INIT_ADD_ALL_DIGESTS, nullptr);
83
ERR_load_CRYPTO_strings(); // Load error strings
84
85
// 假设您已经生成了一个私钥文件 private_key.pem
86
// 可以使用 openssl genpkey -algorithm RSA -out private_key.pem -aes256
87
// 或者 openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout private_key.pem -out public_cert.pem
88
EVP_PKEY* private_key = load_private_key("private_key.pem");
89
if (!private_key) {
90
// 退出或处理错误
91
return 1;
92
}
93
94
std::string message = "This is the data to be signed.";
95
const unsigned char* data = reinterpret_cast<const unsigned char*>(message.c_str());
96
size_t data_len = message.length();
97
98
// 选择哈希算法,例如SHA-256
99
const EVP_MD* md = EVP_sha256(); // 或者EVP_get_digestbyname("sha256")
100
101
// 进行签名
102
std::vector<unsigned char> signature = sign_data_evp(data, data_len, private_key, md);
103
104
// 打印签名结果 (通常是二进制数据,这里简单打印长度)
105
if (!signature.empty()) {
106
std::cout << "Signature generated successfully. Length: " << signature.size() << " bytes." << std::endl;
107
// 可以在这里选择将签名存储到文件或发送
108
// 例如:FILE* sig_fp = fopen("signature.bin", "wb"); fwrite(signature.data(), 1, signature.size(), sig_fp); fclose(sig_fp);
109
} else {
110
std::cerr << "Failed to generate signature." << std::endl;
111
}
112
113
// 清理资源
114
EVP_PKEY_free(private_key);
115
EVP_cleanup(); // For OpenSSL < 1.1.0; 1.1.0+ cleanup is generally automatic
116
ERR_free_strings(); // Free error strings
117
118
return 0;
119
}
代码说明:
⚝ EVP_MD_CTX
(消息摘要上下文)是EVP接口中用于处理哈希和签名的核心结构体。
⚝ EVP_SignInit_ex
用于初始化签名操作,需要指定哈希算法(如EVP_sha256()
)和用于签名的私钥。
⚝ EVP_SignUpdate
用于将数据添加到待签名的消息流中。可以多次调用处理分块的数据。
⚝ EVP_SignFinal
用于计算最终的签名。第一次调用时,如果第二个参数(签名输出缓冲区)为nullptr
,则只返回签名长度。第二次调用时,提供缓冲区和长度,即可获取签名。
⚝ EVP_PKEY
是一个通用结构体,可以表示各种类型的密钥(RSA, ECC, DSA等)。
⚝ 加载密钥时使用了PEM_read_PrivateKey
函数,它从PEM格式的文件中读取私钥。
⚝ 重要的OpenSSL内存管理函数:EVP_PKEY_free
用于释放EVP_PKEY
对象,EVP_MD_CTX_free
用于释放上下文。
⚝ 错误处理:调用OpenSSL函数后,应检查返回值。如果返回表示失败,可以通过ERR_print_errors_fp(stderr)
打印详细的错误栈信息。
6.3 使用EVP接口进行签名验证
签名验证是签名过程的逆操作,接收者收到消息和签名后,需要验证签名是否有效。使用EVP接口进行验证的基本步骤如下:
① 创建一个验证上下文(Verification Context),使用EVP_VerifyInit_ex
函数指定哈希算法和验证算法。
② 使用EVP_VerifyUpdate
函数将待验证的数据喂给上下文。需要使用与签名时完全相同的数据和顺序。
③ 使用EVP_VerifyFinal
函数完成验证计算,并将计算出的哈希值与提供的签名进行比较。此函数内部会使用公钥对签名进行解密(或使用签名算法的验证部分)。
④ 释放验证上下文和其他相关资源。
EVP_VerifyFinal
函数返回一个整数:1
表示验证成功,0
表示验证失败,-1
表示发生错误。
下面是一个使用EVP接口进行签名验证的C++示例代码框架,通常与上面的签名代码配合使用:
1
#include <openssl/evp.h>
2
#include <openssl/pem.h>
3
#include <openssl/err.h>
4
#include <string>
5
#include <vector>
6
#include <iostream>
7
8
// 辅助函数:加载PEM格式的公钥
9
EVP_PKEY* load_public_key(const std::string& key_file_path) {
10
FILE* fp = fopen(key_file_path.c_str(), "r");
11
if (!fp) {
12
std::cerr << "Error opening public key file: " << key_file_path << std::endl;
13
return nullptr;
14
}
15
// 使用PEM_read_PUBKEY读取公钥
16
EVP_PKEY* pkey = PEM_read_PUBKEY(fp, nullptr, nullptr, nullptr);
17
fclose(fp);
18
if (!pkey) {
19
std::cerr << "Error reading public key file." << std::endl;
20
ERR_print_errors_fp(stderr);
21
}
22
return pkey;
23
}
24
25
// 使用EVP接口验证签名
26
// data: 原始数据
27
// data_len: 原始数据长度
28
// signature: 收到的数字签名
29
// signature_len: 数字签名长度
30
// public_key: 用于验证的公钥
31
// md: 使用的哈希算法
32
int verify_signature_evp(const unsigned char* data, size_t data_len,
33
const unsigned char* signature, size_t signature_len,
34
EVP_PKEY* public_key, const EVP_MD* md) {
35
EVP_MD_CTX* mdctx = nullptr;
36
int result = -1; // -1: error, 0: verify failed, 1: verify success
37
38
// 创建验证上下文
39
mdctx = EVP_MD_CTX_new();
40
if (!mdctx) {
41
std::cerr << "Error creating EVP_MD_CTX for verification." << std::endl;
42
goto err;
43
}
44
45
// 初始化验证操作,指定哈希算法和公钥
46
if (EVP_VerifyInit_ex(mdctx, md, nullptr) != 1) {
47
std::cerr << "Error in EVP_VerifyInit_ex." << std::endl;
48
ERR_print_errors_fp(stderr);
49
goto err;
50
}
51
52
// 更新验证数据(将数据喂给哈希函数)
53
if (EVP_VerifyUpdate(mdctx, data, data_len) != 1) {
54
std::cerr << "Error in EVP_VerifyUpdate." << std::endl;
55
ERR_print_errors_fp(stderr);
56
goto err;
57
}
58
59
// 结束验证操作,将计算出的哈希值与提供的签名进行比较
60
result = EVP_VerifyFinal(mdctx, signature, signature_len, public_key);
61
// result is 1 for success, 0 for failure, -1 for error
62
if (result != 1) {
63
if (result == 0) {
64
std::cerr << "Signature verification failed." << std::endl;
65
} else { // result == -1
66
std::cerr << "Error during signature verification." << std::endl;
67
ERR_print_errors_fp(stderr);
68
}
69
}
70
71
err:
72
if (mdctx) {
73
EVP_MD_CTX_free(mdctx);
74
}
75
return result;
76
}
77
78
int main() {
79
// OpenSSL初始化 (同签名部分)
80
OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS | OPENSSL_INIT_ADD_ALL_CIPHERS | OPENSSL_INIT_ADD_ALL_DIGESTS, nullptr);
81
ERR_load_CRYPTO_strings();
82
83
// 假设您有一个公钥文件 public_key.pem (可以从 private_key.pem 导出)
84
// openssl pkey -in private_key.pem -pubout -out public_key.pem
85
EVP_PKEY* public_key = load_public_key("public_key.pem"); // 或者从证书中提取的公钥
86
if (!public_key) {
87
// 退出或处理错误
88
return 1;
89
}
90
91
std::string message = "This is the data to be signed.";
92
const unsigned char* data = reinterpret_cast<const unsigned char*>(message.c_str());
93
size_t data_len = message.length();
94
95
// !!! 在实际应用中,这里的 signature 和 signature_len 是从发送方接收到的 !!!
96
// 为了演示目的,我们在这里生成一个签名
97
EVP_PKEY* private_key_for_demo = load_private_key("private_key.pem"); // 假设私钥存在
98
if (!private_key_for_demo) {
99
EVP_PKEY_free(public_key);
100
EVP_cleanup();
101
ERR_free_strings();
102
return 1;
103
}
104
105
const EVP_MD* md = EVP_sha256();
106
std::vector<unsigned char> signature = sign_data_evp(data, data_len, private_key_for_demo, md);
107
EVP_PKEY_free(private_key_for_demo); // 用完演示私钥后释放
108
109
if (signature.empty()) {
110
std::cerr << "Failed to generate signature for verification demo." << std::endl;
111
EVP_PKEY_free(public_key);
112
EVP_cleanup();
113
ERR_free_strings();
114
return 1;
115
}
116
// !!! 演示目的结束 !!! 实际应用中,接收方只需要验证步骤
117
118
// 进行验证
119
int verify_result = verify_signature_evp(data, data_len, signature.data(), signature.size(), public_key, md);
120
121
if (verify_result == 1) {
122
std::cout << "Signature verification succeeded! 😊" << std::endl;
123
} else if (verify_result == 0) {
124
std::cout << "Signature verification failed. 😢" << std::endl;
125
} else { // verify_result == -1
126
std::cerr << "An error occurred during verification. 😟" << std::endl;
127
}
128
129
// 清理资源
130
EVP_PKEY_free(public_key);
131
EVP_cleanup(); // For OpenSSL < 1.1.0
132
ERR_free_strings();
133
134
return 0;
135
}
代码说明:
⚝ 验证过程与签名过程类似,主要区别在于使用EVP_VerifyInit_ex
初始化上下文,使用公钥进行验证,最后调用EVP_VerifyFinal
。
⚝ EVP_VerifyFinal
的返回值直接指示验证结果(1为成功,0为失败)。
⚝ 加载公钥时使用了PEM_read_PUBKEY
,它从PEM格式的文件中读取公钥。公钥通常从证书(X.509)中提取,或者直接从公钥文件中读取。
使用EVP接口的好处在于,您可以轻松切换底层算法(例如,从RSA+SHA256切换到ECC+SHA384),而签名和验证的逻辑框架保持不变。
6.4 RSA签名与验证
除了EVP接口,OpenSSL也提供了针对特定算法的底层函数。对于RSA算法,OpenSSL提供了RSA_sign
和RSA_verify
函数来进行签名和验证。虽然EVP接口更通用,但在某些特定场景或为了与旧代码兼容时,可能会直接使用这些函数。
RSA_sign
函数:
1
int RSA_sign(int type, const unsigned char *m, unsigned int m_len,
2
unsigned char *sigret, unsigned int *siglen, RSA *rsa);
⚝ type
: 哈希算法的NID(Numeric ID),例如NID_sha256
。
⚝ m
: 待签名的消息摘要(哈希值)。注意,这里直接传入的是已经计算好的哈希值,而不是原始数据。
⚝ m_len
: 消息摘要的长度。
⚝ sigret
: 输出缓冲区,用于存储生成的数字签名。
⚝ siglen
: 输出参数,用于存储数字签名的实际长度。
⚝ rsa
: 用于签名的RSA私钥结构体。
RSA_verify
函数:
1
int RSA_verify(int type, const unsigned char *m, unsigned int m_len,
2
const unsigned char *sigbuf, unsigned int siglen, RSA *rsa);
⚝ type
: 哈希算法的NID。
⚝ m
: 待验证的消息摘要(哈希值)。与RSA_sign
的m
参数对应。
⚝ m_len
: 消息摘要的长度。
⚝ sigbuf
: 待验证的数字签名缓冲区。
⚝ siglen
: 数字签名的长度。
⚝ rsa
: 用于验证的RSA公钥结构体。
RSA_verify
函数返回1表示成功,0表示失败,-1表示错误。
使用RSA_sign
和RSA_verify
进行签名/验证的步骤:
① 签名:
▮▮▮▮ⓑ 计算原始数据的哈希值。
▮▮▮▮ⓒ 使用RSA_sign
函数,传入哈希算法NID、哈希值、哈希值长度、输出缓冲区、签名长度指针和RSA私钥结构体。
② 验证:
▮▮▮▮ⓑ 计算收到的原始数据的哈希值。
▮▮▮▮ⓒ 使用RSA_verify
函数,传入哈希算法NID、计算出的哈希值、哈希值长度、收到的签名、签名长度和RSA公钥结构体。
▮▮▮▮ⓓ 检查RSA_verify
的返回值。
示例代码(简略,省略了完整的初始化、密钥加载和错误处理):
1
#include <openssl/rsa.h>
2
#include <openssl/sha.h>
3
#include <openssl/evp.h> // 需要获取NID
4
#include <vector>
5
#include <iostream>
6
#include <string>
7
8
// 假设 private_rsa_key 和 public_rsa_key 已经加载为 RSA* 类型
9
// 这里的加载过程通常通过 EVP_PKEY_get0_RSA() 从 EVP_PKEY 中获取 RSA 结构体
10
11
/*
12
// 示例加载函数 (需要根据实际情况调整,这里仅为概念演示)
13
RSA* load_rsa_private_key(const std::string& key_file) {
14
FILE* fp = fopen(key_file.c_str(), "r");
15
if (!fp) return nullptr;
16
RSA* rsa_key = PEM_read_RSAPrivateKey(fp, nullptr, nullptr, nullptr); // 或 d2i_RSAPrivateKey_fp
17
fclose(fp);
18
return rsa_key;
19
}
20
RSA* load_rsa_public_key(const std::string& key_file) {
21
FILE* fp = fopen(key_file.c_str(), "r");
22
if (!fp) return nullptr;
23
RSA* rsa_key = PEM_read_RSA_PUBKEY(fp, nullptr, nullptr, nullptr); // 或 d2i_RSA_PUBKEY_fp
24
fclose(fp);
25
return rsa_key;
26
}
27
*/
28
29
// 使用RSA_sign进行签名 (需要先计算哈希值)
30
std::vector<unsigned char> rsa_sign_data(const unsigned char* data, size_t data_len, RSA* rsa_private_key, const EVP_MD* md_type) {
31
std::vector<unsigned char> hash_value(EVP_MD_size(md_type));
32
// 计算哈希值
33
if (!EVP_Digest(data, data_len, hash_value.data(), nullptr, md_type, nullptr)) {
34
std::cerr << "Error computing hash." << std::endl;
35
return {};
36
}
37
38
std::vector<unsigned char> signature(RSA_size(rsa_private_key));
39
unsigned int signature_len = 0;
40
int type_nid = EVP_MD_type(md_type); // 获取哈希算法的NID
41
42
if (RSA_sign(type_nid, hash_value.data(), hash_value.size(),
43
signature.data(), &signature_len, rsa_private_key) != 1) {
44
std::cerr << "Error in RSA_sign." << std::endl;
45
ERR_print_errors_fp(stderr);
46
return {};
47
}
48
49
signature.resize(signature_len);
50
return signature;
51
}
52
53
// 使用RSA_verify进行验证 (需要先计算哈希值)
54
int rsa_verify_signature(const unsigned char* data, size_t data_len,
55
const unsigned char* signature, size_t signature_len,
56
RSA* rsa_public_key, const EVP_MD* md_type) {
57
std::vector<unsigned char> hash_value(EVP_MD_size(md_type));
58
// 计算哈希值
59
if (!EVP_Digest(data, data_len, hash_value.data(), nullptr, md_type, nullptr)) {
60
std::cerr << "Error computing hash for verification." << std::endl;
61
return -1; // error
62
}
63
64
int type_nid = EVP_MD_type(md_type); // 获取哈希算法的NID
65
66
int result = RSA_verify(type_nid, hash_value.data(), hash_value.size(),
67
signature, signature_len, rsa_public_key);
68
69
// result is 1 for success, 0 for failure, -1 for error
70
if (result != 1) {
71
if (result == 0) {
72
std::cerr << "RSA signature verification failed." << std::endl;
73
} else { // result == -1
74
std::cerr << "Error during RSA signature verification." << std::endl;
75
ERR_print_errors_fp(stderr);
76
}
77
}
78
return result;
79
}
80
81
/*
82
int main() {
83
// 需要完整的OpenSSL初始化、加载密钥和错误处理
84
// ...
85
RSA* private_rsa = load_rsa_private_key("private_rsa.pem");
86
RSA* public_rsa = load_rsa_public_key("public_rsa.pem");
87
88
std::string msg = "Test message for RSA sign/verify.";
89
const unsigned char* data = reinterpret_cast<const unsigned char*>(msg.c_str());
90
size_t data_len = msg.length();
91
const EVP_MD* md = EVP_sha256();
92
93
std::vector<unsigned char> sig = rsa_sign_data(data, data_len, private_rsa, md);
94
95
if (!sig.empty()) {
96
std::cout << "RSA signature generated." << std::endl;
97
int verify_res = rsa_verify_signature(data, data_len, sig.data(), sig.size(), public_rsa, md);
98
if (verify_res == 1) {
99
std::cout << "RSA signature verified successfully. 😊" << std::endl;
100
} else {
101
std::cout << "RSA signature verification failed or error. 😢" << std::endl;
102
}
103
} else {
104
std::cerr << "Failed to generate RSA signature." << std::endl;
105
}
106
107
// 释放 RSA 密钥
108
RSA_free(private_rsa);
109
RSA_free(public_rsa);
110
// ... OpenSSL cleanup
111
return 0;
112
}
113
*/
代码说明:
⚝ 直接使用RSA_sign
和RSA_verify
需要先计算消息的哈希值,然后将哈希值传递给这些函数。这与EVP接口不同,EVP接口在内部完成了哈希计算。
⚝ 需要提供哈希算法的NID。可以通过EVP_MD_type
函数从EVP_MD
结构体中获取。
⚝ 需要将密钥加载为RSA*
结构体。这通常通过专门的PEM或DER读取函数,或者从EVP_PKEY
中提取(例如使用EVP_PKEY_get0_RSA
for OpenSSL 1.1.0+ 或 EVP_PKEY_get1_RSA
for older versions, 注意后者的引用计数)。
⚝ 同样需要注意RSA*
结构的内存管理,使用RSA_free
释放。
尽管RSA_sign
和RSA_verify
提供了更底层的控制,但通常情况下,使用EVP接口是更好的选择,因为它提供了算法无关性,并且更容易处理不同的密钥类型和填充模式(Padding Modes)。
6.5 ECC签名与验证(ECDSA)
椭圆曲线密码学(Elliptic Curve Cryptography, ECC)也支持数字签名,最常见的算法是椭圆曲线数字签名算法(Elliptic Curve Digital Signature Algorithm, ECDSA)。ECDSA相比于相同安全强度的RSA,通常需要更短的密钥长度,因此在性能和带宽方面具有优势。
OpenSSL为ECDSA提供了EVP接口和底层的特定算法函数。同样,EVP接口是推荐使用的。
EVP接口进行ECDSA签名/验证:
使用EVP接口进行ECDSA签名和验证的流程与RSA完全一致,只需要在初始化时指定ECC密钥和相应的哈希算法即可。EVP接口会自动处理底层的ECDSA操作。这是EVP接口强大之处的又一体现。
例如,如果您的EVP_PKEY* private_key
和EVP_PKEY* public_key
是ECC密钥,并且您选择了SHA-256作为哈希算法(EVP_sha256()
),那么 Section 6.2 和 Section 6.3 中的sign_data_evp
和verify_signature_evp
函数可以直接用于ECDSA签名和验证,无需修改。
底层ECDSA函数:
OpenSSL也提供了直接操作EC_KEY*
结构体的ECDSA底层函数,例如:
1
ECDSA_SIG *ECDSA_do_sign(const unsigned char *dgst, int dgstlen, EC_KEY *eckey);
2
int ECDSA_do_verify(const unsigned char *dgst, int dgstlen,
3
const ECDSA_SIG *sig, EC_KEY *eckey);
4
// 还有 ECDSA_sign, ECDSA_verify 等函数,提供了更灵活的ASN.1 DER编码处理
5
// 例如:
6
// int ECDSA_sign(int type, const unsigned char *dgst, int dgstlen,
7
// unsigned char *sig, unsigned int *siglen, EC_KEY *eckey);
8
// int ECDSA_verify(int type, const unsigned char *dgst, int dgstlen,
9
// const unsigned char *sig, int siglen, EC_KEY *eckey);
⚝ dgst
: 消息摘要(哈希值)。
⚝ dgstlen
: 消息摘要长度。
⚝ eckey
: 用于签名(私钥)或验证(公钥)的EC密钥结构体。
⚝ sig
(ECDSA_do_sign
返回类型): 签名的结构体表示。
⚝ sig
(ECDSA_verify
参数): 签名的ASN.1 DER编码。
⚝ siglen
: 签名的长度。
⚝ type
(ECDSA_sign
/ECDSA_verify
): 哈希算法的NID。
使用ECDSA_sign
和ECDSA_verify
进行签名/验证的步骤与RSA类似:
① 签名:
▮▮▮▮ⓑ 计算原始数据的哈希值。
▮▮▮▮ⓒ 使用ECDSA_sign
函数,传入哈希算法NID、哈希值、哈希值长度、输出缓冲区、签名长度指针和EC私钥结构体。
② 验证:
▮▮▮▮ⓑ 计算收到的原始数据的哈希值。
▮▮▮▮ⓒ 使用ECDSA_verify
函数,传入哈希算法NID、计算出的哈希值、哈希值长度、收到的签名(ASN.1 DER编码)、签名长度和EC公钥结构体。
▮▮▮▮ⓓ 检查ECDSA_verify
的返回值。
示例代码(简略,省略了完整的初始化、密钥加载和错误处理):
1
#include <openssl/ecdsa.h>
2
#include <openssl/ec.h>
3
#include <openssl/sha.h>
4
#include <openssl/evp.h>
5
#include <vector>
6
#include <iostream>
7
#include <string>
8
9
// 假设 private_ec_key 和 public_ec_key 已经加载为 EC_KEY* 类型
10
// 这里的加载过程通常通过 EVP_PKEY_get0_EC_KEY() 从 EVP_PKEY 中获取 EC_KEY 结构体
11
12
/*
13
// 示例加载函数 (需要根据实际情况调整)
14
EC_KEY* load_ec_private_key(const std::string& key_file) {
15
FILE* fp = fopen(key_file.c_str(), "r");
16
if (!fp) return nullptr;
17
EC_KEY* ec_key = PEM_read_ECPrivateKey(fp, nullptr, nullptr, nullptr); // 或 d2i_ECPrivateKey_fp
18
fclose(fp);
19
return ec_key;
20
}
21
// 从公钥文件加载 EC_KEY* 通常需要知道曲线类型,或者从证书中提取
22
// PEM_read_EC_PUBKEY 可能更直接 (OpenSSL 1.0.2+) 或 PEM_read_bio_EC_PUBKEY
23
EC_KEY* load_ec_public_key(const std::string& key_file) {
24
FILE* fp = fopen(key_file.c_str(), "r");
25
if (!fp) return nullptr;
26
EC_KEY* ec_key = PEM_read_EC_PUBKEY(fp, nullptr, nullptr, nullptr);
27
fclose(fp);
28
return ec_key;
29
}
30
*/
31
32
// 使用ECDSA_sign进行签名 (需要先计算哈希值)
33
std::vector<unsigned char> ecdsa_sign_data(const unsigned char* data, size_t data_len, EC_KEY* ec_private_key, const EVP_MD* md_type) {
34
std::vector<unsigned char> hash_value(EVP_MD_size(md_type));
35
if (!EVP_Digest(data, data_len, hash_value.data(), nullptr, md_type, nullptr)) {
36
std::cerr << "Error computing hash." << std::endl;
37
return {};
38
}
39
40
// ECDSA签名结果长度是可变的,与曲线大小和哈希长度有关
41
// ECDSA_size(eckey) 可以给出最大可能的签名长度
42
std::vector<unsigned char> signature(ECDSA_size(ec_private_key));
43
unsigned int signature_len = 0;
44
int type_nid = EVP_MD_type(md_type);
45
46
if (ECDSA_sign(type_nid, hash_value.data(), hash_value.size(),
47
signature.data(), &signature_len, ec_private_key) != 1) {
48
std::cerr << "Error in ECDSA_sign." << std::endl;
49
ERR_print_errors_fp(stderr);
50
return {};
51
}
52
53
signature.resize(signature_len);
54
return signature;
55
}
56
57
// 使用ECDSA_verify进行验证 (需要先计算哈希值)
58
int ecdsa_verify_signature(const unsigned char* data, size_t data_len,
59
const unsigned char* signature, size_t signature_len,
60
EC_KEY* ec_public_key, const EVP_MD* md_type) {
61
std::vector<unsigned char> hash_value(EVP_MD_size(md_type));
62
if (!EVP_Digest(data, data_len, hash_value.data(), nullptr, md_type, nullptr)) {
63
std::cerr << "Error computing hash for verification." << std::endl;
64
return -1;
65
}
66
67
int type_nid = EVP_MD_type(md_type);
68
69
int result = ECDSA_verify(type_nid, hash_value.data(), hash_value.size(),
70
signature, signature_len, ec_public_key);
71
72
// result is 1 for success, 0 for failure, -1 for error
73
if (result != 1) {
74
if (result == 0) {
75
std::cerr << "ECDSA signature verification failed." << std::endl;
76
} else { // result == -1
77
std::cerr << "Error during ECDSA signature verification." << std::endl;
78
ERR_print_errors_fp(stderr);
79
}
80
}
81
return result;
82
}
83
84
/*
85
int main() {
86
// 需要完整的OpenSSL初始化、加载密钥和错误处理
87
// ...
88
// 生成一个EC密钥,例如 NIST P-256 曲线
89
// EC_KEY* private_ec = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
90
// EC_KEY_generate_key(private_ec);
91
// EC_KEY* public_ec = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); // 创建一个新 EC_KEY 结构体用于公钥
92
// EC_KEY_set_public_key(public_ec, EC_KEY_get0_public_key(private_ec)); // 设置公钥部分
93
94
// 或者从文件加载
95
EC_KEY* private_ec = load_ec_private_key("private_ec.pem");
96
EC_KEY* public_ec = load_ec_public_key("public_ec.pem"); // 或者从证书中提取
97
98
std::string msg = "Test message for ECDSA sign/verify.";
99
const unsigned char* data = reinterpret_cast<const unsigned char*>(msg.c_str());
100
size_t data_len = msg.length();
101
const EVP_MD* md = EVP_sha256(); // ECDSA 通常使用 SHA256 或更强的哈希
102
103
std::vector<unsigned char> sig = ecdsa_sign_data(data, data_len, private_ec, md);
104
105
if (!sig.empty()) {
106
std::cout << "ECDSA signature generated. Length: " << sig.size() << " bytes." << std::endl;
107
int verify_res = ecdsa_verify_signature(data, data_len, sig.data(), sig.size(), public_ec, md);
108
if (verify_res == 1) {
109
std::cout << "ECDSA signature verified successfully. 😊" << std::endl;
110
} else {
111
std::cout << "ECDSA signature verification failed or error. 😢" << std::endl;
112
}
113
} else {
114
std::cerr << "Failed to generate ECDSA signature." << std::endl;
115
}
116
117
// 释放 EC_KEY 密钥
118
EC_KEY_free(private_ec);
119
EC_KEY_free(public_ec);
120
// ... OpenSSL cleanup
121
return 0;
122
}
123
*/
代码说明:
⚝ 底层ECDSA函数需要操作EC_KEY*
结构体。
⚝ 与RSA类似,底层函数需要先计算哈希值,然后传入哈希值和EC密钥。
⚝ ECDSA签名的格式通常是ASN.1 DER编码,ECDSA_sign
和ECDSA_verify
处理这种格式。
⚝ 需要注意EC_KEY*
结构的内存管理,使用EC_KEY_free
释放。
总的来说,虽然可以直接使用底层的RSA或ECDSA函数,但OpenSSL推荐使用EVP接口,因为它提供了统一的编程模型,更容易处理不同算法和参数的切换,减少了代码的复杂性。在实际开发中,除非有特定的理由(例如与只能使用底层函数的遗留系统集成),否则应优先考虑使用EVP接口进行数字签名和验证。
7. 证书(X.509)管理
本章深入讲解 X.509 证书的结构、解析、验证以及如何使用 OpenSSL 生成证书和证书签名请求(CSR)。理解 X.509 证书对于构建基于公钥基础设施(PKI)的安全应用至关重要,它是 TLS/SSL、数字签名验证等多种安全机制的核心。
7.1 X.509 证书结构与格式
X.509 是一种国际电信联盟(ITU-T)标准,定义了公钥证书的格式。它是 PKI (Public Key Infrastructure) 的核心组成部分。一个 X.509 证书将一个公钥 (Public Key) 绑定到一个身份(如个人、组织或设备),并由一个受信任的第三方,即证书颁发机构(CA, Certificate Authority),进行数字签名 (Digital Signature)。
7.1.1 X.509 证书的关键字段
X.509 证书包含了一系列标准字段,这些字段提供了关于证书、持有人、颁发者以及有效性等方面的信息。主要的字段包括:
① 版本号 (Version): 指示证书遵循的 X.509 版本(当前常用的是 v3)。
② 序列号 (Serial Number): CA 分配的唯一标识符,在其颁发的证书中是唯一的。
③ 签名算法标识 (Signature Algorithm Identifier): 用于 CA 签名此证书的算法(如 SHA256 with RSA)。
④ 颁发者名称 (Issuer Name): 颁发此证书的 CA 的可分辨名称(DN, Distinguished Name)。
⑤ 有效期 (Validity Period): 证书的有效时间范围,包括“不早于”(Not Before)和“不晚于”(Not After)两个日期。
⑥ 主题名称 (Subject Name): 证书持有者的可分辨名称。
⑦ 主题公钥信息 (Subject Public Key Info): 包含证书持有者的公钥和公钥算法(如 RSA 公钥、ECC 公钥)。
⑧ 颁发者唯一标识符 (Issuer Unique Identifier) (可选,v2/v3): CA 的唯一标识符,如果名称可能重复。
⑨ 主题唯一标识符 (Subject Unique Identifier) (可选, v2/v3): 持有者的唯一标识符,如果名称可能重复。
⑩ 扩展字段 (Extensions) (v3): X.509 v3 引入的关键增强,提供了额外的功能和信息,如:
▮▮▮▮⚝ 主题备用名称 (Subject Alternative Name, SAN): 允许用域名、IP 地址、电子邮件地址等替代或补充主题名称。
▮▮▮▮⚝ 密钥用法 (Key Usage): 指示公钥的用途(如数字签名、密钥加密、证书签名等)。
▮▮▮▮⚝ 扩展密钥用法 (Extended Key Usage, EKU): 指示证书的特定用途(如服务器认证、客户端认证、代码签名等)。
▮▮▮▮⚝ 基本约束 (Basic Constraints): 指示证书是否是 CA 证书,如果是,可以链式认证的最长路径长度。
▮▮▮▮⚝ 证书策略 (Certificate Policies): 指示 CA 遵循的策略。
▮▮▮▮⚝ 颁发者密钥标识符 (Issuer Key Identifier): 标识用于签名此证书的 CA 的特定密钥。
▮▮▮▮⚝ 权限信息访问 (Authority Information Access, AIA): 指定如何访问 CA 的颁发者证书和在线证书状态(OCSP)。
▮▮▮▮⚝ CRL 分发点 (CRL Distribution Points, CDP): 指定如何获取证书撤销列表(CRL)。
⑪ 签名值 (Signature Value): CA 使用其私钥对证书前面所有字段的哈希值 (Hash Value) 进行加密生成。
X.509 证书的结构是基于 ASN.1 (Abstract Syntax Notation One) 标准定义的,通常使用 DER (Distinguished Encoding Rules) 或 BER (Basic Encoding Rules) 进行编码。
7.1.2 证书的常用格式
证书的二进制数据可以表示为不同的文件格式:
① DER 格式 (DER format):
▮▮▮▮⚝ 二进制格式。
▮▮▮▮⚝ 不包含可读的头部和尾部标记。
▮▮▮▮⚝ 文件扩展名通常是 .der
或 .cer
(有时 .cer
也是 PEM 格式)。
▮▮▮▮⚝ 主要用于 Java 或 Windows 环境。
② PEM 格式 (PEM format):
▮▮▮▮⚝ ASCII 编码格式,基于 Base64。
▮▮▮▮⚝ 包含头部 -----BEGIN ...-----
和尾部 -----END ...-----
标记,使其易于文本编辑器查看和传输。
▮▮▮▮⚝ 文件扩展名通常是 .pem
, .crt
, .cer
, .key
(私钥), .csr
(CSR)。
▮▮▮▮⚝ 这是 OpenSSL 最常用的格式。
OpenSSL 库能够处理这些不同的格式,并在内部表示为特定的数据结构,如 X509
结构体。
7.2 加载和解析证书
在 C++ 开发中,使用 OpenSSL 加载和解析 X.509 证书是常见的操作。这通常涉及到使用 BIO (Basic Input/Output) 对象来读取证书数据,然后使用 OpenSSL 提供的函数将其解析为 X509
结构体。
7.2.1 从文件加载证书
使用 BIO 从文件中加载证书是常见的方式。你需要先创建一个文件 BIO,然后使用相应的 PEM 或 DER 读取函数。
1
#include <openssl/bio.h>
2
#include <openssl/x509.h>
3
#include <openssl/pem.h>
4
#include <openssl/err.h>
5
#include <iostream>
6
7
// 需要包含 OpenSSL 初始化和清理函数,通常在主函数或应用启动时调用
8
// #include <openssl/ssl.h> // 包含 SSL_library_init 等函数
9
// void init_openssl() {
10
// SSL_library_init();
11
// SSL_load_error_strings();
12
// OpenSSL_add_all_algorithms();
13
// }
14
// void cleanup_openssl() {
15
// EVP_cleanup(); // Cleanup algorithms
16
// ERR_free_strings(); // Free error strings
17
// }
18
19
20
X509* load_cert_from_file(const char* filename) {
21
BIO* cert_bio = BIO_new_file(filename, "r");
22
if (!cert_bio) {
23
std::cerr << "Error opening certificate file: " << filename << std::endl;
24
ERR_print_errors_fp(stderr);
25
return nullptr;
26
}
27
28
// 尝试按 PEM 格式读取
29
X509* cert = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr);
30
if (!cert) {
31
// 如果 PEM 读取失败,尝试按 DER 格式读取
32
// 需要重置 BIO 的文件指针或重新打开文件,这里简单起见省略重置逻辑
33
// 更健壮的做法是根据文件扩展名或内容头判断格式
34
BIO_reset(cert_bio); // 尝试重置 BIO
35
cert = d2i_X509_bio(cert_bio, nullptr);
36
if (!cert) {
37
std::cerr << "Error reading certificate from file: " << filename << std::endl;
38
ERR_print_errors_fp(stderr);
39
BIO_free(cert_bio);
40
return nullptr;
41
}
42
}
43
44
BIO_free(cert_bio);
45
return cert; // 调用者负责使用 X509_free 释放
46
}
47
48
int main() {
49
// init_openssl(); // 初始化 OpenSSL
50
51
X509* cert = load_cert_from_file("server.crt"); // 替换为你的证书文件路径
52
53
if (cert) {
54
std::cout << "Certificate loaded successfully!" << std::endl;
55
// 在这里可以进一步解析证书信息
56
X509_free(cert);
57
}
58
59
// cleanup_openssl(); // 清理 OpenSSL
60
return 0;
61
}
⚝ BIO_new_file(filename, "r")
创建一个读取文件的 BIO 对象。
⚝ PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr)
尝试从 BIO 读取 PEM 格式的 X.509 证书。
⚝ d2i_X509_bio(cert_bio, nullptr)
尝试从 BIO 读取 DER 格式的 X.509 证书。d2i
系列函数用于从二进制格式(如 DER)解析到内部结构体(如 X509
),i2d
系列函数则用于将内部结构体序列化为二进制格式。
⚝ 加载成功的 X509*
指针需要在使用完毕后调用 X509_free()
释放内存,遵循 OpenSSL 的内存管理约定。
7.2.2 从内存加载证书
有时证书数据存储在内存缓冲区中(例如,从网络接收)。你可以使用内存 BIO 来加载。
1
#include <openssl/bio.h>
2
#include <openssl/x509.h>
3
#include <openssl/pem.h>
4
#include <openssl/err.h>
5
#include <string>
6
#include <vector>
7
8
X509* load_cert_from_memory(const unsigned char* cert_data, size_t data_len) {
9
// BIO_new_mem_buf 会复制数据,因此 cert_data 可以是临时的
10
// 如果不想复制,可以使用 BIO_new_static_buf,但需要确保内存有效
11
BIO* cert_bio = BIO_new_mem_buf(cert_data, data_len);
12
if (!cert_bio) {
13
std::cerr << "Error creating memory BIO." << std::endl;
14
ERR_print_errors_fp(stderr);
15
return nullptr;
16
}
17
18
X509* cert = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr);
19
if (!cert) {
20
BIO_reset(cert_bio); // 重置 BIO 读写位置
21
cert = d2i_X509_bio(cert_bio, nullptr);
22
if (!cert) {
23
std::cerr << "Error reading certificate from memory buffer." << std::endl;
24
ERR_print_errors_fp(stderr);
25
BIO_free(cert_bio);
26
return nullptr;
27
}
28
}
29
30
BIO_free(cert_bio);
31
return cert; // 调用者负责使用 X509_free 释放
32
}
33
34
// 示例:加载一个 PEM 格式的字符串证书
35
int main() {
36
// init_openssl(); // 初始化 OpenSSL
37
38
std::string pem_cert_string =
39
"-----BEGIN CERTIFICATE-----\n"
40
// ... base64 encoded cert data ...
41
"MIICNTCCAZ6gAwIBAgIBADANBgkqhkiG9w0BAQsFADARMRMwEQYDVQQDDApjZXJ0\n"
42
// ... more data ...
43
"-----END CERTIFICATE-----\n"; // 替换为实际的 PEM 证书内容
44
45
X509* cert = load_cert_from_memory(
46
reinterpret_cast<const unsigned char*>(pem_cert_string.data()),
47
pem_cert_string.size()
48
);
49
50
if (cert) {
51
std::cout << "Certificate loaded from memory successfully!" << std::endl;
52
// 在这里可以进一步解析证书信息
53
X509_free(cert);
54
}
55
56
// cleanup_openssl(); // 清理 OpenSSL
57
return 0;
58
}
7.2.3 访问证书信息
一旦证书被加载到 X509
结构体中,就可以使用一系列 OpenSSL 函数来提取其字段信息。
1
#include <openssl/x509.h>
2
#include <openssl/x509v3.h> // For extensions
3
#include <openssl/pem.h>
4
#include <openssl/bio.h>
5
#include <openssl/err.h>
6
#include <iostream>
7
#include <string>
8
9
// ... load_cert_from_file 或 load_cert_from_memory 函数 ...
10
11
void print_cert_info(X509* cert) {
12
if (!cert) return;
13
14
// 获取主题名称 (Subject Name)
15
X509_NAME* subject_name = X509_get_subject_name(cert);
16
if (subject_name) {
17
char subject_str[256];
18
X509_NAME_oneline(subject_name, subject_str, sizeof(subject_str));
19
std::cout << "Subject: " << subject_str << std::endl;
20
}
21
22
// 获取颁发者名称 (Issuer Name)
23
X509_NAME* issuer_name = X509_get_issuer_name(cert);
24
if (issuer_name) {
25
char issuer_str[256];
26
X509_NAME_oneline(issuer_name, issuer_str, sizeof(issuer_str));
27
std::cout << "Issuer: " << issuer_str << std::endl;
28
}
29
30
// 获取序列号 (Serial Number)
31
ASN1_INTEGER* serial = X509_get_serialNumber(cert);
32
if (serial) {
33
BIGNUM* bn_serial = ASN1_INTEGER_to_BN(serial, nullptr);
34
if (bn_serial) {
35
char* serial_str = BN_bn2hex(bn_serial);
36
std::cout << "Serial Number: " << serial_str << std::endl;
37
OPENSSL_free(serial_str);
38
BN_free(bn_serial);
39
}
40
}
41
42
// 获取有效期 (Validity Period)
43
const ASN1_TIME* not_before = X509_get_notBefore(cert);
44
const ASN1_TIME* not_after = X509_get_notAfter(cert);
45
if (not_before && not_after) {
46
BIO* bio_nb = BIO_new(BIO_s_mem());
47
BIO* bio_na = BIO_new(BIO_s_mem());
48
if (bio_nb && bio_na) {
49
ASN1_TIME_print(bio_nb, not_before);
50
ASN1_TIME_print(bio_na, not_after);
51
char nb_buf[256], na_buf[256];
52
int nb_len = BIO_read(bio_nb, nb_buf, sizeof(nb_buf) - 1);
53
int na_len = BIO_read(bio_na, na_buf, sizeof(na_buf) - 1);
54
if (nb_len > 0) { nb_buf[nb_len] = '\0'; std::cout << "Not Before: " << nb_buf << std::endl; }
55
if (na_len > 0) { na_buf[na_len] = '\0'; std::cout << "Not After: " << na_buf << std::endl; }
56
BIO_free(bio_nb);
57
BIO_free(bio_na);
58
}
59
}
60
61
// 获取公钥 (Public Key)
62
EVP_PKEY* pubkey = X509_get_pubkey(cert);
63
if (pubkey) {
64
int type = EVP_PKEY_id(pubkey);
65
std::cout << "Public Key Type: " << OBJ_nid2sn(type) << std::endl;
66
// 可以在这里进一步检查密钥详情,例如 RSA 密钥长度等
67
EVP_PKEY_free(pubkey);
68
}
69
70
// 打印扩展信息 (Extensions) (例如 Subject Alternative Name)
71
// 这是一个遍历扩展的示例,具体的扩展需要根据OID进行判断和解析
72
STACK_OF(X509_EXTENSION)* extensions = X509_get0_extensions(cert);
73
if (extensions) {
74
std::cout << "Extensions:" << std::endl;
75
int num_exts = sk_X509_EXTENSION_num(extensions);
76
for (int i = 0; i < num_exts; ++i) {
77
X509_EXTENSION* ext = sk_X509_EXTENSION_value(extensions, i);
78
ASN1_OBJECT* obj = X509_EXTENSION_get_object(ext);
79
char ext_name[256];
80
OBJ_obj2txt(ext_name, sizeof(ext_name), obj, 0);
81
82
BIO* bio_ext = BIO_new(BIO_s_mem());
83
if (bio_ext) {
84
if (X509V3_EXT_print_bio(bio_ext, ext, 0, 0) > 0) {
85
BUF_MEM* bptr;
86
BIO_get_mem_ptr(bio_ext, &bptr);
87
std::cout << " " << ext_name << ": " << std::string(bptr->data, bptr->length) << std::endl;
88
}
89
BIO_free(bio_ext);
90
}
91
}
92
}
93
}
94
95
// int main() {
96
// // init_openssl();
97
// X509* cert = load_cert_from_file("server.crt");
98
// if (cert) {
99
// print_cert_info(cert);
100
// X509_free(cert);
101
// }
102
// // cleanup_openssl();
103
// return 0;
104
// }
⚝ X509_get_subject_name()
和 X509_get_issuer_name()
返回 X509_NAME
结构体,表示可分辨名称。
⚝ X509_NAME_oneline()
可以将 X509_NAME
转换为一行字符串,方便打印。
⚝ X509_get_serialNumber()
获取序列号,它是 ASN1_INTEGER
类型,需要转换为 BIGNUM
或字符串以便查看。
⚝ X509_get_notBefore()
和 X509_get_notAfter()
获取有效期,它们是 ASN1_TIME
类型,可以使用 ASN1_TIME_print()
打印。
⚝ X509_get_pubkey()
获取公钥信息,返回 EVP_PKEY
结构体,可以进一步解析具体的公钥类型和参数。
⚝ X509_get0_extensions()
获取证书的扩展列表。需要遍历列表并使用 X509V3_EXT_print_bio()
或其他特定扩展解析函数来获取扩展的值。
7.3 证书链构建与验证
证书链(Certificate Chain)是一系列数字证书,用于将一个终端实体证书(如服务器证书、用户证书)追溯到一个受信任的根 CA 证书(Root Certificate)。链中的每个证书(除了根证书)都由其前一个证书的私钥签名,链中的最后一个证书(通常是根证书)是自签名的,且其公钥被硬编码在操作系统或应用程序的信任存储(Trust Store)中。
7.3.1 证书链的构成
典型的证书链结构如下:
① 叶子证书 (Leaf Certificate) / 终端实体证书 (End-Entity Certificate): 这是你要验证的证书,例如你连接的网站服务器的证书。
② 中级证书 (Intermediate Certificate): 由根 CA 或另一个中级 CA 颁发,用于签发叶子证书或其他中级证书。链中可以有一个或多个中级证书。
③ 根证书 (Root Certificate): 自签名的 CA 证书,是信任链的起点。其公钥预装在大多数操作系统和浏览器中,构成信任锚(Trust Anchor)。
验证一个叶子证书的有效性,就是验证证书链的有效性:
① 验证链中每个证书的签名是否与其前一个证书的公钥匹配。
② 验证链中每个证书的有效期是否在当前时间范围内。
③ 验证链中每个证书是否未被撤销(通过 CRL 或 OCSP)。
④ 验证叶子证书的主题或 SAN 是否与你要验证的身份匹配(例如,网站域名)。
⑤ 验证链中的证书是否有合法的基本约束和密钥用法,以确保其角色正确(例如,CA 证书是否允许签发其他证书)。
⑥ 最终追溯到链中的根证书,并确认该根证书在你的信任存储中是受信任的。
7.3.2 使用 OpenSSL 进行证书链验证
OpenSSL 提供了强大的证书存储和验证机制,主要通过 X509_STORE
和 X509_STORE_CTX
结构体来实现。
⚝ X509_STORE
: 表示一个证书信任存储,你可以将受信任的根证书和中级 CA 证书加载到其中。
⚝ X509_STORE_CTX
: 表示一次特定的证书验证上下文,包含要验证的证书、可能的证书链(用于构建路径)、信任存储以及验证参数和回调函数。
验证步骤通常包括:
① 创建一个 X509_STORE
对象。
② 将信任锚(根证书)和任何必要的中级证书加载到 X509_STORE
中。这些证书可以是文件或目录形式。
③ 为每次验证创建一个 X509_STORE_CTX
对象,并关联 X509_STORE
、待验证的叶子证书以及可能的其他未排序的证书(OpenSSL 会尝试从这些证书构建完整的链)。
④ 设置验证参数,例如时间、标志等。
⑤ 调用 X509_verify_cert()
执行验证。
⑥ 检查返回值,如果失败,可以获取详细的错误信息。
⑦ 清理 X509_STORE_CTX
和 X509_STORE
对象。
1
#include <openssl/x509_vfy.h>
2
#include <openssl/x509.h>
3
#include <openssl/pem.h>
4
#include <openssl/bio.h>
5
#include <openssl/err.h>
6
#include <iostream>
7
#include <vector>
8
9
// ... load_cert_from_file 函数 ...
10
11
// 示例:验证证书链
12
int main() {
13
// init_openssl();
14
15
// 1. 创建并配置 X509_STORE (信任存储)
16
X509_STORE* store = X509_STORE_new();
17
if (!store) {
18
std::cerr << "Error creating X509_STORE." << std::endl;
19
ERR_print_errors_fp(stderr);
20
// cleanup_openssl();
21
return 1;
22
}
23
24
// 加载受信任的根证书或中级证书到信任存储中
25
// 方法 A: 从文件加载单个证书
26
X509* trusted_root_cert = load_cert_from_file("trusted_root.crt"); // 替换为你的根证书文件路径
27
if (trusted_root_cert) {
28
X509_STORE_add_cert(store, trusted_root_cert);
29
X509_free(trusted_root_cert); // STORE 会复制证书,加载后可以释放原始证书
30
} else {
31
// Error loading trusted root
32
X509_STORE_free(store);
33
// cleanup_openssl();
34
return 1;
35
}
36
37
// 方法 B: 从一个目录加载所有信任的证书
38
// if (X509_STORE_load_locations(store, nullptr, "/etc/ssl/certs") != 1) { // 替换为你的信任证书目录
39
// std::cerr << "Error loading trusted certificate locations." << std::endl;
40
// ERR_print_errors_fp(stderr);
41
// X509_STORE_free(store);
42
// // cleanup_openssl();
43
// return 1;
44
// }
45
46
// 2. 加载要验证的叶子证书 (例如,服务器证书)
47
X509* target_cert = load_cert_from_file("server_to_verify.crt"); // 替换为要验证的证书文件路径
48
if (!target_cert) {
49
X509_STORE_free(store);
50
// cleanup_openssl();
51
return 1;
52
}
53
54
// 3. 加载任何可能需要用于构建证书链的中级证书 (可选,如果你的 target_cert 文件不包含完整链)
55
// OpenSSL 验证函数也可以从 X509_STORE 或 SSL_CTX 中获取中级证书
56
STACK_OF(X509)* untrusted_certs = sk_X509_new_null();
57
// 示例:加载一个中级证书文件
58
// X509* intermediate_cert = load_cert_from_file("intermediate_ca.crt");
59
// if (intermediate_cert) {
60
// sk_X509_push(untrusted_certs, intermediate_cert);
61
// }
62
// ... 加载其他中级证书 ...
63
64
65
// 4. 创建 X509_STORE_CTX 并执行验证
66
X509_STORE_CTX* ctx = X509_STORE_CTX_new();
67
if (!ctx) {
68
std::cerr << "Error creating X509_STORE_CTX." << std::endl;
69
ERR_print_errors_fp(stderr);
70
X509_free(target_cert);
71
sk_X509_pop_free(untrusted_certs, X509_free); // 释放 untrusted_certs 栈及其内容
72
X509_STORE_free(store);
73
// cleanup_openssl();
74
return 1;
75
}
76
77
// 初始化验证上下文
78
// X509_STORE_CTX_init(ctx, store, target_cert, untrusted_certs);
79
// 注意:在 OpenSSL 1.1.0+ 中,推荐使用 X509_STORE_CTX_new() 后直接设置 store, cert 和 unchained
80
// 1.1.0+ API:
81
X509_STORE_CTX_set_cert(ctx, target_cert); // 设置要验证的证书
82
X509_STORE_CTX_set0_store(ctx, store); // 设置信任存储 (CTX 不会释放 store)
83
X509_STORE_CTX_set0_untrusted(ctx, untrusted_certs); // 设置非信任证书栈 (CTX 会在清理时释放栈,但不释放栈中的证书指针本身,所以上面 untrusted_certs 的释放需要调整)
84
// 如果使用 set0 系列函数,untrusted_certs 栈本身在 X509_STORE_CTX_free 时会被释放,但栈中的证书指针需要手动释放,除非它们是从别处传入的且不由本函数管理。为了简单且避免内存泄露,这里我们让 OpenSSL 自己管理栈及其内容。
85
// 重写初始化部分,让CTX管理 untrusted_certs 的释放:
86
X509_STORE_CTX_free(ctx); // 先释放旧的 ctx
87
untrusted_certs = sk_X509_new_null();
88
// 加载中级证书到 untrusted_certs...
89
// X509* intermediate_cert = load_cert_from_file("intermediate_ca.crt");
90
// if (intermediate_cert) {
91
// sk_X509_push(untrusted_certs, intermediate_cert); // 将证书指针添加到栈中
92
// }
93
ctx = X509_STORE_CTX_new();
94
if (!ctx) { /* error handling */ }
95
X509_STORE_CTX_set_cert(ctx, target_cert); // 设置要验证的证书
96
X509_STORE_CTX_set0_store(ctx, store); // 设置信任存储 (CTX 不会释放 store)
97
X509_STORE_CTX_set0_untrusted(ctx, untrusted_certs); // 设置非信任证书栈 (CTX 会在清理时释放栈及其内容)
98
99
// 可以设置验证时间等参数
100
// X509_STORE_CTX_set_time(ctx, 0, (time_t)your_validation_time);
101
102
int result = X509_verify_cert(ctx);
103
104
if (result == 1) {
105
std::cout << "Certificate verification successful!" << std::endl;
106
} else {
107
int error_code = X509_STORE_CTX_get_error(ctx);
108
std::cerr << "Certificate verification failed. Error code: " << error_code << std::endl;
109
std::cerr << "Error string: " << X509_verify_cert_error_string(error_code) << std::endl;
110
111
// 可以进一步获取错误所在的证书 (例如,证书链中的哪个证书验证失败)
112
// X509* err_cert = X509_STORE_CTX_get_current_cert(ctx);
113
// if (err_cert) {
114
// char subject_str[256];
115
// X509_NAME_oneline(X509_get_subject_name(err_cert), subject_str, sizeof(subject_str));
116
// std::cerr << "Error occurred in certificate with subject: " << subject_str << std::endl;
117
// }
118
119
// 可以获取构建出的证书链
120
// STACK_OF(X509)* chain = X509_STORE_CTX_get0_chain(ctx);
121
// if (chain) {
122
// std::cerr << "Certificate chain built during verification:" << std::endl;
123
// int num_certs = sk_X509_num(chain);
124
// for (int i = 0; i < num_certs; ++i) {
125
// X509* cert_in_chain = sk_X509_value(chain, i);
126
// char subject_str[256];
127
// X509_NAME_oneline(X509_get_subject_name(cert_in_chain), subject_str, sizeof(subject_str));
128
// std::cerr << " Cert " << i << ": " << subject_str << std::endl;
129
// }
130
// }
131
}
132
133
// 5. 清理
134
X509_STORE_CTX_free(ctx);
135
X509_free(target_cert);
136
// untrusted_certs 在 CTX free 时被释放
137
X509_STORE_free(store);
138
139
// cleanup_openssl();
140
return result == 1 ? 0 : 1;
141
}
⚝ X509_STORE_new()
创建信任存储。
⚝ X509_STORE_add_cert(store, cert)
将一个证书添加到信任存储中。
⚝ X509_STORE_load_locations(store, file, path)
可以从指定的文件加载证书,或从指定目录加载所有证书。
⚝ X509_STORE_CTX_new()
创建验证上下文。
⚝ X509_STORE_CTX_set_cert(ctx, cert)
设置要验证的叶子证书。
⚝ X509_STORE_CTX_set0_store(ctx, store)
设置信任存储。注意 set0
函数通常表示 OpenSSL 不会管理传入参数的内存,调用者负责其生命周期。但在 CTX 的情况下,set0_store
意味着 CTX 不会释放 store
,而 set0_untrusted
意味着 CTX 会释放 untrusted_certs
栈本身及其内容(即栈中的 X509*
指针)。
⚝ X509_STORE_CTX_set0_untrusted(ctx, untrusted_certs)
设置一个 STACK_OF(X509)
,其中包含任何可能需要的中级证书。
⚝ X509_verify_cert(ctx)
执行核心验证逻辑。
⚝ X509_STORE_CTX_get_error(ctx)
获取验证失败时的错误码。
⚝ X509_verify_cert_error_string(error_code)
将错误码转换为可读的字符串。
1
<chapter number="7">
2
<title>证书(X.509)管理</title>
3
本章深入讲解X.509证书的结构、解析、验证以及如何使用OpenSSL生成证书和证书签名请求。
4
<sections>
5
<section number="1">
6
<title>X.509证书结构与格式</title>
7
介绍X.509证书的关键字段(主题、颁发者、有效期、公钥、签名等)和常用格式(PEM, DER)。
8
</section>
9
<section number="2">
10
<title>加载和解析证书</title>
11
讲解如何从文件或内存加载证书(例如:PEM_read_bio_X509, d2i_X509_bio)并访问证书信息(例如:X509_get_subject_name, X509_get_issuer_name)。
12
</section>
13
<section number="3">
14
<title>证书链构建与验证</title>
15
详细介绍如何构建证书链(certificate chain)以及使用OpenSSL进行证书链验证(例如:X509_STORE, X509_STORE_CTX)。
16
</section>
17
<section number="4">
18
<title>创建自签名证书</title>
19
演示如何使用OpenSSL API生成私钥并创建自签名X.509证书。
20
</section>
21
<section number="5">
22
<title>生成证书签名请求(CSR)</title>
23
讲解如何创建证书签名请求(Certificate Signing Request),用于向CA申请证书。
24
</section>
25
<section number="6">
26
<title>证书撤销列表(CRL)与OCSP</title>
27
介绍证书撤销机制(CRL, OCSP)及其在OpenSSL中的基本概念。
28
</section>
29
</sections>
30
</chapter>
7. 证书(X.509)管理
本章深入讲解 X.509 证书的结构、解析、验证以及如何使用 OpenSSL 生成证书和证书签名请求(CSR)。理解 X.509 证书对于构建基于公钥基础设施(PKI, Public Key Infrastructure)的安全应用至关重要,它是 TLS/SSL、数字签名验证等多种安全机制的核心。
7.1 X.509 证书结构与格式
X.509 是一种国际电信联盟(ITU-T, International Telecommunication Union – Telecommunication Standardization Sector)标准,定义了公钥证书(Public Key Certificate)的格式。它是 PKI 的核心组成部分。一个 X.509 证书将一个公钥 (Public Key) 绑定到一个身份(如个人、组织或设备),并由一个受信任的第三方,即证书颁发机构(CA, Certificate Authority),进行数字签名 (Digital Signature)。
7.1.1 X.509 证书的关键字段
X.509 证书包含了一系列标准字段,这些字段提供了关于证书、持有人、颁发者以及有效性等方面的信息。主要的字段包括:
① 版本号 (Version): 指示证书遵循的 X.509 版本(当前常用的是 v3)。
② 序列号 (Serial Number): CA 分配的唯一标识符,在其颁发的证书中是唯一的。
③ 签名算法标识 (Signature Algorithm Identifier): 用于 CA 签名此证书的算法(如 SHA256 with RSA)。
④ 颁发者名称 (Issuer Name): 颁发此证书的 CA 的可分辨名称(DN, Distinguished Name)。
⑤ 有效期 (Validity Period): 证书的有效时间范围,包括“不早于”(Not Before)和“不晚于”(Not After)两个日期。
⑥ 主题名称 (Subject Name): 证书持有者的可分辨名称。
⑦ 主题公钥信息 (Subject Public Key Info): 包含证书持有者的公钥和公钥算法(如 RSA 公钥、ECC 公钥)。
⑧ 颁发者唯一标识符 (Issuer Unique Identifier) (可选,v2/v3): CA 的唯一标识符,如果名称可能重复。
⑨ 主题唯一标识符 (Subject Unique Identifier) (可选, v2/v3): 持有者的唯一标识符,如果名称可能重复。
⑩ 扩展字段 (Extensions) (v3): X.509 v3 引入的关键增强,提供了额外的功能和信息,如:
▮▮▮▮⚝ 主题备用名称 (Subject Alternative Name, SAN): 允许用域名(Domain Name)、IP 地址(IP Address)、电子邮件地址(Email Address)等替代或补充主题名称。
▮▮▮▮⚝ 密钥用法 (Key Usage): 指示公钥的用途(如数字签名、密钥加密、证书签名等)。
▮▮▮▮⚝ 扩展密钥用法 (Extended Key Usage, EKU): 指示证书的特定用途(如服务器认证(Server Authentication)、客户端认证(Client Authentication)、代码签名(Code Signing)等)。
▮▮▮▮⚝ 基本约束 (Basic Constraints): 指示证书是否是 CA 证书,如果是,可以链式认证的最长路径长度(Path Length Constraint)。
▮▮▮▮⚝ 证书策略 (Certificate Policies): 指示 CA 遵循的策略。
▮▮▮▮⚝ 颁发者密钥标识符 (Issuer Key Identifier): 标识用于签名此证书的 CA 的特定密钥。
▮▮▮▮⚝ 权限信息访问 (Authority Information Access, AIA): 指定如何访问 CA 的颁发者证书和在线证书状态(OCSP)。
▮▮▮▮⚝ CRL 分发点 (CRL Distribution Points, CDP): 指定如何获取证书撤销列表(CRL)。
⑪ 签名值 (Signature Value): CA 使用其私钥 (Private Key) 对证书前面所有字段的哈希值 (Hash Value) 进行加密生成。
X.509 证书的结构是基于 ASN.1 (Abstract Syntax Notation One) 标准定义的,通常使用 DER (Distinguished Encoding Rules) 或 BER (Basic Encoding Rules) 进行编码。
7.1.2 证书的常用格式
证书的二进制数据可以表示为不同的文件格式:
① DER 格式 (DER format):
▮▮▮▮⚝ 二进制格式。
▮▮▮▮⚝ 不包含可读的头部和尾部标记。
▮▮▮▮⚝ 文件扩展名通常是 .der
或 .cer
(有时 .cer
也是 PEM 格式)。
▮▮▮▮⚝ 主要用于 Java 或 Windows 环境。
② PEM 格式 (PEM format):
▮▮▮▮⚝ ASCII 编码格式,基于 Base64。
▮▮▮▮⚝ 包含头部 -----BEGIN ...-----
和尾部 -----END ...-----
标记,使其易于文本编辑器查看和传输。
▮▮▮▮⚝ 文件扩展名通常是 .pem
, .crt
, .cer
, .key
(私钥), .csr
(CSR)。
▮▮▮▮⚝ 这是 OpenSSL 最常用的格式。
OpenSSL 库能够处理这些不同的格式,并在内部表示为特定的数据结构,如 X509
结构体。
7.2 加载和解析证书
在 C++ 开发中,使用 OpenSSL 加载和解析 X.509 证书是常见的操作。这通常涉及到使用 BIO (Basic Input/Output) 对象来读取证书数据,然后使用 OpenSSL 提供的函数将其解析为 X509
结构体。
7.2.1 从文件加载证书
使用 BIO 从文件中加载证书是常见的方式。你需要先创建一个文件 BIO,然后使用相应的 PEM 或 DER 读取函数。
1
#include <openssl/bio.h>
2
#include <openssl/x509.h>
3
#include <openssl/pem.h>
4
#include <openssl/err.h>
5
#include <iostream>
6
7
// 通常在应用启动时需要进行 OpenSSL 全局初始化
8
// 在 OpenSSL 1.1.0+ 中推荐使用 OPENSSL_init_ssl
9
// #include <openssl/ssl.h>
10
// #include <openssl/conf.h>
11
12
// void init_openssl() {
13
// OPENSSL_init_ssl(0, NULL);
14
// }
15
16
// void cleanup_openssl() {
17
// // 在 OpenSSL 1.1.0+ 中通常不需要显式清理,内存由 OpenSSL 内部管理
18
// // 如果使用了旧 API 或特定模块,可能需要调用相应的清理函数
19
// }
20
21
22
X509* load_cert_from_file(const char* filename) {
23
BIO* cert_bio = BIO_new_file(filename, "r");
24
if (!cert_bio) {
25
std::cerr << "Error opening certificate file: " << filename << std::endl;
26
// ERR_print_errors_fp(stderr); // 打印 OpenSSL 错误栈
27
return nullptr;
28
}
29
30
X509* cert = nullptr;
31
32
// 尝试按 PEM 格式读取
33
cert = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr);
34
35
if (!cert) {
36
// 如果 PEM 读取失败,尝试按 DER 格式读取
37
// 需要重置 BIO 的文件指针或重新打开文件
38
// 这里为了示例简便,假设文件只包含一个证书且格式一致,
39
// 生产代码应更健壮,可能需要 BIO_seek(cert_bio, 0, SEEK_SET);
40
BIO_free(cert_bio); // 释放旧的 BIO
41
cert_bio = BIO_new_file(filename, "r"); // 重新打开文件以重置指针
42
if (!cert_bio) {
43
std::cerr << "Error re-opening certificate file for DER: " << filename << std::endl;
44
// ERR_print_errors_fp(stderr);
45
return nullptr;
46
}
47
cert = d2i_X509_bio(cert_bio, nullptr);
48
if (!cert) {
49
std::cerr << "Error reading certificate from file (neither PEM nor DER): " << filename << std::endl;
50
// ERR_print_errors_fp(stderr);
51
BIO_free(cert_bio);
52
return nullptr;
53
}
54
}
55
56
BIO_free(cert_bio);
57
return cert; // 调用者负责使用 X509_free 释放
58
}
59
60
// int main() {
61
// // init_openssl(); // 初始化 OpenSSL
62
63
// // 请替换为你的证书文件路径
64
// X509* cert = load_cert_from_file("server.crt");
65
66
// if (cert) {
67
// std::cout << "Certificate loaded successfully!" << std::endl;
68
// // 在这里可以进一步解析证书信息 (见下一小节)
69
// X509_free(cert);
70
// } else {
71
// std::cerr << "Failed to load certificate." << std::endl;
72
// }
73
74
// // cleanup_openssl(); // 清理 OpenSSL
75
// return 0;
76
// }
⚝ BIO_new_file(filename, "r")
创建一个用于读取文件的 BIO 对象。
⚝ PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr)
尝试从 BIO 读取 PEM 格式的 X.509 证书。
⚝ d2i_X509_bio(cert_bio, nullptr)
尝试从 BIO 读取 DER 格式的 X.509 证书。d2i
系列函数用于从二进制格式(如 DER)解析到内部结构体(如 X509
),i2d
系列函数则用于将内部结构体序列化为二进制格式。
⚝ 加载成功的 X509*
指针需要在使用完毕后调用 X509_free()
释放内存,遵循 OpenSSL 的内存管理约定。
7.2.2 从内存加载证书
有时证书数据存储在内存缓冲区中(例如,从网络接收)。你可以使用内存 BIO 来加载。
1
#include <openssl/bio.h>
2
#include <openssl/x509.h>
3
#include <openssl/pem.h>
4
#include <openssl/err.h>
5
#include <string>
6
#include <vector>
7
#include <iostream>
8
9
// ... init_openssl 和 cleanup_openssl 函数 ...
10
11
X509* load_cert_from_memory(const unsigned char* cert_data, size_t data_len) {
12
// BIO_new_mem_buf 会复制数据,因此 cert_data 可以是临时的
13
// 如果不想复制,可以使用 BIO_new_static_buf,但需要确保内存有效且不被释放
14
BIO* cert_bio = BIO_new_mem_buf(cert_data, data_len);
15
if (!cert_bio) {
16
std::cerr << "Error creating memory BIO." << std::endl;
17
// ERR_print_errors_fp(stderr);
18
return nullptr;
19
}
20
21
X509* cert = nullptr;
22
23
// 尝试按 PEM 格式读取
24
cert = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr);
25
26
if (!cert) {
27
// 如果 PEM 读取失败,尝试按 DER 格式读取
28
BIO_reset(cert_bio); // 重置 BIO 的读写位置到开始
29
cert = d2i_X509_bio(cert_bio, nullptr);
30
if (!cert) {
31
std::cerr << "Error reading certificate from memory buffer (neither PEM nor DER)." << std::endl;
32
// ERR_print_errors_fp(stderr);
33
BIO_free(cert_bio);
34
return nullptr;
35
}
36
}
37
38
BIO_free(cert_bio);
39
return cert; // 调用者负责使用 X509_free 释放
40
}
41
42
// 示例:加载一个 PEM 格式的字符串证书
43
// int main() {
44
// // init_openssl(); // 初始化 OpenSSL
45
46
// // 替换为实际的 PEM 证书内容字符串
47
// std::string pem_cert_string =
48
// "-----BEGIN CERTIFICATE-----\n"
49
// "MIICNTCCAZ6gAwIBAgIBADANBgkqhkiG9w0BAQsFADARMRMwEQYDVQQDDApjZXJ0\n"
50
// // ... 更多 base64 编码的证书数据 ...
51
// "-----END CERTIFICATE-----\n";
52
53
// X509* cert = load_cert_from_memory(
54
// reinterpret_cast<const unsigned char*>(pem_cert_string.data()),
55
// pem_cert_string.size()
56
// );
57
58
// if (cert) {
59
// std::cout << "Certificate loaded from memory successfully!" << std::endl;
60
// // 在这里可以进一步解析证书信息 (见下一小节)
61
// X509_free(cert);
62
// } else {
63
// std::cerr << "Failed to load certificate from memory." << std::endl;
64
// }
65
66
// // cleanup_openssl(); // 清理 OpenSSL
67
// return 0;
68
// }
7.2.3 访问证书信息
一旦证书被加载到 X509
结构体中,就可以使用一系列 OpenSSL 函数来提取其字段信息。这些函数通常返回指向内部结构的指针,这些指针不应被直接释放,而是随着 X509
结构体的释放而失效。
1
#include <openssl/x509.h>
2
#include <openssl/x509v3.h> // For extensions
3
#include <openssl/bio.h>
4
#include <openssl/asn1.h>
5
#include <openssl/objects.h>
6
#include <openssl/evp.h>
7
#include <openssl/buffer.h> // For BUF_MEM
8
#include <iostream>
9
#include <string>
10
#include <vector>
11
12
// ... init_openssl 和 cleanup_openssl 函数 ...
13
// ... load_cert_from_file 或 load_cert_from_memory 函数 ...
14
15
void print_cert_info(X509* cert) {
16
if (!cert) return;
17
18
// 获取主题名称 (Subject Name)
19
X509_NAME* subject_name = X509_get_subject_name(cert);
20
if (subject_name) {
21
char subject_str[256];
22
// X509_NAME_oneline 已被弃用,推荐使用 X509_NAME_print_ex
23
// 但 X509_NAME_oneline 简单,此处示例仍然使用
24
X509_NAME_oneline(subject_name, subject_str, sizeof(subject_str));
25
std::cout << "Subject: " << subject_str << std::endl;
26
}
27
28
// 获取颁发者名称 (Issuer Name)
29
X509_NAME* issuer_name = X509_get_issuer_name(cert);
30
if (issuer_name) {
31
char issuer_str[256];
32
X509_NAME_oneline(issuer_name, issuer_str, sizeof(issuer_str));
33
std::cout << "Issuer: " << issuer_str << std::endl;
34
}
35
36
// 获取序列号 (Serial Number)
37
ASN1_INTEGER* serial = X509_get_serialNumber(cert);
38
if (serial) {
39
BIGNUM* bn_serial = ASN1_INTEGER_to_BN(serial, nullptr);
40
if (bn_serial) {
41
char* serial_str = BN_bn2hex(bn_serial); // Convert BIGNUM to hex string
42
if (serial_str) {
43
std::cout << "Serial Number: " << serial_str << std::endl;
44
OPENSSL_free(serial_str); // Free string allocated by BN_bn2hex
45
}
46
BN_free(bn_serial); // Free BIGNUM
47
} else {
48
std::cerr << "Error converting serial number to BIGNUM." << std::endl;
49
}
50
}
51
52
// 获取有效期 (Validity Period)
53
const ASN1_TIME* not_before = X509_get0_notBefore(cert); // Use get0 for const pointers
54
const ASN1_TIME* not_after = X509_get0_notAfter(cert); // Use get0 for const pointers
55
if (not_before && not_after) {
56
BIO* bio_nb = BIO_new(BIO_s_mem());
57
BIO* bio_na = BIO_new(BIO_s_mem());
58
if (bio_nb && bio_na) {
59
// ASN1_TIME_print 打印到 BIO
60
ASN1_TIME_print(bio_nb, not_before);
61
ASN1_TIME_print(bio_na, not_after);
62
63
// 从 BIO 读取打印结果
64
BUF_MEM* bptr_nb;
65
BUF_MEM* bptr_na;
66
BIO_get_mem_ptr(bio_nb, &bptr_nb);
67
BIO_get_mem_ptr(bio_na, &bptr_na);
68
69
if (bptr_nb && bptr_nb->length > 0) {
70
std::cout << "Not Before: " << std::string(bptr_nb->data, bptr_nb->length) << std::endl;
71
}
72
if (bptr_na && bptr_na->length > 0) {
73
std::cout << "Not After: " << std::string(bptr_na->data, bptr_na->length) << std::endl;
74
}
75
76
BIO_free(bio_nb); // Free BIO
77
BIO_free(bio_na); // Free BIO
78
}
79
}
80
81
// 获取公钥 (Public Key)
82
EVP_PKEY* pubkey = X509_get_pubkey(cert); // Get public key
83
if (pubkey) {
84
int type = EVP_PKEY_id(pubkey); // Get key type NID
85
if (type != NID_undef) {
86
const char* key_type_sn = OBJ_nid2sn(type); // Convert NID to short name
87
if (key_type_sn) {
88
std::cout << "Public Key Type: " << key_type_sn << std::endl;
89
}
90
}
91
92
// 可以在这里进一步检查密钥详情,例如 RSA 密钥长度
93
if (type == EVP_PKEY_RSA) {
94
RSA* rsa_key = EVP_PKEY_get1_RSA(pubkey); // Get RSA key structure (increments ref count)
95
if (rsa_key) {
96
std::cout << " RSA Key Bits: " << RSA_bits(rsa_key) << std::endl;
97
RSA_free(rsa_key); // Free the RSA structure obtained by get1
98
}
99
}
100
// 如果是 ECC 密钥
101
if (type == EVP_PKEY_EC) {
102
EC_KEY* ec_key = EVP_PKEY_get1_EC_KEY(pubkey);
103
if (ec_key) {
104
int curve_nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(ec_key));
105
if (curve_nid != NID_undef) {
106
const char* curve_name = OBJ_nid2sn(curve_nid);
107
if (curve_name) {
108
std::cout << " EC Curve: " << curve_name << std::endl;
109
}
110
}
111
EC_KEY_free(ec_key);
112
}
113
}
114
115
EVP_PKEY_free(pubkey); // Free EVP_PKEY obtained by X509_get_pubkey
116
}
117
118
// 打印扩展信息 (Extensions)
119
// X509_get0_extensions 在 OpenSSL 1.1.0+ 中返回 const STACK_OF(X509_EXTENSION)*
120
const STACK_OF(X509_EXTENSION)* extensions = X509_get0_extensions(cert);
121
if (extensions) {
122
std::cout << "Extensions:" << std::endl;
123
int num_exts = sk_X509_EXTENSION_num(extensions);
124
for (int i = 0; i < num_exts; ++i) {
125
X509_EXTENSION* ext = sk_X509_EXTENSION_value(extensions, i);
126
ASN1_OBJECT* obj = X509_EXTENSION_get_object(ext);
127
char ext_name[256];
128
// OBJ_obj2txt 将 OID 转换为文本,如 "subjectAltName"
129
int name_len = OBJ_obj2txt(ext_name, sizeof(ext_name), obj, 0);
130
131
BIO* bio_ext = BIO_new(BIO_s_mem());
132
if (bio_ext) {
133
// X509V3_EXT_print_bio 打印扩展的值到 BIO
134
if (X509V3_EXT_print_bio(bio_ext, ext, 0, 0) > 0) { // 第三个参数是缩进
135
BUF_MEM* bptr;
136
BIO_get_mem_ptr(bio_ext, &bptr);
137
if (bptr && bptr->length > 0) {
138
std::cout << " " << ext_name << ": " << std::string(bptr->data, bptr->length) << std::endl;
139
}
140
}
141
BIO_free(bio_ext);
142
}
143
}
144
}
145
}
146
147
// int main() {
148
// // init_openssl();
149
// X509* cert = load_cert_from_file("server.crt"); // 替换为你的证书文件
150
// if (cert) {
151
// print_cert_info(cert);
152
// X509_free(cert);
153
// } else {
154
// std::cerr << "Failed to load certificate for printing info." << std::endl;
155
// }
156
// // cleanup_openssl();
157
// return 0;
158
// }
⚝ X509_get_subject_name()
和 X509_get_issuer_name()
返回 X509_NAME
结构体,表示可分辨名称。
⚝ X509_NAME_oneline()
可以将 X509_NAME
转换为一行字符串,方便打印(注意此函数在新版本中已弃用)。
⚝ X509_get_serialNumber()
获取序列号,它是 ASN1_INTEGER
类型,需要转换为 BIGNUM
或字符串以便查看。
⚝ X509_get0_notBefore()
和 X509_get0_notAfter()
获取有效期,它们是 const ASN1_TIME*
类型。使用 BIO 和 ASN1_TIME_print
是打印 ASN.1 时间格式的便捷方式。
⚝ X509_get_pubkey()
获取公钥信息,返回 EVP_PKEY
结构体,可以进一步解析具体的公钥类型 (EVP_PKEY_id
) 和参数(如 RSA 密钥长度 RSA_bits
,EC 曲线 EC_GROUP_get_curve_name
)。注意 EVP_PKEY_get1_RSA
等函数会增加引用计数,需要对应的 _free
释放。
⚝ X509_get0_extensions()
获取证书的扩展列表。需要遍历列表并使用 X509V3_EXT_print_bio()
或其他特定扩展解析函数来获取扩展的值。
7.3 证书链构建与验证
证书链(Certificate Chain)是一系列数字证书,用于将一个终端实体证书(如服务器证书、用户证书)追溯到一个受信任的根 CA 证书(Root Certificate)。链中的每个证书(除了根证书)都由其前一个证书的私钥签名,链中的最后一个证书(通常是根证书)是自签名的,且其公钥被硬编码在操作系统或应用程序的信任存储(Trust Store)中。
7.3.1 证书链的构成
典型的证书链结构如下:
① 叶子证书 (Leaf Certificate) / 终端实体证书 (End-Entity Certificate): 这是你要验证的证书,例如你连接的网站服务器的证书。
② 中级证书 (Intermediate Certificate): 由根 CA 或另一个中级 CA 颁发,用于签发叶子证书或其他中级证书。链中可以有一个或多个中级证书。
③ 根证书 (Root Certificate): 自签名的 CA 证书,是信任链的起点。其公钥预装在大多数操作系统和浏览器中,构成信任锚(Trust Anchor)。
验证一个叶子证书的有效性,就是验证证书链的有效性:
① 验证链中每个证书的签名是否与其前一个证书的公钥匹配。
② 验证链中每个证书的有效期是否在当前时间范围内。
③ 验证链中每个证书是否未被撤销(通过 CRL 或 OCSP)。
④ 验证叶子证书的主题或 SAN 是否与你要验证的身份匹配(例如,网站域名)。
⑤ 验证链中的证书是否有合法的基本约束和密钥用法,以确保其角色正确(例如,CA 证书是否允许签发其他证书)。
⑥ 最终追溯到链中的根证书,并确认该根证书在你的信任存储中是受信任的。
7.3.2 使用 OpenSSL 进行证书链验证
OpenSSL 提供了强大的证书存储和验证机制,主要通过 X509_STORE
和 X509_STORE_CTX
结构体来实现。
⚝ X509_STORE
: 表示一个证书信任存储,你可以将受信任的根证书和中级 CA 证书加载到其中。
⚝ X509_STORE_CTX
: 表示一次特定的证书验证上下文,包含要验证的证书、可能的证书链(用于构建路径)、信任存储以及验证参数和回调函数。
验证步骤通常包括:
① 创建一个 X509_STORE
对象。
② 将信任锚(根证书)和任何必要的中级证书加载到 X509_STORE
中。这些证书可以是文件或目录形式。
③ 为每次验证创建一个 X509_STORE_CTX
对象,并关联 X509_STORE
、待验证的叶子证书以及可能的其他未排序的证书(OpenSSL 会尝试从这些证书构建完整的链)。
④ 设置验证参数,例如时间、标志等。
⑤ 调用 X509_verify_cert()
执行验证。
⑥ 检查返回值,如果失败,可以获取详细的错误信息。
⑦ 清理 X509_STORE_CTX
和 X509_STORE
对象。
1
#include <openssl/x509_vfy.h>
2
#include <openssl/x509.h>
3
#include <openssl/pem.h>
4
#include <openssl/bio.h>
5
#include <openssl/err.h>
6
#include <iostream>
7
#include <vector>
8
9
// ... init_openssl 和 cleanup_openssl 函数 ...
10
// ... load_cert_from_file 函数 ...
11
12
// 示例:验证证书链
13
// int main() {
14
// // init_openssl();
15
16
// // 1. 创建并配置 X509_STORE (信任存储)
17
// X509_STORE* store = X509_STORE_new();
18
// if (!store) {
19
// std::cerr << "Error creating X509_STORE." << std::endl;
20
// ERR_print_errors_fp(stderr);
21
// // cleanup_openssl();
22
// return 1;
23
// }
24
25
// // 加载受信任的根证书或中级证书到信任存储中
26
// // 方法 A: 从文件加载单个证书
27
// X509* trusted_root_cert = load_cert_from_file("trusted_root.crt"); // 替换为你的根证书文件路径
28
// if (trusted_root_cert) {
29
// // X509_STORE_add_cert 增加了证书的引用计数,store 会持有它的副本
30
// if (X509_STORE_add_cert(store, trusted_root_cert) != 1) {
31
// std::cerr << "Error adding trusted root cert to store." << std::endl;
32
// ERR_print_errors_fp(stderr);
33
// X509_free(trusted_root_cert);
34
// X509_STORE_free(store);
35
// // cleanup_openssl();
36
// return 1;
37
// }
38
// X509_free(trusted_root_cert); // 加载后可以释放原始证书指针
39
// } else {
40
// std::cerr << "Error loading trusted root cert." << std::endl;
41
// X509_STORE_free(store);
42
// // cleanup_openssl();
43
// return 1;
44
// }
45
46
// // 方法 B: 从一个目录加载所有信任的证书 (例如系统信任库)
47
// // 注意:路径格式和是否存在取决于操作系统
48
// // if (X509_STORE_load_locations(store, nullptr, "/etc/ssl/certs") != 1) {
49
// // std::cerr << "Error loading trusted certificate locations from directory." << std::endl;
50
// // ERR_print_errors_fp(stderr);
51
// // X509_STORE_free(store);
52
// // // cleanup_openssl();
53
// // return 1;
54
// // }
55
// // 方法 C: 从一个文件加载所有信任的证书 (PEM 格式,可能包含多个 CERTIFICATE 块)
56
// // if (X509_STORE_load_locations(store, "trusted_certs.pem", nullptr) != 1) {
57
// // std::cerr << "Error loading trusted certificate locations from file." << std::endl;
58
// // ERR_print_errors_fp(stderr);
59
// // X509_STORE_free(store);
60
// // // cleanup_openssl();
61
// // return 1;
62
// // }
63
64
65
// // 2. 加载要验证的叶子证书 (例如,服务器证书)
66
// X509* target_cert = load_cert_from_file("server_to_verify.crt"); // 替换为要验证的证书文件路径
67
// if (!target_cert) {
68
// std::cerr << "Error loading target cert." << std::endl;
69
// X509_STORE_free(store);
70
// // cleanup_openssl();
71
// return 1;
72
// }
73
74
// // 3. 加载任何可能需要用于构建证书链的中级证书 (可选)
75
// // 这些证书通常不包含在信任存储中,但可能随叶子证书一起提供
76
// STACK_OF(X509)* untrusted_certs = sk_X509_new_null();
77
// if (!untrusted_certs) {
78
// std::cerr << "Error creating untrusted certs stack." << std::endl;
79
// X509_free(target_cert);
80
// X509_STORE_free(store);
81
// // cleanup_openssl();
82
// return 1;
83
// }
84
85
// // 示例:加载一个中级证书文件到 untrusted_certs 栈
86
// // X509* intermediate_cert = load_cert_from_file("intermediate_ca.crt");
87
// // if (intermediate_cert) {
88
// // sk_X509_push(untrusted_certs, intermediate_cert); // 将证书指针添加到栈中
89
// // } else {
90
// // std::cerr << "Warning: Error loading intermediate cert, continuing without it." << std::endl;
91
// // }
92
// // ... 加载其他中级证书到 untrusted_certs ...
93
94
// // 4. 创建 X509_STORE_CTX 并执行验证
95
// X509_STORE_CTX* ctx = X509_STORE_CTX_new();
96
// if (!ctx) {
97
// std::cerr << "Error creating X509_STORE_CTX." << std::endl;
98
// ERR_print_errors_fp(stderr);
99
// X509_free(target_cert);
100
// sk_X509_pop_free(untrusted_certs, X509_free); // 释放 untrusted_certs 栈及其内容
101
// X509_STORE_free(store);
102
// // cleanup_openssl();
103
// return 1;
104
// }
105
106
// // 初始化验证上下文
107
// // 参数: ctx, store, target_cert, untrusted_certs
108
// // X509_STORE_CTX_init(ctx, store, target_cert, untrusted_certs); // 旧 API
109
110
// // OpenSSL 1.1.0+ API:
111
// X509_STORE_CTX_set_cert(ctx, target_cert); // 设置要验证的证书 (CTX 不会释放 target_cert)
112
// X509_STORE_CTX_set0_store(ctx, store); // 设置信任存储 (CTX 不会释放 store)
113
// // 注意 set0_untrusted: CTX 在清理时会释放 untrusted_certs 栈本身,以及栈中的 *所有证书指针*!
114
// // 这意味着如果这些证书是从其他地方获取且需要在验证后继续使用,则不能使用 set0_untrusted。
115
// // 如果这些证书仅用于此次验证且应该被释放,则可以使用 set0_untrusted。
116
// // 为了简单和避免泄露,这里假设 untrusted_certs 及其内容仅用于此次验证。
117
// X509_STORE_CTX_set0_untrusted(ctx, untrusted_certs); // 设置非信任证书栈 (CTX 会释放这个栈及其内容)
118
// untrusted_certs = nullptr; // 将栈指针置空,避免后续误用或重复释放
119
120
// // 可以设置验证时间等参数 (0表示当前时间)
121
// // X509_STORE_CTX_set_time(ctx, 0, (time_t)time(NULL));
122
123
// // 可以设置验证标志,例如 X509_V_FLAG_CRL_CHECK, X509_V_FLAG_OCSP_CHECK 等
124
// // X509_STORE_CTX_set_flags(ctx, X509_V_FLAG_CRL_CHECK);
125
126
// // 可以设置自定义验证回调函数
127
// // X509_STORE_CTX_set_verify_cb(ctx, your_verify_callback);
128
129
130
// int result = X509_verify_cert(ctx);
131
132
// if (result == 1) {
133
// std::cout << "Certificate verification successful!" << std::endl;
134
// } else {
135
// // 验证失败
136
// int error_code = X509_STORE_CTX_get_error(ctx);
137
// std::cerr << "Certificate verification failed. Error code: " << error_code << std::endl;
138
// // 获取更详细的错误信息字符串
139
// std::cerr << "Error string: " << X509_verify_cert_error_string(error_code) << std::endl;
140
141
// // 可以进一步获取错误所在的证书 (例如,证书链中的哪个证书验证失败)
142
// X509* err_cert = X509_STORE_CTX_get_current_cert(ctx);
143
// if (err_cert) {
144
// char subject_str[256];
145
// X509_NAME_oneline(X509_get_subject_name(err_cert), subject_str, sizeof(subject_str));
146
// std::cerr << "Error occurred in certificate with subject: " << subject_str << std::endl;
147
// }
148
149
// // 可以获取构建出的证书链 (仅在验证失败时可用,或设置了特定标志)
150
// // 注意 get0 函数返回的指针不需要释放,它们指向 CTX 内部结构
151
// STACK_OF(X509)* chain = X509_STORE_CTX_get0_chain(ctx);
152
// if (chain) {
153
// std::cerr << "Certificate chain built during verification:" << std::endl;
154
// int num_certs = sk_X509_num(chain);
155
// for (int i = 0; i < num_certs; ++i) {
156
// X509* cert_in_chain = sk_X509_value(chain, i);
157
// char subject_str[256];
158
// X509_NAME_oneline(X509_get_subject_name(cert_in_chain), subject_str, sizeof(subject_str));
159
// std::cerr << " Cert " << i << ": " << subject_str << std::endl;
160
// }
161
// }
162
// }
163
164
// // 5. 清理
165
// X509_STORE_CTX_free(ctx); // 释放 CTX,它会释放 set0_untrusted 传入的 untrusted_certs 栈及其内容
166
// X509_free(target_cert); // 释放 target_cert,因为 CTX 没有释放它
167
// // untrusted_certs 在 CTX free 时被释放
168
// X509_STORE_free(store); // 释放信任存储
169
170
// // cleanup_openssl();
171
// return result == 1 ? 0 : 1;
172
// }
⚝ X509_STORE_new()
创建信任存储。
⚝ X509_STORE_add_cert(store, cert)
将一个证书添加到信任存储中。
⚝ X509_STORE_load_locations(store, file, path)
可以从指定的文件加载证书,或从指定目录加载所有证书。
⚝ X509_STORE_CTX_new()
创建验证上下文。
⚝ X509_STORE_CTX_set_cert(ctx, cert)
设置要验证的叶子证书。
⚝ X509_STORE_CTX_set0_store(ctx, store)
设置信任存储。注意 set0
函数通常表示 OpenSSL 不会管理传入参数的内存,调用者负责其生命周期。在这里,set0_store
意味着 CTX 不会释放 store
。
⚝ X509_STORE_CTX_set0_untrusted(ctx, untrusted_certs)
设置一个 STACK_OF(X509)
,其中包含任何可能需要的中级证书。使用 set0
版本表示 CTX 将接管这个栈及其内容(栈中的所有证书指针)的内存管理,并在 CTX 被释放时一并释放它们。
⚝ X509_verify_cert(ctx)
执行核心验证逻辑。
⚝ X509_STORE_CTX_get_error(ctx)
获取验证失败时的错误码。
⚝ X509_verify_cert_error_string(error_code)
将错误码转换为可读的字符串。
⚝ X509_STORE_CTX_get_current_cert(ctx)
获取导致验证失败的当前证书。
⚝ X509_STORE_CTX_get0_chain(ctx)
获取验证过程中构建出的证书链。这是一个内部指针,不需要释放。
7.4 创建自签名证书
自签名证书(Self-Signed Certificate)是由其自己的私钥签名的证书。它的颁发者和主题名称是相同的。自签名证书不依赖于任何外部 CA,因此通常用于测试、内部应用或不需要第三方信任的环境。
创建自签名证书的步骤包括:
① 生成一个密钥对(私钥和公钥)。
② 创建一个新的 X509
结构体。
③ 设置证书的版本号、序列号、有效期、主题名称、颁发者名称(与主题相同)。
④ 将生成的公钥关联到证书中。
⑤ 添加任何需要的扩展字段(例如,标记为 CA 证书的 Basic Constraints 扩展)。
⑥ 使用步骤①中生成的私钥对证书进行签名。
1
#include <openssl/x509.h>
2
#include <openssl/x509v3.h>
3
#include <openssl/rsa.h>
4
#include <openssl/evp.h>
5
#include <openssl/pem.h>
6
#include <openssl/bio.h>
7
#include <openssl/err.h>
8
#include <openssl/rand.h> // For RAND_bytes or RAND_poll
9
#include <iostream>
10
#include <string>
11
12
// ... init_openssl 和 cleanup_openssl 函数 ...
13
14
// Helper to add an extension
15
int add_ext(X509 *cert, int nid, const char *value) {
16
X509_EXTENSION *ex;
17
X509V3_CTX ctx;
18
19
// 初始化 X509V3_CTX
20
// 注意:对于自签名证书,需要指定 subject 和 issuer 证书为自身
21
X509V3_CTX_init(&ctx);
22
X509V3_set_ctx(&ctx, cert, cert, nullptr, nullptr, 0);
23
24
// 创建扩展
25
ex = X509V3_EXT_conf_nid(nullptr, &ctx, nid, value);
26
if (!ex) {
27
std::cerr << "Error creating X509 extension with NID " << nid << std::endl;
28
ERR_print_errors_fp(stderr);
29
return 0;
30
}
31
32
// 将扩展添加到证书中
33
X509_add_ext(cert, ex, -1);
34
X509_EXTENSION_free(ex); // Add_ext copies the extension, free the original
35
36
return 1;
37
}
38
39
// 示例:创建自签名证书
40
int main() {
41
// init_openssl();
42
43
// 1. 生成密钥对 (这里使用 RSA)
44
EVP_PKEY* pkey = nullptr;
45
RSA* rsa = RSA_new();
46
if (!rsa) {
47
std::cerr << "Error creating RSA key object." << std::endl;
48
ERR_print_errors_fp(stderr);
49
// cleanup_openssl();
50
return 1;
51
}
52
53
// 使用 RSA_generate_key_ex 生成 2048 位的 RSA 密钥
54
BIGNUM* bne = BN_new();
55
if (!bne || BN_set_word(bne, RSA_F4) != 1 || RSA_generate_key_ex(rsa, 2048, bne, nullptr) != 1) {
56
std::cerr << "Error generating RSA key." << std::endl;
57
ERR_print_errors_fp(stderr);
58
BN_free(bne);
59
RSA_free(rsa);
60
// cleanup_openssl();
61
return 1;
62
}
63
BN_free(bne);
64
65
pkey = EVP_PKEY_new();
66
if (!pkey || EVP_PKEY_assign_RSA(pkey, rsa) != 1) {
67
std::cerr << "Error assigning RSA key to EVP_PKEY." << std::endl;
68
ERR_print_errors_fp(stderr);
69
RSA_free(rsa); // EVP_PKEY_assign_RSA 成功会接管 rsa,失败则需要手动释放 rsa
70
EVP_PKEY_free(pkey);
71
// cleanup_openssl();
72
return 1;
73
}
74
// 如果 assign 成功,rsa 已经被 pkey 管理,不用再 free rsa
75
// 如果 assign 失败,pkey 需要 free,rsa 也需要 free
76
77
78
// 2. 创建 X509 证书对象
79
X509* cert = X509_new();
80
if (!cert) {
81
std::cerr << "Error creating X509 certificate object." << std::endl;
82
ERR_print_errors_fp(stderr);
83
EVP_PKEY_free(pkey);
84
// cleanup_openssl();
85
return 1;
86
}
87
88
// 3. 设置证书字段
89
90
// Version (v3)
91
X509_set_version(cert, 2); // Version 3 是索引 2
92
93
// Serial Number (通常是唯一的随机数或序列)
94
ASN1_INTEGER_set(X509_get_serialNumber(cert), 1); // 简单示例,使用固定序列号 1
95
// 更好的方法是生成一个随机序列号
96
// ASN1_INTEGER* serial = ASN1_INTEGER_new();
97
// BIGNUM* bn_serial = BN_rand(BN_new(), 64, 0, 0); // 64 bits random
98
// BN_to_ASN1_INTEGER(bn_serial, serial);
99
// X509_set_serialNumber(cert, serial);
100
// BN_free(bn_serial);
101
// ASN1_INTEGER_free(serial);
102
103
// Issuer and Subject Name (自签名证书两者相同)
104
X509_NAME* name = X509_NAME_new();
105
if (!name) {
106
std::cerr << "Error creating X509_NAME." << std::endl;
107
ERR_print_errors_fp(stderr);
108
X509_free(cert);
109
EVP_PKEY_free(pkey);
110
// cleanup_openssl();
111
return 1;
112
}
113
114
// 添加 Common Name (CN)
115
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_UTF8, (const unsigned char*)"My Self-Signed CA", -1, -1, 0);
116
// 添加 Organization (O)
117
X509_NAME_add_entry_by_txt(name, "O", MBSTRING_UTF8, (const unsigned char*)"My Organization", -1, -1, 0);
118
// 添加 Country (C)
119
X509_NAME_add_entry_by_txt(name, "C", MBSTRING_UTF8, (const unsigned char*)"US", -1, -1, 0);
120
121
X509_set_issuer_name(cert, name); // Set issuer name
122
X509_set_subject_name(cert, name); // Set subject name
123
X509_NAME_free(name); // Name is copied by set_*, free the original
124
125
126
// Validity Period (Start and End Dates)
127
// Not Before: current time
128
X509_gmtime_adj(X509_get_notBefore(cert), 0);
129
// Not After: current time + 365 days (for 1 year)
130
X509_gmtime_adj(X509_get_notAfter(cert), 365 * 24 * 60 * 60L);
131
132
// 4. 将公钥关联到证书
133
X509_set_pubkey(cert, pkey);
134
135
// 5. 添加扩展 (可选但推荐,特别是如果作为 CA 使用)
136
137
// Basic Constraints: CA:TRUE
138
if (!add_ext(cert, NID_basic_constraints, "CA:TRUE")) {
139
std::cerr << "Error adding basic constraints extension." << std::endl;
140
X509_free(cert);
141
EVP_PKEY_free(pkey);
142
// cleanup_openssl();
143
return 1;
144
}
145
146
// Key Usage: digitalSignature, keyCertSign
147
// 需要根据实际用途设置,这里设置为签名证书和签发其他证书
148
if (!add_ext(cert, NID_key_usage, "digitalSignature, keyCertSign, cRLSign")) {
149
std::cerr << "Error adding key usage extension." << std::endl;
150
X509_free(cert);
151
EVP_PKEY_free(pkey);
152
// cleanup_openssl();
153
return 1;
154
}
155
156
// Subject Key Identifier: 帮助快速匹配证书和私钥
157
if (!add_ext(cert, NID_subject_key_identifier, "hash")) {
158
std::cerr << "Error adding subject key identifier extension." << std::endl;
159
X509_free(cert);
160
EVP_PKEY_free(pkey);
161
// cleanup_openssl();
162
return 1;
163
}
164
// Authority Key Identifier: 帮助构建证书链,指向 CA 的公钥
165
if (!add_ext(cert, NID_authority_key_identifier, "keyid:always,issuer")) {
166
std::cerr << "Error adding authority key identifier extension." << std::endl;
167
X509_free(cert);
168
EVP_PKEY_free(pkey);
169
// cleanup_openssl();
170
return 1;
171
}
172
173
174
// 6. 使用私钥对证书签名
175
// 使用 EVP 接口签名
176
const EVP_MD* md = EVP_sha256(); // 使用 SHA-256 哈希算法
177
if (!md) {
178
std::cerr << "Error getting SHA-256 digest." << std::endl;
179
X509_free(cert);
180
EVP_PKEY_free(pkey);
181
// cleanup_openssl();
182
return 1;
183
}
184
if (X509_sign(cert, pkey, md) <= 0) {
185
std::cerr << "Error signing certificate." << std::endl;
186
ERR_print_errors_fp(stderr);
187
X509_free(cert);
188
EVP_PKEY_free(pkey);
189
// cleanup_openssl();
190
return 1;
191
}
192
193
std::cout << "Self-signed certificate created successfully!" << std::endl;
194
195
// 保存证书和私钥到文件 (可选)
196
BIO* cert_bio = BIO_new_file("selfsigned_cert.pem", "w");
197
if (cert_bio) {
198
if (PEM_write_bio_X509(cert_bio, cert) != 1) {
199
std::cerr << "Error writing certificate to file." << std::endl;
200
ERR_print_errors_fp(stderr);
201
}
202
BIO_free_all(cert_bio);
203
}
204
205
BIO* pkey_bio = BIO_new_file("selfsigned_key.pem", "w");
206
if (pkey_bio) {
207
// 如果需要加密私钥,可以使用 PEM_write_bio_PrivateKey(pkey_bio, pkey, EVP_aes_256_cbc(), nullptr, 0, nullptr, "password");
208
if (PEM_write_bio_PrivateKey(pkey_bio, pkey, nullptr, nullptr, 0, nullptr, nullptr) != 1) {
209
std::cerr << "Error writing private key to file." << std::endl;
210
ERR_print_errors_fp(stderr);
211
}
212
BIO_free_all(pkey_bio);
213
}
214
215
216
// 清理
217
X509_free(cert);
218
EVP_PKEY_free(pkey);
219
220
// cleanup_openssl();
221
return 0;
222
}
⚝ 生成密钥对可以使用 RSA_generate_key_ex
或 EC_KEY_generate_key
等函数(详见第 5 章),然后将其封装到 EVP_PKEY
中。
⚝ X509_new()
创建空的证书对象。
⚝ X509_set_version()
设置版本,v3 对应整数 2。
⚝ X509_get_serialNumber()
获取序列号字段的指针,然后使用 ASN1_INTEGER_set()
或生成随机数来设置其值。
⚝ X509_NAME_new()
创建可分辨名称对象,X509_NAME_add_entry_by_txt()
添加像 CN, O, C 这样的属性。
⚝ X509_set_issuer_name()
和 X509_set_subject_name()
设置证书的颁发者和主题。对于自签名证书,两者相同。
⚝ X509_gmtime_adj()
设置证书的有效期。参数 0 表示当前时间,正数表示向后调整的秒数。
⚝ X509_set_pubkey()
将公钥关联到证书。
⚝ add_ext()
辅助函数演示了如何使用 X509V3_EXT_conf_nid()
创建一个标准扩展并使用 X509_add_ext()
将其添加到证书中。X509V3_CTX_init
用于初始化扩展上下文,对于自签名证书,设置 subject
和 issuer
为自身证书。
⚝ X509_sign(cert, pkey, md)
使用指定的私钥 (pkey
) 和哈希算法 (md
) 对证书进行签名。
⚝ 最后,可以使用 PEM_write_bio_X509
和 PEM_write_bio_PrivateKey
将证书和私钥保存到文件。
7.5 生成证书签名请求(CSR)
证书签名请求(CSR, Certificate Signing Request)是申请数字证书时向 CA 提交的文件。它包含了申请者的公钥、身份信息(例如域名、组织名称等),以及由申请者私钥对整个请求的签名。CA 使用 CSR 中的公钥和信息来创建证书,并使用其私钥签名。通过验证 CSR 上的签名,CA 可以确认请求确实来自于拥有对应私钥的申请者。
生成 CSR 的步骤与创建自签名证书类似,但有以下区别:
① 生成密钥对(私钥和公钥)。
② 创建一个新的 X509_REQ
结构体。
③ 设置请求的版本号、主题名称(申请者的身份信息)。
④ 将生成的公钥关联到请求中。
⑤ 添加任何需要的属性(Attributes),例如挑战密码(Challenge Password)或非结构化名称(Unstructured Name),以及重要的扩展信息(通过 req_extensions
属性)。
⑥ 使用步骤①中生成的私钥对请求进行签名。
1
#include <openssl/x509_req.h>
2
#include <openssl/x509v3.h>
3
#include <openssl/rsa.h>
4
#include <openssl/evp.h>
5
#include <openssl/pem.h>
6
#include <openssl/bio.h>
7
#include <openssl/err.h>
8
#include <iostream>
9
#include <string>
10
11
// ... init_openssl 和 cleanup_openssl 函数 ...
12
13
// Helper function to add an extension block to a CSR
14
// This is typically done via the 'req_extensions' attribute in the CSR
15
int add_csr_ext(X509_REQ *req, int nid, const char *value) {
16
X509_EXTENSION *ex;
17
X509V3_CTX ctx;
18
19
// Extensions in a CSR are added as attributes, specifically as the 'req_extensions' attribute
20
// This requires creating a STACK_OF(X509_EXTENSION) and adding it as an attribute
21
// This helper simplifies creating a single extension to be added later
22
23
// Note: OpenSSL command line uses [ req_extensions ] section in config
24
// Programmatically, it's more complex. This helper just creates the extension object.
25
// You would then add this extension object (or multiple) to a STACK_OF(X509_EXTENSION)
26
// and add that stack as an attribute to the X509_REQ.
27
28
// Let's try a simplified approach for common extensions like Subject Alternative Name (SAN)
29
// using X509V3_EXT_conf_nid which works for both certs and CSR attributes implicitly with the right CTX init.
30
// For CSRs, the issuer is NULL in the CTX.
31
32
X509V3_CTX_init(&ctx);
33
X509V3_set_ctx(&ctx, nullptr, nullptr, nullptr, req, 0); // For CSRs, cert is NULL, req is set
34
35
ex = X509V3_EXT_conf_nid(nullptr, &ctx, nid, value);
36
if (!ex) {
37
std::cerr << "Error creating X509 extension for CSR attribute with NID " << nid << std::endl;
38
ERR_print_errors_fp(stderr);
39
return 0;
40
}
41
42
// To actually add extensions to a CSR, you typically create a stack and add it as an attribute:
43
// STACK_OF(X509_EXTENSION)* ext_list = sk_X509_EXTENSION_new_null();
44
// sk_X509_EXTENSION_push(ext_list, ex);
45
// X509_REQ_add_extensions(req, ext_list);
46
// sk_X509_EXTENSION_pop_free(ext_list, X509_EXTENSION_free); // CTX does NOT free extensions in stack added this way
47
48
49
// A simpler method for *some* attributes might be direct functions if available,
50
// but standard extensions like SAN are added via the extension stack attribute.
51
// Let's refactor to correctly add extensions as attributes. This helper will create the extension.
52
// The calling code will need to manage the stack and attribute.
53
54
// Returning the extension object for now. Calling code must free it if not added to stack.
55
// For demonstration, let's modify this to add a common attribute like SAN directly
56
// as an attribute stack (if supported by OpenSSL CSR attribute handling - yes, NID_ext_req)
57
58
// Clean up the created extension if not added to a stack
59
// X509_EXTENSION_free(ex); // Do not free here, the stack will manage it if added.
60
61
// A more direct way to add extensions as attributes to a CSR is often via NID_ext_req
62
// This requires creating a stack of extensions.
63
STACK_OF(X509_EXTENSION)* extensions_stack = sk_X509_EXTENSION_new_null();
64
if (!extensions_stack) {
65
std::cerr << "Error creating extensions stack for CSR." << std::endl;
66
ERR_print_errors_fp(stderr);
67
X509_EXTENSION_free(ex); // Free the extension created above
68
return 0; // Indicate failure
69
}
70
71
// Re-create the extension to add to the stack
72
X509V3_CTX_init(&ctx);
73
X509V3_set_ctx(&ctx, nullptr, nullptr, nullptr, req, 0);
74
ex = X509V3_EXT_conf_nid(nullptr, &ctx, nid, value);
75
if (!ex) {
76
std::cerr << "Error re-creating extension for stack." << std::endl;
77
ERR_print_errors_fp(stderr);
78
sk_X509_EXTENSION_free(extensions_stack); // Free the stack
79
return 0; // Indicate failure
80
}
81
82
sk_X509_EXTENSION_push(extensions_stack, ex); // Add the extension to the stack
83
84
// Add the stack of extensions as the NID_ext_req attribute
85
if (X509_REQ_add_extensions(req, extensions_stack) != 1) {
86
std::cerr << "Error adding extensions stack to CSR as attribute." << std::endl;
87
ERR_print_errors_fp(stderr);
88
sk_X509_EXTENSION_pop_free(extensions_stack, X509_EXTENSION_free); // Free stack and its contents
89
return 0; // Indicate failure
90
}
91
92
// X509_REQ_add_extensions consumes the stack and frees it and its contents.
93
// No need to free extensions_stack or ex here.
94
95
return 1; // Indicate success
96
}
97
98
99
// 示例:生成 CSR
100
int main() {
101
// init_openssl();
102
103
// 1. 生成密钥对 (这里使用 RSA)
104
EVP_PKEY* pkey = nullptr;
105
RSA* rsa = RSA_new();
106
if (!rsa) {
107
std::cerr << "Error creating RSA key object." << std::endl;
108
ERR_print_errors_fp(stderr);
109
// cleanup_openssl();
110
return 1;
111
}
112
113
BIGNUM* bne = BN_new();
114
if (!bne || BN_set_word(bne, RSA_F4) != 1 || RSA_generate_key_ex(rsa, 2048, bne, nullptr) != 1) {
115
std::cerr << "Error generating RSA key." << std::endl;
116
ERR_print_errors_fp(stderr);
117
BN_free(bne);
118
RSA_free(rsa);
119
// cleanup_openssl();
120
return 1;
121
}
122
BN_free(bne);
123
124
pkey = EVP_PKEY_new();
125
if (!pkey || EVP_PKEY_assign_RSA(pkey, rsa) != 1) {
126
std::cerr << "Error assigning RSA key to EVP_PKEY." << std::endl;
127
ERR_print_errors_fp(stderr);
128
RSA_free(rsa); // Assign failed, must free rsa
129
EVP_PKEY_free(pkey);
130
// cleanup_openssl();
131
return 1;
132
}
133
// If assign succeeded, rsa is now managed by pkey
134
135
// 2. 创建 X509_REQ (CSR) 对象
136
X509_REQ* req = X509_REQ_new();
137
if (!req) {
138
std::cerr << "Error creating X509_REQ object." << std::endl;
139
ERR_print_errors_fp(stderr);
140
EVP_PKEY_free(pkey);
141
// cleanup_openssl();
142
return 1;
143
}
144
145
// 3. 设置请求字段
146
147
// Version (v1, 对应索引 0)
148
X509_REQ_set_version(req, 0); // CSR Version 1
149
150
// Subject Name (申请者的身份信息)
151
X509_NAME* name = X509_NAME_new();
152
if (!name) {
153
std::cerr << "Error creating X509_NAME for CSR subject." << std::endl;
154
ERR_print_errors_fp(stderr);
155
X509_REQ_free(req);
156
EVP_PKEY_free(pkey);
157
// cleanup_openssl();
158
return 1;
159
}
160
161
// 通常包含 Common Name (CN), Organization (O), Locality (L), State (ST), Country (C)
162
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_UTF8, (const unsigned char*)"www.example.com", -1, -1, 0);
163
X509_NAME_add_entry_by_txt(name, "O", MBSTRING_UTF8, (const unsigned char*)"My Organization", -1, -1, 0);
164
X509_NAME_add_entry_by_txt(name, "L", MBSTRING_UTF8, (const unsigned char*)"City", -1, -1, 0);
165
X509_NAME_add_entry_by_txt(name, "ST", MBSTRING_UTF8, (const unsigned char*)"State", -1, -1, 0);
166
X509_NAME_add_entry_by_txt(name, "C", MBSTRING_UTF8, (const unsigned char*)"US", -1, -1, 0);
167
168
X509_REQ_set_subject_name(req, name);
169
X509_NAME_free(name); // Name is copied, free the original
170
171
172
// 4. 将公钥关联到请求
173
X509_REQ_set_pubkey(req, pkey);
174
175
// 5. 添加属性 (Attributes) 和扩展 (作为属性)
176
// 常用的属性是挑战密码 (Challenge Password) - NID_pkcs9_challengePassword
177
// X509_REQ_add_attribute_by_txt(req, "challengePassword", MBSTRING_ASC, (const unsigned char*)"my_challenge_password", -1);
178
179
// 添加扩展,如 Subject Alternative Name (SAN) 作为 req_extensions 属性 (NID_ext_req)
180
// SAN 允许多个域名或 IP 地址
181
if (!add_csr_ext(req, NID_subject_alt_name, "DNS:example.com,DNS:www.example.com,IP:192.168.1.1")) {
182
std::cerr << "Failed to add SAN extension to CSR." << std::endl;
183
X509_REQ_free(req);
184
EVP_PKEY_free(pkey);
185
// cleanup_openssl();
186
return 1;
187
}
188
// 其他可能的扩展,如 Key Usage (尽管 CA 可能忽略 CSR 中的 Key Usage 并自己设置)
189
// add_csr_ext(req, NID_key_usage, "digitalSignature, keyAgreement");
190
191
192
// 6. 使用私钥对请求签名
193
const EVP_MD* md = EVP_sha256(); // 使用 SHA-256 哈希算法
194
if (!md) {
195
std::cerr << "Error getting SHA-256 digest for CSR signature." << std::endl;
196
X509_REQ_free(req);
197
EVP_PKEY_free(pkey);
198
// cleanup_openssl();
199
return 1;
200
}
201
202
// X509_REQ_sign 内部使用 EVP_PKEY_sign
203
if (X509_REQ_sign(req, pkey, md) <= 0) {
204
std::cerr << "Error signing CSR." << std::endl;
205
ERR_print_errors_fp(stderr);
206
X509_REQ_free(req);
207
EVP_PKEY_free(pkey);
208
// cleanup_openssl();
209
return 1;
210
}
211
212
std::cout << "CSR created successfully!" << std::endl;
213
214
// 保存 CSR 和私钥到文件
215
BIO* csr_bio = BIO_new_file("request.csr", "w");
216
if (csr_bio) {
217
if (PEM_write_bio_X509_REQ(csr_bio, req) != 1) {
218
std::cerr << "Error writing CSR to file." << std::endl;
219
ERR_print_errors_fp(stderr);
220
}
221
BIO_free_all(csr_bio);
222
}
223
224
BIO* pkey_bio = BIO_new_file("private.key", "w");
225
if (pkey_bio) {
226
// 如果需要加密私钥
227
// if (PEM_write_bio_PrivateKey(pkey_bio, pkey, EVP_aes_256_cbc(), nullptr, 0, nullptr, "password") != 1) {
228
if (PEM_write_bio_PrivateKey(pkey_bio, pkey, nullptr, nullptr, 0, nullptr, nullptr) != 1) {
229
std::cerr << "Error writing private key to file." << std::endl;
230
ERR_print_errors_fp(stderr);
231
}
232
BIO_free_all(pkey_bio);
233
}
234
235
// 清理
236
X509_REQ_free(req);
237
EVP_PKEY_free(pkey);
238
239
// cleanup_openssl();
240
return 0;
241
}
⚝ 生成密钥对(EVP_PKEY
)与创建自签名证书相同。
⚝ X509_REQ_new()
创建空的 CSR 对象。
⚝ X509_REQ_set_version()
设置 CSR 版本,通常是 v1 (对应整数 0)。
⚝ X509_REQ_set_subject_name()
设置申请者的可分辨名称,与证书主题类似。
⚝ X509_REQ_set_pubkey()
将公钥关联到 CSR。
⚝ X509_REQ_add_attribute_by_txt()
可以添加标准属性,如挑战密码。
⚝ 添加扩展(如 SAN)到 CSR 通常通过 req_extensions
属性来实现,这需要创建一个 STACK_OF(X509_EXTENSION)
并使用 X509_REQ_add_extensions
函数添加到 CSR 中。X509V3_CTX
初始化时,需要将 req
参数设置为 CSR 对象,而 cert
参数设置为 nullptr
。
⚝ X509_REQ_sign(req, pkey, md)
使用申请者的私钥对 CSR 进行签名。
⚝ 最后,使用 PEM_write_bio_X509_REQ
和 PEM_write_bio_PrivateKey
将 CSR 和私钥保存到文件。请务必保管好私钥,因为它不能通过 CSR 或证书重新生成。
7.6 证书撤销列表(CRL)与 OCSP
证书撤销是 PKI 中的一个重要机制,用于声明在证书有效期结束前,某个证书已经失效(例如,私钥泄露、持有人身份变更等)。主要有两种在线查询证书状态的方式:证书撤销列表(CRL)和在线证书状态协议(OCSP)。
7.6.1 证书撤销列表(CRL)
证书撤销列表(CRL, Certificate Revocation List)是由 CA 定期发布的一个列表,包含了 CA 已颁发但尚未到期、已被撤销的证书的序列号和撤销时间。
⚝ CA 生成 CRL 并用其私钥签名。
⚝ 用户或应用程序在验证证书时,需要获取相应的 CRL,并检查待验证证书的序列号是否在 CRL 中。
⚝ CRL 通常通过 HTTP 或 LDAP 分发点(在证书的 CDP 扩展中指定)获取。
在 OpenSSL 中处理 CRL:
⚝ X509_CRL
结构体代表一个 CRL。
⚝ 可以使用 PEM_read_bio_X509_CRL
或 d2i_X509_CRL_bio
加载 CRL。
⚝ 可以使用 X509_STORE_add_crl
将 CRL 添加到 X509_STORE
中。
⚝ 在证书验证上下文 X509_STORE_CTX
中设置 X509_V_FLAG_CRL_CHECK
标志,可以启用 CRL 检查。OpenSSL 将尝试在 X509_STORE
中查找链中每个证书对应的 CRL。
⚝ 还可以手动检查证书是否在 CRL 中:获取证书的序列号和颁发者名称,遍历 CRL 中的撤销列表项,进行匹配。
处理 CRL 涉及获取、加载、更新 CRL,并将其集成到证书验证流程中,这比简单的证书链验证更复杂。
7.6.2 在线证书状态协议(OCSP)
在线证书状态协议(OCSP, Online Certificate Status Protocol)提供了一种更实时的证书状态查询方式。客户端向 OCSP 响应者(OCSP Responder)发送待查询证书的信息,OCSP 响应者返回该证书的当前状态:有效(Good)、已撤销(Revoked)或未知(Unknown)。
⚝ OCSP 响应者通常由 CA 或其委托的机构运行。
⚝ OCSP 查询响应由 OCSP 响应者的证书(通常是一个专门的签名证书)签名。
⚝ 客户端在证书验证时,可以从证书的 AIA 扩展中找到 OCSP 服务的 URI,然后向该 URI 发送 OCSP 请求。
在 OpenSSL 中使用 OCSP:
⚝ OpenSSL 提供了 OCSP 相关的结构体和函数,例如 OCSP_REQUEST
, OCSP_RESPONSE
。
⚝ 可以构建 OCSP 请求(OCSP_REQUEST_new
, OCSP_request_add0_certid
), 发送请求,接收响应,并解析响应(OCSP_RESPONSE_new
, OCSP_parse_url
, OCSP_resp_find_status
).
⚝ 可以在 X509_STORE_CTX
中设置 X509_V_FLAG_OCSP_CHECK
标志来启用 OCSP 检查(需要配置 OCSP 相关的参数或回调函数)。
⚝ OCSP 检查通常比 CRL 检查更及时,但依赖于 OCSP 服务的可用性和网络连接。
在实际应用中,通常会同时配置 CRL 和 OCSP 检查,或者根据策略选择使用其中一种或组合使用。OpenSSL 库提供了实现这些检查的 API,但具体的网络通信(获取 CRL 或发送 OCSP 请求)和复杂的策略逻辑需要开发者自行实现或使用更高级的网络库和框架。
<END_OF_CHAPTER/>
8. TLS/SSL协议基础与客户端开发
8.1 TLS/SSL协议概述
欢迎来到本书关于传输层安全(Transport Layer Security, TLS)和其前身安全套接字层(Secure Sockets Layer, SSL)协议的章节。在现代网络通信中,TLS/SSL扮演着至关重要的角色,它是保障数据在客户端和服务器之间安全传输的基石,尤其在网页浏览(HTTPS)、电子邮件(SMTPS, POP3S, IMAPS)、即时通讯等应用中无处不在。
SSL协议最初由网景公司(Netscape)于1990年代中期开发,经历了SSL 2.0和SSL 3.0版本。SSL 3.0在设计上存在一些缺陷。为了改进并将其标准化,互联网工程任务组(Internet Engineering Task Force, IETF)在SSL 3.0的基础上发布了TLS 1.0协议(RFC 2246),可以认为是SSL 3.0的升级版本。随后,TLS经历了1.1 (RFC 4346)、1.2 (RFC 5246) 和最新的1.3 (RFC 8446) 版本演进。目前,SSL版本(2.0和3.0)因其固有的安全漏洞已被废弃,推荐使用TLS 1.2或更高的版本。
TLS/SSL协议的核心目标是提供以下安全服务:
⚝ 保密性(Confidentiality): 通过对称加密(Symmetric Encryption)确保数据传输过程不被第三方窃听。
⚝ 完整性(Integrity): 通过哈希函数(Hash Function)和消息认证码(Message Authentication Code, HMAC)确保数据在传输过程中未被篡改。
⚝ 身份认证(Authentication): 通过非对称加密(Asymmetric Encryption)和数字证书(Digital Certificate)验证通信对端的身份,通常是服务器验证,也可以选择进行客户端验证(双向认证)。
TLS/SSL协议在概念上可以分为两个主要子协议:
① TLS握手协议(Handshake Protocol):
▮▮▮▮此协议负责在客户端和服务器之间协商确定本次连接使用的协议版本、密码套件(Cipher Suite)、交换必要的密钥参数、验证双方(通常是服务器)的身份。这是建立安全连接的第一步。
② TLS记录协议(Record Protocol):
▮▮▮▮此协议运行在握手协议之上,负责对应用层数据进行分段、压缩(可选)、计算MAC、加密,然后传输。在接收端,负责解密、验证MAC、解压缩、重组数据,然后提交给应用层。一旦握手完成,所有应用数据都通过记录协议传输,受到握手协商的安全参数保护。
OpenSSL库提供了丰富的API来支持TLS/SSL协议的客户端和服务器实现,本章将重点聚焦于TLS/SSL客户端的开发。
8.2 TLS/SSL握手过程详解
TLS/SSL握手协议是客户端和服务器在应用数据传输之前建立安全通信的关键步骤。其过程相对复杂,涉及多个消息交换。理解握手过程对于正确使用OpenSSL开发TLS/SSL应用程序至关重要。下面是典型的TLS 1.2握手流程(TLS 1.3有所简化,本书主要基于OpenSSL广泛支持的TLS 1.2进行讲解,并在后续章节简要提及TLS 1.3的新特性):
① 客户端你好(ClientHello) 👋:
▮▮▮▮客户端发起连接后,发送ClientHello消息到服务器。此消息包含:
▮▮▮▮ⓐ 支持的最高TLS/SSL协议版本列表(例如:TLS 1.2, TLS 1.1, SSL 3.0)。
▮▮▮▮ⓑ 客户端生成的随机数(Client Random),用于后续生成会话密钥。
▮▮▮▮ⓒ 支持的密码套件列表(Cipher Suites),每个密码套件定义了一种密钥交换算法、一种认证算法、一种对称加密算法和一种哈希算法的组合(例如:TLS_RSA_WITH_AES_128_CBC_SHA
)。
▮▮▮▮ⓓ 支持的压缩算法列表(Compression Methods)。
▮▮▮▮ⓔ TLS扩展(Extensions)(可选),如服务器名称指示(Server Name Indication, SNI)、应用层协议协商(Application-Layer Protocol Negotiation, ALPN)等。
② 服务器你好(ServerHello) 👋:
▮▮▮▮服务器接收ClientHello后,从客户端提供的列表中选择本次连接使用的协议版本、密码套件和压缩算法,并发送ServerHello消息。此消息包含:
▮▮▮▮ⓐ 服务器选择的协议版本。
▮▮▮▮ⓑ 服务器选择的密码套件。
▮▮▮▮ⓒ 服务器选择的压缩算法。
▮▮▮▮ⓓ 服务器生成的随机数(Server Random),用于后续生成会话密钥。
▮▮▮▮ⓔ 会话ID(Session ID)(可选),用于会话复用。
▮▮▮▮ⓕ TLS扩展(Extensions)(可选),服务器响应客户端请求的扩展。
③ 证书(Certificate) 📜:
▮▮▮▮服务器发送其数字证书链(Certificate Chain)给客户端。证书链通常包含服务器证书以及用于签发服务器证书的中间CA(Certificate Authority)证书,直至根CA证书(Root CA Certificate)。客户端需要验证此证书链的有效性。
④ 服务器密钥交换(ServerKeyExchange) 🔑 (如果协商的密钥交换算法需要):
▮▮▮▮如果协商的密码套件使用了Diffie-Hellman或ECDH等算法(而不是基于RSA证书直接加密预主密钥),服务器会发送此消息,包含用于密钥交换的公开参数(例如DH参数或ECC公钥)。
⑤ 证书请求(CertificateRequest) ❓ (如果需要客户端认证):
▮▮▮▮如果服务器配置了需要客户端认证,它会发送此消息,列出服务器信任的CA列表,请求客户端提供符合要求的证书。
⑥ 服务器你好结束(ServerHelloDone) 👍:
▮▮▮▮服务器发送此消息,告知客户端服务器端的握手消息已经发送完毕,等待客户端的响应。
⑦ 证书(Certificate) 📜 (如果服务器请求了客户端认证):
▮▮▮▮客户端如果收到CertificateRequest消息,并且有合适的证书,会发送自己的证书链。
⑧ 客户端密钥交换(ClientKeyExchange) 🔑:
▮▮▮▮客户端发送此消息,包含密钥交换的关键信息。
▮▮▮▮ⓐ 如果使用RSA密钥交换(非Perfect Forward Secrecy):客户端生成一个预主密钥(Pre-Master Secret),使用服务器证书中的公钥进行加密后发送。
▮▮▮▮ⓑ 如果使用Diffie-Hellman或ECDH密钥交换(支持Perfect Forward Secrecy):客户端生成自己的DH参数或ECC密钥对,并发送其公开参数。
⑨ 证书验证(CertificateVerify) ✅ (如果客户端发送了证书):
▮▮▮▮如果客户端发送了证书,它会发送此消息。客户端使用自己的私钥对之前握手消息的哈希值进行签名,发送签名结果,证明客户端拥有其证书对应的私钥。
⑩ 改变密码规格(ChangeCipherSpec) 🔒:
▮▮▮▮客户端发送此消息,通知服务器后续发送的消息将使用之前协商好的会话密钥和密码套件进行加密保护。
⑪ 结束(Finished) 🎉:
▮▮▮▮客户端发送此消息,其中包含之前所有握手消息的哈希值,并使用协商好的会话密钥和密码套件进行加密。服务器接收并解密此消息,验证哈希值是否匹配,以确认握手过程未被篡改。
⑫ 改变密码规格(ChangeCipherSpec) 🔒:
▮▮▮▮服务器发送此消息,通知客户端后续发送的消息将使用协商好的会话密钥和密码套件进行加密保护。
⑬ 结束(Finished) 🎉:
▮▮▮▮服务器发送此消息,其中包含之前所有握手消息的哈希值(包括客户端Finished之前的所有消息),并使用协商好的会话密钥和密码套件进行加密。客户端接收并解密此消息,验证哈希值,确认握手成功。
至此,TLS/SSL握手完成,双方已安全地协商出用于本次会话的对称加密密钥。接下来的所有应用数据传输都将使用记录协议进行加密、认证和传输。
在OpenSSL中,大部分握手过程由库内部函数自动处理(例如SSL_connect
或SSL_accept
),但开发者需要正确配置SSL_CTX
和SSL
对象,特别是关于证书、密钥和验证设置,以确保握手能够顺利进行并满足安全要求。
8.3 OpenSSL SSL上下文(SSL_CTX)
在OpenSSL中,SSL_CTX
(SSL Context)对象是TLS/SSL编程中的一个核心概念,它代表了一个连接工厂和一组共享的配置参数。你可以创建一个SSL_CTX
对象,并用它来创建多个SSL
对象,这些SSL
对象代表独立的TLS/SSL连接,但它们共享同一个SSL_CTX
的配置,例如协议版本、密码套件、证书、私钥、验证设置等。
使用SSL_CTX
的主要优势在于:
⚝ 集中管理配置:多个连接可以共享相同的安全配置,避免重复设置。
⚝ 提高性能:某些昂贵的操作(如加载证书、配置密码套件)只需要执行一次。
⚝ 内存效率:共享某些数据结构。
8.3.1 创建SSL_CTX对象
创建SSL_CTX
对象需要指定一个SSL_METHOD
,它决定了上下文是用于客户端还是服务器,以及支持的协议版本范围。
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
4
// 创建 SSL_CTX 对象
5
SSL_CTX* ctx = nullptr;
6
const SSL_METHOD* method = nullptr;
7
8
// 选择合适的SSL/TLS方法
9
// 对于客户端,推荐使用 TLS_client_method(),它会自动协商最高支持的TLS版本
10
method = TLS_client_method(); // 或者 SSLv23_client_method() for older versions
11
12
if (method == nullptr) {
13
// 处理错误
14
ERR_print_errors_fp(stderr);
15
// ...
16
}
17
18
ctx = SSL_CTX_new(method);
19
if (ctx == nullptr) {
20
// 处理错误
21
ERR_print_errors_fp(stderr);
22
// ...
23
}
24
25
// 后续进行配置...
26
27
// 清理 SSL_CTX 对象 (在所有使用它的SSL对象都被释放后)
28
// SSL_CTX_free(ctx);
常用的SSL_METHOD
函数(OpenSSL 1.1.0+):
⚝ TLS_method()
: 同时支持TLS服务器和客户端,自动协商最高版本。
⚝ TLS_client_method()
: 仅支持TLS客户端,自动协商最高版本。
⚝ TLS_server_method()
: 仅支持TLS服务器,自动协商最高版本。
⚝ TLS_ANY_METHOD
: 已废弃,功能类似TLS_method()
。
对于旧版本OpenSSL (1.0.2及更早),常用的方法包括 SSLv23_method()
, TLSv1_2_method()
, SSLv3_method()
等。推荐使用新版本的通用方法,并通过SSL_CTX_set_min_proto_version
和SSL_CTX_set_max_proto_version
来控制允许的协议版本范围,以增强安全性。
8.3.2 配置SSL_CTX
创建SSL_CTX
后,需要对其进行配置以满足应用需求。以下是一些常见的配置操作:
① 设置协议版本范围:
▮▮▮▮控制允许使用的最低和最高TLS协议版本。
1
// 强制使用TLS 1.2及以上版本 (推荐)
2
if (SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) == 0) {
3
// 错误处理
4
}
5
// 可以选择设置最高版本,但不设置通常更好,以便支持未来的新版本
6
// if (SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION) == 0) {
7
// // 错误处理
8
// }
② 设置密码套件列表(Cipher List):
▮▮▮▮指定客户端或服务器接受或偏好的密码套件列表。一个安全的密码套件列表至关重要。
1
// 设置一个相对安全的密码套件列表
2
// 这个字符串格式比较复杂,可以参考OpenSSL文档或在线生成器
3
const char* cipher_list = "HIGH:!aNULL:!MD5:!RC4"; // 示例:高强度密码套件,排除匿名认证、MD5、RC4
4
if (SSL_CTX_set_cipher_list(ctx, cipher_list) == 0) {
5
// 错误处理
6
}
③ 加载证书和私钥(服务器端或客户端认证):
▮▮▮▮对于服务器端,需要加载服务器证书和对应的私钥。对于需要客户端认证的客户端,也需要加载客户端证书和私钥。
1
// 示例:加载PEM格式的证书和私钥文件
2
// const char* cert_file = "server_cert.pem";
3
// const char* key_file = "server_key.pem";
4
// if (SSL_CTX_use_certificate_file(ctx, cert_file, SSL_FILETYPE_PEM) <= 0) {
5
// ERR_print_errors_fp(stderr); // 错误处理
6
// }
7
// if (SSL_CTX_use_PrivateKey_file(ctx, key_file, SSL_FILETYPE_PEM) <= 0) {
8
// ERR_print_errors_fp(stderr); // 错误处理
9
// }
10
// 检查证书和私钥是否匹配
11
// if (SSL_CTX_check_private_key(ctx) == 0) {
12
// fprintf(stderr, "Private key does not match the certificate\n");
13
// // 错误处理
14
// }
④ 加载信任根证书或CA证书:
▮▮▮▮对于客户端,需要加载信任的CA证书,用于验证服务器提供的证书链。对于需要客户端认证的服务器,也需要加载信任的CA证书,用于验证客户端证书。
1
// 从文件中加载信任的CA证书 (PEM格式)
2
const char* ca_file = "ca_certificates.pem"; // 包含一个或多个CA证书
3
if (SSL_CTX_load_verify_locations(ctx, ca_file, nullptr) == 0) {
4
ERR_print_errors_fp(stderr); // 错误处理
5
}
6
// 或者加载系统默认的信任路径
7
// if (SSL_CTX_set_default_verify_paths(ctx) == 0) {
8
// ERR_print_errors_fp(stderr); // 错误处理
9
// }
⑤ 设置验证模式:
▮▮▮▮配置是否需要验证对端证书,以及验证失败时的行为。
1
// 客户端验证服务器证书
2
// SSL_VERIFY_PEER: 要求验证对端证书
3
// SSL_VERIFY_FAIL_IF_NO_PEER_CERT: 如果对端没有提供证书则验证失败 (仅在SSL_VERIFY_PEER设置时有效)
4
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr /* 可选的验证回调函数 */);
5
6
// 服务器端请求并验证客户端证书
7
// SSL_VERIFY_PEER: 要求验证客户端证书
8
// SSL_VERIFY_FAIL_IF_NO_PEER_CERT: 如果客户端没有提供证书则验证失败 (实现双向认证)
9
// SSL_VERIFY_CLIENT_ONCE: 仅在第一次建立连接时验证客户端证书 (会话复用时不再验证)
10
// SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr);
⑥ 设置选项(Options):
▮▮▮▮通过各种标志位开启或禁用OpenSSL的特定功能或特性。
1
// 禁用SSLv2和SSLv3 (因为存在安全漏洞)
2
// SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 是 OpenSSL 1.1.0+ 中的推荐做法
3
// 在更旧的版本可能使用不同的宏
4
SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION); // 禁用压缩,防止CRIME攻击
8.3.3 清理SSL_CTX对象
当SSL_CTX
对象不再需要时,应使用SSL_CTX_free
函数释放其占用的资源。注意,只有当所有使用此SSL_CTX
创建的SSL
对象都被释放后,SSL_CTX
才能安全释放。
1
// 在应用程序结束或上下文不再需要时调用
2
if (ctx != nullptr) {
3
SSL_CTX_free(ctx);
4
ctx = nullptr;
5
}
正确地创建和配置SSL_CTX
是构建安全TLS/SSL应用程序的基础。它为后续的SSL
对象提供了必要的安全上下文。
8.4 OpenSSL SSL对象(SSL)
SSL
对象在OpenSSL中代表一个独立的TLS/SSL连接。它是SSL_CTX
的实例,继承了SSL_CTX
的配置,但可以根据具体连接进行一些特定的调整。所有涉及具体连接的操作(如握手、数据读写、关闭)都是通过SSL
对象进行的。
8.4.1 创建SSL对象
通过一个已经配置好的SSL_CTX
对象来创建SSL
对象:
1
#include <openssl/ssl.h>
2
3
// 假设 ctx 是一个已经创建并配置好的 SSL_CTX 对象
4
SSL* ssl = nullptr;
5
6
ssl = SSL_new(ctx);
7
if (ssl == nullptr) {
8
// 错误处理
9
ERR_print_errors_fp(stderr);
10
// ...
11
}
12
13
// 后续关联I/O,进行连接...
14
15
// 清理 SSL 对象
16
// SSL_free(ssl);
一个SSL_CTX
可以创建任意数量的SSL
对象,每个SSL
对象代表一个独立的、可能并发的连接。
8.4.2 关联I/O
SSL
对象本身不直接进行底层的网络通信,它需要与一个底层传输通道关联起来。通常,这个通道是一个套接字(Socket)。OpenSSL提供了两种主要方式将SSL
对象与底层I/O关联:
① 使用文件描述符(File Descriptor)或套接字句柄:
▮▮▮▮这是最直接的方式,将已连接的套接字的文件描述符或句柄关联到SSL
对象。
1
#include <openssl/ssl.h>
2
#include <sys/socket.h> // for socket types
3
4
int sock_fd = -1; // 假设这是一个已连接的套接字文件描述符
5
6
if (SSL_set_fd(ssl, sock_fd) <= 0) {
7
// 错误处理
8
ERR_print_errors_fp(stderr);
9
// ...
10
}
11
// 此时,OpenSSL会直接使用这个文件描述符进行底层读写
▮▮▮▮这种方法简单,但不够灵活,例如难以集成到异步I/O框架中。
② 使用BIO抽象层:
▮▮▮▮BIO (Basic Input/Output) 是OpenSSL提供的一个通用I/O抽象层。使用BIO可以将SSL
对象与各种不同的I/O源(如内存缓冲区、文件、套接字)关联起来,提供了更大的灵活性。对于网络通信,通常使用套接字BIO。
1
#include <openssl/ssl.h>
2
#include <openssl/bio.h>
3
4
int sock_fd = -1; // 假设这是一个已连接的套接字文件描述符
5
6
// 创建一个套接字BIO
7
BIO* bio = BIO_new_socket(sock_fd, BIO_NOCLOSE); // BIO_NOCLOSE表示关闭BIO时不关闭底层套接字
8
if (bio == nullptr) {
9
// 错误处理
10
ERR_print_errors_fp(stderr);
11
// ...
12
}
13
14
// 将BIO关联到SSL对象
15
SSL_set_bio(ssl, bio, bio); // 读写都使用同一个BIO
16
// SSL_set_bio 内部会增加 BIO 的引用计数,所以如果不需要手动管理 BIO,
17
// 可以在设置后释放 bio 指针,实际的 BIO 对象生命周期由 SSL 对象管理。
18
// BIO_free(bio); // 如果 BIO_NOCLOSE,可以在这里释放 bio 指针
▮▮▮▮推荐使用BIO进行I/O关联,特别是对于复杂的应用场景或需要集成异步I/O时。
8.4.3 建立连接与握手
关联好I/O后,就可以启动TLS/SSL握手过程来建立安全连接了。
① 客户端: 使用SSL_connect
发起握手。
1
int ret = SSL_connect(ssl);
2
if (ret <= 0) {
3
int err = SSL_get_error(ssl, ret);
4
// 处理握手失败或需要重试的情况
5
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
6
// 底层I/O操作需要更多数据或等待写入,稍后重试
7
// 对于阻塞I/O,这通常表示错误或连接中断
8
// 对于非阻塞I/O,需要等待套接字可读/写后再次调用 SSL_connect
9
} else {
10
// 真正的错误
11
ERR_print_errors_fp(stderr);
12
// ...
13
}
14
} else {
15
// 握手成功!
16
fprintf(stdout, "SSL/TLS connection established.\n");
17
fprintf(stdout, "Protocol: %s\n", SSL_get_version(ssl));
18
fprintf(stdout, "Cipher: %s\n", SSL_get_cipher(ssl));
19
}
② 服务器端: 使用SSL_accept
接受连接并执行握手。
1
// 在服务器端接受客户端连接后,为新的套接字创建并关联SSL对象
2
// ...
3
int ret = SSL_accept(ssl);
4
if (ret <= 0) {
5
int err = SSL_get_error(ssl, ret);
6
// 处理握手失败或需要重试
7
// ...
8
} else {
9
// 握手成功
10
// ...
11
}
SSL_connect
和SSL_accept
会执行完整的TLS/SSL握手过程。它们可能是阻塞的,取决于底层I/O(套接字)是阻塞模式还是非阻塞模式。在非阻塞模式下,如果握手需要等待底层I/O,这些函数会返回 <= 0
,并且SSL_get_error
会返回SSL_ERROR_WANT_READ
或SSL_ERROR_WANT_WRITE
,此时应用层应该等待相应的I/O事件就绪后再调用相同的函数继续握手。
8.4.4 数据传输
握手成功后,就可以使用SSL_read
和SSL_write
函数通过安全的TLS/SSL通道传输应用数据了。
① 发送数据: 使用SSL_write
。
1
const char* data_to_send = "Hello, secure world!";
2
int data_len = strlen(data_to_send);
3
int sent_len = SSL_write(ssl, data_to_send, data_len);
4
5
if (sent_len <= 0) {
6
int err = SSL_get_error(ssl, sent_len);
7
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
8
// 需要等待底层I/O就绪后重试
9
} else if (err == SSL_ERROR_SYSCALL) {
10
// 底层系统调用错误,检查 errno
11
perror("Socket error during SSL_write");
12
}
13
else {
14
// 其他SSL错误
15
ERR_print_errors_fp(stderr);
16
}
17
} else if (sent_len < data_len) {
18
// 只发送了部分数据,需要处理剩余数据的发送
19
} else {
20
// 数据全部发送成功
21
}
② 接收数据: 使用SSL_read
。
1
char buffer[4096];
2
int received_len = SSL_read(ssl, buffer, sizeof(buffer) - 1);
3
4
if (received_len <= 0) {
5
int err = SSL_get_error(ssl, received_len);
6
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
7
// 需要等待底层I/O就绪后重试
8
} else if (err == SSL_ERROR_ZERO_RETURN) {
9
// 对端已正常关闭连接 (TLS/SSL shutdown complete)
10
fprintf(stdout, "Peer closed connection.\n");
11
} else if (err == SSL_ERROR_SYSCALL) {
12
// 底层系统调用错误,检查 errno
13
if (received_len == 0) { // errno == 0 且 received_len == 0 通常表示连接被粗暴关闭
14
fprintf(stderr, "Connection closed unexpectedly by peer.\n");
15
} else {
16
perror("Socket error during SSL_read");
17
}
18
}
19
else {
20
// 其他SSL错误
21
ERR_print_errors_fp(stderr);
22
}
23
} else {
24
// 成功读取到数据
25
buffer[received_len] = '\0'; // C风格字符串结束符
26
fprintf(stdout, "Received data: %s\n", buffer);
27
// 处理接收到的数据
28
}
SSL_read
和SSL_write
的返回值和错误处理与普通的read
/write
系统调用类似,但需要额外检查SSL_get_error
返回的SSL层面的错误。
8.4.5 关闭连接
关闭TLS/SSL连接需要一个协商过程来安全地结束会话,防止截断攻击。使用SSL_shutdown
。
1
int ret = SSL_shutdown(ssl);
2
if (ret == 1) {
3
// 完全关闭成功 (双向shutdown完成)
4
} else if (ret == 0) {
5
// 第一次调用 shutdown,已发送shutdown通知给对端,需要再次调用等待对端响应
6
// 对于阻塞套接字,通常会阻塞直到对方也调用shutdown并收到Finished消息
7
// 对于非阻塞套接字,会立即返回0,需要等待套接字可读后再次调用SSL_shutdown
8
ret = SSL_shutdown(ssl); // 再次调用以完成双向shutdown
9
if (ret != 1) {
10
// 第二次调用仍然未完成,处理错误或重试
11
// ...
12
}
13
} else { // ret < 0
14
int err = SSL_get_error(ssl, ret);
15
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
16
// 需要等待底层I/O就绪后重试
17
} else {
18
// 错误发生
19
ERR_print_errors_fp(stderr);
20
}
21
}
22
23
// 在SSL_shutdown成功返回1后,可以关闭底层套接字
24
// close(sock_fd); // Linux/Unix
25
// closesocket(sock_fd); // Windows
SSL_shutdown
执行TLS/SSL层的关闭握手。理想情况下,应该执行双向(two-way)shutdown,即双方都发送和接收到关闭通知。第一次调用SSL_shutdown
会发送关闭通知给对端,返回0;再次调用会在收到对端通知后完成关闭,返回1。如果在第一次调用前对端已经发送了关闭通知,则第一次调用可能直接返回1。如果只需要单向(one-way)关闭(不等待对端响应),可以使用SSL_set_shutdown(ssl, SSL_RECEIVED_SHUTDOWN);
然后在调用一次SSL_shutdown
,它将只发送关闭通知但不等待响应,返回1。但在大多数情况下,推荐执行双向关闭。
8.4.6 清理SSL对象
当一个TLS/SSL连接不再需要时,应该使用SSL_free
函数释放SSL
对象及其关联的资源。请注意,SSL_free
不会关闭底层套接字,也不会释放关联的BIO(除非BIO是内部创建的,例如通过SSL_set_fd
)。如果使用了BIO_new_socket
并使用了BIO_NOCLOSE
,则需要手动关闭套接字。
1
if (ssl != nullptr) {
2
SSL_free(ssl);
3
ssl = nullptr;
4
}
8.5 实现一个基本TLS/SSL客户端
本节将通过一个完整的C++代码示例,演示如何使用OpenSSL库实现一个基本的TLS/SSL客户端程序,连接到一个HTTPS服务器(例如 www.google.com:443
),发送一个HTTP GET请求,接收并打印响应。
示例代码:
1
#include <iostream>
2
#include <string>
3
#include <vector>
4
#include <cstring> // For strlen
5
6
#include <openssl/ssl.h>
7
#include <openssl/err.h>
8
9
#ifdef _WIN32
10
#include <winsock2.h>
11
#include <ws2tcpip.h>
12
#pragma comment(lib, "ws2_32.lib")
13
#pragma comment(lib, "crypt32.lib")
14
#pragma comment(lib, "ssl.lib")
15
#pragma comment(lib, "crypto.lib")
16
#else
17
#include <sys/socket.h>
18
#include <arpa/inet.h>
19
#include <unistd.h> // For close
20
#endif
21
22
// 函数声明
23
void initialize_winsock();
24
void cleanup_winsock();
25
int create_socket(const std::string& host, int port);
26
SSL_CTX* create_ssl_context();
27
void configure_ssl_context(SSL_CTX* ctx);
28
void cleanup_ssl_context(SSL_CTX* ctx);
29
SSL* create_ssl_object(SSL_CTX* ctx, int sock_fd);
30
void cleanup_ssl_object(SSL* ssl);
31
void ssl_perform_handshake(SSL* ssl);
32
void ssl_send_data(SSL* ssl, const std::string& data);
33
std::string ssl_receive_data(SSL* ssl);
34
void ssl_shutdown(SSL* ssl);
35
void openssl_global_init();
36
void openssl_global_cleanup();
37
void print_ssl_error(const std::string& func_name, int ret);
38
39
// 全局或静态初始化函数 (OpenSSL 1.1.0+)
40
void openssl_global_init() {
41
// SSL_library_init() and SSL_load_error_strings() are deprecated in 1.1.0+
42
// OPENSSL_init_ssl is the new way.
43
// Pass OPENSSL_INIT_LOAD_SSL_STRINGS to load error strings
44
// Pass OPENSSL_INIT_ADD_SSL_CIPHERS to load SSL ciphers
45
// Pass OPENSSL_INIT_ADD_ALL_DIGESTS/ciphers to load all algorithms
46
// Pass OPENSSL_INIT_LOAD_CONFIG for configuration file
47
// Combined flags for common use:
48
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_ADD_SSL_CIPHERS | OPENSSL_INIT_ADD_ALL_DIGESTS, nullptr);
49
std::cout << "OpenSSL initialized." << std::endl;
50
}
51
52
// 全局或静态清理函数
53
void openssl_global_cleanup() {
54
// 在OpenSSL 1.1.0+中,清理通常由系统自动处理,但显式调用可以确保资源释放
55
// EVP_cleanup(); // Deprecated in 1.1.0+
56
// CRYPTO_cleanup_all_ex_data(); // Deprecated in 1.1.0+
57
// ERR_free_strings(); // Deprecated in 1.1.0+
58
// 更简单的做法是依赖系统退出时的清理,或者使用 OPENSSL_cleanup() (experimental)
59
// OPENSSL_cleanup(); // Use with caution, might not be fully stable
60
std::cout << "OpenSSL cleanup may be handled by system." << std::endl;
61
}
62
63
64
#ifdef _WIN32
65
void initialize_winsock() {
66
WSADATA wsaData;
67
int ret = WSAStartup(MAKEWORD(2, 2), &wsaData);
68
if (ret != 0) {
69
std::cerr << "WSAStartup failed: " << ret << std::endl;
70
exit(EXIT_FAILURE);
71
}
72
std::cout << "Winsock initialized." << std::endl;
73
}
74
75
void cleanup_winsock() {
76
WSACleanup();
77
std::cout << "Winsock cleanup." << std::endl;
78
}
79
#endif
80
81
// 创建并连接套接字
82
int create_socket(const std::string& host, int port) {
83
int sock_fd = -1;
84
struct addrinfo hints, *res, *p;
85
int status;
86
87
memset(&hints, 0, sizeof hints);
88
hints.ai_family = AF_UNSPEC; // 允许 IPv4 或 IPv6
89
hints.ai_socktype = SOCK_STREAM; // TCP 流套接字
90
91
std::string port_str = std::to_string(port);
92
93
if ((status = getaddrinfo(host.c_str(), port_str.c_str(), &hints, &res)) != 0) {
94
std::cerr << "getaddrinfo error: " << gai_strerror(status) << std::endl;
95
return -1;
96
}
97
98
for (p = res; p != nullptr; p = p->ai_next) {
99
#ifdef _WIN32
100
sock_fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
101
#else
102
sock_fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
103
#endif
104
if (sock_fd == -1) {
105
continue; // 尝试下一个地址
106
}
107
108
#ifdef _WIN32
109
if (connect(sock_fd, p->ai_addr, (int)p->ai_addrlen) == SOCKET_ERROR) {
110
closesocket(sock_fd);
111
sock_fd = -1;
112
continue; // 尝试下一个地址
113
}
114
#else
115
if (connect(sock_fd, p->ai_addr, p->ai_addrlen) == -1) {
116
close(sock_fd);
117
sock_fd = -1;
118
continue; // 尝试下一个地址
119
}
120
#endif
121
break; // 连接成功
122
}
123
124
freeaddrinfo(res); // 释放地址信息
125
126
if (sock_fd == -1) {
127
std::cerr << "Failed to connect to " << host << ":" << port << std::endl;
128
} else {
129
std::cout << "Socket connected to " << host << ":" << port << std::endl;
130
}
131
132
return sock_fd;
133
}
134
135
// 创建SSL上下文
136
SSL_CTX* create_ssl_context() {
137
const SSL_METHOD* method = TLS_client_method(); // 推荐使用 TLS_client_method()
138
if (method == nullptr) {
139
ERR_print_errors_fp(stderr);
140
return nullptr;
141
}
142
143
SSL_CTX* ctx = SSL_CTX_new(method);
144
if (ctx == nullptr) {
145
ERR_print_errors_fp(stderr);
146
return nullptr;
147
}
148
149
std::cout << "SSL_CTX created." << std::endl;
150
return ctx;
151
}
152
153
// 配置SSL上下文 (加载信任的CA证书等)
154
void configure_ssl_context(SSL_CTX* ctx) {
155
// 加载系统默认的CA证书路径,用于验证服务器证书
156
if (SSL_CTX_set_default_verify_paths(ctx) == 0) {
157
ERR_print_errors_fp(stderr);
158
std::cerr << "Failed to load default CA certificates." << std::endl;
159
} else {
160
std::cout << "Default CA certificates loaded." << std::endl;
161
}
162
163
// 设置验证模式:要求验证对端证书 (服务器证书)
164
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr);
165
166
// 设置最小协议版本为 TLS 1.2 (推荐)
167
if (SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) == 0) {
168
ERR_print_errors_fp(stderr);
169
std::cerr << "Failed to set minimum TLS version." << std::endl;
170
}
171
172
// 可以设置一些选项,例如禁用不安全的协议或特性
173
SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION);
174
std::cout << "SSL_CTX configured." << std::endl;
175
}
176
177
// 清理SSL上下文
178
void cleanup_ssl_context(SSL_CTX* ctx) {
179
if (ctx != nullptr) {
180
SSL_CTX_free(ctx);
181
std::cout << "SSL_CTX freed." << std::endl;
182
}
183
}
184
185
// 创建SSL对象并关联套接字
186
SSL* create_ssl_object(SSL_CTX* ctx, int sock_fd) {
187
SSL* ssl = SSL_new(ctx);
188
if (ssl == nullptr) {
189
ERR_print_errors_fp(stderr);
190
return nullptr;
191
}
192
193
// 将套接字关联到SSL对象
194
#ifdef _WIN32
195
if (SSL_set_fd(ssl, (SOCKET)sock_fd) <= 0) {
196
#else
197
if (SSL_set_fd(ssl, sock_fd) <= 0) {
198
#endif
199
ERR_print_errors_fp(stderr);
200
SSL_free(ssl); // 关联失败,释放SSL对象
201
return nullptr;
202
}
203
204
std::cout << "SSL object created and associated with socket." << std::endl;
205
return ssl;
206
}
207
208
// 清理SSL对象
209
void cleanup_ssl_object(SSL* ssl) {
210
if (ssl != nullptr) {
211
SSL_free(ssl);
212
std::cout << "SSL object freed." << std::endl;
213
}
214
}
215
216
// 执行TLS/SSL握手
217
void ssl_perform_handshake(SSL* ssl) {
218
int ret = SSL_connect(ssl);
219
if (ret <= 0) {
220
print_ssl_error("SSL_connect", ret);
221
exit(EXIT_FAILURE); // 握手失败,退出
222
} else {
223
std::cout << "SSL/TLS handshake successful." << std::endl;
224
std::cout << "Protocol: " << SSL_get_version(ssl) << std::endl;
225
std::cout << "Cipher: " << SSL_get_cipher(ssl) << std::endl;
226
227
// 握手成功后,检查服务器证书验证结果
228
long verify_result = SSL_get_verify_result(ssl);
229
if (verify_result != X509_V_OK) {
230
std::cerr << "Server certificate verification failed: " <<
231
X509_verify_cert_error_string(verify_result) << std::endl;
232
// 根据需要决定是否继续或退出
233
// exit(EXIT_FAILURE);
234
} else {
235
std::cout << "Server certificate verified successfully." << std::endl;
236
}
237
}
238
}
239
240
// 发送数据
241
void ssl_send_data(SSL* ssl, const std::string& data) {
242
int len = data.length();
243
int total_sent = 0;
244
while (total_sent < len) {
245
int sent = SSL_write(ssl, data.c_str() + total_sent, len - total_sent);
246
if (sent <= 0) {
247
print_ssl_error("SSL_write", sent);
248
// 处理 SSL_ERROR_WANT_READ/WRITE 或其他错误
249
int err = SSL_get_error(ssl, sent);
250
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
251
// 对于阻塞套接字,这不应该经常发生,但如果发生可能表示问题。
252
// 对于非阻塞套接字,需要等待I/O就绪。
253
std::cerr << "SSL_write wants read/write. Handling depends on socket mode." << std::endl;
254
// 这里简单起见,作为错误处理
255
exit(EXIT_FAILURE);
256
} else {
257
exit(EXIT_FAILURE);
258
}
259
}
260
total_sent += sent;
261
}
262
std::cout << "Sent " << total_sent << " bytes." << std::endl;
263
}
264
265
// 接收数据
266
std::string ssl_receive_data(SSL* ssl) {
267
std::string received_data;
268
char buffer[4096];
269
int received_len;
270
271
while (true) {
272
received_len = SSL_read(ssl, buffer, sizeof(buffer) - 1);
273
if (received_len > 0) {
274
buffer[received_len] = '\0';
275
received_data.append(buffer, received_len);
276
// 对于简单的HTTP/1.0请求,接收到一个完整的响应后可能需要判断结束标志
277
// 但对于流式协议或大型响应,可能需要多次读取
278
// 这里假设一次读取或简单循环读取直到SSL_read返回<=0
279
} else {
280
int err = SSL_get_error(ssl, received_len);
281
if (err == SSL_ERROR_ZERO_RETURN) {
282
// 对端已正常关闭连接
283
std::cout << "Peer closed connection during read." << std::endl;
284
break; // 退出循环
285
} else if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
286
// 需要等待底层I/O就绪
287
std::cerr << "SSL_read wants read/write. Handling depends on socket mode." << std::endl;
288
// 这里简单起见,作为错误处理
289
break;
290
} else {
291
print_ssl_error("SSL_read", received_len);
292
break; // 发生错误,退出循环
293
}
294
}
295
}
296
std::cout << "Received " << received_data.length() << " bytes." << std::endl;
297
return received_data;
298
}
299
300
// 安全关闭TLS/SSL连接
301
void ssl_shutdown(SSL* ssl) {
302
if (ssl == nullptr) return;
303
304
int ret = SSL_shutdown(ssl);
305
306
if (ret < 0) {
307
print_ssl_error("SSL_shutdown", ret);
308
} else if (ret == 0) {
309
// 第一次调用,需要等待对端响应,再次调用完成双向关闭
310
std::cout << "SSL_shutdown sent close notify, waiting for peer response..." << std::endl;
311
// 在阻塞模式下,第二次调用会阻塞直到对端响应
312
// 在非阻塞模式下,需要等待套接字可读后再次调用 SSL_shutdown
313
int ret2 = SSL_shutdown(ssl);
314
if (ret2 < 0) {
315
print_ssl_error("SSL_shutdown (second call)", ret2);
316
} else if (ret2 == 0) {
317
std::cerr << "SSL_shutdown second call returned 0, should be 1 for blocking socket." << std::endl;
318
} else { // ret2 == 1
319
std::cout << "SSL_shutdown complete (bidirectional)." << std::endl;
320
}
321
} else { // ret == 1
322
std::cout << "SSL_shutdown complete (unidirectional or already received peer close notify)." << std::endl;
323
}
324
}
325
326
// 打印SSL错误信息
327
void print_ssl_error(const std::string& func_name, int ret) {
328
int err = SSL_get_error(nullptr, ret); // Pass nullptr SSL to get general error
329
if (err == SSL_ERROR_SYSCALL) {
330
#ifdef _WIN32
331
std::cerr << func_name << " failed with SYSCALL error, WSAGetLastError: " << WSAGetLastError() << std::endl;
332
#else
333
std::cerr << func_name << " failed with SYSCALL error, errno: " << errno << std::endl;
334
#endif
335
} else {
336
std::cerr << func_name << " failed with SSL error code " << err << std::endl;
337
ERR_print_errors_fp(stderr);
338
}
339
}
340
341
342
int main() {
343
#ifdef _WIN32
344
initialize_winsock();
345
#endif
346
347
openssl_global_init(); // 初始化OpenSSL库
348
349
SSL_CTX* ctx = nullptr;
350
SSL* ssl = nullptr;
351
int sock_fd = -1;
352
353
try {
354
// 1. 创建和配置SSL上下文
355
ctx = create_ssl_context();
356
if (ctx == nullptr) {
357
throw std::runtime_error("Failed to create SSL context.");
358
}
359
configure_ssl_context(ctx); // 配置信任根证书等
360
361
// 2. 创建并连接底层套接字
362
std::string host = "www.google.com";
363
int port = 443;
364
sock_fd = create_socket(host, port);
365
if (sock_fd < 0) {
366
throw std::runtime_error("Failed to create or connect socket.");
367
}
368
369
// 3. 创建SSL对象并关联套接字
370
ssl = create_ssl_object(ctx, sock_fd);
371
if (ssl == nullptr) {
372
throw std::runtime_error("Failed to create SSL object.");
373
}
374
375
// 4. 执行TLS/SSL握手
376
ssl_perform_handshake(ssl);
377
378
// 5. 发送HTTP GET请求
379
std::string request = "GET / HTTP/1.0\r\nHost: " + host + "\r\nConnection: close\r\n\r\n";
380
ssl_send_data(ssl, request);
381
382
// 6. 接收HTTP响应
383
std::string response = ssl_receive_data(ssl);
384
std::cout << "\n--- Received Response ---" << std::endl;
385
std::cout << response << std::endl;
386
std::cout << "-------------------------" << std::endl;
387
388
// 7. 安全关闭TLS/SSL连接
389
ssl_shutdown(ssl);
390
391
} catch (const std::exception& e) {
392
std::cerr << "Exception: " << e.what() << std::endl;
393
}
394
395
// 8. 清理资源
396
cleanup_ssl_object(ssl); // 先释放SSL对象
397
#ifdef _WIN32
398
if (sock_fd != -1) closesocket(sock_fd); // 然后关闭底层套接字 (如果是用SSL_set_fd)
399
#else
400
if (sock_fd != -1) close(sock_fd);
401
#endif
402
cleanup_ssl_context(ctx); // 最后释放SSL上下文
403
404
openssl_global_cleanup(); // 清理OpenSSL库
405
406
#ifdef _WIN32
407
cleanup_winsock();
408
#endif
409
410
return 0;
411
}
代码说明:
⚝ 初始化(Initialization): 在程序开始时,需要调用OpenSSL的初始化函数。在OpenSSL 1.1.0及更高版本中,推荐使用OPENSSL_init_ssl
。这个函数负责加载加密算法、错误字符串等。同时,如果在Windows平台上,还需要初始化Winsock库。
⚝ 套接字连接(Socket Connection): 客户端首先需要建立与服务器的底层TCP连接。这里使用了标准的套接字API(getaddrinfo
, socket
, connect
)。
⚝ 创建SSL_CTX: 使用SSL_CTX_new
创建一个SSL上下文,指定使用TLS_client_method()
表示这是一个客户端上下文。
⚝ 配置SSL_CTX: 调用configure_ssl_context
函数进行配置。最重要的配置包括设置信任的CA证书位置(这里使用SSL_CTX_set_default_verify_paths
加载系统默认路径)和设置验证模式为SSL_VERIFY_PEER
,这指示OpenSSL在握手时自动验证服务器证书。此外,还设置了最小协议版本和一些安全选项。
⚝ 创建SSL对象: 使用SSL_new
基于配置好的SSL_CTX
创建一个SSL
对象。
⚝ 关联套接字: 使用SSL_set_fd
将已连接的套接字文件描述符关联到SSL
对象。
⚝ 执行握手: 调用SSL_connect
函数启动TLS/SSL握手过程。OpenSSL会在此函数内部完成与服务器的协商、密钥交换和证书验证。如果SSL_VERIFY_PEER
设置正确且信任链有效,握手会成功;否则会失败。握手成功后,会打印出协商的协议版本和密码套件,并检查SSL_get_verify_result
获取证书验证的详细结果。
⚝ 数据传输: 握手成功后,使用SSL_write
发送HTTP GET请求,使用SSL_read
接收服务器的HTTP响应。请注意SSL_write
和SSL_read
的返回值和错误处理,特别是SSL_ERROR_WANT_READ
和SSL_ERROR_WANT_WRITE
,尽管在阻塞模式下不常见,但在非阻塞模式下是必须处理的。
⚝ 关闭连接: 调用SSL_shutdown
安全地关闭TLS/SSL连接。推荐执行双向关闭。
⚝ 清理(Cleanup): 在程序结束前,释放所有OpenSSL对象(SSL
,SSL_CTX
)以及底层资源(套接字,Winsock)。注意释放顺序:先SSL
,再SSL_CTX
,最后底层库(OpenSSL全局清理,Winsock清理)。
这个示例代码提供了一个使用OpenSSL进行TLS/SSL客户端开发的最小工作框架。在实际应用中,错误处理、对SSL_ERROR_WANT_*
的处理(特别是非阻塞I/O)、以及更复杂的证书验证逻辑会更加完善。
8.6 客户端证书验证
在大多数TLS/SSL连接中,通常只有服务器需要向客户端证明其身份(通过服务器证书)。然而,在某些场景下,如企业内部应用或需要更高安全级别的服务,可能需要客户端也向服务器证明其身份,这被称为双向认证(Mutual Authentication) 或 客户端证书认证(Client Certificate Authentication)。
在本章的客户端开发语境下,“客户端证书验证”主要包含两个层面:
① 客户端验证服务器证书:这是TLS/SSL客户端的默认行为和核心安全功能。客户端接收服务器发送的证书链,并验证其是否有效、是否由信任的CA签发、是否吊销、以及证书中的主机名是否与连接的服务器主机名匹配。
② 客户端提供客户端证书供服务器验证:如果服务器请求进行客户端认证,客户端需要发送自己的证书链和对应的私钥用于签名握手消息。
本节主要聚焦于第一个层面,即客户端如何配置OpenSSL来正确验证服务器证书。第二个层面(提供客户端证书)在需要双向认证时是必要的。
8.6.1 验证服务器证书
TLS/SSL客户端验证服务器证书是防止中间人攻击(Man-in-the-Middle Attack)的关键步骤。验证过程通常包括:
⚝ 证书链有效性检查: 验证证书的签名链是否能够追溯到一个客户端信任的根证书(Trust Anchor)。
⚝ 有效期检查: 验证证书是否在有效期内。
⚝ 吊销状态检查: 检查证书是否已被其签发CA吊销(通过CRL或OCSP)。
⚝ 主机名匹配检查: 验证服务器证书中的主机名(在Subject Alternative Name扩展或Subject字段的Common Name中)是否与客户端尝试连接的主机名一致。这是许多OpenSSL版本中默认不自动执行但至关重要的步骤。
在OpenSSL中,配置客户端进行服务器证书验证主要涉及以下步骤:
① 设置验证模式: 在创建SSL_CTX
后,必须调用SSL_CTX_set_verify
设置验证标志。
1
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr); // SSL_VERIFY_PEER 要求验证对端证书
设置SSL_VERIFY_PEER
后,OpenSSL会在SSL_connect
(或SSL_do_handshake
)期间自动执行证书链验证。
② 加载信任锚(Trust Anchor)/CA证书: 客户端需要知道哪些CA是被信任的,这样才能验证服务器证书链的起点。这通常通过加载包含一个或多个信任CA证书的文件或目录来完成。
1
// 从单个文件加载PEM格式的CA证书
2
// if (SSL_CTX_load_verify_locations(ctx, "path/to/ca-certificates.pem", nullptr) == 0) {
3
// ERR_print_errors_fp(stderr);
4
// // 错误处理
5
// }
6
7
// 从目录加载CA证书
8
// if (SSL_CTX_load_verify_locations(ctx, nullptr, "path/to/ca-certificates-directory") == 0) {
9
// ERR_print_errors_fp(stderr);
10
// // 错误处理
11
// }
12
13
// 加载系统默认的CA证书路径 (推荐,方便移植)
14
if (SSL_CTX_set_default_verify_paths(ctx) == 0) {
15
ERR_print_errors_fp(stderr);
16
// 错误处理
17
}
SSL_CTX_set_default_verify_paths
是一个便捷函数,它尝试加载系统默认位置的CA证书。这些位置通常在编译OpenSSL时确定。如果需要更灵活的控制,应该使用SSL_CTX_load_verify_locations
指定特定的文件或目录。
③ 处理主机名匹配: 虽然OpenSSL会自动验证证书链的有效性和信任根,但默认情况下不自动进行主机名匹配检查。这是一个常见的安全疏忽点。在OpenSSL 1.0.2及更高版本中,可以通过设置Hostname Validation参数来实现。
1
// 在 SSL 对象上设置期望的主机名
2
const char* expected_hostname = "www.google.com";
3
if (SSL_set1_host(ssl, expected_hostname) == 0) {
4
ERR_print_errors_fp(stderr);
5
// 错误处理
6
}
7
// 设置 SSL_CTX 上的验证标志,结合 SSL_set1_host 使用
8
// SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, nullptr); // SSL_VERIFY_CLIENT_ONCE 是服务器端标志,客户端不需要
9
10
// 另一种方式是使用 SSL_set_verify 传入一个自定义回调函数,手动检查主机名
11
// SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, my_verify_callback);
使用SSL_set1_host
是最简便且推荐的方式来实现主机名匹配(在OpenSSL 1.0.2+)。OpenSSL在证书验证过程中会自动检查证书的Subject Alternative Name (SAN) 扩展或Common Name (CN) 是否与设置的主机名匹配。
④ 检查验证结果: 握手完成后,可以通过SSL_get_verify_result
获取证书验证的详细结果码。返回X509_V_OK
表示验证成功,其他值表示失败及其原因。
1
long verify_result = SSL_get_verify_result(ssl);
2
if (verify_result != X509_V_OK) {
3
std::cerr << "Server certificate verification failed: " <<
4
X509_verify_cert_error_string(verify_result) << std::endl;
5
// 根据安全策略,这里应该中断连接
6
} else {
7
std::cout << "Server certificate verified successfully." << std::endl;
8
}
即使SSL_connect
成功返回,也强烈建议检查SSL_get_verify_result
,因为某些验证错误可能被配置为警告而不是致命错误(尽管在SSL_VERIFY_PEER
模式下,除非有自定义回调,大部分验证错误都是致命的)。
⑤ 自定义验证回调函数: 对于更复杂的验证逻辑,例如根据特定策略检查证书的某个扩展字段、或实现自定义的吊销检查,可以编写一个验证回调函数,并通过SSL_CTX_set_verify
的第三个参数设置。回调函数会在OpenSSL进行证书链验证的每个阶段被调用,允许开发者介入并控制验证过程。
1
int my_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) {
2
// preverify_ok 是OpenSSL内部验证的结果 (1表示成功,0表示失败)
3
// 可以获取当前正在验证的证书、错误码等信息进行自定义判断
4
// int err = X509_STORE_CTX_get_error(ctx);
5
// X509 *cert = X509_STORE_CTX_get_current_cert(ctx);
6
// ... 执行自定义验证逻辑 ...
7
// 返回1表示验证通过,0表示验证失败
8
return preverify_ok; // 默认情况下,只返回OpenSSL的内部验证结果
9
}
10
11
// 设置自定义回调
12
// SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, my_verify_callback);
自定义回调函数非常强大,但也需要谨慎实现,避免引入新的安全漏洞。
8.6.2 提供客户端证书(双向认证)
如果服务器配置了需要客户端证书认证,客户端需要在SSL_CTX
或SSL
对象上加载自己的证书和私钥,并在握手时发送给服务器。
① 加载客户端证书和私钥:
1
// 假设 client_cert.pem 是客户端证书,client_key.pem 是对应的私钥
2
// 通常在 SSL_CTX 上设置,这样由该上下文创建的所有 SSL 对象都可以使用此证书
3
if (SSL_CTX_use_certificate_file(ctx, "path/to/client_cert.pem", SSL_FILETYPE_PEM) <= 0) {
4
ERR_print_errors_fp(stderr);
5
// 错误处理
6
}
7
if (SSL_CTX_use_PrivateKey_file(ctx, "path/to/client_key.pem", SSL_FILETYPE_PEM) <= 0) {
8
ERR_print_errors_fp(stderr);
9
// 错误处理
10
}
11
// 检查证书和私钥是否匹配
12
if (SSL_CTX_check_private_key(ctx) == 0) {
13
fprintf(stderr, "Client private key does not match the certificate\n");
14
// 错误处理
15
}
这些函数应该在SSL_connect
之前调用。OpenSSL会在握手过程中检测服务器的CertificateRequest
消息,如果客户端配置了证书,会自动发送证书链和CertificateVerify
消息。
通过以上配置,TLS/SSL客户端就能够有效地验证服务器的身份,并在需要时提供自己的身份证明,从而建立起一个安全可靠的通信通道。证书验证是保障TLS/SSL连接安全性的核心环节,开发者必须予以足够的重视并正确配置。
9. TLS/SSL服务器开发
在本章中,我们将从TLS/SSL(传输层安全/安全套接字层)协议的服务器端视角出发,深入探讨如何使用OpenSSL库构建一个功能齐全、安全可靠的TLS/SSL服务器应用程序。理解服务器端的工作原理和实现细节对于构建安全的网络服务至关重要。本章旨在指导读者掌握服务器证书和私钥管理、客户端连接处理、数据安全传输以及进阶的客户端身份验证等核心技术。无论是简单的服务还是复杂的应用,本章的内容都将为你奠定坚实的基础。
9.1 OpenSSL SSL服务器上下文配置
构建TLS/SSL服务器的第一步是创建和配置一个SSL上下文(SSL Context),即SSL_CTX
对象。SSL_CTX
存储了服务器的配置信息,包括TLS/SSL协议版本、支持的密码套件(cipher suite)、服务器证书、私钥以及其他各种握手行为设置。一个SSL_CTX
可以用于创建多个SSL
对象,这些SSL
对象将继承SSL_CTX
的配置,用于处理单个客户端连接。
创建SSL_CTX
时,需要指定希望使用的TLS/SSL协议版本。通常推荐使用最新的TLS版本,并禁用不安全的旧版本(如SSLv2、SSLv3)。OpenSSL提供了多种工厂函数(factory function)来创建不同默认配置的SSL_CTX
。
① 创建SSL上下文
使用SSL_CTX_new
函数并传入一个方法对象来创建SSL_CTX
。方法对象(Method Object)决定了上下文支持的协议版本和类型(客户端或服务器)。推荐使用通用方法,如TLS_server_method()
,它会根据OpenSSL库和操作系统支持的情况,自动选择最新的安全协议版本(例如TLSv1.0, TLSv1.1, TLSv1.2, TLSv1.3)。
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
4
// ... OpenSSL初始化(在程序启动时进行)
5
6
SSL_CTX* ctx = nullptr;
7
const SSL_METHOD* method = TLS_server_method(); // 推荐使用通用方法
8
9
if (!method) {
10
// 错误处理
11
ERR_print_errors_fp(stderr);
12
// ...
13
}
14
15
ctx = SSL_CTX_new(method);
16
if (!ctx) {
17
// 错误处理
18
ERR_print_errors_fp(stderr);
19
// ...
20
}
21
22
// 后续配置 ctx ...
23
24
// ... SSL_CTX 清理(在程序退出时进行)
25
// SSL_CTX_free(ctx);
② 配置协议版本和选项
创建上下文后,可以通过SSL_CTX_set_options
和SSL_CTX_set_min_proto_version
/SSL_CTX_set_max_proto_version
等函数来精细控制支持的协议版本和启用/禁用特定特性。例如,禁用不安全的SSLv2和SSLv3:
1
SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
2
3
// 或者指定最小/最大协议版本(OpenSSL 1.1.1+)
4
// SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
5
// SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION);
③ 配置密码套件
密码套件(Cipher Suite)决定了在TLS/SSL握手过程中使用的加密算法、认证算法、密钥交换算法和哈希算法的组合。服务器需要配置一个密码套件列表,客户端会从中选择一个双方都支持的、安全性最高的套件。使用SSL_CTX_set_cipher_list
函数来设置。设置一个合理的密码套件列表是保证服务器安全性的关键步骤。
1
// 设置推荐的密码套件列表(示例,实际使用应参考安全建议)
2
// 这里的字符串格式遵循OpenSSL的密码套件字符串规则
3
if (SSL_CTX_set_cipher_list(ctx, "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:HIGH:!aNULL:!MD5") <= 0) {
4
ERR_print_errors_fp(stderr);
5
// ...
6
}
④ 加载服务器证书和私钥
服务器必须向客户端证明自己的身份,这通过发送服务器证书来实现。客户端会验证此证书的有效性。服务器还需要对应的私钥来执行加密(用于密钥交换)和签名(用于握手消息)。证书通常是X.509格式,私钥可以是RSA或ECC等。
⚝ 加载证书:使用SSL_CTX_use_certificate_file
(从文件加载)或SSL_CTX_use_certificate
(从内存加载X509结构体)。文件格式通常是PEM。
⚝ 加载私钥:使用SSL_CTX_use_PrivateKey_file
(从文件加载)或SSL_CTX_use_PrivateKey
(从内存加载EVP_PKEY结构体)。文件格式通常是PEM,如果私钥被加密,还需要提供密码。
1
// 加载服务器证书文件(假设是PEM格式)
2
if (SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM) <= 0) {
3
ERR_print_errors_fp(stderr);
4
// ...
5
}
6
7
// 加载服务器私钥文件(假设是PEM格式)
8
// 如果私钥有密码,需要提供回调函数或预设密码
9
if (SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM) <= 0) {
10
ERR_print_errors_fp(stderr);
11
// ...
12
}
13
14
// 检查私钥与证书是否匹配
15
if (!SSL_CTX_check_private_key(ctx)) {
16
fprintf(stderr, "Private key does not match the certificate\n");
17
ERR_print_errors_fp(stderr);
18
// ...
19
}
⑤ 加载信任的根证书或中间证书
服务器可能需要向客户端发送完整的证书链(从服务器证书到根证书)。这可以通过将中间CA证书添加到上下文中来实现。此外,在需要客户端证书认证(见9.4节)时,服务器还需要加载用于验证客户端证书的根CA证书集合。
⚝ 加载CA证书用于发送证书链:使用SSL_CTX_add_extra_chain_cert
添加额外的证书。
⚝ 加载CA证书用于验证客户端证书:使用SSL_CTX_load_verify_locations
加载包含信任CA证书的文件或目录。
1
// 加载CA证书链(可选,取决于证书颁发者)
2
// if (SSL_CTX_add_extra_chain_cert(ctx, ca_cert) <= 0) { ... }
3
4
// 加载信任的CA证书用于客户端证书认证(如果启用客户端认证)
5
// if (SSL_CTX_load_verify_locations(ctx, "ca-certificates.crt", NULL) <= 0) {
6
// ERR_print_errors_fp(stderr);
7
// // ...
8
// }
正确配置SSL_CTX
是构建安全TLS/SSL服务器的基石。所有后续的SSL连接都将基于此上下文的配置进行。
9.2 接受TLS/SSL连接
TLS/SSL连接建立在可靠的传输层协议之上,通常是TCP。服务器端首先进行标准的TCP三次握手,建立TCP连接。然后,在这个已建立的TCP连接之上,客户端和服务器会进行TLS/SSL握手,协商加密参数并建立安全会话。
接受TLS/SSL连接的过程通常遵循以下步骤:
① 标准的TCP连接建立
这部分是标准的网络编程:
⚝ 创建一个监听套接字(listening socket)。
⚝ 绑定(bind)到服务器的IP地址和端口。
⚝ 监听(listen)传入连接。
⚝ 使用accept
函数接受客户端的TCP连接请求。accept
返回一个新的套接字,用于与特定客户端通信。
1
#include <sys/socket.h>
2
#include <netinet/in.h>
3
#include <unistd.h> // For close
4
5
// ... TCP socket programming setup (create, bind, listen)
6
7
int client_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &client_len);
8
if (client_sockfd < 0) {
9
perror("Unable to accept connection");
10
// ... 错误处理
11
}
12
13
// client_sockfd 现在是一个已建立的TCP连接
② 创建SSL对象并关联套接字
对于每一个新接受的客户端TCP连接(由accept
返回的套接字),都需要创建一个新的SSL
对象来管理这个连接的TLS/SSL状态。SSL
对象通过SSL_new
函数从之前创建的SSL_CTX
上下文派生。
1
// ctx 是之前配置好的 SSL_CTX*
2
SSL* ssl = SSL_new(ctx);
3
if (!ssl) {
4
ERR_print_errors_fp(stderr);
5
// ... 错误处理,关闭 client_sockfd
6
}
7
8
// 将 SSL 对象与客户端套接字关联
9
// SSL_set_fd 适用于基于文件描述符(如套接字)的I/O
10
if (SSL_set_fd(ssl, client_sockfd) <= 0) {
11
ERR_print_errors_fp(stderr);
12
// ... 错误处理,SSL_free(ssl), close(client_sockfd)
13
}
③ 执行TLS/SSL握手
在SSL
对象与套接字关联后,服务器通过调用SSL_accept
函数启动TLS/SSL握手过程。这个函数会处理与客户端之间的所有握手消息交换,包括协议版本和密码套件协商、证书交换和验证、密钥生成和交换等。SSL_accept
是一个阻塞函数,直到握手完成或发生错误。
1
int ret = SSL_accept(ssl);
2
if (ret <= 0) {
3
int err = SSL_get_error(ssl, ret);
4
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
5
// 在非阻塞模式下,需要等待套接字可读写后重试 SSL_accept
6
// 对于阻塞模式,这通常表示出现了其他错误
7
fprintf(stderr, "SSL accept error: %d (WANT_READ/WRITE usually means other error in blocking mode)\n", err);
8
ERR_print_errors_fp(stderr);
9
} else {
10
fprintf(stderr, "SSL accept failed with error code %d\n", err);
11
ERR_print_errors_fp(stderr);
12
}
13
// ... 错误处理,SSL_free(ssl), close(client_sockfd)
14
} else {
15
// 握手成功!现在可以通过 ssl 对象进行加密通信了
16
printf("SSL handshake successful!\n");
17
// ... 进入数据传输阶段
18
}
SSL_accept
的返回值:
⚝ 1
:握手成功。
⚝ 0
:握手关闭。
⚝ <0
:发生错误。需要调用SSL_get_error
来确定具体的错误类型。SSL_ERROR_WANT_READ
和SSL_ERROR_WANT_WRITE
通常在非阻塞模式下出现,表示需要等待底层I/O准备好后重试;在阻塞模式下,它们可能指示更深层的问题或配置错误。其他错误码表示握手失败的具体原因。
握手成功后,服务器和客户端之间建立了一个安全的通道。所有通过SSL
对象进行的读写操作都将自动进行加密和解密。
9.3 实现一个基本TLS/SSL服务器
现在我们将把前面介绍的TLS/SSL上下文配置和连接接受步骤整合起来,构建一个基本的TLS/SSL回显服务器示例。这个服务器将监听一个端口,接受客户端连接,执行TLS/SSL握手,然后接收客户端发送的数据并将其回显(echo)回去,直到客户端断开连接。
1
#include <iostream>
2
#include <string>
3
#include <vector>
4
#include <cstring> // For strlen, memcpy etc.
5
#include <stdexcept>
6
7
#include <sys/socket.h>
8
#include <netinet/in.h>
9
#include <arpa/inet.h>
10
#include <unistd.h>
11
12
#include <openssl/ssl.h>
13
#include <openssl/err.h>
14
15
// 定义端口号和缓冲区大小
16
const int PORT = 4433;
17
const int BUFFER_SIZE = 1024;
18
19
// OpenSSL初始化和清理封装
20
struct OpenSSLInitializer {
21
OpenSSLInitializer() {
22
SSL_load_error_strings(); // 加载错误字符串
23
OpenSSL_add_ssl_algorithms(); // 加载加密和SSL算法
24
}
25
~OpenSSLInitializer() {
26
EVP_cleanup(); // 清理EVP
27
SSL_COMP_free_compression_methods(); // 清理压缩方法
28
ERR_free_strings(); // 清理错误字符串
29
}
30
};
31
32
// 错误检查宏
33
#define CHECK(condition, message) if (!(condition)) { std::cerr << "Error: " << message << std::endl; ERR_print_errors_fp(stderr); throw std::runtime_error(message); }
34
35
int main() {
36
try {
37
// ① 初始化OpenSSL库
38
OpenSSLInitializer ssl_init;
39
std::cout << "OpenSSL initialized." << std::endl;
40
41
// ② 创建SSL上下文
42
const SSL_METHOD* method = TLS_server_method();
43
CHECK(method != nullptr, "Failed to create SSL method.");
44
SSL_CTX* ctx = SSL_CTX_new(method);
45
CHECK(ctx != nullptr, "Failed to create SSL CTX.");
46
std::cout << "SSL CTX created." << std::endl;
47
48
// 配置SSL上下文(示例:禁用旧版本协议)
49
SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
50
std::cout << "SSL CTX options set." << std::endl;
51
52
// 加载服务器证书和私钥
53
const char* cert_file = "server.crt"; // 请替换为你的证书文件路径
54
const char* key_file = "server.key"; // 请替换为你的私钥文件路径
55
56
CHECK(SSL_CTX_use_certificate_file(ctx, cert_file, SSL_FILETYPE_PEM) > 0,
57
"Failed to load server certificate.");
58
std::cout << "Server certificate loaded: " << cert_file << std::endl;
59
60
CHECK(SSL_CTX_use_PrivateKey_file(ctx, key_file, SSL_FILETYPE_PEM) > 0,
61
"Failed to load server private key.");
62
std::cout << "Server private key loaded: " << key_file << std::endl;
63
64
// 检查私钥和证书是否匹配
65
CHECK(SSL_CTX_check_private_key(ctx) > 0,
66
"Server certificate and private key do not match.");
67
std::cout << "Certificate and private key match." << std::endl;
68
69
// ③ 创建TCP监听套接字
70
int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
71
CHECK(listen_sockfd >= 0, "Failed to create listening socket.");
72
std::cout << "Listening socket created." << std::endl;
73
74
// 设置套接字选项,允许地址重用
75
int opt = 1;
76
setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
77
78
// 绑定地址和端口
79
sockaddr_in server_addr;
80
memset(&server_addr, 0, sizeof(server_addr));
81
server_addr.sin_family = AF_INET;
82
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用接口
83
server_addr.sin_port = htons(PORT);
84
85
CHECK(bind(listen_sockfd, (sockaddr*)&server_addr, sizeof(server_addr)) >= 0,
86
"Failed to bind socket.");
87
std::cout << "Socket bound to port " << PORT << std::endl;
88
89
// 监听连接
90
CHECK(listen(listen_sockfd, 5) >= 0, "Failed to listen on socket.");
91
std::cout << "Server listening on port " << PORT << "..." << std::endl;
92
93
// ④ 循环接受客户端连接
94
while (true) {
95
sockaddr_in client_addr;
96
socklen_t client_len = sizeof(client_addr);
97
int client_sockfd = accept(listen_sockfd, (sockaddr*)&client_addr, &client_len);
98
if (client_sockfd < 0) {
99
perror("Failed to accept client connection");
100
continue; // 继续接受下一个连接
101
}
102
std::cout << "Accepted connection from "
103
<< inet_ntoa(client_addr.sin_addr) << ":"
104
<< ntohs(client_addr.sin_port) << std::endl;
105
106
// 为新连接创建SSL对象
107
SSL* ssl = SSL_new(ctx);
108
if (!ssl) {
109
ERR_print_errors_fp(stderr);
110
close(client_sockfd);
111
continue;
112
}
113
114
// 将SSL对象与客户端套接字关联
115
if (SSL_set_fd(ssl, client_sockfd) <= 0) {
116
ERR_print_errors_fp(stderr);
117
SSL_free(ssl);
118
close(client_sockfd);
119
continue;
120
}
121
122
// 执行TLS/SSL握手
123
int ssl_accept_ret = SSL_accept(ssl);
124
if (ssl_accept_ret <= 0) {
125
int err = SSL_get_error(ssl, ssl_accept_ret);
126
fprintf(stderr, "SSL accept failed for client %s:%d with error code %d\n",
127
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), err);
128
ERR_print_errors_fp(stderr);
129
SSL_free(ssl);
130
close(client_sockfd);
131
continue;
132
}
133
134
std::cout << "SSL handshake successful with client "
135
<< inet_ntoa(client_addr.sin_addr) << ":"
136
<< ntohs(client_addr.sin_port) << std::endl;
137
std::cout << "Using cipher: " << SSL_get_cipher(ssl) << std::endl;
138
139
// ⑤ 处理客户端数据
140
char buffer[BUFFER_SIZE] = {0};
141
int bytes_read;
142
while ((bytes_read = SSL_read(ssl, buffer, sizeof(buffer) - 1)) > 0) {
143
buffer[bytes_read] = '\0'; // Null terminate
144
std::cout << "Received from client: " << buffer << std::endl;
145
146
// 回显数据
147
if (SSL_write(ssl, buffer, bytes_read) <= 0) {
148
int err = SSL_get_error(ssl, -1); // Use -1 for write errors
149
fprintf(stderr, "SSL write failed with error code %d\n", err);
150
ERR_print_errors_fp(stderr);
151
break; // Exit inner read loop on write error
152
}
153
}
154
155
// ⑥ 关闭SSL连接和套接字
156
if (bytes_read <= 0) {
157
int err = SSL_get_error(ssl, bytes_read);
158
if (err == SSL_ERROR_ZERO_RETURN) {
159
// 客户端正常关闭SSL连接
160
std::cout << "Client closed the connection cleanly." << std::endl;
161
} else {
162
fprintf(stderr, "SSL read error: %d\n", err);
163
ERR_print_errors_fp(stderr);
164
}
165
}
166
167
// 执行TLS/SSL关闭握手
168
SSL_shutdown(ssl); // 尝试执行双向关闭
169
170
SSL_free(ssl); // 释放SSL对象
171
close(client_sockfd); // 关闭底层套接字
172
std::cout << "Connection closed with client "
173
<< inet_ntoa(client_addr.sin_addr) << ":"
174
<< ntohs(client_addr.sin_port) << std::endl;
175
}
176
177
// 清理监听套接字和SSL上下文(理论上循环不会退出,但在实际应用中需要考虑优雅退出)
178
close(listen_sockfd);
179
SSL_CTX_free(ctx);
180
181
} catch (const std::exception& e) {
182
std::cerr << "Caught exception: " << e.what() << std::endl;
183
return 1;
184
}
185
186
return 0;
187
}
注意事项:
⚝ 运行此示例需要替换 server.crt
和 server.key
为你自己的证书和私钥文件。你可以使用OpenSSL命令行工具生成测试用的自签名证书:
1
# 生成私钥
2
openssl genrsa -out server.key 2048
3
# 生成自签名证书
4
openssl req -new -x509 -key server.key -out server.crt -days 365
在生成证书时,需要填写一些信息(国家、省份、城市、组织、通用名 Common Name 等)。通用名通常填写服务器的域名或IP地址。
⚝ 这个示例服务器是单线程阻塞的,一次只能处理一个客户端连接。在生产环境中,需要使用多进程、多线程或异步I/O模型来处理并发连接(见9.5节)。
⚝ 错误处理使用了简单的CHECK
宏和ERR_print_errors_fp
。实际应用中,错误处理需要更健壮。
⚝ SSL_shutdown
尝试执行TLS/SSL关闭握手。它可能需要多次调用,特别是在非阻塞模式下,直到返回1表示成功关闭。在阻塞模式下,它通常会一次完成。即使关闭失败(例如,客户端异常断开),也应该继续释放资源。
9.4 客户端证书认证(Mutual Authentication)
在标准的TLS/SSL连接中,通常只有服务器向客户端出示证书以证明其身份(单向认证)。但在某些安全要求较高的场景下,例如内部服务间通信或特定的API访问,服务器可能需要验证连接客户端的身份,这时就需要使用客户端证书认证,也称为双向认证(Mutual Authentication)。
双向认证的工作流程:
1. TLS/SSL握手期间,服务器配置为请求(request)或要求(require)客户端提供证书。
2. 客户端收到服务器的请求后,如果配置了客户端证书,会将其发送给服务器。
3. 服务器接收到客户端证书后,会验证其有效性,包括:
▮▮▮▮⚝ 证书格式和签名是否正确。
▮▮▮▮⚝ 证书是否在有效期内。
▮▮▮▮⚝ 证书是否由服务器信任的根CA或中间CA颁发(即验证证书链)。
▮▮▮▮⚝ 证书是否被吊销(通过CRL或OCSP,但这部分通常更复杂,可能不在OpenSSL原生API中直接完成)。
4. 如果客户端证书验证成功,握手继续。如果失败,服务器会终止握手或连接。
在OpenSSL中实现服务器端的客户端证书认证,主要涉及SSL_CTX
的配置和可能的验证回调函数的设置。
① 配置SSL上下文以请求或要求客户端证书
使用SSL_CTX_set_verify
函数来配置服务器对客户端证书的验证行为。这个函数需要指定验证模式(mode)和一个可选的回调函数。
常用的验证模式:
⚝ SSL_VERIFY_NONE
:不验证客户端证书(默认)。
⚝ SSL_VERIFY_PEER
:请求客户端证书,如果客户端提供了,就进行验证。如果验证失败,握手失败;如果客户端没有提供证书,握手继续(取决于是否设置了SSL_VERIFY_FAIL_IF_NO_PEER_CERT
)。
⚝ SSL_VERIFY_FAIL_IF_NO_PEER_CERT
:与SSL_VERIFY_PEER
结合使用。如果客户端没有提供证书,握手失败。
⚝ SSL_VERIFY_CLIENT_ONCE
:只在初始握手时请求客户端证书。
通常,要强制进行客户端证书认证,应该使用 SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT
。
1
// 在创建和配置 ctx 后调用
2
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);
3
// 第三个参数是验证回调函数,NULL 表示使用OpenSSL默认的验证逻辑
4
std::cout << "Client certificate verification required." << std::endl;
② 加载信任的客户端CA证书
服务器需要一个信任锚(Trust Anchor)来验证客户端证书链的有效性。这个信任锚通常是颁发客户端证书的根CA或中间CA证书。使用SSL_CTX_load_verify_locations
函数加载这些信任的CA证书文件或目录。
1
// 加载信任的CA证书文件(例如,包含多个信任CA证书的PEM文件)
2
const char* client_ca_certs_file = "client-ca-certs.pem"; // 替换为你的客户端CA证书文件路径
3
CHECK(SSL_CTX_load_verify_locations(ctx, client_ca_certs_file, NULL) > 0,
4
"Failed to load client CA certificates for verification.");
5
std::cout << "Client CA certificates loaded: " << client_ca_certs_file << std::endl;
6
7
// 可选:如果CA证书在一个目录中,可以使用以下方式
8
// CHECK(SSL_CTX_load_verify_locations(ctx, NULL, "/path/to/ca/certs/directory") > 0,
9
// "Failed to load client CA certificates directory.");
③ 验证回调函数(可选,用于自定义验证逻辑)
如果你需要更复杂的验证逻辑,例如根据证书的特定字段(如Subject Alternative Name, SAN)来判断是否允许连接,或者集成OCSP/CRL检查,可以实现一个自定义的验证回调函数,并将其传递给SSL_CTX_set_verify
。
回调函数原型大致如下:
int verify_callback(int preverify_ok, X509_STORE_CTX* ctx);
⚝ preverify_ok
:OpenSSL内部默认验证过程的结果(1表示成功,0表示失败)。
⚝ X509_STORE_CTX* ctx
:包含了正在验证的证书链、验证结果信息等。
在回调函数中,你可以获取证书信息(例如,X509_STORE_CTX_get_current_cert(ctx)
获取当前正在验证的证书),执行自定义检查。如果自定义检查通过,回调函数应返回1;如果失败,应返回0。即使preverify_ok
为1,你的回调也可以返回0拒绝连接。
1
// 自定义验证回调函数示例
2
int my_verify_callback(int preverify_ok, X509_STORE_CTX* store_ctx) {
3
// 总是先检查OpenSSL默认验证结果
4
if (!preverify_ok) {
5
// OpenSSL默认验证失败,打印错误信息
6
int err = X509_STORE_CTX_get_error(store_ctx);
7
fprintf(stderr, "Certificate verification error: %s (depth=%d)\n",
8
X509_lookup_error_string(err),
9
X509_STORE_CTX_get_error_depth(store_ctx));
10
// 你可以选择在这里直接拒绝,或者忽略某些特定错误
11
// return 0; // 拒绝连接
12
}
13
14
// 获取当前正在验证的证书 (通常是客户端的叶子证书)
15
X509* cert = X509_STORE_CTX_get_current_cert(store_ctx);
16
if (!cert) {
17
fprintf(stderr, "Could not get current certificate from store context.\n");
18
return 0; // 没有证书,拒绝
19
}
20
21
// 可以在这里执行自定义检查,例如检查证书的CN或SAN
22
// 例如,检查证书通用名 (CN)
23
char subject_name[256];
24
X509_NAME* subject = X509_get_subject_name(cert);
25
if (subject && X509_NAME_oneline(subject, subject_name, sizeof(subject_name))) {
26
printf("Verifying certificate with subject: %s\n", subject_name);
27
// 例如:只允许特定CN的证书通过
28
// if (strcmp(subject_name, "/CN=MyAllowedClient") != 0) {
29
// fprintf(stderr, "Client certificate CN not allowed: %s\n", subject_name);
30
// return 0; // 自定义拒绝
31
// }
32
}
33
34
// 如果OpenSSL默认验证成功,并且自定义检查也通过,则返回1
35
return preverify_ok;
36
}
37
38
// 在配置 ctx 时设置回调函数
39
// SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, my_verify_callback);
④ 获取客户端证书信息
握手成功后,如果客户端提供了证书,服务器可以通过SSL_get_peer_certificate(ssl)
函数获取客户端的X509
证书对象。然后可以使用各种X509_*
函数解析证书信息(如主题、颁发者、有效期等)。
1
// 在 SSL_accept 成功后调用
2
X509* client_cert = SSL_get_peer_certificate(ssl);
3
if (client_cert) {
4
std::cout << "Client certificate received." << std::endl;
5
char subject_name[256];
6
X509_NAME* subject = X509_get_subject_name(client_cert);
7
if (subject && X509_NAME_oneline(subject, subject_name, sizeof(subject_name))) {
8
std::cout << "Client certificate subject: " << subject_name << std::endl;
9
}
10
11
// 在使用完证书对象后,必须释放它
12
X509_free(client_cert);
13
} else {
14
std::cout << "Client did not present a certificate." << std::endl;
15
// 如果配置了 SSL_VERIFY_FAIL_IF_NO_PEER_CERT,这种情况不会发生,
16
// 握手已经在 SSL_accept 阶段失败了。
17
}
通过以上步骤,服务器端就可以实现对连接客户端的身份验证,增加连接的安全性。
9.5 处理多个客户端连接
在实际的服务器应用中,需要同时处理多个客户端连接是常见的需求。单线程阻塞模型(如9.3节的基本示例)无法满足并发处理的需求。处理并发连接通常有以下几种策略:
① 多进程模型(Multi-process Model)
⚝ 原理:主进程负责监听套接字和接受新的连接。每当接受一个新连接,主进程就fork
出一个子进程,将客户端套接字传递给子进程,由子进程负责与该客户端进行TLS/SSL握手和数据通信。
⚝ OpenSSL集成:SSL_CTX
可以在主进程中创建和配置,但每个子进程在处理其客户端连接时,需要创建自己的SSL
对象,并与传递过来的客户端套接字关联。OpenSSL库本身在多进程环境下通常是安全的,因为每个进程有独立的内存空间,但在fork
之前可能需要一些特殊的初始化(例如,确保随机数生成器状态不共享,但对于现代OpenSSL版本通常不是问题)。
⚝ 优点:进程之间内存隔离,一个客户端的处理失败不会影响其他客户端或主进程,健壮性高。
⚝ 缺点:进程创建开销较大,进程间通信(IPC)复杂(如果需要共享信息的话),资源消耗相对较高。
② 多线程模型(Multi-thread Model)
⚝ 原理:主线程负责监听套接字和接受新的连接。每当接受一个新连接,主线程就创建一个新线程,将客户端套接字传递给新线程,由新线程负责与该客户端进行TLS/SSL握手和数据通信。
⚝ OpenSSL集成:SSL_CTX
可以在主线程创建并在所有工作线程中共享。但是,每个线程处理自己的连接时必须使用独立的SSL
对象。OpenSSL库本身并非完全线程安全(特别是早于1.1.0的版本,或者某些全局状态),在多线程环境中使用OpenSSL需要特别注意。你需要配置OpenSSL的线程回调函数(locking callbacks和thread ID callback),以便OpenSSL在内部进行必要的锁定。OpenSSL 1.1.0及更高版本对线程安全性做了大幅改进,核心对象(如SSL_CTX
, SSL
)本身可以在不同线程中安全使用,但共享资源(如错误栈、随机数生成器状态)仍需要OpenSSL内部或外部的同步机制。尽管1.1.0+简化了,但理解其线程模型仍然重要(可参考第11章)。
⚝ 优点:线程创建和切换开销小于进程,线程间数据共享(通过共享内存)相对容易,资源消耗相对较低。
⚝ 缺点:共享内存带来同步问题,需要使用锁等机制;一个线程的崩溃可能影响整个进程的稳定性。需要正确处理OpenSSL的线程安全性。
③ 异步I/O模型(Asynchronous I/O Model)
⚝ 原理:一个或少数几个线程负责管理所有客户端连接。这些线程不阻塞在单个连接的读写操作上,而是使用多路复用(multiplexing)机制(如select
, poll
, epoll
在Linux, kqueue
在macOS/BSD, I/O Completion Ports/IOCP在Windows)来同时监控多个套接字的文件描述符,当某个套接字有数据可读或可写时,才对其进行相应的操作。这种模型常结合事件驱动(event-driven)编程。
⚝ OpenSSL集成:在异步I/O模型中使用OpenSSL需要特别注意SSL_read
和SSL_write
等函数的行为。这些函数在内部执行TLS/SSL协议逻辑,可能不仅仅是简单的读写底层套接字。当SSL_read
或SSL_write
返回<=0,且SSL_get_error
返回SSL_ERROR_WANT_READ
或SSL_ERROR_WANT_WRITE
时,这表示OpenSSL需要底层套接字进行读或写操作才能继续。在异步模型中,你不应该阻塞等待,而是记录当前状态,并将套接字添加到I/O监控列表中,等待下次可读/写事件发生时,再次调用同样的SSL_read
/SSL_write
函数(传入相同的参数)。
⚝ 优点:资源消耗低(线程/进程数量少),高并发性能好,避免了大量线程/进程切换的开销。
⚝ 缺点:编程模型复杂,特别是要正确处理SSL_ERROR_WANT_READ
/SSL_ERROR_WANT_WRITE
状态机。
示例(异步I/O中的OpenSSL读操作逻辑片段):
1
// 假设在epoll/select/poll事件循环中,发现sockfd可读
2
// client_data 结构体存储每个连接的状态,包括其 SSL* 对象和当前的SSL操作状态
3
SSL* ssl = client_data->ssl;
4
char buffer[BUFFER_SIZE];
5
6
int ret = SSL_read(ssl, buffer, sizeof(buffer));
7
8
if (ret > 0) {
9
// 成功读取到 ret 字节的解密后的应用数据
10
// 处理读取到的数据 buffer
11
// ...
12
} else {
13
int err = SSL_get_error(ssl, ret);
14
if (err == SSL_ERROR_WANT_READ) {
15
// OpenSSL需要更多数据来完成解密。
16
// 不执行任何操作,等待套接字再次可读。
17
// 确保套接字仍在监控读事件。
18
std::cout << "SSL_read returned WANT_READ, waiting for more data." << std::endl;
19
} else if (err == SSL_ERROR_WANT_WRITE) {
20
// 罕见,但在 SSL_read 期间 OpenSSL 可能需要写数据(例如,发送报警消息)
21
// 确保套接字仍在监控写事件。
22
std::cout << "SSL_read returned WANT_WRITE, waiting for socket write readiness." << std::endl;
23
} else if (err == SSL_ERROR_ZERO_RETURN) {
24
// 客户端正常关闭了SSL连接
25
std::cout << "Client closed SSL connection cleanly." << std::endl;
26
// 执行 SSL_shutdown 并清理连接
27
// ...
28
} else {
29
// 其他错误
30
fprintf(stderr, "SSL_read error: %d\n", err);
31
ERR_print_errors_fp(stderr);
32
// 错误处理,清理连接
33
// ...
34
}
35
}
类似的状态处理逻辑也适用于SSL_write
、SSL_connect
(客户端)和SSL_accept
(服务器)。
总结:
选择哪种并发模型取决于应用的需求、开发团队的经验以及对性能和复杂度的权衡。对于小型应用或学习目的,多线程模型通常是最容易实现的。对于需要处理数千甚至数万并发连接的高性能服务器,异步I/O模型配合事件循环是更优的选择。在实现多线程模型时,务必注意OpenSSL的线程安全配置(尤其在1.1.0之前版本)。
10. 高级TLS/SSL特性与实践
欢迎回到我们的OpenSSL C++开发课程!在前一章,我们掌握了如何构建基本的TLS/SSL客户端和服务器应用程序。但是,在构建生产级别的安全通信系统时,仅仅实现基础功能是远远不够的。我们需要考虑性能、可扩展性、更灵活的控制以及最重要的——安全性。
本章将带您深入探索TLS/SSL协议的一些高级特性及其在OpenSSL中的实现。我们将学习如何优化TLS/SSL连接的性能,特别是通过会话复用来减少开销;如何处理单台服务器上的多个安全站点(SNI);如何在应用层协商使用最佳协议(ALPN);如何实现自定义的证书验证逻辑来满足特定的安全策略;以及如何配置OpenSSL以增强安全性,抵御潜在的威胁。
掌握这些高级特性,将帮助您构建更高效、更灵活、更安全的TLS/SSL应用程序。这不仅是对OpenSSL API更深入的了解,也是对TLS/SSL协议本身更深刻的认知。让我们开始吧!
10.1 性能优化
TLS/SSL握手(handshake)是一个计算密集型的过程,涉及到非对称加密(如RSA或ECC)进行密钥交换和身份认证。对于每个新建的TLS/SSL连接,都需要执行一次完整的握手,这会显著增加连接建立的延迟和服务器的CPU负载,尤其是在高并发场景下。因此,优化TLS/SSL性能是构建高性能安全应用的关键。
主要的TLS/SSL性能优化方向包括:
① 减少握手次数:这主要通过会话复用(session reuse)来实现,我们将在下一节详细讨论。
② 加速加密计算:利用硬件加速或优化的软件库。
③ 减少数据传输开销:例如通过压缩(尽管SSL/TLS压缩存在安全风险,通常建议禁用)或更高效的协议(如HTTP/2)。
④ 优化I/O操作:使用高效的I/O模型(异步I/O)和BIO层管理。
10.1.1 加速加密计算:硬件加速(Engine)
OpenSSL提供了一个引擎(Engine)接口,允许将加密操作卸载到硬件加速卡或其他优化的软件库。如果您的系统配备了支持加密加速的硬件(例如,带有AES-NI指令集的CPU或专用的加密卡),或者安装了第三方的优化加密库,您就可以通过配置OpenSSL引擎来利用它们。
使用引擎的基本步骤:
① 加载所需的引擎模块。
② 初始化并设置引擎。
③ (可选)将特定算法操作绑定到引擎。
④ 使用OpenSSL进行加密操作时,如果相应的算法被绑定到已加载并激活的引擎,OpenSSL将通过引擎接口调用底层的硬件或优化库。
在OpenSSL中,可以使用 ENGINE_load_builtin_engines()
加载内置引擎,使用 ENGINE_by_id()
获取特定ID的引擎(例如,"aead"
for AEAD ciphers, "rsa"
for RSA operations),使用 ENGINE_init()
初始化引擎,使用 ENGINE_set_default_*()
系列函数设置默认引擎,使用 ENGINE_finish()
释放引擎。
以下是一个加载并设置默认RSA引擎的简化示例(实际使用中需要更严格的错误处理和资源管理):
1
#include <openssl/engine.h>
2
#include <iostream>
3
4
void initialize_openssl_engines() {
5
// 加载内置引擎列表
6
ENGINE_load_builtin_engines();
7
// 注册所有加载的引擎
8
ENGINE_register_all_ciphers();
9
ENGINE_register_all_digests();
10
ENGINE_register_all_complete();
11
12
// 如果需要使用特定的硬件引擎,可以通过其ID加载
13
// ENGINE* hw_engine = ENGINE_by_id("dynamic"); // 示例:加载动态引擎
14
// if (hw_engine) {
15
// if (ENGINE_init(hw_engine)) {
16
// // 例如,设置此引擎为RSA操作的默认引擎
17
// // ENGINE_set_default_RSA(hw_engine);
18
// std::cout << "Hardware engine loaded and initialized." << std::endl;
19
// } else {
20
// unsigned long err = ERR_get_error();
21
// std::cerr << "ENGINE_init failed: " << ERR_error_string(err, nullptr) << std::endl;
22
// }
23
// // ENGINE_free(hw_engine); // 注意:设置为默认后,通常不需要立即free
24
// } else {
25
// unsigned long err = ERR_get_error();
26
// std::cerr << "Could not load hardware engine: " << ERR_error_string(err, nullptr) << std::endl;
27
// }
28
29
// 对于大多数情况,加载内置引擎并让OpenSSL自动选择即可
30
std::cout << "OpenSSL built-in engines loaded." << std::endl;
31
}
32
33
// 在程序退出前调用
34
void cleanup_openssl_engines() {
35
ENGINE_cleanup();
36
std::cout << "OpenSSL engines cleaned up." << std::endl;
37
}
38
39
int main() {
40
// OpenSSL全局初始化 (在OpenSSL 1.1.0+ 中推荐使用 OPENSSL_init_ssl)
41
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
42
ERR_load_CRYPTO_strings();
43
ERR_load_SSL_strings();
44
45
initialize_openssl_engines();
46
47
// ... 您的TLS/SSL代码 ...
48
49
cleanup_openssl_engines();
50
// OpenSSL全局清理 (在OpenSSL 1.1.0+ 中可能不需要显式调用,取决于初始化方式)
51
EVP_cleanup(); // 清理EVP对象
52
// CRYPTO_cleanup_all_ex_data(); // 清理ex_data,如果使用
53
ERR_free_strings(); // 清理错误字符串
54
55
return 0;
56
}
注意: 引擎的使用通常与OpenSSL的构建和安装有关,需要确保OpenSSL编译时启用了引擎支持,并且硬件厂商提供了相应的引擎模块。在OpenSSL 3.0+ 中,引擎的概念被 Providers (提供者) 所取代,提供了更灵活和模块化的方式来加载加密实现。如果您使用OpenSSL 3.0+,应该关注 Provider 相关API。
10.1.2 BIO层与缓冲
有效地使用BIO (Basic Input/Output) 层是优化I/O性能的关键。TLS/SSL的读写操作(SSL_read
, SSL_write
)实际上是通过底层的传输BIO(例如,套接字BIO)进行的。这些操作可能会因为网络延迟或系统调用开销而变慢。
使用缓冲BIO (Buffering BIO) 可以减少底层系统调用次数。例如,BIO_new(BIO_s_buffer())
创建的缓冲BIO可以在其内部积累数据,然后一次性写入到底层BIO,或者从底层BIO读取大量数据后分块提供给上层。
在TLS/SSL中,OpenSSL通常会自动在SSL
对象内部管理一个读写缓冲区,但理解BIO层的工作原理有助于诊断性能问题或在需要时进行更细粒度的控制。例如,当TLS记录被加密后,它会被写入到底层BIO。如果底层BIO是套接字,这会导致一次send()
或write()
系统调用。合理地管理BIO链(例如,在套接字BIO之上添加缓冲BIO)可以聚合多个小的TLS记录,减少系统调用次数。
此外,确保您的应用程序高效地处理OpenSSL的I/O状态也很重要。SSL_read
和SSL_write
可能返回需要重试的状态(例如,SSL_ERROR_WANT_READ
或SSL_ERROR_WANT_WRITE
),表明底层BIO需要更多数据或可以写入更多数据。在异步I/O模型(如使用select
, poll
, epoll
, kqueue
, IOCP等)中,正确处理这些状态是实现高性能的关键。您需要将相应的套接字添加到I/O事件循环中,并在事件就绪时再次调用OpenSSL的读写函数。
10.2 会话复用
会话复用(Session Reuse)是TLS/SSL性能优化的最重要手段之一。在TLS/SSL协议中,客户端和服务器在第一次建立连接时会执行一个完整的握手过程,协商加密参数、交换密钥并进行身份认证。这个过程开销较大。如果后续有客户端再次连接到同一服务器,并且双方都支持会话复用,那么可以跳过大部分握手步骤,直接使用之前协商好的会话参数(特别是主密钥),从而显著加快连接建立速度并降低CPU负载。
TLS/SSL支持两种主要的会话复用机制:
① 会话ID复用(Session ID Reuse): 服务器在完成握手后会给会话分配一个唯一的ID。客户端在随后的连接尝试中,可以在ClientHello消息中包含这个ID。如果服务器找到了对应的缓存会话,并且该会话仍然有效,则可以进行简化的握手(称为简略握手/Abbreviated Handshake或会话恢复/Session Resumption)。服务器向客户端发送一个带有相同会话ID的ServerHello消息,表明同意复用会话。
② 会话票证(Session Tickets): 这是一个更现代的机制(在TLSv1.0及更高版本中可选,TLSv1.3中是主要的复用机制)。服务器会将所有会话状态加密后存储在一个会话票证(Session Ticket)中发送给客户端。客户端保存这个票证。在后续连接时,客户端在ClientHello中包含这个票证。服务器接收到票证后,自己解密(或者交给一个共享密钥的外部组件解密),从而恢复会话状态,无需在服务器端维护大量的会话缓存。这对于拥有大量服务器的大型服务特别有用,因为它们可以共享用于加密/解密票证的密钥,而不需要在所有服务器之间同步会话状态。
10.2.1 OpenSSL中的会话复用配置
在OpenSSL中,会话复用主要通过配置SSL_CTX
对象来实现。
10.2.1.1 服务器端会话缓存配置
服务器端需要启用会话缓存,并设置缓存模式。
⚝ 启用内部会话缓存: OpenSSL可以在SSL_CTX
内部维护一个会话缓存。这是默认启用的一种方式。
1
// 启用服务器端内部会话缓存
2
SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_SERVER);
3
// 设置内部缓存的最大容量 (可选,默认为1024*20或20480)
4
// SSL_CTX_sess_set_cache_size(ctx, 20000);
5
// 设置会话超时时间 (可选,默认为300秒)
6
// SSL_CTX_set_timeout(ctx, 600);
通过 SSL_SESS_CACHE_SERVER
启用服务器端缓存,OpenSSL会自动管理会话ID的缓存和查找。当客户端携带一个已知的会话ID连接时,OpenSSL会自动尝试恢复会话。
⚝ 自定义外部会话缓存: 对于需要更精细控制会话管理(例如,跨进程/线程共享缓存)的应用,可以实现自己的会话缓存机制,并通过回调函数注册到SSL_CTX
。
1
// 设置查找会话的回调函数 (客户端提供session ID时调用)
2
SSL_CTX_sess_set_get_cb(ctx, your_get_session_callback);
3
// 设置新建会话的回调函数 (握手成功生成新会话时调用)
4
SSL_CTX_sess_set_new_cb(ctx, your_new_session_callback);
5
// 设置删除会话的回调函数 (会话从缓存中删除时调用)
6
SSL_CTX_sess_set_remove_cb(ctx, your_remove_session_callback);
7
// 启用外部缓存模式 (不使用OpenSSL内部缓存,完全依赖回调)
8
SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_NO_AUTO_STORE | SSL_SESS_CACHE_NO_INTERNAL_STORE);
在回调函数中,您将接收到 SSL_SESSION
对象及其ID,您可以将其存储到数据库、分布式缓存等外部存储中,并在需要时查找和返回。
⚝ 禁用会话缓存: 如果出于某种原因不想使用会话复用,可以禁用它。
1
SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF);
10.2.1.2 客户端会话缓存配置
客户端也需要启用会话缓存,以便在第一次连接成功后保存服务器的会话信息(会话ID或会话票证),并在后续连接时尝试使用它们。
⚝ 启用客户端内部会话缓存:
1
// 启用客户端内部会话缓存
2
SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_CLIENT);
当 SSL_SESS_CACHE_CLIENT
启用时,OpenSSL会自动管理客户端接收到的 SSL_SESSION
对象,并在下次调用 SSL_connect
到同一服务器时尝试发送缓存的会话信息。
⚝ 获取和设置会话对象: 您也可以手动获取和设置会话对象。
1
// 在连接成功后获取会话对象
2
SSL_SESSION* session = SSL_get1_session(ssl);
3
if (session) {
4
// 保存 session 对象(例如,序列化到文件或内存)
5
// ...
6
SSL_SESSION_free(session); // 获取的 session 需要手动释放
7
}
8
9
// 在下次连接前设置会话对象
10
// 假设您已经加载了之前保存的 session 数据到 session_data
11
// SSL_SESSION* session_to_reuse = d2i_SSL_SESSION(NULL, &session_data, session_data_len);
12
// if (session_to_reuse) {
13
// SSL_set_session(ssl, session_to_reuse);
14
// SSL_SESSION_free(session_to_reuse); // 设置后,ssl对象会持有其引用,这里可以释放
15
// }
10.2.1.3 会话票证配置 (TLSv1.2 及以下)
对于TLSv1.2及以下版本,会话票证需要服务器端配置加密密钥。
⚝ 服务器端配置会话票证密钥: 服务器需要一个主密钥来加密和解密会话票证。这个密钥需要在所有需要共享票证的服务器之间保持一致。
1
// 生成一个48字节的会话票证密钥 (24字节用于加密,24字节用于HMAC)
2
unsigned char ticket_key[48];
3
RAND_bytes(ticket_key, sizeof(ticket_key));
4
5
// 设置会话票证回调函数 (推荐使用,更灵活)
6
// SSL_CTX_set_tlsext_ticket_key_cb(ctx, your_ticket_key_callback);
7
8
// 或者直接设置密钥 (较旧的方法)
9
// SSL_CTX_set_tlsext_ticket_keys(ctx, ticket_key, sizeof(ticket_key)); // deprecated in 1.1.1+
10
// 在OpenSSL 1.1.1+,请使用SSL_CTX_set_tlsext_ticket_keys_early或SSL_CTX_set_tlsext_ticket_keys
11
// 但更推荐使用回调函数或新的Provider机制(OpenSSL 3.0+)
会话票证密钥的生成、管理和轮换是一个重要的安全话题,应定期更换密钥以限制潜在的票证泄露影响范围。
⚝ 客户端接收和发送会话票证: 如果服务器支持并发送会话票证,客户端(如果启用了会话缓存)会自动接收并存储。在后续连接时,客户端会自动将票证包含在ClientHello的扩展中发送给服务器。
10.2.1.4 TLSv1.3的会话复用
在TLSv1.3中,会话复用机制发生了变化。它不再使用会话ID,而是完全依赖于预共享密钥(PSK, Pre-Shared Key)。简化的握手是通过客户端在ClientHello中提供一个或多个之前会话生成的PSK标识符来启动的。如果服务器接受其中一个PSK,则双方使用该PSK导出新的密钥,并进行一个0-RTT (Zero Round Trip Time) 或1-RTT握手。0-RTT允许客户端在发送ClientHello时就发送应用数据,但存在重放攻击的风险,需要应用层采取措施防范。
OpenSSL对TLSv1.3的PSK复用提供了API支持,通常与会话票证功能集成在一起。服务器通过发送"NewSessionTicket"消息给客户端来提供PSK。客户端接收并存储这些票证,并在后续连接时使用 SSL_CTX_set_session
或内部缓存机制来复用。
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
#include <openssl/rand.h>
4
#include <iostream>
5
#include <string>
6
7
// 模拟一个简单的获取会话票证密钥的回调函数
8
// 实际应用中,密钥应该安全地存储和管理
9
static int ticket_key_callback(SSL *s, unsigned char *keyname,
10
unsigned char *iv, SSL_SESSION_ticket_ext *exd,
11
void *arg) {
12
// 在实际应用中,arg 可以用来传递密钥或其他上下文信息
13
static unsigned char current_key[48]; // 静态存储一个密钥示例
14
static bool key_initialized = false;
15
16
if (!key_initialized) {
17
// 仅在第一次调用时生成密钥
18
if (RAND_bytes(current_key, sizeof(current_key)) <= 0) {
19
std::cerr << "Error generating session ticket key." << std::endl;
20
return -1; // 失败
21
}
22
std::cout << "Generated new session ticket key." << std::endl;
23
key_initialized = true;
24
}
25
26
// OpenSSL 1.1.0 及以上版本
27
// 如果 exd->data 为空,表示 OpenSSL 需要我们提供一个新票证
28
if (exd->data == nullptr) {
29
// 服务器端生成票证,提供密钥名称、IV和票证值(加密后的会话状态)
30
// keyname:标识密钥
31
// iv:初始化向量,用于加密,需唯一
32
// ticket:加密后的会话状态,由 OpenSSL 生成
33
// ticket_len:票证长度
34
35
// 设置 keyname (例如,一个简单的版本号或索引)
36
memset(keyname, 0, SSL_MAX_SSL_SESSION_ID_LENGTH);
37
keyname[0] = 1; // 简单版本号
38
39
// 生成随机 IV
40
if (RAND_bytes(iv, SSL_TLS_SESSION_TICKET_KEY_IV_LEN) <= 0) {
41
std::cerr << "Error generating session ticket IV." << std::endl;
42
return -1;
43
}
44
45
// OpenSSL 会自己生成并加密票证数据
46
// 我们只需返回成功指示并让 OpenSSL 继续处理
47
48
return 1; // 成功,指示 OpenSSL 生成并加密票证
49
} else {
50
// 客户端提供票证,服务器需要解密验证
51
// exd->data:客户端提供的票证数据
52
// exd->length:票证数据长度
53
// keyname:从票证中提取的密钥名称
54
// iv:从票证中提取的 IV
55
56
// 在实际应用中,这里需要根据 keyname 查找对应的解密密钥
57
// 简单示例:只使用当前密钥
58
if (keyname[0] != 1) {
59
std::cerr << "Unknown ticket key name." << std::endl;
60
return 0; // 无法解密
61
}
62
63
// OpenSSL 会自动使用 ticket_key_callback 设置的密钥来尝试解密 exd->data
64
// 我们只需返回 1 表示成功解密,或 0 无法解密,或 -1 发生错误
65
// 成功解密后,OpenSSL 会从票证中恢复会话状态
66
67
std::cout << "Attempting to decrypt session ticket..." << std::endl;
68
return 1; // 假设可以使用当前密钥尝试解密 (OpenSSL 内部完成解密逻辑)
69
}
70
}
71
72
// 在 OpenSSL 1.1.1+ 中,设置票证密钥推荐使用 SSL_CTX_set_tlsext_ticket_keys
73
// 但其接口略有不同,需要提供一个回调函数,或者直接提供密钥结构体。
74
// 并且密钥的管理更加复杂,需要考虑轮换。
75
// 最新的 OpenSSL 3.0+ 推荐使用 provider 和 store 来管理密钥。
76
77
// 简单演示如何设置用于票证加密/解密的密钥(OpenSSL 1.1.0+ 推荐方式)
78
// 在 SSL_CTX 初始化后调用
79
int setup_ticket_keys(SSL_CTX* ctx) {
80
// 方式一:使用回调函数 (推荐)
81
// SSL_CTX_set_tlsext_ticket_key_cb(ctx, ticket_key_callback);
82
83
// 方式二:直接设置密钥结构体 (需要自己管理密钥轮换,更复杂)
84
// 需要一个 SSL_SESSION_TICKET_KEYS 结构体数组
85
// static SSL_SESSION_TICKET_KEYS ticket_keys[2]; // 示例:两个密钥,用于轮换
86
// // 初始化并生成密钥...
87
// SSL_CTX_set_tlsext_ticket_keys(ctx, ticket_keys, 2); // 设置密钥数组和数量
88
89
// 方式三:使用内置的自动密钥管理 (OpenSSL 1.1.1+)
90
// SSL_CTX_set_options(ctx, SSL_OP_NO_TICKET); // 禁用内置票证管理
91
// SSL_CTX_clear_options(ctx, SSL_OP_NO_TICKET); // 启用内置票证管理 (默认是启用的)
92
// 如果启用了内置管理,OpenSSL 会自动生成和轮换密钥,但无法跨进程/服务器共享。
93
94
// 考虑到跨进程/服务器共享密钥的需求,通常需要实现自定义的密钥管理
95
// 这通常涉及到一个外部服务来提供和管理密钥。
96
// 回调函数方式更适合这种场景,但在 OpenSSL 1.1.1+ 其接口也有变化。
97
98
// 鉴于 OpenSSL 3.0+ 推荐使用 provider 和 store 进行密钥管理,
99
// 且早期版本的直接密钥设置方式已被弃用,
100
// 这里不再提供直接设置密钥的复杂示例。
101
// 更稳健的方案是:
102
// 1. 使用 OpenSSL 1.1.1+ 并实现符合其新接口的 tlsext_ticket_key_cb 回调。
103
// 2. 升级到 OpenSSL 3.0+ 并使用其新的 provider/store 机制管理密钥。
104
// 3. 对于简单的测试或单进程应用,可以依赖 OpenSSL 内置的票证管理 (如果启用)。
105
106
// 假设我们依赖内置票证管理或者已经通过某种方式配置了密钥
107
std::cout << "Session ticket configuration placeholder." << std::endl;
108
return 1; // 假设成功
109
}
会话复用总结:
会话复用是提高TLS/SSL性能的重要手段。服务器端可以通过启用内置缓存或实现外部缓存来支持会话ID复用。客户端通过启用缓存或手动保存/设置SSL_SESSION
对象来支持会话ID复用。会话票证是另一种复用机制,在TLSv1.3中成为主流,它需要服务器端安全地管理加密密钥。正确配置和管理会话缓存及密钥是实现高效和安全会话复用的关键。
10.3 SNI (Server Name Indication)
服务器名称指示(Server Name Indication, SNI) 是TLS协议的一个扩展,它允许客户端在发起TLS握手请求时,在ClientHello消息中指定其尝试连接的主机名(hostname)。这对于在同一个IP地址和端口上托管多个使用不同证书的HTTPS网站的服务器来说至关重要。
在没有SNI的情况下,服务器在接收到ClientHello后,无法得知客户端想要访问哪个域名,因此无法选择正确的服务器证书。它只能使用默认的证书,这会导致大多数虚拟主机的TLS连接失败,因为客户端会检查证书中的域名是否与请求的域名匹配。
通过SNI,客户端将请求的域名发送给服务器,服务器就可以根据这个域名选择加载并发送相应的证书。
10.3.1 OpenSSL中服务器端处理SNI
在OpenSSL中,服务器端可以通过设置一个回调函数来处理SNI扩展。当OpenSSL在ClientHello中检测到SNI扩展时,它会调用这个回调函数。回调函数接收到客户端指定的主机名,然后服务器可以根据这个主机名加载并设置对应的证书和私钥。
使用 SSL_CTX_set_tlsext_servername_callback()
函数来设置SNI回调函数:
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
#include <iostream>
4
#include <string>
5
#include <map>
6
7
// 模拟一个证书管理器,根据 hostname 返回证书和私钥路径
8
struct CertInfo {
9
std::string cert_path;
10
std::string key_path;
11
};
12
13
static std::map<std::string, CertInfo> cert_map;
14
15
// SNI 回调函数
16
// 参数:ssl - 当前连接的 SSL 对象
17
// ad - 未使用参数 (通常为 SSL_AD_NO_ALERT)
18
// arg - SSL_CTX_set_tlsext_servername_arg() 设置的参数
19
// 返回值:SSL_TLSEXT_ERR_OK - 成功
20
// SSL_TLSEXT_ERR_NOACK - 服务器不支持该名称或没有匹配证书 (将发送警告)
21
// SSL_TLSEXT_ERR_ALERT_FATAL - 发生致命错误 (将中断连接)
22
int sni_callback(SSL *ssl, int *ad, void *arg) {
23
// 获取客户端请求的 hostname
24
const char* servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
25
26
if (servername) {
27
std::cout << "SNI callback received hostname: " << servername << std::endl;
28
29
// 根据 hostname 查找对应的证书和私钥
30
auto it = cert_map.find(servername);
31
if (it != cert_map.end()) {
32
const CertInfo& info = it->second;
33
std::cout << "Loading certificate for hostname: " << servername << std::endl;
34
35
// 为当前的 SSL 对象加载证书和私钥
36
// 注意:这里使用的是 SSL_use_certificate_file 和 SSL_use_privatekey_file
37
// 它们只影响当前的 SSL 对象,而不是 SSL_CTX
38
if (SSL_use_certificate_file(ssl, info.cert_path.c_str(), SSL_FILETYPE_PEM) <= 0) {
39
unsigned long err = ERR_get_error();
40
std::cerr << "Error loading certificate file " << info.cert_path << ": " << ERR_error_string(err, nullptr) << std::endl;
41
*ad = SSL_AD_INTERNAL_ERROR;
42
return SSL_TLSEXT_ERR_ALERT_FATAL;
43
}
44
45
if (SSL_use_privatekey_file(ssl, info.key_path.c_str(), SSL_FILETYPE_PEM) <= 0) {
46
unsigned long err = ERR_get_error();
47
std::cerr << "Error loading private key file " << info.key_path << ": " << ERR_error_string(err, nullptr) << std::endl;
48
*ad = SSL_AD_INTERNAL_ERROR;
49
return SSL_TLSEXT_ERR_ALERT_FATAL;
50
}
51
52
// 检查私钥是否与证书匹配
53
if (!SSL_check_private_key(ssl)) {
54
std::cerr << "Private key does not match the certificate for " << servername << std::endl;
55
*ad = SSL_AD_INTERNAL_ERROR;
56
return SSL_TLSEXT_ERR_ALERT_FATAL;
57
}
58
59
std::cout << "Certificate loaded successfully for " << servername << std::endl;
60
return SSL_TLSEXT_ERR_OK; // 成功
61
} else {
62
std::cerr << "No certificate found for hostname: " << servername << std::endl;
63
// 没有找到匹配的证书,可以返回 NOACK,OpenSSL 会发送一个警告并继续使用 SSL_CTX 的默认证书(如果设置了)
64
// 或者返回 ALERT_FATAL 中断连接,取决于安全策略
65
*ad = SSL_AD_UNRECOGNIZED_NAME; // 或 SSL_AD_HANDSHAKE_FAILURE
66
return SSL_TLSEXT_ERR_NOACK; // 或者 SSL_TLSEXT_ERR_ALERT_FATAL
67
}
68
} else {
69
// 客户端没有发送 SNI 扩展
70
std::cout << "SNI callback: No hostname received from client." << std::endl;
71
// 此时将使用 SSL_CTX 设置的默认证书
72
return SSL_TLSEXT_ERR_OK;
73
}
74
}
75
76
// 在服务器端配置 SSL_CTX 后调用此函数
77
int setup_sni_callback(SSL_CTX* ctx) {
78
// 填充模拟的证书映射表 (在实际应用中,这些信息应从配置文件或数据库加载)
79
cert_map["server1.example.com"] = {"path/to/server1_cert.pem", "path/to/server1_key.pem"};
80
cert_map["server2.example.com"] = {"path/to/server2_cert.pem", "path/to/server2_key.pem"};
81
cert_map["default.example.com"] = {"path/to/default_cert.pem", "path/to/default_key.pem"}; // 可选:设置一个默认证书
82
83
// 设置 SNI 回调函数
84
SSL_CTX_set_tlsext_servername_callback(ctx, sni_callback);
85
// 设置传递给回调函数的参数 (这里我们不需要额外参数,arg 可以是 NULL)
86
SSL_CTX_set_tlsext_servername_arg(ctx, NULL);
87
88
std::cout << "SNI callback setup completed." << std::endl;
89
return 1; // 成功
90
}
91
92
// 主函数中的 SSL_CTX 初始化和调用示例
93
int main_sni_example() {
94
// OpenSSL 全局初始化 (OpenSSL 1.1.0+)
95
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
96
ERR_load_CRYPTO_strings();
97
ERR_load_SSL_strings();
98
99
SSL_CTX* ctx = nullptr;
100
// 选择合适的协议版本和方法
101
const SSL_METHOD* method = TLS_server_method(); // 推荐使用 TLS_server_method
102
103
ctx = SSL_CTX_new(method);
104
if (!ctx) {
105
unsigned long err = ERR_get_error();
106
std::cerr << "Unable to create SSL context: " << ERR_error_string(err, nullptr) << std::endl;
107
return 1;
108
}
109
110
// 设置默认证书和私钥 (如果客户端不发送 SNI 或发送的名称没有匹配证书时使用)
111
// SSL_CTX_use_certificate_file(ctx, "path/to/default_cert.pem", SSL_FILETYPE_PEM);
112
// SSL_CTX_use_private_key_file(ctx, "path/to/default_key.pem", SSL_FILETYPE_PEM);
113
// SSL_CTX_check_private_key(ctx);
114
115
// 设置 SNI 回调
116
setup_sni_callback(ctx);
117
118
// ... 后续的 Socket 监听、接受连接、创建 SSL 对象、SSL_accept 等代码 ...
119
// 在 SSL_accept 执行期间或之前,如果客户端发送 SNI,会触发 sni_callback
120
121
// 清理
122
SSL_CTX_free(ctx);
123
EVP_cleanup();
124
ERR_free_strings();
125
126
return 0;
127
}
在上面的示例中,sni_callback
函数根据客户端提供的 servername
在预设的 cert_map
中查找对应的证书和私钥文件路径,并使用 SSL_use_certificate_file
和 SSL_use_private_key_file
将证书和私钥加载到当前的 SSL
对象中。这样,服务器就会使用正确的证书与该客户端完成握手。
重要提示: 在多线程服务器中,sni_callback
会在处理特定连接的线程中被调用。在回调函数中加载证书和私钥时,需要确保文件I/O操作是线程安全的,或者证书文件已经在程序启动时加载并缓存好。直接在回调中进行文件I/O可能导致性能问题。更高效的方式是在服务启动时加载所有证书/私钥,并在回调中通过查找缓存的结构体(例如 X509*
和 EVP_PKEY*
指针)来设置 SSL
对象。可以使用 SSL_use_certificate
和 SSL_use_privatekey
函数来加载已加载到内存的证书和私钥对象。
10.3.2 客户端支持SNI
作为客户端,您通常不需要做太多特殊配置来“发送”SNI。大多数现代TLS库(包括OpenSSL)在连接到远程主机时,如果使用主机名而非IP地址,会自动在ClientHello中包含SNI扩展。
如果您需要手动控制客户端发送的SNI名称(例如,连接到一个IP地址,但希望它使用特定的域名进行SNI),可以使用 SSL_set_tlsext_host_name()
函数:
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
#include <iostream>
4
#include <string>
5
6
int main_client_sni_example(const std::string& hostname, int port) {
7
// OpenSSL 全局初始化 (OpenSSL 1.1.0+)
8
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
9
ERR_load_CRYPTO_strings();
10
ERR_load_SSL_strings();
11
12
SSL_CTX* ctx = nullptr;
13
SSL* ssl = nullptr;
14
BIO* bio = nullptr;
15
16
const SSL_METHOD* method = TLS_client_method();
17
18
ctx = SSL_CTX_new(method);
19
if (!ctx) {
20
unsigned long err = ERR_get_error();
21
std::cerr << "Unable to create SSL context: " << ERR_error_string(err, nullptr) << std::endl;
22
return 1;
23
}
24
25
// ... 配置客户端证书、信任存储等 ...
26
// SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); // 启用服务器证书验证
27
// SSL_CTX_load_verify_locations(ctx, "path/to/ca_certs.pem", NULL);
28
29
ssl = SSL_new(ctx);
30
if (!ssl) {
31
unsigned long err = ERR_get_error();
32
std::cerr << "Unable to create SSL object: " << ERR_error_string(err, nullptr) << std::endl;
33
SSL_CTX_free(ctx);
34
return 1;
35
}
36
37
// 创建连接 BIO
38
std::string hostport = hostname + ":" + std::to_string(port);
39
bio = BIO_new_connect(hostport.c_str());
40
if (!bio) {
41
unsigned long err = ERR_get_error();
42
std::cerr << "Unable to create connect BIO: " << ERR_error_string(err, nullptr) << std::endl;
43
SSL_free(ssl);
44
SSL_CTX_free(ctx);
45
return 1;
46
}
47
48
// 将 BIO 关联到 SSL 对象
49
SSL_set_bio(ssl, bio, bio); // SSL_set_bio 会增加 BIO 的引用计数,如果 BIO 是新创建的,这里可以不再持有 bio 指针
50
51
// !! 手动设置 SNI 主机名 !!
52
// OpenSSL 通常会自动从 BIO_new_connect 的 host:port 中提取主机名作为 SNI
53
// 只有在需要覆盖这个默认行为时才手动调用 SSL_set_tlsext_host_name
54
if (SSL_set_tlsext_host_name(ssl, hostname.c_str()) <= 0) {
55
unsigned long err = ERR_get_error();
56
std::cerr << "Error setting SNI hostname: " << ERR_error_string(err, nullptr) << std::endl;
57
// 继续连接可能会失败,但不是致命错误,取决于服务器配置
58
} else {
59
std::cout << "Client set SNI hostname: " << hostname << std::endl;
60
}
61
62
63
// 发起 TLS 握手
64
if (SSL_connect(ssl) <= 0) {
65
unsigned long err = ERR_get_error();
66
int ssl_err = SSL_get_error(ssl, err);
67
std::cerr << "SSL_connect failed. SSL error code: " << ssl_err << ", OpenSSL error: " << ERR_error_string(err, nullptr) << std::endl;
68
// 根据 ssl_err 处理 WANT_READ/WANT_WRITE 等情况,或者判断为致命错误
69
SSL_free(ssl); // 注意:SSL_free 会同时释放关联的 BIO
70
SSL_CTX_free(ctx);
71
return 1;
72
}
73
74
std::cout << "TLS/SSL connection established successfully with " << hostname << std::endl;
75
76
// ... 数据发送/接收 ...
77
78
// 关闭连接
79
SSL_shutdown(ssl);
80
81
// 清理
82
SSL_free(ssl); // 释放 SSL 对象及其关联的 BIO
83
SSL_CTX_free(ctx);
84
EVP_cleanup();
85
ERR_free_strings();
86
87
return 0;
88
}
89
90
// 调用示例:main_client_sni_example("server1.example.com", 443);
客户端通过 SSL_set_tlsext_host_name()
设置的名称将被包含在ClientHello消息的扩展中,服务器端的SNI回调函数即可获取到这个名称。
10.4 ALPN (Application-Layer Protocol Negotiation)
应用层协议协商(Application-Layer Protocol Negotiation, ALPN)是TLS协议的另一个扩展,它允许客户端和服务器在TLS握手过程中协商确定在加密连接建立后使用哪种应用层协议。这个扩展是在TLS握手阶段,在任何应用数据发送之前发生的。
ALPN 的主要应用场景包括:
① HTTP/2: HTTP/2 标准要求通过TLS使用ALPN来协商是从HTTP/1.1升级还是直接使用HTTP/2。客户端在ClientHello中提供支持的应用层协议列表(例如,h2
, http/1.1
),服务器从中选择一个并告知客户端。
② 其他协议: 除了HTTP/2,其他应用层协议也可以利用ALPN来协商不同的版本或变体,而无需使用额外的协议特定机制。
ALPN的工作流程:
① 客户端在ClientHello消息中包含一个ALPN扩展,其中列出了它支持的应用层协议列表,按优先级排序。
② 服务器接收到ClientHello,检查ALPN扩展。如果服务器也支持ALPN,它会根据客户端提供的列表和自己的偏好选择一个共同支持的应用层协议。
③ 服务器在ServerHello或EncryptedExtensions消息中包含一个ALPN扩展,其中指明了协商选定的协议。
④ 客户端接收并确认选定的协议。如果服务器没有选择任何客户端支持的协议,客户端可以选择中止连接或回退到默认协议。
⑤ 握手完成后,客户端和服务器都知道应该使用哪种应用层协议进行通信。
10.4.1 OpenSSL中服务器端处理ALPN
在OpenSSL服务器端,可以使用 SSL_CTX_set_alpn_select_cb()
函数设置一个回调函数,用于处理ALPN协商。
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
#include <iostream>
4
#include <vector>
5
#include <string>
6
#include <cstring>
7
8
// 模拟服务器支持的应用层协议列表 (按照偏好顺序排列)
9
// 例如,优先支持 h2 (HTTP/2), 然后是 http/1.1
10
static const unsigned char server_alpn_protos[] =
11
"\x02h2" // length 2, protocol "h2"
12
"\x08http/1.1"; // length 8, protocol "http/1.1"
13
14
15
// ALPN 选择回调函数
16
// 参数:ssl - 当前连接的 SSL 对象
17
// out - 输出参数,指向服务器选定的协议名称的指针
18
// outlen - 输出参数,选定的协议名称的长度
19
// in - 客户端提供的协议列表的字节串
20
// inlen - 客户端协议列表的长度
21
// arg - SSL_CTX_set_alpn_select_cb() 设置的参数
22
// 返回值:SSL_TLSEXT_ERR_OK - 成功选择了一个协议
23
// SSL_TLSEXT_ERR_NOACK - 服务器不支持客户端的任何协议 (将发送警告)
24
// SSL_TLSEXT_ERR_ALERT_FATAL - 发生致命错误
25
int alpn_select_callback(SSL *ssl, const unsigned char **out,
26
unsigned char *outlen, const unsigned char *in,
27
unsigned int inlen, void *arg) {
28
std::cout << "ALPN select callback invoked." << std::endl;
29
30
// 使用 SSL_select_next_proto 在客户端和服务器的协议列表中进行匹配
31
// server_alpn_protos 是服务器支持的协议列表 (格式: len1+proto1+len2+proto2...)
32
// in 是客户端支持的协议列表 (格式同上)
33
int rv = SSL_select_next_proto((unsigned char **)out, outlen,
34
server_alpn_protos, sizeof(server_alpn_protos) - 1,
35
in, inlen);
36
37
if (rv == OPENSSL_NPN_NEGOTIATED) {
38
// 成功协商到一个共同协议
39
std::cout << "ALPN negotiated protocol: " << std::string((const char*)*out, *outlen) << std::endl;
40
return SSL_TLSEXT_ERR_OK;
41
} else {
42
// 没有共同协议
43
std::cout << "ALPN: No mutually supported protocol found." << std::endl;
44
// 可以选择回退到默认协议(如 HTTP/1.1)或发送 NOACK 警告,取决于应用需求
45
// 返回 SSL_TLSEXT_ERR_NOACK 表示没有协商成功,OpenSSL 可能使用默认行为或发送警告
46
*out = nullptr;
47
*outlen = 0;
48
return SSL_TLSEXT_ERR_NOACK;
49
}
50
}
51
52
// 在服务器端配置 SSL_CTX 后调用此函数
53
int setup_alpn_callback(SSL_CTX* ctx) {
54
// 设置 ALPN 选择回调函数
55
// 注意:OpenSSL 1.0.2 及以下版本使用的是 NPN (Next Protocol Negotiation),
56
// 1.0.2 及以上版本开始支持 ALPN。推荐使用 ALPN。
57
// 如果需要同时支持 NPN (针对老客户端),需要设置 NPN 回调。
58
SSL_CTX_set_alpn_select_cb(ctx, alpn_select_callback, NULL); // 最后一个参数arg可以传递自定义数据
59
60
std::cout << "ALPN select callback setup completed." << std::endl;
61
return 1; // 成功
62
}
63
64
// 主函数中的 SSL_CTX 初始化和调用示例
65
int main_alpn_server_example() {
66
// OpenSSL 全局初始化 (OpenSSL 1.1.0+)
67
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
68
ERR_load_CRYPTO_strings();
69
ERR_load_SSL_strings();
70
71
SSL_CTX* ctx = nullptr;
72
const SSL_METHOD* method = TLS_server_method();
73
74
ctx = SSL_CTX_new(method);
75
if (!ctx) {
76
unsigned long err = ERR_get_error();
77
std::cerr << "Unable to create SSL context: " << ERR_error_string(err, nullptr) << std::endl;
78
return 1;
79
}
80
81
// ... 配置服务器证书、私钥等 ...
82
// SSL_CTX_use_certificate_file(ctx, "path/to/cert.pem", SSL_FILETYPE_PEM);
83
// SSL_CTX_use_private_key_file(ctx, "path/to/key.pem", SSL_FILETYPE_PEM);
84
// SSL_CTX_check_private_key(ctx);
85
86
// 设置 ALPN 回调
87
setup_alpn_callback(ctx);
88
89
// ... 后续的 Socket 监听、接受连接、创建 SSL 对象、SSL_accept 等代码 ...
90
// 在 SSL_accept 执行期间,如果客户端发送 ALPN,会触发 alpn_select_callback
91
92
// 握手成功后,可以在服务器端获取协商的协议
93
// const unsigned char *alpn_proto = nullptr;
94
// unsigned int alpn_protolen = 0;
95
// SSL_get0_alpn_negotiated(ssl, &alpn_proto, &alpn_protolen);
96
// if (alpn_proto) {
97
// std::cout << "Negotiated ALPN protocol after handshake: " << std::string((const char*)alpn_proto, alpn_protolen) << std::endl;
98
// }
99
100
// 清理
101
SSL_CTX_free(ctx);
102
EVP_cleanup();
103
ERR_free_strings();
104
105
return 0;
106
}
在回调函数中,SSL_select_next_proto
是一个OpenSSL提供的辅助函数,用于在两个遵循 ALPN 协议列表格式(长度+协议名,长度+协议名...)的字节串中查找第一个共同支持的协议。服务器将自己的支持列表 (server_alpn_protos
) 和客户端的支持列表 (in
) 传递进去,该函数会返回一个指向客户端列表中匹配协议的指针及其长度。
10.4.2 OpenSSL中客户端支持ALPN
客户端需要构建自己支持的应用层协议列表,并在发起连接前设置到 SSL
或 SSL_CTX
对象中。通常,将协议列表设置到 SSL
对象会覆盖 SSL_CTX
的设置。
使用 SSL_set_alpn_protos()
函数设置客户端支持的ALPN协议列表:
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
#include <iostream>
4
#include <vector>
5
#include <string>
6
#include <cstring>
7
8
// 客户端支持的应用层协议列表 (按照偏好顺序排列)
9
static const unsigned char client_alpn_protos[] =
10
"\x08http/1.1"; // length 8, protocol "http/1.1"
11
"\x02h2"; // length 2, protocol "h2" // 客户端可以优先支持 HTTP/1.1 或 H2
12
13
14
int main_alpn_client_example(const std::string& hostname, int port) {
15
// OpenSSL 全局初始化 (OpenSSL 1.1.0+)
16
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
17
ERR_load_CRYPTO_strings();
18
ERR_load_SSL_strings();
19
20
SSL_CTX* ctx = nullptr;
21
SSL* ssl = nullptr;
22
BIO* bio = nullptr;
23
24
const SSL_METHOD* method = TLS_client_method();
25
26
ctx = SSL_CTX_new(method);
27
if (!ctx) {
28
unsigned long err = ERR_get_error();
29
std::cerr << "Unable to create SSL context: " << ERR_error_string(err, nullptr) << std::endl;
30
return 1;
31
}
32
33
// ... 配置客户端证书、信任存储等 ...
34
// SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); // 启用服务器证书验证
35
// SSL_CTX_load_verify_locations(ctx, "path/to/ca_certs.pem", NULL);
36
37
ssl = SSL_new(ctx);
38
if (!ssl) {
39
unsigned long err = ERR_get_error();
40
std::cerr << "Unable to create SSL object: " << ERR_error_string(err, nullptr) << std::endl;
41
SSL_CTX_free(ctx);
42
return 1;
43
}
44
45
// 创建连接 BIO
46
std::string hostport = hostname + ":" + std::to_string(port);
47
bio = BIO_new_connect(hostport.c_str());
48
if (!bio) {
49
unsigned long err = ERR_get_error();
50
std::cerr << "Unable to create connect BIO: " << ERR_error_string(err, nullptr) << std::endl;
51
SSL_free(ssl);
52
SSL_CTX_free(ctx);
53
return 1;
54
}
55
56
// 将 BIO 关联到 SSL 对象
57
SSL_set_bio(ssl, bio, bio);
58
59
// !! 设置客户端支持的 ALPN 协议列表 !!
60
// 列表格式为长度+协议名,长度+协议名...
61
if (SSL_set_alpn_protos(ssl, client_alpn_protos, sizeof(client_alpn_protos) - 1) != 0) {
62
unsigned long err = ERR_get_error();
63
std::cerr << "Error setting ALPN protos: " << ERR_error_string(err, nullptr) << std::endl;
64
// 非致命错误,但可能影响协议协商结果
65
} else {
66
std::cout << "Client set ALPN protos." << std::endl;
67
}
68
69
70
// 发起 TLS 握手
71
if (SSL_connect(ssl) <= 0) {
72
unsigned long err = ERR_get_error();
73
int ssl_err = SSL_get_error(ssl, err);
74
std::cerr << "SSL_connect failed. SSL error code: " << ssl_err << ", OpenSSL error: " << ERR_error_string(err, nullptr) << std::endl;
75
SSL_free(ssl);
76
SSL_CTX_free(ctx);
77
return 1;
78
}
79
80
std::cout << "TLS/SSL connection established successfully with " << hostname << std::endl;
81
82
// 握手成功后,客户端可以获取协商的协议
83
const unsigned char *alpn_proto = nullptr;
84
unsigned int alpn_protolen = 0;
85
SSL_get0_alpn_negotiated(ssl, &alpn_proto, &alpn_protolen);
86
if (alpn_proto) {
87
std::cout << "Negotiated ALPN protocol: " << std::string((const char*)alpn_proto, alpn_protolen) << std::endl;
88
// 根据协商的协议(例如 "h2" 或 "http/1.1")选择相应的应用层处理逻辑
89
} else {
90
std::cout << "ALPN negotiation failed or server did not support it." << std::endl;
91
// 可能需要回退到默认协议,例如 HTTP/1.1
92
}
93
94
// ... 数据发送/接收 ...
95
96
// 关闭连接
97
SSL_shutdown(ssl);
98
99
// 清理
100
SSL_free(ssl); // 释放 SSL 对象及其关联的 BIO
101
SSL_CTX_free(ctx);
102
EVP_cleanup();
103
ERR_free_strings();
104
105
return 0;
106
}
107
108
// 调用示例:main_alpn_client_example("server.example.com", 443);
客户端使用 SSL_set_alpn_protos
设置支持的协议列表。格式是使用单个字节表示后续协议名称的长度,紧跟着协议名称的字节序列,如此重复。例如,"\x02h2\x08http/1.1"
表示客户端支持 "h2" (长度2) 和 "http/1.1" (长度8)。
握手成功后,无论客户端还是服务器,都可以使用 SSL_get0_alpn_negotiated()
函数来获取最终协商确定的应用层协议。
10.5 自定义证书验证
TLS/SSL握手的一个关键步骤是服务器(通常)和客户端(如果配置了相互认证,mutual authentication)的身份验证,这通常通过验证对方提供的X.509证书(X.509 Certificate)来实现。OpenSSL提供了强大的证书验证功能,包括构建证书链(certificate chain)、检查签名、验证有效期、检查吊销状态(CRL/OCSP)以及匹配主机名等。
然而,有时默认的验证逻辑可能无法满足特定的安全需求。例如:
① 需要使用非标准的信任锚(trust anchor)集合。
② 需要实现自定义的证书策略检查(例如,检查证书的特定扩展字段)。
③ 需要更灵活地处理某些验证错误。
④ 需要在验证过程中记录详细日志或与其他系统集成。
在这种情况下,可以通过设置自定义证书验证回调函数(custom certificate verification callback)来扩展或完全替代OpenSSL的默认验证行为。
10.5.1 OpenSSL默认证书验证流程
在深入自定义验证之前,理解OpenSSL的默认验证流程是很重要的。当启用证书验证时(例如,服务器端设置 SSL_VERIFY_PEER
),OpenSSL会执行以下步骤:
① 客户端/服务器发送证书链。
② 接收方获取对方的根证书信任存储(由 SSL_CTX_load_verify_locations
或 SSL_CTX_set_cert_store
设置)。
③ 接收方尝试构建从对方提供的证书到信任锚的证书链。
④ 验证链中的每个证书:检查签名、有效期、使用策略(key usage, extended key usage)、名称约束(name constraints)等。
⑤ 检查终端实体证书的主机名是否与连接的主机名匹配(客户端)。
⑥ 检查证书是否被吊销(如果配置了CRL或OCSP)。
如果任何一步失败,验证过程就会中断并返回错误码。
10.5.2 设置自定义证书验证回调函数
可以使用 SSL_CTX_set_verify()
或 SSL_set_verify()
函数来设置证书验证模式和回调函数。设置到 SSL_CTX
会影响所有基于该上下文创建的 SSL
对象,而设置到特定的 SSL
对象会覆盖 SSL_CTX
的设置。
SSL_CTX_set_verify(SSL_CTX *ctx, int mode, int (*callback)(int, X509_STORE_CTX *))
⚝ ctx
: SSL_CTX
对象。
⚝ mode
: 验证模式。常见的模式有 SSL_VERIFY_NONE
(不验证), SSL_VERIFY_PEER
(验证对方证书), SSL_VERIFY_FAIL_IF_NO_PEER_CERT
(如果对方没有发送证书则失败), SSL_VERIFY_CLIENT_ONCE
(服务器只在第一次握手时请求客户端证书)。通常客户端至少使用 SSL_VERIFY_PEER
,服务器在需要客户端认证时使用 SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT
。
⚝ callback
: 自定义验证回调函数。
回调函数的签名是 int callback(int preverify_ok, X509_STORE_CTX *ctx)
。
⚝ preverify_ok
: 这是一个整数,表示OpenSSL内置的证书验证器到目前为止的验证结果。非零表示成功,零表示失败。
⚝ ctx
: 一个指向 X509_STORE_CTX
结构的指针,包含了进行证书验证所需的所有上下文信息,例如当前的证书、证书链、信任存储、验证错误码等。
⚝ 返回值:回调函数需要返回一个整数。非零值表示您认为验证应该继续或最终成功;零值表示您认为验证应该失败。
回调函数在OpenSSL内置验证流程的多个点被调用,最常见的是在内置验证器完成其所有检查之后。通过检查 preverify_ok
和 X509_STORE_CTX
中的错误码,您可以决定是接受内置验证的结果,还是基于自定义逻辑覆盖它。
一个基本的自定义验证回调函数示例:
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
#include <openssl/x509v3.h>
4
#include <iostream>
5
#include <string>
6
#include <vector>
7
8
// 自定义证书验证回调函数
9
// 这个回调函数会在 OpenSSL 内置验证过程中的多个阶段被调用
10
// 最常见的调用时机是:
11
// 1. 在处理证书链中的每个证书时
12
// 2. 在完成整个证书链验证后 (检查 X509_STORE_CTX_get_error() 获取最终错误码)
13
int custom_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) {
14
// 获取当前正在验证的证书
15
X509 *cert = X509_STORE_CTX_get_current_cert(ctx);
16
// 获取当前验证阶段的错误码 (如果 preverify_ok == 0)
17
int err = X509_STORE_CTX_get_error(ctx);
18
// 获取错误字符串
19
const char* err_str = X509_STORE_CTX_get_error_ глубины (ctx) ? X509_STORE_CTX_get_error_string(ctx) : "No error"; // 错误的深度
20
int depth = X509_STORE_CTX_get_error_depth(ctx);
21
22
23
std::cout << "Verify callback: depth=" << depth << ", preverify_ok=" << preverify_ok << ", error=" << err << " (" << err_str << ")" << std::endl;
24
25
// 打印证书的主题名
26
X509_NAME* subject_name = X509_get_subject_name(cert);
27
char subject_str[256];
28
X509_NAME_oneline(subject_name, subject_str, sizeof(subject_str));
29
std::cout << " Subject: " << subject_str << std::endl;
30
31
// 打印证书的颁发者名
32
X509_NAME* issuer_name = X509_get_issuer_name(cert);
33
char issuer_str[256];
34
X509_NAME_oneline(issuer_name, issuer_str, sizeof(issuer_str));
35
std::cout << " Issuer: " << issuer_str << std::endl;
36
37
// --- 自定义逻辑 ---
38
39
// 示例1: 忽略证书过期错误 (仅用于测试!)
40
if (err == X509_V_ERR_CERT_HAS_EXPIRED) {
41
std::cerr << " Ignoring certificate expiration error for demonstration!" << std::endl;
42
preverify_ok = 1; // 覆盖内置验证结果,表示接受此证书
43
}
44
45
// 示例2: 要求证书链深度不超过某个值 (OpenSSL 内置通常会检查,这里仅为示例)
46
// if (depth > 2) {
47
// std::cerr << " Rejecting certificate due to excessive chain depth." << std::endl;
48
// preverify_ok = 0; // 拒绝此证书
49
// // 可以在这里设置一个自定义错误码,但这可能影响 OpenSSL 的最终错误报告
50
// // X509_STORE_CTX_set_error(ctx, X509_V_ERR_UNSPECIFIED);
51
// }
52
53
// 示例3: 检查证书的特定字段 (例如,subjectAltName 中的特定 SAN 或 OID)
54
// const char* required_san = "my_special_identifier";
55
// int san_nid = OBJ_txt2nid("subjectAltName");
56
// if (san_nid != NID_undef) {
57
// GENERAL_NAMES *sname = (GENERAL_NAMES*)X509_get_ext_d2i(cert, san_nid, NULL, NULL);
58
// if (sname) {
59
// bool found_special_id = false;
60
// for (int i = 0; i < sk_GENERAL_NAME_num(sname); i++) {
61
// GENERAL_NAME *gen = sk_GENERAL_NAME_value(sname, i);
62
// if (gen->type == GEN_OTHERNAME) {
63
// // 进一步检查 OID 和值
64
// // if (OBJ_cmp(gen->d.otherName->type_id, ...) == 0) { ... }
65
// } else if (gen->type == GEN_DNS) {
66
// // 检查 DNS 名称是否符合预期
67
// // ASN1_IA5STRING *dn = gen->d.dNSName;
68
// // if (dn && dn->data && strcmp((const char*)dn->data, required_san) == 0) {
69
// // found_special_id = true;
70
// // break;
71
// // }
72
// }
73
// // 其他类型如 GEN_IPADD, GEN_URI 等...
74
// }
75
// GENERAL_NAMES_free(sname);
76
// // if (!found_special_id) {
77
// // std::cerr << " Rejecting certificate: Missing required subjectAltName." << std::endl;
78
// // preverify_ok = 0;
79
// // }
80
// }
81
// }
82
83
84
// --- 决定返回值 ---
85
// 返回 preverify_ok 表示接受 OpenSSL 内置验证的结果 (可能是成功或失败)
86
// 返回 1 表示强制接受证书 (即使 preverify_ok 为 0)
87
// 返回 0 表示强制拒绝证书 (即使 preverify_ok 为 1)
88
89
// 通常情况下,如果 preverify_ok 为 0 (OpenSSL 发现错误),并且您不想忽略这个错误,就返回 0。
90
// 如果 preverify_ok 为 1 (OpenSSL 认为没问题),或者您决定忽略某个错误,就返回 1。
91
92
if (preverify_ok == 0) {
93
std::cerr << " Certificate verification failed by OpenSSL built-in check." << std::endl;
94
// 您可以在这里根据 err 的值决定是否忽略某些错误
95
// if (err == X509_V_ERR_... ) { /* 忽略 */ return 1; }
96
// 否则,遵循内置验证结果
97
return 0; // 拒绝证书
98
}
99
100
// 如果 preverify_ok 为 1,或者您忽略了之前的错误,这里可以添加额外的自定义检查
101
// 例如,检查证书的特定颁发者,或者是否在您的自定义吊销列表中
102
// bool custom_check_ok = perform_additional_checks(cert);
103
// if (!custom_check_ok) {
104
// std::cerr << " Certificate rejected by custom checks." << std::endl;
105
// X509_STORE_CTX_set_error(ctx, X509_V_ERR_UNSPECIFIED); // 设置一个错误码 (可选)
106
// return 0; // 拒绝证书
107
// }
108
109
110
// 如果内置验证成功 (或被忽略) 并且所有自定义检查通过,返回 1
111
std::cout << " Certificate verification passed custom checks." << std::endl;
112
return 1; // 接受证书
113
}
114
115
// 在配置 SSL_CTX 或 SSL 对象时设置回调函数
116
void setup_custom_verify_callback(SSL_CTX* ctx) {
117
// 设置验证模式和回调函数
118
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, custom_verify_callback);
119
120
// 可选:设置证书链验证的最大深度
121
// SSL_CTX_set_verify_depth(ctx, 5);
122
123
std::cout << "Custom verify callback setup completed." << std::endl;
124
}
125
126
// 主函数中的 SSL_CTX 初始化和调用示例 (客户端)
127
int main_custom_verify_example_client() {
128
// OpenSSL 全局初始化 (OpenSSL 1.1.0+)
129
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
130
ERR_load_CRYPTO_strings();
131
ERR_load_SSL_strings();
132
133
SSL_CTX* ctx = nullptr;
134
SSL* ssl = nullptr;
135
BIO* bio = nullptr;
136
137
const SSL_METHOD* method = TLS_client_method();
138
139
ctx = SSL_CTX_new(method);
140
if (!ctx) {
141
unsigned long err = ERR_get_error();
142
std::cerr << "Unable to create SSL context: " << ERR_error_string(err, nullptr) << std::endl;
143
return 1;
144
}
145
146
// 加载信任锚证书 (根 CA 证书)
147
// SSL_CTX_load_verify_locations(ctx, "path/to/ca_certs.pem", NULL);
148
149
// !! 设置自定义验证回调 !!
150
setup_custom_verify_callback(ctx);
151
152
153
ssl = SSL_new(ctx);
154
if (!ssl) {
155
unsigned long err = ERR_get_error();
156
std::cerr << "Unable to create SSL object: " << ERR_error_string(err, nullptr) << std::endl;
157
SSL_CTX_free(ctx);
158
return 1;
159
}
160
161
// ... 创建并关联 BIO ...
162
// bio = BIO_new_connect("server.example.com:443");
163
// SSL_set_bio(ssl, bio, bio);
164
165
// 发起 TLS 握手
166
// int ret = SSL_connect(ssl);
167
// if (ret <= 0) {
168
// unsigned long err = ERR_get_error();
169
// int ssl_err = SSL_get_error(ssl, ret);
170
// std::cerr << "SSL_connect failed. SSL error code: " << ssl_err << ", OpenSSL error: " << ERR_error_string(err, nullptr) << std::endl;
171
// // 可以在这里检查 SSL_get_verify_result(ssl) 获取最终验证错误码
172
// long verify_err = SSL_get_verify_result(ssl);
173
// if (verify_err != X509_V_OK) {
174
// std::cerr << "Certificate verification error: " << X509_verify_cert_error_string(verify_err) << std::endl;
175
// }
176
// SSL_free(ssl);
177
// SSL_CTX_free(ctx);
178
// return 1;
179
// }
180
181
// std::cout << "TLS/SSL connection established successfully." << std::endl;
182
183
184
// 清理
185
SSL_free(ssl); // 释放 SSL 对象及其关联的 BIO
186
SSL_CTX_free(ctx);
187
EVP_cleanup();
188
ERR_free_strings();
189
190
return 0;
191
}
回调函数的执行时机: 回调函数可能会在验证过程中的多个点被调用。当 depth
为0时,通常表示正在验证终端实体证书;当 depth > 0
时,表示正在验证链中的中间CA或根CA证书。当OpenSSL完成整个链的内置验证后,还会最后调用一次回调函数,此时 preverify_ok
反映了整体验证结果。
在回调函数中做什么:
⚝ 检查 preverify_ok
和 err
:了解OpenSSL内置验证器发现了什么问题。
⚝ 访问证书信息:使用 X509_STORE_CTX_get_current_cert(ctx)
获取当前证书,然后使用 X509_get_subject_name
, X509_get_issuer_name
等函数检查证书详情。
⚝ 检查证书链:使用 X509_STORE_CTX_get0_chain(ctx)
获取已构建的证书链(STACK_OF(X509)*
类型),遍历链中的证书进行检查。
⚝ 访问信任存储:使用 X509_STORE_CTX_get0_store(ctx)
获取信任存储对象(X509_STORE*
)。
⚝ 进行自定义检查:实现您自己的逻辑,例如检查证书的特定扩展、与自定义策略匹配等。
⚝ 决定返回值:根据内置结果和自定义检查结果返回1(接受)或0(拒绝)。
重要提示:
⚝ 在回调函数中进行耗时操作要谨慎,因为它会阻塞TLS握手过程。
⚝ 不要在回调函数中释放 X509_STORE_CTX
及其内部的对象(如证书、链等),这些由OpenSSL管理。
⚝ 如果您返回0(拒绝证书),TLS握手将失败,并返回相应的错误码(通常是验证错误)。客户端或服务器可以通过 SSL_get_verify_result()
获取最终的验证结果码。
通过自定义验证回调,您可以实现非常灵活和细粒度的证书验证逻辑,满足复杂的安全策略需求。
10.6 TLS/SSL安全加固
即使正确实现了TLS/SSL通信,不安全的配置也可能导致连接容易受到各种攻击,例如降级攻击(downgrade attacks)、中间人攻击(man-in-the-middle attacks)、信息泄露(如Heartbleed,Padding Oracle等)。对OpenSSL进行安全加固是构建健壮安全应用的关键步骤。
主要的TLS/SSL安全加固方向包括:
① 选择安全的协议版本。
② 配置强密码套件。
③ 禁用不安全的特性。
④ 防范已知漏洞。
10.6.1 选择安全的协议版本
随着时间推移,TLS/SSL协议的不同版本被发现存在安全漏洞。例如,SSLv2、SSLv3、TLSv1.0、TLSv1.1 都已被认为是不安全的或不推荐使用。目前,TLSv1.2 和 TLSv1.3 是推荐使用的版本。
可以使用 SSL_CTX_set_min_proto_version()
和 SSL_CTX_set_max_proto_version()
函数来限制允许使用的最低和最高协议版本。
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
#include <iostream>
4
5
void secure_protocol_versions(SSL_CTX* ctx) {
6
// 推荐配置:最低 TLSv1.2
7
if (SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) <= 0) {
8
unsigned long err = ERR_get_error();
9
std::cerr << "Failed to set minimum TLS version: " << ERR_error_string(err, nullptr) << std::endl;
10
} else {
11
std::cout << "Set minimum TLS version to TLSv1.2." << std::endl;
12
}
13
14
// 如果希望同时支持 TLSv1.3,则最高版本不限制或设置为 TLS1_3_VERSION
15
// 如果只希望支持 TLSv1.2,则最高版本也设置为 TLS1_2_VERSION
16
// 例如,允许 TLSv1.2 和 TLSv1.3
17
if (SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION) <= 0) {
18
unsigned long err = ERR_get_error();
19
std::cerr << "Failed to set maximum TLS version: " << ERR_error_string(err, nullptr) << std::endl;
20
} else {
21
std::cout << "Set maximum TLS version to TLSv1.3." << std::endl;
22
}
23
24
// 禁用所有 SSLv2/SSLv3 (通常已被默认禁用,但显式设置更安全)
25
// SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
26
// 禁用 TLSv1.0 和 TLSv1.1 (如果设置了最低 TLSv1.2,则会自动禁用它们,但显式设置更清晰)
27
// SSL_CTX_set_options(ctx, SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1);
28
29
// 使用 OpenSSL 选项来禁用旧版本(兼容旧API)
30
// SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2);
31
// SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv3);
32
// SSL_CTX_set_options(ctx, SSL_OP_NO_TLSv1);
33
// SSL_CTX_set_options(ctx, SSL_OP_NO_TLSv1_1);
34
}
35
36
// 在创建 SSL_CTX 后调用
37
// main_ssl_ctx = SSL_CTX_new(TLS_method());
38
// secure_protocol_versions(main_ssl_ctx);
TLS1_2_VERSION
和 TLS1_3_VERSION
等宏定义在 openssl/ssl.h
中。使用 TLS_client_method()
和 TLS_server_method()
(而非特定版本的如 SSLv23_method()
) 创建 SSL_CTX
是推荐的做法,因为它们会根据编译时的OpenSSL版本自动支持可用的最高协议版本,然后您再通过 set_min_proto_version
/set_max_proto_version
来进一步限制。
10.6.2 配置强密码套件
密码套件(Cipher Suite)定义了TLS/SSL连接中使用的加密、认证、密钥交换和哈希算法的组合。例如,一个密码套件可能是 TLS_AES_128_GCM_SHA256
,表示使用AES-128 GCM进行加密,SHA256进行完整性校验(在TLSv1.3中)。
配置强密码套件是防止许多TLS/SSL攻击的关键。您应该禁用弱密码套件(如使用DES、RC4等算法)、匿名密码套件(允许未认证连接)以及存在已知漏洞的密码套件。
可以使用 SSL_CTX_set_cipher_list()
函数来配置允许使用的密码套件列表(对于TLSv1.2及以下版本),或 SSL_CTX_set_ciphersuites()
(对于TLSv1.3版本)。OpenSSL使用一个特定的字符串格式来定义密码套件列表。
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
#include <iostream>
4
#include <string>
5
6
void secure_cipher_suites(SSL_CTX* ctx) {
7
// 推荐的 TLSv1.2 密码套件列表 (示例,实际生产环境应参考安全指南)
8
// 优先使用支持前向保密 (PFS) 的算法,如 ECDHE 或 DHE
9
// 优先使用强大的加密算法,如 AES-GCM 或 CHACHA20-POLY1305
10
// 禁用弱密钥交换 (如 RSA without PFS)
11
// 禁用弱哈希算法 (如 MD5, SHA1)
12
const char* cipher_list_tls12 =
13
"ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:"
14
"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:"
15
"DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:"
16
"!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!DSS:!SEED:!CAMELLIA"; // 禁用匿名、导出、弱算法等
17
18
if (SSL_CTX_set_cipher_list(ctx, cipher_list_tls12) <= 0) {
19
unsigned long err = ERR_get_error();
20
std::cerr << "Failed to set cipher list for TLSv1.2: " << ERR_error_string(err, nullptr) << std::endl;
21
} else {
22
std::cout << "Set cipher list for TLSv1.2: " << cipher_list_tls12 << std::endl;
23
}
24
25
// TLSv1.3 密码套件列表 (更简单,OpenSSL 3.0+ 默认就很少)
26
// OpenSSL 1.1.1+ 开始支持 TLSv1.3。TLSv1.3 的密码套件与旧版本不同,且数量较少。
27
const char* cipher_suites_tls13 =
28
"TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"; // TLSv1.3 标准定义的几个主要套件
29
30
if (SSL_CTX_set_ciphersuites(ctx, cipher_suites_tls13) <= 0) {
31
unsigned long err = ERR_get_error();
32
std::cerr << "Failed to set cipher suites for TLSv1.3: " << ERR_error_string(err, nullptr) << std::endl;
33
} else {
34
std::cout << "Set cipher suites for TLSv1.3: " << cipher_suites_tls13 << std::endl;
35
}
36
}
37
38
// 在创建 SSL_CTX 后调用
39
// main_ssl_ctx = SSL_CTX_new(TLS_method());
40
// secure_cipher_suites(main_ssl_ctx);
密码套件字符串的语法比较复杂,man ciphers
命令可以查看详细说明和各种别名。HIGH
、MEDIUM
、LOW
、EXP
、aNULL
、eNULL
、!aNULL
等是常用的关键字和修饰符。例如,HIGH
表示高强度加密,!aNULL
表示排除匿名认证的套件。
10.6.3 禁用不安全的特性
OpenSSL提供了许多选项来禁用TLS/SSL协议中被认为不安全或存在风险的特性。这些选项可以使用 SSL_CTX_set_options()
或 SSL_set_options()
函数来设置。
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
#include <iostream>
4
5
void disable_insecure_options(SSL_CTX* ctx) {
6
// 禁用 SSL 压缩 (防止 CRIME/BREACH 攻击)
7
// 在 OpenSSL 1.0.0 之后通常默认禁用,但显式设置更安全
8
SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION);
9
std::cout << "Disabled SSL compression." << std::endl;
10
11
// 禁用会话票证 (如果担心密钥泄露导致的历史会话被解密,但会牺牲性能)
12
// SSL_CTX_set_options(ctx, SSL_OP_NO_TICKET);
13
// std::cout << "Disabled session tickets." << std::endl;
14
15
16
// 启用安全重协商 (防止 TLS 重协商攻击)
17
// OpenSSL 1.0.1+ 默认启用安全重协商。
18
// SSL_CTX_set_options(ctx, SSL_OP_NO_RENEGOTIATION); // 禁用所有重协商 (如果应用不需要)
19
// SSL_CTX_set_options(ctx, SSL_OP_ENABLE_CTX_INCREASES_PRIMALITY_CHECKS); // TLS 1.2+ 强制安全重协商
20
21
// 避免某些已知实现问题的选项 (通常在特定版本或特定平台需要)
22
// 例如,应对一些老旧客户端或服务器的兼容性问题,但可能降低安全性
23
// SSL_CTX_set_options(ctx, SSL_OP_SINGLE_ECDH_USE); // 确保每次使用新的 ECDH 密钥
24
// SSL_CTX_set_options(ctx, SSL_OP_SINGLE_DH_USE); // 确保每次使用新的 DH 密钥
25
// SSL_CTX_set_options(ctx, SSL_OP_NO_QUERY_MTU); // 避免 MTU 查询问题
26
27
// 启用服务器偏好密码套件顺序 (通常推荐服务器控制密码套件顺序)
28
SSL_CTX_set_options(ctx, SSL_OP_CIPHER_SERVER_PREFERENCE);
29
std::cout << "Enabled server cipher preference." << std::endl;
30
31
// 其他可能的选项,取决于OpenSSL版本和具体安全建议
32
// 例如,对于 OpenSSL 3.0+,可能需要配置 provider 和 properties 来进一步控制算法选择。
33
}
34
35
// 在创建 SSL_CTX 后调用
36
// main_ssl_ctx = SSL_CTX_new(TLS_method());
37
// disable_insecure_options(main_ssl_ctx);
常用的选项包括 SSL_OP_NO_SSLv2
, SSL_OP_NO_SSLv3
, SSL_OP_NO_TLSv1
, SSL_OP_NO_TLSv1_1
, SSL_OP_NO_COMPRESSION
, SSL_OP_CIPHER_SERVER_PREFERENCE
等。查阅OpenSSL官方文档 (man SSL_CTX_set_options
) 获取完整的选项列表及其含义。
10.6.4 防范已知漏洞
及时更新OpenSSL库是防范已知安全漏洞(如Heartbleed、POODLE、FREAK、Logjam等)的最重要措施。OpenSSL项目会定期发布安全更新来修复漏洞。
除了更新库,一些漏洞也可以通过合理的配置来缓解或阻止。例如:
⚝ POODLE (Padding Oracle On Downgraded Legacy Encryption): 影响SSLv3。通过禁用SSLv3 (SSL_OP_NO_SSLv3
或设置最低协议版本到TLSv1.0+) 来防范。
⚝ Heartbleed: 这是OpenSSL实现中的一个漏洞,需要更新OpenSSL库才能修复。配置无法阻止此漏洞。
⚝ FREAK / Logjam: 这些漏洞涉及弱导出级密码套件和弱Diffie-Hellman参数。通过配置强密码套件列表(禁用EXPORT套件,使用足够长的DH参数或优先使用ECDHE)来防范。对于DH参数,服务器端可以使用 SSL_CTX_set_tmp_dh()
或 SSL_CTX_set_dh_auto()
(OpenSSL 1.1.0+) 来配置。
1
// 示例:设置自动 DH 参数 (OpenSSL 1.1.0+)
2
void setup_auto_dh(SSL_CTX* ctx) {
3
// 启用自动 DH 参数选择
4
// OpenSSL 会根据协商的密码套件选择合适的、足够强的 DH 参数
5
// 如果您使用 DHE 或 EDH 密码套件,推荐启用此选项
6
SSL_CTX_set_options(ctx, SSL_OP_SINGLE_DH_USE); // 每次握手使用新的 DH 密钥对
7
SSL_CTX_set_options(ctx, SSL_OP_SINGLE_ECDH_USE); // 每次握手使用新的 ECDH 密钥对 (对于静态 ECDH 套件)
8
SSL_CTX_set_dh_auto(ctx, 1); // 启用自动 DH 参数选择 (OpenSSL 1.1.0+)
9
std::cout << "Enabled automatic DH parameter selection." << std::endl;
10
}
11
12
// 在创建 SSL_CTX 后调用
13
// main_ssl_ctx = SSL_CTX_new(TLS_method());
14
// setup_auto_dh(main_ssl_ctx);
在OpenSSL 3.0+ 中,DH参数的管理更多地依赖于Provider和Property机制。
持续关注安全动态: TLS/SSL的安全形势在不断变化,新的攻击和漏洞可能随时出现。作为开发者,您应该持续关注OpenSSL项目的安全公告、行业安全建议以及相关标准更新,并及时更新您的OpenSSL库和应用配置。
安全加固总结:
TLS/SSL安全加固不是一次性的任务,而是一个持续的过程。关键在于选择安全的协议版本和密码套件,禁用不安全的特性,及时更新OpenSSL库,并根据最新的安全建议调整配置。服务器端尤其需要谨慎配置,因为一个不安全的配置可能影响所有连接到它的客户端。使用在线工具(如 Qualys SSL Labs Server Test)可以帮助您评估服务器的TLS/SSL配置安全性。
本章内容丰富,涵盖了TLS/SSL的高级特性和实践技巧。掌握这些知识,您就能构建更高效、更灵活且更安全的TLS/SSL应用程序。下一章,我们将讨论在多线程C++应用程序中安全使用OpenSSL的方法。
好的,各位同学,欢迎来到《OpenSSL C++ 开发:深度解析与实战》的第11章。在前面的章节中,我们已经深入探讨了OpenSSL在加密、哈希、签名、证书管理以及TLS/SSL协议方面的应用。然而,在现代C++应用程序中,多线程(multithreading)已经成为常态。如何在多线程环境下安全、高效地使用OpenSSL库,是一个至关重要的问题。本章就将详细讲解OpenSSL的线程安全(thread safety)问题以及解决方案。
11. OpenSSL线程安全
在多线程应用程序中使用任何共享资源,都需要仔细考虑其线程安全问题。OpenSSL库作为一个底层且功能丰富的密码学库,其内部管理着一些全局状态(global state)和数据结构。如果在多个线程中不加保护地调用OpenSSL函数,可能会导致数据竞争(data race)、内存损坏(memory corruption)甚至程序崩溃。本章旨在帮助读者理解OpenSSL的线程安全机制,并掌握在C++多线程程序中正确使用OpenSSL的方法。
11.1 OpenSSL与线程
OpenSSL的线程安全问题主要源于其设计初期的一些历史遗留问题和内部实现细节。特别是在OpenSSL 1.1.0版本之前,库的许多部分并非原生线程安全(thread-safe)。
⚝ 挑战与要求
OpenSSL在多线程环境下使用面临的挑战主要体现在以下几个方面:
▮▮▮▮⚝ 全局状态:早期的OpenSSL库维护了一些全局可访问的状态,例如随机数生成器(RNG, Random Number Generator)的状态、错误栈(error stack)以及算法注册信息等。多个线程同时读写这些全局状态会引发问题。
▮▮▮▮⚝ 静态数据:某些内部数据结构可能是静态的,或通过全局指针访问,需要同步访问。
▮▮▮▮⚝ 内存管理:OpenSSL有自己的内存分配和释放机制(虽然底层通常使用系统的malloc/free)。在多线程环境中,如果不对这些操作进行适当的同步,也可能导致问题。
▮▮▮▮⚝ 错误栈:OpenSSL的错误栈是线程本地的(thread-local),这在获取错误信息时是方便的,但也意味着错误信息不会在线程间共享,每个线程需要独立处理自己的错误。然而,错误栈的底层管理可能依赖于一些需要同步的资源。
▮▮▮▮⚝ BIOs和SSL对象:虽然大部分BIO
和SSL
对象本身可以在创建它们的线程中使用,但如果这些对象被多个线程共享(例如,一个线程创建了SSL对象,另一个线程使用它进行读写),或者它们内部依赖于全局状态,就需要额外的保护。
在OpenSSL 1.1.0版本之前,为了解决这些问题,用户需要在应用程序层面为OpenSSL配置线程回调函数。这要求开发者深入理解OpenSSL的内部同步需求,并提供一套系统原生的锁机制。
11.2 配置线程回调函数
(注:本节主要介绍 OpenSSL 1.1.0 版本之前的线程安全配置方法。对于使用 OpenSSL 1.1.0 及更高版本的读者,可以直接跳到下一节,因为这些版本在内部已经处理了大部分线程安全问题。了解本节内容有助于理解历史背景和维护旧代码。)
在 OpenSSL 1.1.0 之前,用户必须显式地设置回调函数来告诉 OpenSSL 如何执行锁定操作(locking)和获取当前线程的ID(thread ID)。这是因为 OpenSSL 库本身不包含操作系统相关的线程和同步代码。
⚝ 必需的回调函数
为了使旧版本的OpenSSL在多线程中安全工作,至少需要设置两个回调函数:
① 锁定回调函数 (Locking Callback):
▮▮▮▮这是最重要的一个回调函数。OpenSSL在访问共享数据结构之前会调用此函数来获取锁,在访问完成后释放锁。你需要提供一个函数,它接受三个参数:一个模式(mode,表示加锁还是解锁)、一个锁的索引(n,表示哪个锁)和一个文件/行号(file/line,用于调试)。你需要维护一个全局的锁数组,并在回调函数中根据索引n
对相应的锁进行操作。
▮▮▮▮对应的设置函数是 void CRYPTO_set_locking_callback(void (*func)(int mode, int n, const char *file, int line));
▮▮▮▮模式 mode
可以是 CRYPTO_LOCK
(加锁), CRYPTO_UNLOCK
(解锁), CRYPTO_READ
(读锁), CRYPTO_WRITE
(写锁) 等的组合。
② 线程ID回调函数 (Thread ID Callback):
▮▮▮▮这个回调函数用于让OpenSSL获取当前线程的唯一标识符。这主要用于管理线程本地的错误栈。你需要提供一个函数,它返回一个 unsigned long
类型的线程ID。
▮▮▮▮对应的设置函数是 void CRYPTO_set_id_callback(unsigned long (*func)(void));
⚝ 实现锁定回调的思路
实现锁定回调通常需要:
① 定义锁数组:确定OpenSSL内部需要多少个锁。可以通过 CRYPTO_num_locks()
函数获取锁的数量。然后创建一个相应大小的互斥锁(mutex)数组。
② 初始化锁:在程序启动早期,创建并初始化这个互斥锁数组中的所有锁。
③ 实现回调函数:在回调函数中,根据传入的锁索引 n
,选择数组中对应的锁,并根据模式 mode
调用操作系统提供的加锁或解锁函数(例如,在POSIX系统中使用 pthread_mutex_lock
/pthread_mutex_unlock
,在Windows中使用 EnterCriticalSection
/LeaveCriticalSection
)。
④ 实现线程ID回调:调用操作系统提供的获取当前线程ID的函数(例如,在POSIX系统中使用 pthread_self
,在Windows中使用 GetCurrentThreadId
)。注意将返回类型转换为 unsigned long
。
⑤ 设置回调函数:在调用任何OpenSSL函数之前,使用 CRYPTO_set_locking_callback
和 CRYPTO_set_id_callback
注册你实现的函数。
⑥ 清理锁:在程序退出时,销毁创建的互斥锁数组。
一个简化的伪代码示例:
1
// 假设使用 pthreads
2
#include <pthread.h>
3
#include <openssl/crypto.h>
4
#include <vector> // 用于存储锁
5
6
// 全局锁数组
7
std::vector<pthread_mutex_t> crypto_locks;
8
9
// 锁定回调函数实现
10
void my_locking_callback(int mode, int n, const char *file, int line) {
11
if (n < 0 || n >= crypto_locks.size()) {
12
// 错误处理
13
return;
14
}
15
16
if (mode & CRYPTO_LOCK) {
17
pthread_mutex_lock(&crypto_locks[n]);
18
} else if (mode & CRYPTO_UNLOCK) {
19
pthread_mutex_unlock(&crypto_locks[n]);
20
}
21
}
22
23
// 线程ID回调函数实现
24
unsigned long my_thread_id_callback() {
25
return static_cast<unsigned long>(pthread_self());
26
}
27
28
// 初始化函数 (在main或其他入口点早期调用)
29
void init_openssl_threading() {
30
// 获取需要的锁数量
31
int num_locks = CRYPTO_num_locks();
32
crypto_locks.resize(num_locks);
33
34
// 初始化锁数组
35
for (int i = 0; i < num_locks; ++i) {
36
pthread_mutex_init(&crypto_locks[i], nullptr);
37
}
38
39
// 设置回调函数
40
CRYPTO_set_locking_callback(my_locking_callback);
41
CRYPTO_set_id_callback(my_thread_id_callback);
42
43
// 早期的版本可能还需要 CRYPTO_thread_setup(),但它已被废弃
44
// 最好检查 OpenSSL 版本的文档
45
}
46
47
// 清理函数 (在程序退出前调用)
48
void cleanup_openssl_threading() {
49
// 清理锁数组
50
for (int i = 0; i < crypto_locks.size(); ++i) {
51
pthread_mutex_destroy(&crypto_locks[i]);
52
}
53
54
// 取消回调函数 (可选,但推荐)
55
CRYPTO_set_locking_callback(nullptr);
56
CRYPTO_set_id_callback(nullptr);
57
}
58
59
// 在 main() 函数或其他地方:
60
int main() {
61
// ... 其他初始化
62
// init_openssl_threading(); // 对于 1.1.0 之前的版本必须调用
63
// ... 使用 OpenSSL 的多线程代码
64
// cleanup_openssl_threading(); // 对于 1.1.0 之前的版本必须调用
65
// ...
66
return 0;
67
}
这个过程相当繁琐且容易出错,是早期OpenSSL在多线程环境下使用的一大痛点。
11.3 新的线程安全API (OpenSSL 1.1.0+)
随着C++11及更高版本引入了标准的线程和同步原语,以及操作系统对线程支持的成熟,OpenSSL项目在1.1.0版本中对内部线程安全机制进行了重大改进。
⚝ 核心变化
从 OpenSSL 1.1.0 开始,以下是关键的改进:
① 内部线程安全:OpenSSL库的内部数据结构和全局状态大部分都已通过库内部的同步机制(例如,互斥锁、原子操作)进行了保护。这意味着对于绝大多数常见的OpenSSL操作,你不再需要设置外部的锁定和线程ID回调函数。
② OPENSSL_init_ssl
函数:初始化OpenSSL库的标准函数 OPENSSL_init_ssl
(或 OPENSSL_init_crypto
)现在会负责处理库的内部线程初始化,包括设置内部的锁定机制。你只需调用这个函数一次即可。
③ 对象生命周期:OpenSSL对象(如 EVP_CIPHER_CTX
, RSA
, X509
, SSL_CTX
, SSL
等)的设计更加注重线程安全。通常,一个线程创建的对象在其生命周期内可以在该线程中安全使用。
④ 废弃旧API:CRYPTO_set_locking_callback
, CRYPTO_set_id_callback
, CRYPTO_num_locks
, CRYPTO_thread_setup
等与旧的外部线程配置相关的API已被标记为废弃(deprecated),并在新版本中不再需要调用。
⚝ 使用 OpenSSL 1.1.0+ 的线程安全指南
尽管OpenSSL 1.1.0+ 在内部实现了大部分线程安全,但作为用户,你仍然需要注意以下几点:
① 初始化:确保在程序的生命周期内,所有线程开始使用OpenSSL之前,调用 OPENSSL_init_ssl()
(或 OPENSSL_init_crypto()
)一次。通常这在主线程的程序启动阶段完成。
② 对象所有权与共享:虽然OpenSSL内部是线程安全的,但如果你手动在多个线程之间共享同一个OpenSSL对象实例(例如,多个线程共用一个 EVP_CIPHER_CTX
来加解密数据流,或者共用一个 SSL
对象进行读写),你需要自己负责对该对象的访问进行同步。最佳实践通常是让每个线程拥有自己的对象实例,或者使用更高级的抽象(如连接池)。
③ SSL_CTX
的共享:在服务器端开发中,通常会创建一个 SSL_CTX
对象,并在接受到新的客户端连接时,基于此 SSL_CTX
创建新的 SSL
对象。SSL_CTX
对象在被创建后,其配置通常是只读的。在 OpenSSL 1.1.0+ 中,从同一个 SSL_CTX
创建 SSL
对象 (SSL_new
) 是线程安全的。但是,如果在多个线程中同时修改同一个 SSL_CTX
的配置(例如,动态加载证书),则需要外部同步。推荐的做法是在程序启动时完成 SSL_CTX
的所有配置。
④ 错误栈:错误栈 (error stack
) 是线程本地的。使用 ERR_get_error
等函数获取的错误信息是当前线程的。这通常是你期望的行为。
⑤ 回调函数:如果你在OpenSSL中注册了任何自定义回调函数(例如,证书验证回调 SSL_CTX_set_verify
),确保你的回调函数本身是线程安全的,如果它访问或修改共享数据。
一个使用 OpenSSL 1.1.0+ 的基本多线程代码框架:
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
#include <thread>
4
#include <vector>
5
#include <iostream>
6
7
// 初始化 OpenSSL (在程序启动时调用一次)
8
void initialize_openssl() {
9
// OPENSSL_INIT_LOAD_SSL_STRINGS: 加载 SSL 错误字符串
10
// OPENSSL_INIT_LOAD_CRYPTO_STRINGS: 加载 Crypto 错误字符串
11
// OPENSSL_INIT_ADD_ALL_CIPHERS: 加载所有密码算法
12
// OPENSSL_INIT_ADD_ALL_DIGESTS: 加载所有哈希算法
13
// OPENSSL_INIT_LOAD_CONFIG: 加载配置文件 (如果存在)
14
// 在 1.1.0+ 中,这些标志通常就足够了,线程初始化会自动处理
15
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS | OPENSSL_INIT_ADD_ALL_CIPHERS | OPENSSL_INIT_ADD_ALL_DIGESTS, nullptr);
16
// ERR_load_BIO_strings(); // BIO 错误字符串,有时需要加载
17
// 其他可能的初始化,取决于你的需求
18
std::cout << "OpenSSL initialized." << std::endl;
19
}
20
21
// 清理 OpenSSL (在程序退出前调用一次)
22
void cleanup_openssl() {
23
// 在 1.1.0+ 中,大多数清理是自动的或不必要的
24
// 但某些资源可能需要显式释放
25
// 例如,SSL_CTX 或 SSL 对象的释放
26
EVP_cleanup(); // 清理 EVP 对象缓存
27
// CRYPTO_cleanup_all_ex_data(); // 清理扩展数据 (如果使用了)
28
// ERR_free_strings(); // 释放错误字符串
29
// OPENSSL_cleanup(); // 这个函数在 1.1.0+ 中主要用于Provider等新特性,不常用作通用清理
30
// 更常见的是确保所有创建的 OpenSSL 对象都被正确释放 (例如,使用 *_free 函数)
31
std::cout << "OpenSSL cleaned up." << std::endl;
32
}
33
34
// 每个线程执行的任务
35
void thread_task(int id) {
36
std::cout << "Thread " << id << " started." << std::endl;
37
38
// 在线程内部可以安全地进行 OpenSSL 操作
39
// 例如,生成随机数
40
unsigned char rand_bytes[16];
41
if (RAND_bytes(rand_bytes, sizeof(rand_bytes)) == 1) {
42
std::cout << "Thread " << id << " generated random bytes." << std::endl;
43
} else {
44
unsigned long err = ERR_get_error();
45
char err_buf[256];
46
ERR_error_string_r(err, err_buf, sizeof(err_buf));
47
std::cerr << "Thread " << id << " RAND_bytes failed: " << err_buf << std::endl;
48
}
49
50
// 例如,创建和使用一个 EVP 上下文
51
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
52
if (ctx) {
53
std::cout << "Thread " << id << " created EVP_CIPHER_CTX." << std::endl;
54
EVP_CIPHER_CTX_free(ctx); // 确保在线程结束前释放对象
55
} else {
56
std::cerr << "Thread " << id << " failed to create EVP_CIPHER_CTX." << std::endl;
57
}
58
59
// 注意:如果在多个线程之间共享同一个 ctx,则需要额外的锁
60
// 更好的做法是每个线程创建自己的 ctx
61
62
std::cout << "Thread " << id << " finished." << std::endl;
63
}
64
65
int main() {
66
// 1. 在所有线程启动前初始化 OpenSSL
67
initialize_openssl();
68
69
// 2. 创建并启动多个线程
70
const int num_threads = 5;
71
std::vector<std::thread> threads;
72
for (int i = 0; i < num_threads; ++i) {
73
threads.emplace_back(thread_task, i);
74
}
75
76
// 3. 等待所有线程完成
77
for (std::thread& t : threads) {
78
if (t.joinable()) {
79
t.join();
80
}
81
}
82
83
// 4. 在所有线程结束后清理 OpenSSL
84
cleanup_openssl();
85
86
return 0;
87
}
这个示例展示了在OpenSSL 1.1.0+中,多线程使用 OpenSSL 对象(如 RAND_bytes
, EVP_CIPHER_CTX_new
)通常是直观且线程安全的,只要对象不被多个线程同时修改。相比于旧版本繁琐的回调函数设置,极大地简化了多线程编程的复杂性。
⚝ 总结
在OpenSSL 1.1.0及后续版本中,库本身在内部处理了绝大多数线程安全问题,极大地减轻了开发者的负担。你不再需要手动设置全局锁定和线程ID回调函数。只需要在程序启动时调用一次 OPENSSL_init_ssl
进行全局初始化,并在使用OpenSSL对象时遵循“每个线程拥有自己的对象实例”或对共享对象进行外部同步的原则即可。对于仍然需要维护或使用旧版本OpenSSL的项目,则必须严格按照第11.2节介绍的方法配置线程回调函数。理解这些历史演进对于正确地在多线程C++应用程序中使用OpenSSL至关重要。
12. 实战案例分析
本章将理论知识与实际应用相结合,通过构建具体的软件工具和系统组件,演示如何在真实的C++项目中使用OpenSSL库解决信息安全问题。这些案例涵盖了文件加密、网络通信安全(HTTPS)以及安全API交互等多种场景,旨在帮助读者巩固前面章节学习的知识,提升将OpenSSL功能集成到复杂应用程序中的实践能力。无论您是初学者、中级开发者还是希望深入理解实际应用的专家,本章都能提供有价值的洞察和可操作的代码思路。
12.1 安全文件传输工具
在本节中,我们将设计并实现一个简易的安全文件传输工具。这个工具的核心功能是使用对称加密算法加密文件,并使用数字签名确保文件的完整性和来源的可信性。接收方可以使用相应的密钥和公钥来解密文件并验证签名。这个案例将综合运用对称加密、哈希、数字签名以及密钥管理等知识点。
功能的流程如下:
① 发送方:
▮▮▮▮ⓑ 生成一对非对称密钥(用于签名)。
▮▮▮▮ⓒ 生成一个随机对称密钥(用于加密文件)。
▮▮▮▮ⓓ 使用对称密钥和选定的对称加密算法(例如:AES-256-CBC)加密待传输的文件。
▮▮▮▮ⓔ 计算加密后文件的哈希值(例如:SHA-256)。
▮▮▮▮ⓕ 使用发送方的私钥对哈希值进行数字签名。
▮▮▮▮ⓖ 将加密文件、对称密钥、数字签名以及发送方的公钥(或其证书)打包传输给接收方。注意:对称密钥本身也需要通过某种安全方式传输,例如使用接收方的公钥进行非对称加密后再传输,或者通过预先共享的秘密。在本例中,为简化演示,我们假设对称密钥可以与加密文件一起传输,但实际应用中这是不安全的,对称密钥必须通过安全的通道(如使用接收方公钥加密,或通过TLS通道协商)传输。
⑧ 接收方:
▮▮▮▮ⓘ 接收加密文件、对称密钥、数字签名和发送方的公钥。
▮▮▮▮ⓙ 使用发送方的公钥验证数字签名是否有效。如果验证失败,文件可能被篡改或不是由声称的发送方发送,应拒绝接收。
▮▮▮▮ⓚ 如果签名验证成功,使用接收到的对称密钥和相同的对称加密算法解密文件。
下面是实现这些功能的OpenSSL API(应用程序接口)调用思路:
① 发送方 - 生成密钥
▮▮▮▮⚝ 生成RSA或ECC密钥对(EVP_PKEY_generate()
或 RSA_generate_key_ex()
, EC_KEY_generate_key()
)。
▮▮▮▮⚝ 生成随机对称密钥(RAND_bytes()
)。
② 发送方 - 文件加密
▮▮▮▮⚝ 选择一个对称加密算法(EVP_aes_256_cbc()
)。
▮▮▮▮⚝ 创建并初始化加密上下文(EVP_CIPHER_CTX_new()
, EVP_CipherInit_ex()
, EVP_CIPHER_CTX_set_key_and_iv()
)。
▮▮▮▮⚝ 分块读取文件,对每个数据块进行加密(EVP_CipherUpdate()
)。
▮▮▮▮⚝ 处理最后一个数据块并获取最终的填充数据(EVP_CipherFinal_ex()
)。
▮▮▮▮⚝ 释放加密上下文(EVP_CIPHER_CTX_free()
)。
③ 发送方 - 哈希与签名
▮▮▮▮⚝ 选择一个哈希算法(EVP_sha256()
)。
▮▮▮▮⚝ 创建并初始化摘要上下文(EVP_MD_CTX_new()
, EVP_DigestInit_ex()
)。
▮▮▮▮⚝ 分块读取加密后的文件内容,计算哈希(EVP_DigestUpdate()
)。
▮▮▮▮⚝ 获取最终的哈希值(EVP_DigestFinal_ex()
)。
▮▮▮▮⚝ 释放摘要上下文(EVP_MD_CTX_free()
)。
▮▮▮▮⚝ 创建并初始化签名上下文(EVP_MD_CTX_new()
, EVP_SignInit_ex()
)。
▮▮▮▮⚝ 将计算出的哈希值提供给签名上下文(EVP_SignUpdate()
)。
▮▮▮▮⚝ 使用发送方的私钥进行签名(EVP_SignFinal()
)。
▮▮▮▮⚝ 释放签名上下文(EVP_MD_CTX_free()
)。
④ 接收方 - 签名验证
▮▮▮▮⚝ 加载发送方的公钥(例如从文件读取,PEM_read_bio_PUBKEY()
或 d2i_PUBKEY_bio()
)。
▮▮▮▮⚝ 计算接收到的加密文件的哈希值(与发送方使用相同算法和步骤,EVP_DigestInit_ex()
, EVP_DigestUpdate()
, EVP_DigestFinal_ex()
)。
▮▮▮▮⚝ 创建并初始化验证上下文(EVP_MD_CTX_new()
, EVP_VerifyInit_ex()
)。
▮▮▮▮⚝ 将计算出的哈希值提供给验证上下文(EVP_VerifyUpdate()
)。
▮▮▮▮⚝ 使用发送方的公钥和接收到的数字签名进行验证(EVP_VerifyFinal()
)。该函数返回1表示成功,0表示失败,-1表示错误。
▮▮▮▮⚝ 释放验证上下文(EVP_MD_CTX_free()
)。
⑤ 接收方 - 文件解密
▮▮▮▮⚝ 使用接收到的对称密钥和初始化向量(IV)选择相同的对称加密算法(EVP_aes_256_cbc()
)。注意:IV通常与加密数据一起传输,它不需要保密,但需要保证其完整性和唯一性。
▮▮▮▮⚝ 创建并初始化解密上下文(EVP_CIPHER_CTX_new()
, EVP_CipherInit_ex()
)。注意:EVP_CipherInit_ex()
在解密模式下设置密钥和IV。
▮▮▮▮⚝ 分块读取加密文件,对每个数据块进行解密(EVP_CipherUpdate()
)。
▮▮▮▮⚝ 处理最后一个数据块并移除填充(EVP_CipherFinal_ex()
)。
▮▮▮▮⚝ 释放解密上下文(EVP_CIPHER_CTX_free()
)。
此案例实现了一个基础的安全文件传输机制,但实际应用还需要考虑密钥的安全分发、文件元数据(如文件名、大小)的保护、错误处理的健壮性以及网络传输本身的可靠性和安全性(例如,可以在TLS通道上传输这些数据)。
1
#include <openssl/evp.h>
2
#include <openssl/rsa.h>
3
#include <openssl/pem.h>
4
#include <openssl/rand.h>
5
#include <openssl/err.h>
6
#include <iostream>
7
#include <fstream>
8
#include <vector>
9
#include <string>
10
11
// 简单的错误处理宏
12
#define HANDLE_OPENSSL_ERROR() do { ERR_print_errors_fp(stderr); exit(1); } while (0)
13
14
// 发送方函数声明
15
bool generate_rsa_keypair(EVP_PKEY** pkey);
16
bool generate_aes_key_iv(unsigned char* key, unsigned char* iv, int key_len, int iv_len);
17
bool encrypt_file(const std::string& input_filepath, const std::string& output_filepath, const unsigned char* key, const unsigned char* iv);
18
bool sign_file(const std::string& filepath, EVP_PKEY* pkey, std::vector<unsigned char>& signature);
19
20
// 接收方函数声明
21
bool verify_signature(const std::string& filepath, const std::vector<unsigned char>& signature, EVP_PKEY* pubkey);
22
bool decrypt_file(const std::string& input_filepath, const std::string& output_filepath, const unsigned char* key, const unsigned char* iv);
23
24
// Helper函数:将EVP_PKEY保存到文件 (PEM格式)
25
bool save_pkey_to_file(EVP_PKEY* pkey, const std::string& filepath, bool is_public) {
26
FILE* fp = fopen(filepath.c_str(), "wb");
27
if (!fp) {
28
std::cerr << "Error opening file for writing key: " << filepath << std::endl;
29
return false;
30
}
31
bool success = false;
32
if (is_public) {
33
if (PEM_write_PUBKEY(fp, pkey) == 1) {
34
success = true;
35
} else {
36
HANDLE_OPENSSL_ERROR();
37
}
38
} else {
39
// 可以添加密码保护,这里简化不加
40
if (PEM_write_PrivateKey(fp, pkey, nullptr, nullptr, 0, nullptr, nullptr) == 1) {
41
success = true;
42
} else {
43
HANDLE_OPENSSL_ERROR();
44
}
45
}
46
fclose(fp);
47
return success;
48
}
49
50
// Helper函数:从文件加载公钥 (PEM格式)
51
EVP_PKEY* load_pubkey_from_file(const std::string& filepath) {
52
FILE* fp = fopen(filepath.c_str(), "rb");
53
if (!fp) {
54
std::cerr << "Error opening file for reading public key: " << filepath << std::endl;
55
return nullptr;
56
}
57
EVP_PKEY* pubkey = PEM_read_PUBKEY(fp, nullptr, nullptr, nullptr);
58
fclose(fp);
59
if (!pubkey) {
60
HANDLE_OPENSSL_ERROR();
61
}
62
return pubkey;
63
}
64
65
// Helper函数:从文件加载私钥 (PEM格式)
66
EVP_PKEY* load_privkey_from_file(const std::string& filepath) {
67
FILE* fp = fopen(filepath.c_str(), "rb");
68
if (!fp) {
69
std::cerr << "Error opening file for reading private key: " << filepath << std::endl;
70
return nullptr;
71
}
72
EVP_PKEY* privkey = PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr);
73
fclose(fp);
74
if (!privkey) {
75
HANDLE_OPENSSL_ERROR();
76
}
77
return privkey;
78
}
79
80
81
// 主函数框架(演示发送方流程)
82
int main_sender() {
83
std::cout << "--- 安全文件传输工具 (发送方) ---" << std::endl;
84
85
// 1. OpenSSL库初始化
86
// 在OpenSSL 1.1.0+版本,初始化已简化,但手动初始化和清理是良好实践
87
// OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS | OPENSSL_INIT_ADD_ALL_CIPHERS | OPENSSL_INIT_ADD_ALL_DIGESTS, nullptr);
88
ERR_load_RAND_strings();
89
ERR_load_EVP_strings();
90
OpenSSL_add_all_algorithms();
91
ERR_load_crypto_strings();
92
93
EVP_PKEY* rsa_keypair = nullptr;
94
unsigned char aes_key[AES_KEY_LEN] = {0}; // 假定AES_KEY_LEN为32 (256 bits)
95
unsigned char aes_iv[AES_BLOCK_SIZE] = {0}; // 假定AES_BLOCK_SIZE为16 (128 bits)
96
97
// 2. 生成密钥对和对称密钥/IV
98
std::cout << "生成 RSA 密钥对..." << std::endl;
99
if (!generate_rsa_keypair(&rsa_keypair)) return 1;
100
std::cout << "生成 AES 对称密钥和 IV..." << std::endl;
101
if (!generate_aes_key_iv(aes_key, aes_iv, AES_KEY_LEN, AES_BLOCK_SIZE)) {
102
EVP_PKEY_free(rsa_keypair);
103
return 1;
104
}
105
106
// 保存公钥和私钥(用于演示,实际中私钥应妥善保管)
107
std::cout << "保存 RSA 密钥对到文件..." << std::endl;
108
if (!save_pkey_to_file(rsa_keypair, "sender_private.pem", false)) {
109
EVP_PKEY_free(rsa_keypair); return 1;
110
}
111
if (!save_pkey_to_file(rsa_keypair, "sender_public.pem", true)) {
112
EVP_PKEY_free(rsa_keypair); return 1;
113
}
114
115
116
// 3. 加密文件
117
std::string input_file = "plaintext.txt"; // 待加密文件
118
std::string encrypted_file = "encrypted.bin"; // 加密输出文件
119
std::cout << "加密文件: " << input_file << " -> " << encrypted_file << " 使用 AES-256-CBC..." << std::endl;
120
if (!encrypt_file(input_file, encrypted_file, aes_key, aes_iv)) {
121
EVP_PKEY_free(rsa_keypair); return 1;
122
}
123
124
// 4. 对加密文件进行签名
125
std::vector<unsigned char> signature;
126
std::cout << "对加密文件计算 SHA-256 哈希并进行 RSA 签名..." << std::endl;
127
if (!sign_file(encrypted_file, rsa_keypair, signature)) {
128
EVP_PKEY_free(rsa_keypair); return 1;
129
}
130
std::cout << "签名生成成功,大小为: " << signature.size() << " 字节" << std::endl;
131
132
// 在实际应用中,你需要将 encrypted_file, aes_key, aes_iv, signature, sender_public.pem 传输给接收方。
133
// 这里简化,将 signature 保存到文件
134
std::ofstream sig_file("signature.bin", std::ios::binary);
135
if (sig_file) {
136
sig_file.write((char*)signature.data(), signature.size());
137
sig_file.close();
138
std::cout << "签名保存到文件: signature.bin" << std::endl;
139
} else {
140
std::cerr << "Error saving signature file." << std::endl;
141
EVP_PKEY_free(rsa_keypair); return 1;
142
}
143
144
// 5. 清理资源
145
EVP_PKEY_free(rsa_keypair);
146
// 其他资源如文件流、向量会在作用域结束时自动释放或需要手动管理
147
148
// OPENSSL_cleanup(); // OpenSSL 1.1.0+ 版本可能不需要显式调用
149
150
std::cout << "发送方流程完成。" << std::endl;
151
return 0;
152
}
153
154
// 主函数框架(演示接收方流程)
155
int main_receiver() {
156
std::cout << "--- 安全文件传输工具 (接收方) ---" << std::endl;
157
158
// 1. OpenSSL库初始化
159
// OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS | OPENSSL_INIT_ADD_ALL_CIPHERS | OPENSSL_INIT_ADD_ALL_DIGESTS, nullptr);
160
ERR_load_RAND_strings(); // 可能不需要
161
ERR_load_EVP_strings();
162
OpenSSL_add_all_algorithms();
163
ERR_load_crypto_strings();
164
165
// 假定接收方已通过安全渠道接收到或拥有这些文件/数据
166
std::string encrypted_file = "encrypted.bin";
167
std::string signature_file = "signature.bin";
168
std::string sender_pubkey_file = "sender_public.pem";
169
unsigned char aes_key[AES_KEY_LEN] = { /* 从发送方安全接收 */ }; // 假设此处已填充正确的密钥
170
unsigned char aes_iv[AES_BLOCK_SIZE] = { /* 从发送方接收 */ }; // 假设此处已填充正确的IV
171
172
// 模拟从文件读取签名
173
std::vector<unsigned char> signature;
174
std::ifstream sig_file(signature_file, std::ios::binary | std::ios::ate);
175
if (sig_file) {
176
std::streamsize size = sig_file.tellg();
177
signature.resize(size);
178
sig_file.seekg(0, std::ios::beg);
179
sig_file.read((char*)signature.data(), size);
180
sig_file.close();
181
std::cout << "从文件加载签名: " << signature_file << std::endl;
182
} else {
183
std::cerr << "Error loading signature file: " << signature_file << std::endl;
184
return 1;
185
}
186
187
188
// 2. 加载发送方公钥
189
EVP_PKEY* sender_pubkey = load_pubkey_from_file(sender_pubkey_file);
190
if (!sender_pubkey) return 1;
191
std::cout << "加载发送方公钥成功: " << sender_pubkey_file << std::endl;
192
193
// 3. 验证签名
194
std::cout << "验证文件签名..." << std::endl;
195
if (verify_signature(encrypted_file, signature, sender_pubkey)) {
196
std::cout << "签名验证成功!文件完整且来源可信。" << std::endl;
197
} else {
198
std::cerr << "签名验证失败!文件可能被篡改或来源不可信。" << std::endl;
199
EVP_PKEY_free(sender_pubkey);
200
return 1;
201
}
202
203
// 4. 解密文件
204
std::string decrypted_file = "decrypted.txt"; // 解密输出文件
205
std::cout << "解密文件: " << encrypted_file << " -> " << decrypted_file << " 使用 AES-256-CBC..." << std::endl;
206
if (!decrypt_file(encrypted_file, decrypted_file, aes_key, aes_iv)) {
207
EVP_PKEY_free(sender_pubkey);
208
return 1;
209
}
210
std::cout << "文件解密成功: " << decrypted_file << std::endl;
211
212
213
// 5. 清理资源
214
EVP_PKEY_free(sender_pubkey);
215
// 其他资源如文件流、向量会在作用域结束时自动释放或需要手动管理
216
217
// OPENSSL_cleanup(); // OpenSSL 1.1.0+ 版本可能不需要显式调用
218
219
std::cout << "接收方流程完成。" << std::endl;
220
return 0;
221
}
222
223
224
// 以下是实现细节函数
225
226
bool generate_rsa_keypair(EVP_PKEY** pkey) {
227
EVP_PKEY_CTX* ctx = nullptr;
228
bool success = false;
229
230
/* 创建密钥生成上下文 */
231
ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
232
if (!ctx) HANDLE_OPENSSL_ERROR();
233
234
/* 初始化生成过程 */
235
if (EVP_PKEY_keygen_init(ctx) <= 0) HANDLE_OPENSSL_ERROR();
236
237
/* 设置密钥长度 */
238
if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048) <= 0) HANDLE_OPENSSL_ERROR(); // 通常使用2048或4096位
239
240
/* 生成密钥对 */
241
if (EVP_PKEY_keygen(ctx, pkey) <= 0) HANDLE_OPENSSL_ERROR();
242
243
success = true;
244
245
cleanup:
246
EVP_PKEY_CTX_free(ctx);
247
return success;
248
}
249
250
bool generate_aes_key_iv(unsigned char* key, unsigned char* iv, int key_len, int iv_len) {
251
if (RAND_bytes(key, key_len) <= 0) {
252
HANDLE_OPENSSL_ERROR();
253
return false;
254
}
255
if (RAND_bytes(iv, iv_len) <= 0) {
256
HANDLE_OPENSSL_ERROR();
257
return false;
258
}
259
return true;
260
}
261
262
bool encrypt_file(const std::string& input_filepath, const std::string& output_filepath, const unsigned char* key, const unsigned char* iv) {
263
std::ifstream infile(input_filepath, std::ios::binary);
264
std::ofstream outfile(output_filepath, std::ios::binary);
265
if (!infile || !outfile) {
266
std::cerr << "Error opening files for encryption." << std::endl;
267
return false;
268
}
269
270
EVP_CIPHER_CTX* ctx = nullptr;
271
int len;
272
int ciphertext_len;
273
unsigned char inbuf[4096];
274
unsigned char outbuf[4096 + AES_BLOCK_SIZE]; // Output buffer might be larger due to padding
275
bool success = false;
276
277
/* 创建并初始化加密上下文 */
278
ctx = EVP_CIPHER_CTX_new();
279
if (!ctx) HANDLE_OPENSSL_ERROR();
280
281
/* 初始化加密操作. EVP_aes_256_cbc() 函数选择算法 */
282
if (EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, key, iv) <= 0) HANDLE_OPENSSL_ERROR();
283
284
/* 加密文件内容 */
285
while (infile.read((char*)inbuf, sizeof(inbuf))) {
286
len = infile.gcount();
287
if (EVP_EncryptUpdate(ctx, outbuf, &ciphertext_len, inbuf, len) <= 0) HANDLE_OPENSSL_ERROR();
288
outfile.write((char*)outbuf, ciphertext_len);
289
}
290
// 处理剩余的可能不足inbuf大小的数据
291
len = infile.gcount();
292
if (len > 0) {
293
if (EVP_EncryptUpdate(ctx, outbuf, &ciphertext_len, inbuf, len) <= 0) HANDLE_OPENSSL_ERROR();
294
outfile.write((char*)outbuf, ciphertext_len);
295
}
296
297
298
/* 完成加密操作并获取最终的填充数据 */
299
if (EVP_EncryptFinal_ex(ctx, outbuf, &ciphertext_len) <= 0) HANDLE_OPENSSL_ERROR();
300
outfile.write((char*)outbuf, ciphertext_len);
301
302
success = true;
303
304
cleanup:
305
EVP_CIPHER_CTX_free(ctx);
306
infile.close();
307
outfile.close();
308
return success;
309
}
310
311
312
bool decrypt_file(const std::string& input_filepath, const std::string& output_filepath, const unsigned char* key, const unsigned char* iv) {
313
std::ifstream infile(input_filepath, std::ios::binary);
314
std::ofstream outfile(output_filepath, std::ios::binary);
315
if (!infile || !outfile) {
316
std::cerr << "Error opening files for decryption." << std::endl;
317
return false;
318
}
319
320
EVP_CIPHER_CTX* ctx = nullptr;
321
int len;
322
int plaintext_len;
323
unsigned char inbuf[4096 + AES_BLOCK_SIZE]; // Input buffer size should be large enough for block cipher output
324
unsigned char outbuf[4096];
325
bool success = false;
326
327
/* 创建并初始化解密上下文 */
328
ctx = EVP_CIPHER_CTX_new();
329
if (!ctx) HANDLE_OPENSSL_ERROR();
330
331
/* 初始化解密操作 */
332
if (EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, key, iv) <= 0) HANDLE_OPENSSL_ERROR();
333
334
/* 解密文件内容 */
335
while (infile.read((char*)inbuf, sizeof(inbuf))) {
336
len = infile.gcount();
337
if (EVP_DecryptUpdate(ctx, outbuf, &plaintext_len, inbuf, len) <= 0) HANDLE_OPENSSL_ERROR();
338
outfile.write((char*)outbuf, plaintext_len);
339
}
340
// 处理剩余的可能不足inbuf大小的数据
341
len = infile.gcount();
342
if (len > 0) {
343
if (EVP_DecryptUpdate(ctx, outbuf, &plaintext_len, inbuf, len) <= 0) HANDLE_OPENSSL_ERROR();
344
outfile.write((char*)outbuf, plaintext_len);
345
}
346
347
348
/* 完成解密操作并处理填充 */
349
if (EVP_DecryptFinal_ex(ctx, outbuf, &plaintext_len) <= 0) {
350
// 解密失败,可能是密钥、IV不匹配,或文件被篡改
351
std::cerr << "Decryption finalization failed. Check key, IV, or file integrity." << std::endl;
352
HANDLE_OPENSSL_ERROR(); // 打印OpenSSL错误栈
353
goto cleanup; // 跳到清理
354
}
355
outfile.write((char*)outbuf, plaintext_len);
356
357
success = true;
358
359
cleanup:
360
EVP_CIPHER_CTX_free(ctx);
361
infile.close();
362
outfile.close();
363
// 如果解密失败,可能需要删除部分解密的文件以避免使用损坏数据
364
if (!success) {
365
remove(output_filepath.c_str());
366
}
367
return success;
368
}
369
370
371
bool sign_file(const std::string& filepath, EVP_PKEY* pkey, std::vector<unsigned char>& signature) {
372
std::ifstream infile(filepath, std::ios::binary);
373
if (!infile) {
374
std::cerr << "Error opening file for signing: " << filepath << std::endl;
375
return false;
376
}
377
378
EVP_MD_CTX* mdctx = nullptr;
379
unsigned char msg[4096];
380
unsigned int sig_len;
381
bool success = false;
382
383
/* 创建并初始化签名上下文 */
384
mdctx = EVP_MD_CTX_new();
385
if (!mdctx) HANDLE_OPENSSL_ERROR();
386
387
/* 初始化签名操作,使用SHA256作为摘要算法 */
388
if (EVP_SignInit_ex(mdctx, EVP_sha256(), nullptr) <= 0) HANDLE_OPENSSL_ERROR();
389
390
/* 对文件内容进行哈希计算 */
391
while (infile.read((char*)msg, sizeof(msg))) {
392
int len = infile.gcount();
393
if (EVP_SignUpdate(mdctx, msg, len) <= 0) HANDLE_OPENSSL_ERROR();
394
}
395
int len = infile.gcount();
396
if (len > 0) {
397
if (EVP_SignUpdate(mdctx, msg, len) <= 0) HANDLE_OPENSSL_ERROR();
398
}
399
400
401
/* 分配签名缓冲区 */
402
signature.resize(EVP_PKEY_size(pkey));
403
404
/* 使用私钥生成签名 */
405
if (EVP_SignFinal(mdctx, signature.data(), &sig_len, pkey) <= 0) HANDLE_OPENSSL_ERROR();
406
407
signature.resize(sig_len); // 调整向量大小到实际签名长度
408
success = true;
409
410
cleanup:
411
EVP_MD_CTX_free(mdctx);
412
infile.close();
413
return success;
414
}
415
416
bool verify_signature(const std::string& filepath, const std::vector<unsigned char>& signature, EVP_PKEY* pubkey) {
417
std::ifstream infile(filepath, std::ios::binary);
418
if (!infile) {
419
std::cerr << "Error opening file for signature verification: " << filepath << std::endl;
420
return false;
421
}
422
423
EVP_MD_CTX* mdctx = nullptr;
424
unsigned char msg[4096];
425
int verify_result = -1;
426
bool success = false;
427
428
/* 创建并初始化验证上下文 */
429
mdctx = EVP_MD_CTX_new();
430
if (!mdctx) HANDLE_OPENSSL_ERROR();
431
432
/* 初始化验证操作,使用与签名时相同的摘要算法 */
433
if (EVP_VerifyInit_ex(mdctx, EVP_sha256(), nullptr) <= 0) HANDLE_OPENSSL_ERROR();
434
435
/* 对文件内容进行哈希计算 (与签名时步骤相同) */
436
while (infile.read((char*)msg, sizeof(msg))) {
437
int len = infile.gcount();
438
if (EVP_VerifyUpdate(mdctx, msg, len) <= 0) HANDLE_OPENSSL_ERROR();
439
}
440
int len = infile.gcount();
441
if (len > 0) {
442
if (EVP_VerifyUpdate(mdctx, msg, len) <= 0) HANDLE_OPENSSL_ERROR();
443
}
444
445
446
/* 使用公钥和接收到的签名进行验证 */
447
verify_result = EVP_VerifyFinal(mdctx, signature.data(), signature.size(), pubkey);
448
449
/* 检查验证结果 */
450
if (verify_result == 1) {
451
success = true; // 验证成功
452
} else if (verify_result == 0) {
453
// 验证失败,签名不匹配或文件被修改
454
success = false;
455
} else {
456
// 发生错误
457
HANDLE_OPENSSL_ERROR();
458
success = false; // 标记失败
459
}
460
461
cleanup:
462
EVP_MD_CTX_free(mdctx);
463
infile.close();
464
return success;
465
}
466
467
// 为了运行主函数,你需要一个调用 main_sender() 和 main_receiver() 的入口点
468
// 并提供实际的文件路径和模拟的密钥/IV传输逻辑。
469
// 这里的 main 函数仅作为示例框架,实际使用时需要完善。
470
/*
471
int main() {
472
// 模拟发送方
473
// main_sender();
474
475
// 模拟接收方 (需要在运行发送方后生成必要的文件)
476
// 注意:aes_key 和 aes_iv 需要通过某种安全方式从发送方传递给接收方
477
// unsigned char received_aes_key[AES_KEY_LEN] = { ... };
478
// unsigned char received_aes_iv[AES_BLOCK_SIZE] = { ... };
479
// 在 main_receiver 中填充 received_aes_key 和 received_aes_iv
480
// main_receiver();
481
482
return 0;
483
}
484
*/
代码说明:
⚝ 上述代码提供了一个基本的框架,演示了如何使用OpenSSL的EVP
接口进行对称加密、哈希和数字签名。
⚝ generate_rsa_keypair
函数生成一个RSA密钥对。
⚝ generate_aes_key_iv
函数使用OpenSSL的随机数生成器 (RAND_bytes
) 生成对称密钥和初始化向量(IV)。
⚝ encrypt_file
函数读取输入文件,使用EVP_EncryptInit_ex
、EVP_EncryptUpdate
和EVP_EncryptFinal_ex
进行加密,并将结果写入输出文件。
⚝ sign_file
函数读取文件内容,使用EVP_SignInit_ex
、EVP_SignUpdate
和EVP_SignFinal
计算文件的哈希值并用私钥进行签名。
⚝ verify_signature
函数读取文件内容计算哈希,并使用EVP_VerifyInit_ex
、EVP_VerifyUpdate
和EVP_VerifyFinal
使用公钥验证签名。
⚝ decrypt_file
函数读取加密文件,使用EVP_DecryptInit_ex
、EVP_DecryptUpdate
和EVP_DecryptFinal_ex
进行解密,并将结果写入输出文件。
⚝ save_pkey_to_file
和 load_pubkey_from_file
/ load_privkey_from_file
是方便演示密钥保存和加载的辅助函数。
⚝ 错误处理使用了简单的宏HANDLE_OPENSSL_ERROR
,实际应用中应使用更健壮的机制。
⚝ 请注意,为了运行完整的发送和接收流程,你需要一个主函数来协调这些调用,并实现密钥、IV和签名的安全传输机制(例如,发送方可以用接收方的公钥加密AES密钥和IV,然后将它们与加密文件、签名和自己的公钥一起发送)。
这个案例是一个很好的起点,读者可以在此基础上进行扩展,例如增加对文件元数据(如原始文件名、大小)的保护、支持多种算法选择、增加密钥安全交换机制、实现用户认证等。
12.2 构建简易HTTPS服务器
HTTPS (Hypertext Transfer Protocol Secure) 是在HTTP协议基础上,通过TLS/SSL协议提供加密、身份认证和数据完整性保护的安全协议。构建一个HTTPS服务器需要将HTTP服务器功能与OpenSSL的TLS/SSL功能结合起来。本节将重点演示如何使用OpenSSL库为TCP服务器添加TLS/SSL支持,使其成为一个简易的HTTPS服务器。
核心步骤如下:
① 服务器初始化:
▮▮▮▮⚝ 初始化OpenSSL库。
▮▮▮▮⚝ 创建SSL上下文对象(SSL_CTX
)。这个对象包含了配置TLS/SSL连接所需的所有信息,如协议版本、密码套件、证书、私钥、验证模式等。
▮▮▮▮⚝ 配置SSL_CTX
,例如加载服务器的证书和私钥(SSL_CTX_use_certificate_file()
, SSL_CTX_use_PrivateKey_file()
),设置可接受的协议版本和密码套件。
② 接受连接:
▮▮▮▮⚝ 创建监听套接字(socket)并绑定到指定端口(通常是443)。
▮▮▮▮⚝ 监听连接并接受新的客户端连接请求,获取客户端套接字。
③ 建立TLS/SSL连接:
▮▮▮▮⚝ 为每个客户端套接字创建一个SSL对象(SSL_new()
),并将SSL对象与客户端套接字关联起来(通常通过BIO
,如SSL_set_fd()
或 SSL_set_bio()
)。
▮▮▮▮⚝ 在新的SSL对象上执行TLS/SSL握手(SSL_accept()
)。这是TLS/SSL协议中最关键的步骤,服务器和客户端在此阶段协商加密算法、交换密钥,并进行身份验证。
④ 安全数据传输:
▮▮▮▮⚝ 握手成功后,所有通过该SSL对象发送和接收的数据都会被OpenSSL库自动加密和解密。使用SSL_read()
和SSL_write()
函数代替标准的socket读写函数。
⑤ 关闭连接:
▮▮▮▮⚝ 安全地关闭TLS/SSL连接(SSL_shutdown()
)。
▮▮▮▮⚝ 关闭底层套接字。
⑥ 清理:
▮▮▮▮⚝ 释放SSL对象(SSL_free()
)。
▮▮▮▮⚝ 释放SSL上下文对象(SSL_CTX_free()
)。
▮▮▮▮⚝ 清理OpenSSL库资源。
下面是一个简化的HTTPS服务器框架,着重展示OpenSSL相关的部分:
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
#include <iostream>
4
#include <string>
5
#include <vector>
6
#include <sys/socket.h> // Standard socket headers
7
#include <netinet/in.h>
8
#include <unistd.h>
9
10
// 简单的错误处理宏
11
#define HANDLE_OPENSSL_ERROR() do { ERR_print_errors_fp(stderr); exit(1); } while (0)
12
13
// 证书和私钥文件路径 (需要您自己生成这些文件)
14
const char* server_cert_file = "server.crt"; // 服务器证书文件 (PEM 格式)
15
const char* server_key_file = "server.key"; // 服务器私钥文件 (PEM 格式)
16
const int listen_port = 4433; // HTTPS 监听端口
17
18
int main_https_server() {
19
std::cout << "--- 简易 HTTPS 服务器 ---" << std::endl;
20
21
SSL_CTX* ctx = nullptr;
22
int listen_fd = -1;
23
24
// 1. OpenSSL库初始化
25
// 在OpenSSL 1.1.0+版本,初始化已简化
26
// SSL_library_init(); // Old way, deprecated
27
// OpenSSL_add_all_algorithms(); // Old way, deprecated
28
// SSL_load_error_strings(); // Old way, deprecated
29
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_ADD_ALL_CIPHERS | OPENSSL_INIT_ADD_ALL_DIGESTS, nullptr);
30
ERR_load_BIO_strings();
31
ERR_load_crypto_strings();
32
ERR_load_SSL_strings();
33
34
// 2. 创建 SSL Context
35
// 选择合适的 TLS 版本方法, 例如 TLS_server_method() 支持当前推荐的最高版本
36
ctx = SSL_CTX_new(TLS_server_method());
37
if (!ctx) HANDLE_OPENSSL_ERROR();
38
39
// 可选: 配置 SSL Context 的选项
40
// SSL_OP_NO_SSLv2, SSL_OP_NO_SSLv3, SSL_OP_NO_TLSv1, SSL_OP_NO_TLSv1_1 用于禁用旧版本协议
41
SSL_CTX_set_options(ctx, SSL_OP_SINGLE_ECDH_USE | SSL_OP_SINGLE_DH_USE | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
42
43
// 3. 加载服务器证书和私钥
44
if (SSL_CTX_use_certificate_file(ctx, server_cert_file, SSL_FILETYPE_PEM) <= 0) {
45
std::cerr << "Error loading server certificate file." << std::endl;
46
HANDLE_OPENSSL_ERROR();
47
}
48
if (SSL_CTX_use_PrivateKey_file(ctx, server_key_file, SSL_FILETYPE_PEM) <= 0) {
49
std::cerr << "Error loading server private key file." << std::endl;
50
HANDLE_OPENSSL_ERROR();
51
}
52
53
// 检查私钥是否与证书匹配
54
if (!SSL_CTX_check_private_key(ctx)) {
55
std::cerr << "Private key does not match the certificate public key." << std::endl;
56
SSL_CTX_free(ctx);
57
return 1;
58
}
59
std::cout << "服务器证书和私钥加载成功。" << std::endl;
60
61
62
// 4. 创建并绑定监听套接字
63
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
64
if (listen_fd < 0) {
65
std::cerr << "Error creating socket." << std::endl;
66
SSL_CTX_free(ctx);
67
return 1;
68
}
69
70
sockaddr_in server_addr;
71
server_addr.sin_family = AF_INET;
72
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
73
server_addr.sin_port = htons(listen_port);
74
75
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
76
std::cerr << "Error binding socket to port " << listen_port << std::endl;
77
close(listen_fd);
78
SSL_CTX_free(ctx);
79
return 1;
80
}
81
82
// 5. 开始监听连接
83
if (listen(listen_fd, 5) < 0) { // 5 是待处理连接队列的最大长度
84
std::cerr << "Error listening on socket." << std::endl;
85
close(listen_fd);
86
SSL_CTX_free(ctx);
87
return 1;
88
}
89
std::cout << "HTTPS 服务器正在监听端口: " << listen_port << "..." << std::endl;
90
91
// 6. 接受客户端连接并处理 (简化的单线程模型)
92
while (true) {
93
sockaddr_in client_addr;
94
socklen_t client_len = sizeof(client_addr);
95
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
96
if (client_fd < 0) {
97
std::cerr << "Error accepting client connection." << std::endl;
98
continue; // 继续接受下一个连接
99
}
100
std::cout << "接受到一个新的客户端连接。" << std::endl;
101
102
SSL* ssl = nullptr;
103
BIO* bio = nullptr; // 使用 BIO 封装 socket
104
105
// 7. 为客户端连接创建 SSL 对象并关联 socket
106
ssl = SSL_new(ctx);
107
if (!ssl) {
108
std::cerr << "Error creating SSL object." << std::endl;
109
HANDLE_OPENSSL_ERROR();
110
close(client_fd);
111
continue;
112
}
113
// 将 socket fd 绑定到 SSL 对象,OpenSSL 将接管 socket 的 I/O
114
// SSL_set_fd(ssl, client_fd); // 另一种直接关联 fd 的方式
115
116
// 推荐使用 BIO 封装 socket
117
bio = BIO_new_socket(client_fd, BIO_NOCLOSE); // BIO_NOCLOSE 表示 BIO 不会负责关闭底层的 fd
118
if (!bio) {
119
std::cerr << "Error creating BIO object." << std::endl;
120
SSL_free(ssl);
121
close(client_fd);
122
continue;
123
}
124
SSL_set_bio(ssl, bio, bio); // 设置读写 BIO
125
126
// 8. 执行 TLS/SSL 握手
127
if (SSL_accept(ssl) <= 0) {
128
std::cerr << "SSL Handshake failed." << std::endl;
129
HANDLE_OPENSSL_ERROR(); // 打印握手失败的详细信息
130
SSL_free(ssl); // 释放 SSL 对象 (也会释放关联的 BIO)
131
close(client_fd); // 关闭 socket
132
continue;
133
}
134
std::cout << "SSL 握手成功!" << std::endl;
135
136
// 9. 安全数据传输 (读取客户端请求并发送响应 - 简化的HTTP处理)
137
char read_buffer[4096];
138
int bytes_read = SSL_read(ssl, read_buffer, sizeof(read_buffer) - 1); // 使用 SSL_read 读取加密数据
139
if (bytes_read > 0) {
140
read_buffer[bytes_read] = '\0';
141
std::cout << "接收到客户端请求 (部分):" << std::endl << read_buffer << std::endl;
142
143
// 构造一个简单的 HTTP 响应
144
std::string http_response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, HTTPS!";
145
if (SSL_write(ssl, http_response.c_str(), http_response.length()) <= 0) { // 使用 SSL_write 发送加密数据
146
std::cerr << "Error writing SSL data." << std::endl;
147
HANDLE_OPENSSL_ERROR();
148
} else {
149
std::cout << "发送 HTTPS 响应成功。" << std::endl;
150
}
151
} else if (bytes_read == 0) {
152
std::cout << "客户端连接关闭。" << std::endl;
153
} else {
154
int ssl_err = SSL_get_error(ssl, bytes_read);
155
if (ssl_err == SSL_ERROR_WANT_READ || ssl_err == SSL_ERROR_WANT_WRITE) {
156
// 在非阻塞模式下会遇到,此处是阻塞模式,通常表示错误
157
std::cerr << "SSL_read returned WANT, unexpected in blocking mode." << std::endl;
158
HANDLE_OPENSSL_ERROR();
159
} else {
160
std::cerr << "Error reading SSL data." << std::endl;
161
HANDLE_OPENSSL_ERROR();
162
}
163
}
164
165
// 10. 安全关闭连接
166
SSL_shutdown(ssl); // 执行 TLS 关闭握手
167
168
// 11. 清理当前连接资源
169
SSL_free(ssl); // 释放 SSL 对象和关联的 BIO
170
close(client_fd); // 关闭底层 socket
171
std::cout << "客户端连接关闭。" << std::endl;
172
}
173
174
// 12. 服务器关闭清理 (通常不会到达这里,除非接收到关闭信号)
175
close(listen_fd);
176
SSL_CTX_free(ctx);
177
// EVP_cleanup(); // Old way, deprecated
178
// ERR_free_strings(); // Old way, deprecated
179
180
std::cout << "HTTPS 服务器关闭。" << std::endl;
181
182
return 0;
183
}
184
185
// 请注意:要运行此代码,您需要:
186
// 1. 安装 OpenSSL 开发库。
187
// 2. 生成 server.crt (证书) 和 server.key (私钥) 文件。
188
// 您可以使用 OpenSSL 命令行工具生成自签名证书用于测试:
189
// openssl req -x509 -nodes -newkey rsa:2048 -keyout server.key -out server.crt -days 365
190
// 在生成过程中会提示填写证书信息 (国家、省份、城市、组织、Common Name 等),Common Name 可以填写 localhost 或服务器的 IP/域名。
191
// 3. 将上述代码片段组合成一个完整的可编译程序。
192
// 4. 运行程序,然后使用浏览器或其他 HTTPS 客户端访问 https://localhost:4433/。由于是自签名证书,浏览器可能会提示证书不受信任,需要手动接受。
代码说明:
⚝ 代码演示了HTTPS服务器的基本生命周期:初始化OpenSSL、创建并配置SSL_CTX
、设置监听套接字、接受连接、为每个连接创建SSL
对象、执行SSL_accept
进行握手、使用SSL_read
和SSL_write
进行加密通信、最后通过SSL_shutdown
安全关闭连接。
⚝ 使用了TLS_server_method()
来支持现代TLS版本。
⚝ 通过SSL_CTX_use_certificate_file()
和SSL_CTX_use_PrivateKey_file()
加载了服务器身份所需的证书和私钥。
⚝ 错误处理通过ERR_print_errors_fp(stderr)
将OpenSSL错误信息打印到标准错误输出。
⚝ 此示例是一个简化的单线程服务器,实际生产环境需要使用多线程、多进程或异步I/O框架来处理并发连接,并且HTTP请求/响应的处理逻辑也需要进一步完善。
⚝ 安全加固方面,可以通过SSL_CTX_set_options
、SSL_CTX_set_cipher_list
等函数禁用不安全的协议版本和弱密码套件。
这个案例是理解TLS/SSL服务器端工作原理和OpenSSL libssl
库使用的重要实践。读者可以尝试在此基础上添加客户端证书认证(Mutual Authentication)、SNI支持、性能优化等高级特性。
12.3 安全API通信
在现代分布式系统中,不同的应用程序或服务之间常常通过API进行通信。为了保护这些API调用的数据安全性和完整性,防止中间人攻击(Man-in-the-Middle attack),通常需要使用TLS/SSL对通信信道进行加密。本节将演示如何使用OpenSSL库在两个C++应用程序之间建立安全的TLS/SSL连接,并通过该连接进行API风格的数据交换。这本质上是前面HTTPS服务器和TLS客户端案例的应用,但更侧重于应用程序间的点对点安全通信。
场景描述:
一个客户端C++应用需要调用一个服务器S++应用提供的API。为了保证通信安全,客户端和服务器之间建立TLS连接。
客户端:
① 初始化OpenSSL库。
② 创建SSL上下文对象(SSL_CTX
),配置为客户端模式(TLS_client_method()
)。
③ 可选:加载服务器的CA证书或自签名证书,用于验证服务器身份(SSL_CTX_load_verify_locations()
)。
④ 创建客户端套接字并连接到服务器的地址和端口。
⑤ 为已连接的客户端套接字创建SSL对象(SSL_new()
),并关联套接字(SSL_set_fd()
或使用BIO)。
⑥ 执行TLS/SSL握手(SSL_connect()
)。客户端在此过程中验证服务器证书。
⑦ 握手成功后,使用SSL_write()
发送API请求数据,使用SSL_read()
接收API响应数据。
⑧ 关闭连接(SSL_shutdown()
)。
⑨ 清理资源。
服务器:
① 初始化OpenSSL库。
② 创建SSL上下文对象(SSL_CTX
),配置为服务器模式(TLS_server_method()
)。
③ 加载服务器的证书和私钥(SSL_CTX_use_certificate_file()
, SSL_CTX_use_PrivateKey_file()
)。
④ 创建监听套接字并绑定、监听。
⑤ 接受新的客户端连接。
⑥ 为每个客户端套接字创建SSL对象(SSL_new()
),并关联套接字。
⑦ 执行TLS/SSL握手(SSL_accept()
)。
⑧ 握手成功后,使用SSL_read()
接收客户端的API请求数据,处理请求,然后使用SSL_write()
发送API响应数据。
⑨ 关闭连接(SSL_shutdown()
)。
⑩ 清理资源。
这个案例与构建HTTPS服务器和TLS客户端非常相似,主要的区别在于应用层协议不再是HTTP,而是自定义的API调用格式。OpenSSL关注的是如何建立安全的TLS通道,而通道建立后的数据格式和处理逻辑是应用层需要负责的。
下面是一个简化的客户端和服务器代码片段,展示如何建立TLS连接并交换少量数据:
1
// --- 客户端示例 ---
2
#include <openssl/ssl.h>
3
#include <openssl/err.h>
4
#include <iostream>
5
#include <string>
6
#include <vector>
7
#include <sys/socket.h>
8
#include <netinet/in.h>
9
#include <arpa/inet.h> // for inet_pton
10
#include <unistd.h>
11
12
#define HANDLE_OPENSSL_ERROR() do { ERR_print_errors_fp(stderr); exit(1); } while (0)
13
14
const char* server_ip = "127.0.0.1";
15
const int server_port = 4433;
16
const char* ca_cert_file = "ca.crt"; // 用于验证服务器证书的CA证书
17
18
int main_api_client() {
19
std::cout << "--- 安全 API 客户端 ---" << std::endl;
20
21
SSL_CTX* ctx = nullptr;
22
SSL* ssl = nullptr;
23
int sockfd = -1;
24
25
// 1. OpenSSL库初始化
26
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_ADD_ALL_CIPHERS | OPENSSL_INIT_ADD_ALL_DIGESTS, nullptr);
27
ERR_load_BIO_strings();
28
ERR_load_crypto_strings();
29
ERR_load_SSL_strings();
30
31
// 2. 创建 SSL Context (客户端模式)
32
ctx = SSL_CTX_new(TLS_client_method());
33
if (!ctx) HANDLE_OPENSSL_ERROR();
34
35
// 可选: 加载 CA 证书用于验证服务器
36
if (SSL_CTX_load_verify_locations(ctx, ca_cert_file, nullptr) <= 0) {
37
std::cerr << "Warning: Could not load CA certificate file. Server authentication will likely fail unless using self-signed and trust store is configured." << std::endl;
38
// HANDLE_OPENSSL_ERROR(); // 如果证书是必须的,这里应该退出
39
} else {
40
std::cout << "CA 证书加载成功,将用于验证服务器。" << std::endl;
41
// 设置默认的验证模式:要求验证服务器证书
42
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr);
43
}
44
45
46
// 3. 创建套接字并连接服务器
47
sockfd = socket(AF_INET, SOCK_STREAM, 0);
48
if (sockfd < 0) {
49
std::cerr << "Error creating socket." << std::endl;
50
SSL_CTX_free(ctx);
51
return 1;
52
}
53
54
sockaddr_in server_addr;
55
server_addr.sin_family = AF_INET;
56
server_addr.sin_port = htons(server_port);
57
if (inet_pton(AF_INET, server_ip, &server_addr.sin_addr) <= 0) {
58
std::cerr << "Invalid address/ Address not supported" << std::endl;
59
close(sockfd);
60
SSL_CTX_free(ctx);
61
return 1;
62
}
63
64
std::cout << "连接到服务器 " << server_ip << ":" << server_port << "..." << std::endl;
65
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
66
std::cerr << "Connection failed." << std::endl;
67
close(sockfd);
68
SSL_CTX_free(ctx);
69
return 1;
70
}
71
std::cout << "套接字连接成功。" << std::endl;
72
73
74
// 4. 为套接字创建 SSL 对象并关联
75
ssl = SSL_new(ctx);
76
if (!ssl) {
77
std::cerr << "Error creating SSL object." << std::endl;
78
HANDLE_OPENSSL_ERROR();
79
close(sockfd);
80
SSL_CTX_free(ctx);
81
return 1;
82
}
83
SSL_set_fd(ssl, sockfd); // 将 socket 关联到 SSL 对象
84
85
// 5. 执行 TLS/SSL 握手
86
std::cout << "执行 SSL 握手..." << std::endl;
87
if (SSL_connect(ssl) <= 0) {
88
std::cerr << "SSL Handshake failed." << std::endl;
89
HANDLE_OPENSSL_ERROR(); // 打印握手失败信息
90
SSL_free(ssl);
91
close(sockfd);
92
SSL_CTX_free(ctx);
93
return 1;
94
}
95
std::cout << "SSL 握手成功!连接已加密。" << std::endl;
96
97
// 可选: 验证服务器证书(如果之前设置了SSL_VERIFY_PEER)
98
X509* server_cert = SSL_get_peer_certificate(ssl);
99
if (server_cert) {
100
long verify_result = SSL_get_verify_result(ssl);
101
if (verify_result != X5509_V_OK) {
102
std::cerr << "Server certificate verification failed: " << X509_verify_cert_error_string(verify_result) << std::endl;
103
// 通常这里应该中断连接
104
X509_free(server_cert);
105
SSL_free(ssl);
106
close(sockfd);
107
SSL_CTX_free(ctx);
108
return 1;
109
}
110
std::cout << "服务器证书验证成功。" << std::endl;
111
// 可以进一步检查证书的主题、颁发者等信息
112
// X509_NAME_print_ex_fp(stdout, X509_get_subject_name(server_cert), 0, XN_FLAG_RFC2253);
113
// X509_NAME_print_ex_fp(stdout, X509_get_issuer_name(server_cert), 0, XN_FLAG_RFC2253);
114
X509_free(server_cert);
115
} else {
116
// 如果设置了 SSL_VERIFY_PEER 但没有收到证书,或者未设置 SSL_VERIFY_PEER
117
if (SSL_CTX_get_verify_mode(ctx) & SSL_VERIFY_PEER) {
118
std::cerr << "Error: Server did not send a certificate, but verification was required." << std::endl;
119
SSL_free(ssl);
120
close(sockfd);
121
SSL_CTX_free(ctx);
122
return 1;
123
} else {
124
std::cout << "服务器未发送证书 (服务器认证未被强制要求)。" << std::endl;
125
}
126
}
127
128
129
// 6. 安全发送和接收数据 (模拟 API 调用)
130
std::string api_request = "{\"action\":\"getData\", \"params\":{\"id\":\"123\"}}";
131
std::cout << "发送 API 请求: " << api_request << std::endl;
132
if (SSL_write(ssl, api_request.c_str(), api_request.length()) <= 0) {
133
std::cerr << "Error writing SSL data." << std::endl;
134
HANDLE_OPENSSL_ERROR();
135
SSL_free(ssl);
136
close(sockfd);
137
SSL_CTX_free(ctx);
138
return 1;
139
}
140
141
char read_buffer[4096];
142
int bytes_read = SSL_read(ssl, read_buffer, sizeof(read_buffer) - 1);
143
if (bytes_read > 0) {
144
read_buffer[bytes_read] = '\0';
145
std::cout << "接收到 API 响应: " << read_buffer << std::endl;
146
} else if (bytes_read == 0) {
147
std::cout << "服务器关闭连接。" << std::endl;
148
} else {
149
std::cerr << "Error reading SSL data." << std::endl;
150
HANDLE_OPENSSL_ERROR();
151
}
152
153
154
// 7. 安全关闭连接
155
SSL_shutdown(ssl); // 执行 TLS 关闭握手
156
157
// 8. 清理资源
158
SSL_free(ssl);
159
close(sockfd);
160
SSL_CTX_free(ctx);
161
// OPENSSL_cleanup(); // OpenSSL 1.1.0+ 版本可能不需要显式调用
162
163
std::cout << "客户端流程完成。" << std::endl;
164
return 0;
165
}
166
167
// --- 服务器端示例 (简化版,仅处理单个连接) ---
168
// 需要包含前面 HTTPS 服务器示例中的相关头文件和宏
169
170
int main_api_server() {
171
std::cout << "--- 安全 API 服务器 ---" << std::endl;
172
173
SSL_CTX* ctx = nullptr;
174
int listen_fd = -1;
175
176
// 1. OpenSSL库初始化
177
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_ADD_ALL_CIPHERS | OPENSSL_INIT_ADD_ALL_DIGESTS, nullptr);
178
ERR_load_BIO_strings();
179
ERR_load_crypto_strings();
180
ERR_load_SSL_strings();
181
182
// 2. 创建 SSL Context (服务器模式)
183
ctx = SSL_CTX_new(TLS_server_method());
184
if (!ctx) HANDLE_OPENSSL_ERROR();
185
186
// 可选: 如果需要客户端证书认证 (Mutual Authentication),在这里配置
187
// SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr);
188
// SSL_CTX_load_verify_locations(ctx, ca_cert_file_for_clients, nullptr);
189
190
191
// 3. 加载服务器证书和私钥 (使用与 HTTPS 服务器示例相同的文件)
192
if (SSL_CTX_use_certificate_file(ctx, server_cert_file, SSL_FILETYPE_PEM) <= 0) {
193
std::cerr << "Error loading server certificate file." << std::endl;
194
HANDLE_OPENSSL_ERROR();
195
}
196
if (SSL_CTX_use_PrivateKey_file(ctx, server_key_file, SSL_FILETYPE_PEM) <= 0) {
197
std::cerr << "Error loading server private key file." << std::endl;
198
HANDLE_OPENSSL_ERROR();
199
}
200
if (!SSL_CTX_check_private_key(ctx)) {
201
std::cerr << "Private key does not match the certificate public key." << std::endl;
202
SSL_CTX_free(ctx);
203
return 1;
204
}
205
std::cout << "服务器证书和私钥加载成功。" << std::endl;
206
207
208
// 4. 创建并绑定监听套接字 (使用与 HTTPS 服务器示例相同的端口)
209
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
210
if (listen_fd < 0) {
211
std::cerr << "Error creating socket." << std::endl;
212
SSL_CTX_free(ctx);
213
return 1;
214
}
215
216
sockaddr_in server_addr;
217
server_addr.sin_family = AF_INET;
218
server_addr.sin_addr.s_addr = INADDR_ANY;
219
server_addr.sin_port = htons(listen_port);
220
221
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
222
std::cerr << "Error binding socket to port " << listen_port << std::endl;
223
close(listen_fd);
224
SSL_CTX_free(ctx);
225
return 1;
226
}
227
228
// 5. 开始监听连接
229
if (listen(listen_fd, 5) < 0) {
230
std::cerr << "Error listening on socket." << std::endl;
231
close(listen_fd);
232
SSL_CTX_free(ctx);
233
return 1;
234
}
235
std::cout << "安全 API 服务器正在监听端口: " << listen_port << "..." << std::endl;
236
237
// 6. 接受客户端连接并处理 (简化的单连接处理)
238
sockaddr_in client_addr;
239
socklen_t client_len = sizeof(client_addr);
240
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
241
if (client_fd < 0) {
242
std::cerr << "Error accepting client connection." << std::endl;
243
close(listen_fd);
244
SSL_CTX_free(ctx);
245
return 1;
246
}
247
std::cout << "接受到一个新的客户端连接。" << std::endl;
248
249
SSL* ssl = nullptr;
250
251
// 7. 为客户端连接创建 SSL 对象并关联 socket
252
ssl = SSL_new(ctx);
253
if (!ssl) {
254
std::cerr << "Error creating SSL object." << std::endl;
255
HANDLE_OPENSSL_ERROR();
256
close(client_fd);
257
close(listen_fd);
258
SSL_CTX_free(ctx);
259
return 1;
260
}
261
SSL_set_fd(ssl, client_fd);
262
263
// 8. 执行 TLS/SSL 握手
264
std::cout << "执行 SSL 握手..." << std::endl;
265
if (SSL_accept(ssl) <= 0) {
266
std::cerr << "SSL Handshake failed." << std::endl;
267
HANDLE_OPENSSL_ERROR();
268
SSL_free(ssl);
269
close(client_fd);
270
close(listen_fd);
271
SSL_CTX_free(ctx);
272
return 1;
273
}
274
std::cout << "SSL 握手成功!" << std::endl;
275
276
// 9. 安全接收和发送数据 (模拟 API 处理)
277
char read_buffer[4096];
278
int bytes_read = SSL_read(ssl, read_buffer, sizeof(read_buffer) - 1);
279
if (bytes_read > 0) {
280
read_buffer[bytes_read] = '\0';
281
std::cout << "接收到 API 请求: " << read_buffer << std::endl;
282
283
// 模拟 API 处理并构造响应
284
std::string api_response = "{\"status\":\"success\", \"data\":\"some data\"}";
285
std::cout << "发送 API 响应: " << api_response << std::endl;
286
if (SSL_write(ssl, api_response.c_str(), api_response.length()) <= 0) {
287
std::cerr << "Error writing SSL data." << std::endl;
288
HANDLE_OPENSSL_ERROR();
289
}
290
} else if (bytes_read == 0) {
291
std::cout << "客户端连接关闭。" << std::endl;
292
} else {
293
std::cerr << "Error reading SSL data." << std::endl;
294
HANDLE_OPENSSL_ERROR();
295
}
296
297
// 10. 安全关闭连接
298
SSL_shutdown(ssl);
299
300
// 11. 清理资源
301
SSL_free(ssl);
302
close(client_fd);
303
close(listen_fd);
304
SSL_CTX_free(ctx);
305
// OPENSSL_cleanup();
306
307
std::cout << "服务器流程完成。" << std::endl;
308
309
return 0;
310
}
代码说明:
⚝ 客户端使用TLS_client_method()
创建SSL_CTX
,服务器使用TLS_server_method()
。
⚝ 客户端示例中展示了如何加载CA证书并通过SSL_CTX_set_verify
和SSL_get_peer_certificate
/SSL_get_verify_result
来验证服务器身份。这在实际应用中至关重要,以防止连接到伪造的服务器。
⚝ 服务器示例展示了如何加载自身的证书和私钥。如果需要,可以配置服务器要求客户端也提供证书进行双向认证。
⚝ 数据传输使用SSL_read
和SSL_write
,这些函数处理数据的加密/解密、完整性校验等底层细节。
⚝ 这里的服务器示例是单连接阻塞式的,实际应用中需要采用多进程、多线程或异步I/O模型来处理并发客户端连接。
⚝ API请求和响应的数据格式(例如JSON、Protocol Buffers等)以及具体的处理逻辑,是应用层需要实现的,OpenSSL层只负责传输的安全性。
通过此案例,读者可以了解到如何将OpenSSL的TLS/SSL功能集成到应用程序间通信中,从而构建安全的API交互通道。
12.4 使用OpenSSL处理特定加密标准
在某些行业(如金融、医疗、政府)或特定的应用场景中,可能需要遵循严格的加密标准或法规要求(例如:FIPS 140-2, HIPAA, PCI DSS)。这些标准通常会指定允许使用的加密算法、密钥长度、哈希函数、协议版本甚至随机数生成器等。OpenSSL作为一个功能强大的加密库,提供了丰富的配置选项来满足这些特定需求。本节将演示如何利用OpenSSL API来强制使用特定的加密设置。
核心要点:
① 选择合适的协议版本:某些标准可能要求禁用TLSv1.0、TLSv1.1,甚至仅允许TLSv1.2或TLSv1.3。可以使用SSL_CTX_set_options()
或SSL_set_options()
配合SSL_OP_NO_TLSv1
, SSL_OP_NO_TLSv1_1
, SSL_OP_NO_TLSv1_2
等标志来实现。或者在创建SSL_CTX
时直接使用特定版本的方法,如TLSv1_2_method()
。
② 配置允许的密码套件(Cipher Suites):密码套件定义了TLS连接中使用的密钥交换算法、认证算法、加密算法和哈希函数组合。标准通常会列出允许或禁止的密码套件。可以使用SSL_CTX_set_cipher_list()
或SSL_set_cipher_list()
函数,传入符合OpenSSL格式的密码套件字符串来限制可用选项。例如,"AES256-GCM-SHA384:EECDH+AESGCM"
可能是一个符合某些要求的强密码套件列表。
1
#include <openssl/ssl.h>
2
#include <openssl/err.h>
3
#include <openssl/evp.h>
4
#include <openssl/rand.h>
5
#include <iostream>
6
#include <string>
7
8
// 简单的错误处理宏
9
#define HANDLE_OPENSSL_ERROR() do { ERR_print_errors_fp(stderr); /*exit(1); // 在示例中不直接退出*/ } while (0)
10
11
12
int main_standard_compliance() {
13
std::cout << "--- 使用 OpenSSL 处理特定加密标准 ---" << std::endl;
14
15
// 1. OpenSSL库初始化
16
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_ADD_ALL_CIPHERS | OPENSSL_INIT_ADD_ALL_DIGESTS, nullptr);
17
ERR_load_crypto_strings();
18
ERR_load_SSL_strings();
19
ERR_load_EVP_strings();
20
21
// --- 示例 1: TLS 服务器强制使用 TLS 1.2 或更高版本,并限制密码套件 ---
22
std::cout << "\n--- 示例 1: 配置 TLS 服务器强制使用高版本协议和特定密码套件 ---" << std::endl;
23
SSL_CTX* server_ctx = nullptr;
24
try {
25
// 创建 SSL Context (服务器模式)
26
// 使用 TLS_server_method() 支持当前推荐的最高版本,然后通过选项禁用旧版本
27
server_ctx = SSL_CTX_new(TLS_server_method());
28
if (!server_ctx) {
29
std::cerr << "Error creating server SSL_CTX." << std::endl;
30
HANDLE_OPENSSL_ERROR();
31
throw std::runtime_error("SSL_CTX creation failed");
32
}
33
34
// 强制禁用 TLSv1.0 和 TLSv1.1 (以满足某些要求 TLSv1.2+ 的标准)
35
long options = SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1;
36
SSL_CTX_set_options(server_ctx, options);
37
std::cout << "禁用 TLSv1.0 和 TLSv1.1。" << std::endl;
38
39
// 限制允许的密码套件列表
40
// 例如,只允许使用带前向保密性的 AES-GCM 加密和 SHA256/SHA384 哈希的套件
41
const char* cipher_list = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256";
42
if (SSL_CTX_set_cipher_list(server_ctx, cipher_list) <= 0) {
43
std::cerr << "Error setting cipher list." << std::endl;
44
HANDLE_OPENSSL_ERROR();
45
throw std::runtime_error("Setting cipher list failed");
46
}
47
std::cout << "已设置密码套件列表: " << cipher_list << std::endl;
48
49
// 在实际服务器中,会在这里加载证书、私钥,然后绑定监听 socket,接受连接,并在每个连接上创建 SSL 对象并执行 SSL_accept
50
// ... (省略 socket 和连接处理代码)
51
52
std::cout << "TLS 服务器配置示例完成。" << std::endl;
53
54
} catch (const std::runtime_error& e) {
55
std::cerr << "Server configuration failed: " << e.what() << std::endl;
56
}
57
58
// 清理服务器 Context
59
if (server_ctx) {
60
SSL_CTX_free(server_ctx);
61
server_ctx = nullptr;
62
}
63
64
65
// --- 示例 2: 使用特定的哈希算法和密钥长度进行文件摘要 ---
66
std::cout << "\n--- 示例 2: 使用 SHA3-512 计算文件摘要 ---" << std::endl;
67
EVP_MD_CTX* mdctx = nullptr;
68
const char* file_content = "This is a test message for hashing.";
69
unsigned char digest[EVP_MAX_MD_SIZE];
70
unsigned int digest_len;
71
72
try {
73
// 获取特定的哈希算法实现 (如 SHA3-512)
74
// 在 OpenSSL 3.0+ 中,EVP_get_digestbyname 可能需要 Provider 已加载
75
const EVP_MD* md = EVP_get_digestbyname("SHA3-512");
76
if (!md) {
77
std::cerr << "Error: SHA3-512 digest not available. Make sure providers are loaded correctly in OpenSSL 3.0+." << std::endl;
78
// HANDLE_OPENSSL_ERROR(); // EVP_get_digestbyname 通常不设置 OpenSSL 错误栈
79
throw std::runtime_error("SHA3-512 digest not found");
80
}
81
82
// 创建并初始化摘要上下文
83
mdctx = EVP_MD_CTX_new();
84
if (!mdctx) HANDLE_OPENSSL_ERROR();
85
86
// 初始化摘要操作
87
if (EVP_DigestInit_ex(mdctx, md, nullptr) <= 0) HANDLE_OPENSSL_ERROR();
88
89
// 更新摘要计算 (可以是分块数据)
90
if (EVP_DigestUpdate(mdctx, file_content, strlen(file_content)) <= 0) HANDLE_OPENSSL_ERROR();
91
92
// 完成摘要计算并获取结果
93
if (EVP_DigestFinal_ex(mdctx, digest, &digest_len) <= 0) HANDLE_OPENSSL_ERROR();
94
95
std::cout << "使用 SHA3-512 计算的摘要 (" << digest_len << " 字节): ";
96
for (unsigned int i = 0; i < digest_len; ++i) {
97
printf("%02x", digest[i]);
98
}
99
printf("\n");
100
101
std::cout << "摘要计算示例完成。" << std::endl;
102
103
} catch (const std::runtime_error& e) {
104
std::cerr << "Digest calculation failed: " << e.what() << std::endl;
105
}
106
107
108
// 清理摘要 Context
109
if (mdctx) {
110
EVP_MD_CTX_free(mdctx);
111
mdctx = nullptr;
112
}
113
114
115
// --- 示例 3: 生成指定长度的 RSA 密钥对 ---
116
std::cout << "\n--- 示例 3: 生成 4096 位 RSA 密钥对 ---" << std::endl;
117
EVP_PKEY* rsa_keypair = nullptr;
118
EVP_PKEY_CTX* keygen_ctx = nullptr;
119
120
try {
121
// 创建密钥生成上下文
122
keygen_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
123
if (!keygen_ctx) HANDLE_OPENSSL_ERROR();
124
125
// 初始化生成过程
126
if (EVP_PKEY_keygen_init(keygen_ctx) <= 0) HANDLE_OPENSSL_ERROR();
127
128
// 设置密钥长度 (如 4096 位,符合更高安全标准的要求)
129
if (EVP_PKEY_CTX_set_rsa_keygen_bits(keygen_ctx, 4096) <= 0) HANDLE_OPENSSL_ERROR();
130
131
// 生成密钥对
132
if (EVP_PKEY_keygen(keygen_ctx, &rsa_keypair) <= 0) HANDLE_OPENSSL_ERROR();
133
134
std::cout << "成功生成 4096 位 RSA 密钥对。" << std::endl;
135
// 在实际应用中,你需要保存这个密钥对
136
137
std::cout << "密钥生成示例完成。" << std::endl;
138
139
} catch (const std::runtime_error& e) {
140
std::cerr << "Key generation failed: " << e.what() << std::endl;
141
}
142
143
// 清理密钥对和上下文
144
if (rsa_keypair) {
145
EVP_PKEY_free(rsa_keypair);
146
rsa_keypair = nullptr;
147
}
148
if (keygen_ctx) {
149
EVP_PKEY_CTX_free(keygen_ctx);
150
keygen_ctx = nullptr;
151
}
152
153
// --- 示例 4: 使用指定的椭圆曲线生成 ECC 密钥对 ---
154
std::cout << "\n--- 示例 4: 使用 secp256r1 曲线生成 ECC 密钥对 ---" << std::endl;
155
EVP_PKEY* ecc_keypair = nullptr;
156
EVP_PKEY_CTX* ecc_keygen_ctx = nullptr;
157
158
try {
159
// 获取椭圆曲线 NIST P-256 的 NID (Numeric ID)
160
// 在 OpenSSL 3.0+ 中,这可能依赖于 Provider
161
int curve_nid = NID_X9_62_prime256v1; // secp256r1 aka prime256v1
162
if (curve_nid == NID_undef) {
163
std::cerr << "Error: Curve secp256r1 not available." << std::endl;
164
throw std::runtime_error("Curve not found");
165
}
166
167
// 创建密钥生成上下文
168
ecc_keygen_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr);
169
if (!ecc_keygen_ctx) HANDLE_OPENSSL_ERROR();
170
171
// 初始化生成过程
172
if (EVP_PKEY_keygen_init(ecc_keygen_ctx) <= 0) HANDLE_OPENSSL_ERROR();
173
174
// 设置椭圆曲线
175
if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ecc_keygen_ctx, curve_nid) <= 0) HANDLE_OPENSSL_ERROR();
176
177
// 生成密钥对
178
if (EVP_PKEY_keygen(ecc_keygen_ctx, &ecc_keypair) <= 0) HANDLE_OPENSSL_ERROR();
179
180
std::cout << "成功生成使用 secp256r1 曲线的 ECC 密钥对。" << std::endl;
181
// 在实际应用中,你需要保存这个密钥对
182
183
std::cout << "ECC 密钥生成示例完成。" << std::endl;
184
185
} catch (const std::runtime_error& e) {
186
std::cerr << "ECC Key generation failed: " << e.what() << std::endl;
187
}
188
189
190
// 清理 ECC 密钥对和上下文
191
if (ecc_keypair) {
192
EVP_PKEY_free(ecc_keypair);
193
ecc_keypair = nullptr;
194
}
195
if (ecc_keygen_ctx) {
196
EVP_PKEY_CTX_free(ecc_keygen_ctx);
197
ecc_keygen_ctx = nullptr;
198
}
199
200
201
// 5. OpenSSL库清理
202
// EVP_cleanup(); // Old way, deprecated in 1.1.0+
203
// ERR_free_strings(); // Old way, deprecated in 1.1.0+
204
// 清理由 OPENSSL_init_ssl 加载的错误字符串
205
// ERR_free_strings(); // In OpenSSL 1.1.0+ ERR_free_strings is not needed for initialisation loaded strings
206
207
std::cout << "\nOpenSSL 特定标准配置示例完成。" << std::endl;
208
209
return 0;
210
}
代码说明:
⚝ 示例 1 (TLS 配置):演示了如何创建SSL_CTX
后,使用SSL_CTX_set_options
禁用旧版本的TLS协议(如TLSv1.0, TLSv1.1),并使用SSL_CTX_set_cipher_list
指定允许的密码套件列表。这对于满足要求TLS 1.2+和强加密算法的标准非常有用。
⚝ 示例 2 (哈希算法):展示了如何通过EVP_get_digestbyname
获取特定哈希算法(如SHA3-512)的EVP_MD
对象,然后在EVP_DigestInit_ex
中使用它来执行摘要计算。这允许您使用标准规定的特定哈希函数。需要注意的是,OpenSSL 3.0+ 的新算法可能需要正确配置和加载Provider。
⚝ 示例 3 (RSA 密钥长度):演示了使用EVP_PKEY_CTX
生成密钥时,如何通过EVP_PKEY_CTX_set_rsa_keygen_bits
设置RSA密钥的长度(例如4096位),以符合更严格的安全要求。
⚝ 示例 4 (ECC 椭圆曲线):展示了如何通过NID
指定特定的椭圆曲线(如secp256r1),然后使用EVP_PKEY_CTX_set_ec_paramgen_curve_nid
来生成基于该曲线的ECC密钥对。不同的标准可能要求使用特定的、经批准的曲线。
通过这些示例,读者可以了解到OpenSSL提供了灵活的API,允许开发者精细控制所使用的加密原语和协议参数,从而满足各种严格的加密标准和合规性要求。在实际项目中,应仔细查阅相关标准文档,确定所需的具体算法、参数和配置,并利用OpenSSL提供的相应函数进行设置。同时,应关注OpenSSL的版本兼容性,特别是在从旧版本迁移到OpenSSL 3.0+ 时,新的Provider模型对算法的可用性有影响。
好的,同学们,我们今天将深入探讨OpenSSL库的演进历程、当前最新版本(尤其是3.0+系列)的关键特性,以及这个重要开源项目的维护机制和社区资源。这有助于我们理解OpenSSL的设计哲学、API的变化原因,并为将来使用或迁移到新版本做好准备。
13. OpenSSL的演进与未来
欢迎来到本书的最后一章内容章节。在前面的章节中,我们系统地学习了如何使用OpenSSL库在C++中实现各种密码学功能和构建安全通信应用程序。我们看到了OpenSSL提供的强大功能,但也可能注意到了其API的复杂性和一些历史遗留问题。在本章中,我们将把目光投向OpenSSL本身,了解它的过去、现在和未来。理解其演进路线对于我们更高效、更安全地使用它至关重要。我们将回顾OpenSSL的主要版本迭代及其API的变化,重点解析OpenSSL 3.0及后续版本引入的开创性特性——提供者(Provider)机制,最后探讨OpenSSL项目的维护模式和社区资源。
13.1 OpenSSL版本历史与API变化
OpenSSL是一个历史悠久且不断发展的开源项目。其API随着密码学的发展、安全需求的演变以及开发者对库设计的改进而不断变化。了解其主要版本历史和API的演进方向,有助于我们理解为何存在多种使用OpenSSL的方式,以及如何在不同版本之间迁移代码。
OpenSSL的版本历史可以大致划分为几个重要的阶段:
① 早期版本 (例如:0.9.x 系列)
▮▮▮▮这些是OpenSSL的早期稳定版本。它们的API通常比较直接地暴露了内部结构体的细节,例如,许多函数会直接接收或返回结构体指针。
▮▮▮▮内存管理和错误处理往往需要开发者非常小心地手动处理。
▮▮▮▮线程安全需要开发者通过设置全局的回调函数来配置(CRYPTO_set_locking_callback
等),这在使用上相对繁琐且容易出错。
▮▮▮▮这些版本现在已经被标记为过时(deprecated),不应再用于新的开发项目。
② 1.0.x 系列
▮▮▮▮1.0.x 系列是OpenSSL历史上使用最广泛、持续时间最长的版本系列之一。它在稳定性、性能和功能上都有显著提升。
▮▮▮▮尽管在1.0.x中已经引入了一些改进,但其核心API风格与0.9.x系列相比,变化相对较小,很多内部结构仍然暴露在外,线程安全仍然需要开发者手动设置回调。
▮▮▮▮此系列的主要版本(如1.0.2)已经达到了生命周期结束(End-of-Life, EOL),不再接收安全更新,使用这些版本是危险的。
③ 1.1.x 系列
▮▮▮▮1.1.x 系列是一个非常重要的过渡版本。它引入了许多旨在改进API设计和库内部结构的变化:
▮▮▮▮ⓐ 不透明结构体(Opaque Structures): 这是1.1.x中最显著的变化之一。许多之前直接暴露内部成员的结构体(例如 RSA
, EVP_CIPHER_CTX
, X509
等)现在变成了不透明的指针类型(例如 RSA *
, EVP_CIPHER_CTX *
, X509 *
)。开发者无法直接访问结构体内部成员,只能通过提供的API函数进行操作。
▮▮▮▮▮▮▮▮❷ 这样做的目的是为了封装实现细节,使得OpenSSL库的内部实现可以在不破坏兼容性的情况下进行更改,提高了库的可维护性和未来演进的灵活性。
▮▮▮▮▮▮▮▮❸ 这也强制开发者使用官方提供的函数来访问和操作对象,减少了因直接访问内部成员导致的错误和安全隐患。
▮▮▮▮ⓓ 简化的线程安全模型: 1.1.0版本后,OpenSSL库本身变得更加线程友好。全局的锁定和ID回调函数不再需要由应用开发者手动设置,库内部会处理这些。这极大地简化了在多线程C++应用程序中使用OpenSSL的复杂性。
▮▮▮▮ⓔ API清理与废弃: 1.1.x废弃(deprecate)了大量老旧、不安全或设计不佳的API,并引入了新的、更一致的函数命名和参数约定。
▮▮▮▮1.1.1 是一个长期支持(Long-Term Support, LTS)版本,在OpenSSL 3.0发布后仍然被广泛使用,但最终也将在特定日期达到EOL。
④ 3.0+ 系列
▮▮▮▮OpenSSL 3.0 版本是OpenSSL历史上最具颠覆性的更新之一。它引入了全新的架构和API模型:
▮▮▮▮ⓐ 提供者(Provider)机制: 这是3.0的核心变化,我们将在下一节详细讨论。它将算法实现从核心库中解耦出来,提供了前所未有的灵活性。
▮▮▮▮ⓑ 全新的获取API(Fetching API): 为了配合Provider机制,引入了一套新的API来根据算法名称和属性从Provider中获取具体的算法实现。例如,不再直接调用 EVP_aes_256_cbc()
函数获取一个固定的算法实现,而是使用 EVP_CIPHER_fetch(libctx, "AES-256-CBC", "provider=default")
来动态获取。
▮▮▮▮ⓒ API的进一步重组与废弃: 3.0版本废弃了1.1.x中大量不再推荐使用的API,并对现有API进行了进一步的组织和命名规范化。这导致从1.1.x迁移到3.0+需要进行相当多的代码修改。
▮▮▮▮ⓓ OpenSSL上下文(Library Context): 引入了 OSSL_LIB_CTX
对象,用于管理Provider和相关的配置信息。这使得在同一进程中隔离不同的OpenSSL配置成为可能。
总的来说,OpenSSL的API演进是一个从底层暴露细节到逐步抽象和封装,从手动管理到自动化处理(线程安全),最终实现模块化和灵活性的过程。从0.9.x到1.1.x,API变得更加健壮和易用;而从1.1.x到3.0+,则是一个革命性的架构转变,为未来的发展奠定了基础。对于开发者来说,理解这些变化,尤其是1.1.x的不透明结构体和3.0+的Provider模型,是掌握现代OpenSSL开发的关键。
13.2 OpenSSL 3.0+ 的新特性
OpenSSL 3.0 版本于2021年发布,标志着OpenSSL库进入了一个新的时代。其最重要的变化在于引入了提供者(Provider)的概念,并围绕这一概念构建了全新的API模型。
13.2.1 提供者(Provider)机制
提供者(Provider)是OpenSSL 3.0+ 中最核心的新概念。简单来说,它是一个包含密码学算法实现集合的模块。
⚝ 提供者的作用:
▮▮▮▮⚝ 模块化(Modularity):将特定的算法实现(例如 AES、RSA、SHA-256、TLS握手协议等)封装在独立的提供者模块中。
▮▮▮▮⚝ 灵活性(Flexibility):允许应用程序在运行时选择或切换不同的提供者,从而使用不同的算法实现。例如,可以选择使用软件实现的提供者、硬件加速的提供者、符合特定标准(如 FIPS)的提供者,或者由第三方开发的提供者。
▮▮▮▮⚝ 合规性(Compliance):某些安全标准(如 FIPS 140-2/3)要求使用经过认证的密码学模块。通过提供者机制,OpenSSL可以提供符合这些标准的模块,而不会影响其他使用非认证模块的场景。
▮▮▮▮⚝ 更新与维护(Updates and Maintenance):可以在不影响核心OpenSSL库的情况下更新或替换某个提供者的算法实现。
⚝ OpenSSL自带的提供者:
▮▮▮▮OpenSSL 3.0+ 默认提供了几个内置的提供者:
▮▮▮▮ⓐ Default Provider (默认提供者):包含了大多数常用的、性能良好的密码学算法的软件实现。除非特别配置,这是应用程序最常使用的提供者。
▮▮▮▮ⓑ Base Provider (基础提供者):包含了一些基础算法(如 Ed25519, Ed448, X25519, X448)以及其他一些在早期版本中较少使用的算法。这些算法被移出Default Provider是为了保持Default Provider的大小相对紧凑。
▮▮▮▮ⓒ Legacy Provider (传统提供者):包含了OpenSSL早期版本中支持但现在被认为不安全或不再推荐使用的算法(如 MD5, SHA-1 用于签名, DES/3DES 不用于 TLS 等)。为了兼容性而保留,但在新的应用中应尽量避免使用。
▮▮▮▮ⓓ FIPS Provider (FIPS 提供者):包含了符合 FIPS 140-2 或 140-3 标准的密码学算法实现。使用此提供者需要特定的构建和配置步骤。
▮▮▮▮ⓔ Null Provider (空提供者):不包含任何算法实现。用于某些特殊配置场景。
一个算法可以在多个提供者中都有实现,例如 AES 算法可能存在于 Default Provider 和 FIPS Provider 中。应用程序可以通过属性字符串(property string)来指定希望从哪个提供者获取算法,或者获取具有特定属性(如 fips=true
, provider=default
)的算法实现。
13.2.2 全新的获取API(Fetching API)
为了与提供者机制配合,OpenSSL 3.0+ 引入了一套新的API来获取和管理密码学对象。获取(Fetching)是从提供者中加载特定算法实现的过程。
⚝ 核心获取函数示例:
▮▮▮▮⚝ EVP_CIPHER_fetch(OSSL_LIB_CTX *libctx, const char *algorithm, const char *properties)
: 用于获取对称加密算法(Cipher)的实现。
▮▮▮▮⚝ EVP_MD_fetch(OSSL_LIB_CTX *libctx, const char *algorithm, const char *properties)
: 用于获取哈希算法(Message Digest)的实现。
▮▮▮▮⚝ EVP_KEM_fetch(...)
: 用于获取密钥封装机制(Key Encapsulation Mechanism)。
▮▮▮▮⚝ EVP_KDF_fetch(...)
: 用于获取密钥派生函数(Key Derivation Function)。
▮▮▮▮⚝ EVP_RAND_fetch(...)
: 用于获取随机数生成器(Random Number Generator)。
▮▮▮▮⚝ EVP_PKEY_CTX_new_from_name(OSSL_LIB_CTX *libctx, const char *type, const char *properties)
: 用于根据名称(例如 "RSA", "EC", "Ed25519")创建公钥算法上下文。
这些函数通常需要一个 OSSL_LIB_CTX
对象(库上下文),算法的名称字符串,以及可选的属性字符串。成功获取后,它们返回一个指向不透明结构体的指针(例如 const EVP_CIPHER *
),这个指针代表了从特定提供者加载的算法实现。
与1.1.x及之前版本直接调用如 EVP_aes_256_cbc()
这样的函数不同,3.0+ 的方法更加动态和灵活。应用程序不再硬编码指向特定算法实现的全局指针,而是在运行时根据需要获取。
13.2.3 OSSL_PARAM
在OpenSSL 3.0+ 中,参数的传递方式也发生了变化。许多配置和操作函数不再使用大量独立的函数参数,而是使用一个 OSSL_PARAM
结构体数组来传递参数。
OSSL_PARAM
是一个类型化的键值对(key-value pair)结构,用于向各种OpenSSL对象(如算法上下文、密钥对象等)设置或获取参数。例如,设置 AES GCM 的标签(tag)或获取 RSA 密钥的模数(modulus)都可以通过操作 OSSL_PARAM
数组来完成。
使用 OSSL_PARAM
使得API更加整洁和可扩展。新的算法或特性可以通过定义新的参数键来引入,而无需修改现有的函数签名。
从1.1.x迁移到3.0+是需要投入精力的一项任务,特别是对于大量使用了旧有API的代码库。开发者需要学习新的Provider和Fetching API模型,并根据编译器的警告或错误信息逐步替换废弃的函数调用。OpenSSL官方文档提供了详细的迁移指南。
13.3 OpenSSL的维护与社区
OpenSSL是一个由全球开发者社区共同维护的开源项目。其维护模式、安全更新流程和丰富的社区资源是它能够持续发展和保持重要地位的关键。
13.3.1 OpenSSL的维护模式
OpenSSL项目遵循一个清晰的版本发布和维护策略:
① 长期支持版本(LTS - Long-Term Support)
▮▮▮▮LTS 版本是 OpenSSL 项目最推荐用于生产环境的版本系列。这些版本会获得为期五年的支持,包括安全修复和关键的错误修正。
▮▮▮▮例如,OpenSSL 1.1.1 是一个 LTS 版本,而 3.0, 3.1, 3.2 都是 LTS 版本(从 3.0 开始,每两年发布一个 LTS 版本)。
▮▮▮▮选择 LTS 版本可以确保你的应用程序在较长的时间内能够获得持续的安全支持,降低频繁升级的负担。
② 标准版本(Standard Release)
▮▮▮▮标准版本是 LTS 版本之间的发布系列(例如,在 3.0 LTS 之后是 3.1, 3.2 等非 LTS 版本)。
▮▮▮▮这些版本会引入新的特性,但支持周期较短(通常只有两年)。
▮▮▮▮它们适用于希望尝试最新特性、为下一个 LTS 版本做准备或者不介意频繁升级的用户。
③ 安全更新(Security Updates)
▮▮▮▮OpenSSL项目非常重视安全性。当发现安全漏洞时,项目团队会发布安全公告(Security Advisory)并尽快发布包含修复补丁的新版本。
▮▮▮▮安全更新通常以版本号的第三位(例如,从 3.0.1 升级到 3.0.2)或第四位(如果是 LTS 版本,例如 1.1.1k 到 1.1.1l)来表示。
▮▮▮▮对于使用OpenSSL的应用程序来说,及时跟踪OpenSSL的安全公告并应用安全补丁至关重要,以保护应用程序免受已知漏洞的攻击。
13.3.2 社区资源
OpenSSL拥有一个庞大而活跃的社区,为使用者提供了丰富的学习和支持资源:
⚝ 官方网站 (www.openssl.org)
▮▮▮▮⚝ 这是OpenSSL项目的官方门户,提供最新的新闻、版本发布信息、安全公告、文档链接等。
⚝ 文档(Documentation)
▮▮▮▮⚝ OpenSSL提供了详细的手册页(man pages),这是理解OpenSSL API最权威的资料。几乎所有的OpenSSL函数、结构体和概念都有对应的手册页。学习如何查阅这些手册页是掌握OpenSSL的关键一步(例如,在命令行输入 man EVP_encrypt_init
查看 EVP_encrypt_init
函数的文档)。
▮▮▮▮⚝ 官方网站还提供了安装指南、移植指南(特别是从1.1.x到3.0+的迁移指南)、概念性文档等。
⚝ 邮件列表(Mailing Lists)
▮▮▮▮⚝ OpenSSL项目维护着几个邮件列表,其中 openssl-users
是用户讨论和提问的主要场所。在这里,你可以提出你在使用OpenSSL过程中遇到的问题,并从社区成员那里获得帮助。openssl-project
列表则用于讨论项目本身的开发和方向。
⚝ GitHub 代码库(GitHub Repository)
▮▮▮▮⚝ OpenSSL的源代码托管在GitHub上(github.com/openssl/openssl)。你可以在这里浏览源代码、提交错误报告(Issue)、提交改进建议或代码贡献(Pull Request)。
⚝ 第三方资源
▮▮▮▮⚝ 除了官方资源,互联网上还有大量的博客、教程、Stack Overflow 问题、以及本书这样的技术书籍,它们提供了从不同角度学习和理解OpenSSL的途径。
积极利用这些社区资源,不仅可以帮助你解决开发中遇到的具体问题,还能让你了解OpenSSL的最新动态和最佳实践,甚至有机会参与到这个重要的开源项目中。
通过本章的学习,我们回顾了OpenSSL的发展历程和重要的API变化,深入了解了OpenSSL 3.0+ 版本引入的Provider机制和新的API模型,并认识了OpenSSL项目的维护策略和社区资源。希望这能帮助大家更好地理解OpenSSL的设计哲学,自信地选择合适的版本,并在未来的开发中更好地利用OpenSSL的强大功能。
Appendix A: OpenSSL常用函数参考
本附录旨在为读者提供一个OpenSSL开发中常用的关键函数和数据结构参考列表。这些函数和数据结构是OpenSSL库的核心构建块,理解它们的功能和使用方法对于编写安全可靠的C++应用程序至关重要。请注意,OpenSSL库庞大且持续发展,此列表无法涵盖所有API,而是侧重于本书中讲解和实战演练涉及到的最常用部分。对于完整的、权威的API文档,请查阅OpenSSL官方手册页(man pages)。
Appendix A1: 初始化与清理
OpenSSL库在使用前通常需要进行全局初始化,并在程序结束时进行清理,以确保资源(如内部状态、错误栈等)得到正确管理。
⚝ OPENSSL_init_ssl
(OpenSSL 1.1.0+):
▮▮▮▮初始化OpenSSL库,包括SSL/TLS部分。这是OpenSSL 1.1.0及之后版本推荐的初始化方式,它替代了之前的一些初始化函数(如SSL_library_init
, SSL_load_error_strings
, OpenSSL_add_all_algorithms
等)。
▮▮▮▮函数原型示例:int OPENSSL_init_ssl(uint64_t opts, const OPENSSL_INIT_SETTINGS *settings);
▮▮▮▮通常简单的初始化可以传递 OPENSSL_INIT_SSL
和 NULL
作为参数:OPENSSL_init_ssl(OPENSSL_INIT_SSL | OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
⚝ OPENSSL_cleanup
(OpenSSL 1.1.0+):
▮▮▮▮清理OpenSSL库占用的全局资源。在程序退出前调用。
▮▮▮▮函数原型:void OPENSSL_cleanup(void);
⚝ 旧版本初始化函数 (OpenSSL < 1.1.0):
▮▮▮▮SSL_library_init()
:初始化SSL库。
▮▮▮▮SSL_load_error_strings()
:加载SSL错误字符串。
▮▮▮▮OpenSSL_add_all_algorithms()
:加载所有加密算法和哈希函数。
▮▮▮▮这些函数在1.1.0版本后已废弃,但对于维护旧代码仍然重要。
Appendix A2: 错误处理
OpenSSL使用错误栈(error stack)机制来记录错误信息。理解如何获取和解析错误信息对于调试至关重要。
⚝ ERR_get_error
:
▮▮▮▮从当前线程的错误栈中获取最旧(最早发生)的错误码(error code)。调用此函数会从栈中移除该错误。如果栈为空,返回0。
▮▮▮▮函数原型:unsigned long ERR_get_error(void);
⚝ ERR_peek_error
:
▮▮▮▮与ERR_get_error
类似,但不会从错误栈中移除错误。
▮▮▮▮函数原型:unsigned long ERR_peek_error(void);
⚝ ERR_error_string
/ ERR_error_string_n
:
▮▮▮▮将OpenSSL错误码转换为人类可读的字符串描述。ERR_error_string
不推荐使用,因为它内部使用了静态缓冲区,不是线程安全的。推荐使用线程安全的ERR_error_string_n
。
▮▮▮▮函数原型示例:char *ERR_error_string(unsigned long e, char *buf);
▮▮▮▮void ERR_error_string_n(unsigned long e, char *buf, size_t len);
⚝ ERR_print_errors_fp
:
▮▮▮▮将当前线程错误栈中的所有错误信息打印到指定的文件指针(例如:stderr
)。
▮▮▮▮函数原型:void ERR_print_errors_fp(FILE *fp);
Appendix A3: BIO I/O抽象层
BIO (Basic Input/Output) 是OpenSSL提供的一个通用的I/O抽象层,它允许在不修改核心加密逻辑的情况下,通过不同的通道(如文件、内存、套接字)进行数据读写。
⚝ BIO
:
▮▮▮▮表示一个BIO对象的数据结构。它是BIO抽象层的核心。
⚝ BIO_new
:
▮▮▮▮创建一个新的BIO对象。需要指定BIO类型(例如:BIO_s_file()
, BIO_s_mem()
, BIO_s_socket()
, BIO_s_ssl()
)。
▮▮▮▮函数原型:BIO *BIO_new(BIO_METHOD *type);
⚝ BIO_free
/ BIO_vfree
:
▮▮▮▮释放BIO对象。BIO_free
释放单个BIO,BIO_vfree
用于释放BIO链。
▮▮▮▮函数原型:int BIO_free(BIO *a);
void BIO_vfree(BIO *a);
⚝ BIO_read
:
▮▮▮▮从BIO中读取数据。
▮▮▮▮函数原型:int BIO_read(BIO *b, void *buf, int len);
⚝ BIO_write
:
▮▮▮▮向BIO中写入数据。
▮▮▮▮函数原型:int BIO_write(BIO *b, const void *buf, int len);
⚝ BIO_ctrl
:
▮▮▮▮执行BIO的控制命令,用于获取/设置BIO的状态或执行特定操作(例如:获取文件描述符、获取内存BIO中的数据、刷新缓冲区)。
▮▮▮▮函数原型:long BIO_ctrl(BIO *bp, int cmd, long larg, void *ptr);
⚝ BIO_push
/ BIO_pop
:
▮▮▮▮构建BIO链。BIO_push
将一个BIO添加到链的头部,BIO_pop
移除头部的BIO。SSL BIO通常通过BIO_push
添加到基础传输BIO(如套接字BIO)上。
▮▮▮▮函数原型:BIO *BIO_push(BIO *b, BIO *append);
BIO *BIO_pop(BIO *b);
Appendix A4: EVP高级加密接口
EVP (Envelope) 接口提供了一套高级、统一的API,用于执行对称加密、哈希、消息认证码、签名和验证等操作,它屏蔽了底层具体算法的差异,使得代码更具灵活性。
⚝ EVP_CIPHER_CTX
:
▮▮▮▮对称加密/解密上下文的数据结构。用于存储算法、密钥、IV、模式等信息。
⚝ EVP_CIPHER_CTX_new
/ EVP_CIPHER_CTX_free
:
▮▮▮▮创建和释放EVP_CIPHER_CTX
对象。
▮▮▮▮函数原型:EVP_CIPHER_CTX *EVP_CIPHER_CTX_new(void);
void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *c);
⚝ EVP_CipherInit_ex
:
▮▮▮▮初始化加密/解密上下文。需要指定上下文、加密算法(如EVP_aes_256_cbc()
)、提供者(provider,OpenSSL 3.0+)、密钥、IV和操作模式(加密或解密)。
▮▮▮▮函数原型:int EVP_CipherInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, ENGINE *impl, const unsigned char *key, const unsigned char *iv, int enc);
⚝ EVP_CipherUpdate
:
▮▮▮▮处理待加密/解密的数据块。可以多次调用。
▮▮▮▮函数原型:int EVP_CipherUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl, const unsigned char *in, int inl);
⚝ EVP_CipherFinal_ex
:
▮▮▮▮完成加密/解密过程,处理最后的数据块(包括填充 Padding)。
▮▮▮▮函数原型:int EVP_CipherFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm, int *outl);
⚝ EVP_MD_CTX
:
▮▮▮▮哈希计算或消息认证码(HMAC)上下文的数据结构。
⚝ EVP_MD_CTX_new
/ EVP_MD_CTX_free
:
▮▮▮▮创建和释放EVP_MD_CTX
对象。
▮▮▮▮函数原型:EVP_MD_CTX *EVP_MD_CTX_new(void);
void EVP_MD_CTX_free(EVP_MD_CTX *ctx);
⚝ EVP_DigestInit_ex
:
▮▮▮▮初始化哈希计算或HMAC上下文。需要指定哈希算法(如EVP_sha256()
)或HMAC密钥。
▮▮▮▮函数原型:int EVP_DigestInit_ex(EVP_MD_CTX *ctx, const EVP_MD *type, ENGINE *impl);
⚝ EVP_DigestUpdate
:
▮▮▮▮处理待哈希计算的数据块。可以多次调用。
▮▮▮▮函数原型:int EVP_DigestUpdate(EVP_MD_CTX *ctx, const void *d, size_t cnt);
⚝ EVP_DigestFinal_ex
:
▮▮▮▮完成哈希计算,获取最终的哈希值。
▮▮▮▮函数原型:int EVP_DigestFinal_ex(EVP_MD_CTX *ctx, unsigned char *md, unsigned int *s);
⚝ EVP_PKEY
:
▮▮▮▮通用私钥/公钥数据结构。EVP接口通过EVP_PKEY
来统一处理不同类型的密钥(RSA, ECC, HMAC密钥等)。
⚝ EVP_PKEY_new
/ EVP_PKEY_free
:
▮▮▮▮创建和释放EVP_PKEY
对象。
▮▮▮▮函数原型:EVP_PKEY *EVP_PKEY_new(void);
void EVP_PKEY_free(EVP_PKEY *pkey);
⚝ EVP_PKEY_generate
(OpenSSL 3.0+):
▮▮▮▮使用Provider生成新的EVP_PKEY
(密钥对)。
▮▮▮▮函数原型示例:EVP_PKEY *EVP_PKEY_generate(EVP_PKEY_CTX *ctx);
⚝ EVP_SignInit_ex
/ EVP_SignUpdate
/ EVP_SignFinal
:
▮▮▮▮使用EVP接口执行数字签名操作。类似于哈希函数的流程,但需要提供用于签名的私钥 (EVP_PKEY
)。
▮▮▮▮函数原型示例:
▮▮▮▮int EVP_SignInit_ex(EVP_MD_CTX *ctx, const EVP_MD *type, ENGINE *impl);
▮▮▮▮int EVP_SignUpdate(EVP_MD_CTX *ctx, const void *d, size_t cnt);
▮▮▮▮int EVP_SignFinal(EVP_MD_CTX *ctx, unsigned char *sigret, unsigned int *siglen, EVP_PKEY *pkey);
⚝ EVP_VerifyInit_ex
/ EVP_VerifyUpdate
/ EVP_VerifyFinal
:
▮▮▮▮使用EVP接口执行数字签名验证操作。类似于哈希函数的流程,但需要提供用于验证的公钥 (EVP_PKEY
) 和待验证的签名值。
▮▮▮▮函数原型示例:
▮▮▮▮int EVP_VerifyInit_ex(EVP_MD_CTX *ctx, const EVP_MD *type, ENGINE *impl);
▮▮▮▮int EVP_VerifyUpdate(EVP_MD_CTX *ctx, const void *d, size_t cnt);
▮▮▮▮int EVP_VerifyFinal(EVP_MD_CTX *ctx, const unsigned char *sigbuf, unsigned int siglen, EVP_PKEY *pkey);
Appendix A5: 加密算法特定的函数 (非EVP)
尽管推荐使用EVP接口,但了解一些底层算法特定的函数对于理解OpenSSL内部工作原理或处理特定需求仍然有益。
⚝ RSA
:
▮▮▮▮RSA密钥和操作的数据结构。
⚝ RSA_new
/ RSA_free
:
▮▮▮▮创建和释放RSA
对象。
▮▮▮▮函数原型:RSA *RSA_new(void);
void RSA_free(RSA *r);
⚝ RSA_generate_key_ex
:
▮▮▮▮生成RSA密钥对。
▮▮▮▮函数原型:int RSA_generate_key_ex(RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb);
(bits为密钥长度,e为公钥指数)
⚝ RSA_public_encrypt
/ RSA_private_decrypt
:
▮▮▮▮使用RSA公钥加密或私钥解密数据。
▮▮▮▮函数原型示例:
▮▮▮▮int RSA_public_encrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
▮▮▮▮int RSA_private_decrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
⚝ RSA_private_encrypt
/ RSA_public_decrypt
:
▮▮▮▮使用RSA私钥加密或公钥解密数据,主要用于签名。
▮▮▮▮函数原型示例:
▮▮▮▮int RSA_private_encrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
▮▮▮▮int RSA_public_decrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
⚝ RSA_sign
/ RSA_verify
:
▮▮▮▮使用RSA私钥对哈希值进行签名,或使用公钥验证签名。
▮▮▮▮函数原型示例:
▮▮▮▮int RSA_sign(int type, const unsigned char *m, unsigned int m_len, unsigned char *sigret, unsigned int *siglen, RSA *rsa);
(type 指定哈希算法)
▮▮▮▮int RSA_verify(int type, const unsigned char *m, unsigned int m_len, const unsigned char *sigbuf, unsigned int siglen, RSA *rsa);
⚝ EC_KEY
:
▮▮▮▮椭圆曲线(ECC)密钥和操作的数据结构。
⚝ EC_KEY_new
/ EC_KEY_free
:
▮▮▮▮创建和释放EC_KEY
对象。
▮▮▮▮函数原型:EC_KEY *EC_KEY_new(void);
void EC_KEY_free(EC_KEY *key);
⚝ EC_KEY_generate_key
:
▮▮▮▮生成ECC密钥对。需要先设置曲线群组。
▮▮▮▮函数原型:int EC_KEY_generate_key(EC_KEY *key);
⚝ ECDSA_sign
/ ECDSA_verify
:
▮▮▮▮使用ECC私钥(ECDSA)对哈希值进行签名,或使用公钥验证签名。
▮▮▮▮函数原型示例:
▮▮▮▮int ECDSA_sign(int type, const unsigned char *dgst, int dgstlen, unsigned char *sig, unsigned int *siglen, EC_KEY *eckey);
▮▮▮▮int ECDSA_verify(int type, const unsigned char *dgst, int dgstlen, const unsigned char *sig, int siglen, EC_KEY *eckey);
Appendix A6: 证书(X.509)管理
处理数字证书是PKI (Public Key Infrastructure) 的核心,OpenSSL提供了丰富的函数来解析、验证、创建和管理X.509证书。
⚝ X509
:
▮▮▮▮表示一个X.509证书的数据结构。
⚝ X509_new
/ X509_free
:
▮▮▮▮创建和释放X509
对象。
▮▮▮▮函数原型:X509 *X509_new(void);
void X509_free(X509 *a);
⚝ X509_NAME
:
▮▮▮▮表示X.509证书中的主体(Subject)或颁发者(Issuer)名称的数据结构。
⚝ X509_get_subject_name
/ X509_get_issuer_name
:
▮▮▮▮获取证书的主体或颁发者名称。
▮▮▮▮函数原型:X509_NAME *X509_get_subject_name(const X509 *a);
X509_NAME *X509_get_issuer_name(const X509 *a);
⚝ X509_NAME_print_ex
:
▮▮▮▮将X509_NAME
结构打印成字符串。
▮▮▮▮函数原型:int X509_NAME_print_ex(BIO *out, const X509_NAME *nm, int indent, unsigned long flags);
⚝ X509_get_pubkey
:
▮▮▮▮从证书中提取公钥,并返回一个EVP_PKEY
对象。
▮▮▮▮函数原型:EVP_PKEY *X509_get_pubkey(X509 *x);
⚝ X509_STORE
:
▮▮▮▮证书信任存储区的数据结构,用于存放信任锚(Trust Anchor,如根证书)和中间证书,以便验证证书链。
⚝ X509_STORE_new
/ X509_STORE_free
:
▮▮▮▮创建和释放X509_STORE
对象。
▮▮▮▮函数原型:X509_STORE *X509_STORE_new(void);
void X509_STORE_free(X509_STORE *v);
⚝ X509_STORE_add_cert
:
▮▮▮▮将一个证书添加到信任存储区。通常用于添加信任根证书或中间CA证书。
▮▮▮▮函数原型:int X509_STORE_add_cert(X509_STORE *ctx, X509 *x);
⚝ X509_STORE_load_locations
:
▮▮▮▮从文件或目录加载信任证书到存储区。
▮▮▮▮函数原型:int X509_STORE_load_locations(X509_STORE *ctx, const char *file, const char *dir);
⚝ X509_STORE_CTX
:
▮▮▮▮证书验证上下文的数据结构。包含待验证证书、信任存储、验证选项等信息。
⚝ X509_STORE_CTX_new
/ X509_STORE_CTX_free
:
▮▮▮▮创建和释放X509_STORE_CTX
对象。创建时需要指定信任存储、待验证证书和证书链(如果存在)。
▮▮▮▮函数原型:X509_STORE_CTX *X509_STORE_CTX_new(X509_STORE *store, X509 *x509, STACK_OF(X509) *chain);
void X509_STORE_CTX_free(X509_STORE_CTX *ctx);
⚝ X509_verify_cert
:
▮▮▮▮执行证书链验证。这是证书验证的核心函数。
▮▮▮▮函数原型:int X509_verify_cert(X509_STORE_CTX *ctx);
⚝ X509_REQ
:
▮▮▮▮表示证书签名请求(CSR - Certificate Signing Request)的数据结构。
⚝ X509_REQ_new
/ X509_REQ_free
:
▮▮▮▮创建和释放X509_REQ
对象。
▮▮▮▮函数原型:X509_REQ *X509_REQ_new(void);
void X509_REQ_free(X509_REQ *a);
Appendix A7: PEM/DER 编解码
PEM和DER是常用的证书、密钥等密码学对象的编码格式。OpenSSL提供了一系列函数用于在这些格式和内存中的结构体之间转换。
⚝ PEM_read_bio_PrivateKey
/ PEM_write_bio_PrivateKey
:
▮▮▮▮从BIO读取/向BIO写入PEM格式的私钥。
▮▮▮▮函数原型示例:EVP_PKEY *PEM_read_bio_PrivateKey(BIO *bp, EVP_PKEY **x, pem_password_cb *cb, void *arg);
int PEM_write_bio_PrivateKey(BIO *bp, EVP_PKEY *x, const EVP_CIPHER *enc, unsigned char *kstr, int klen, pem_password_cb *cb, void *arg);
⚝ PEM_read_bio_PUBKEY
/ PEM_write_bio_PUBKEY
:
▮▮▮▮从BIO读取/向BIO写入PEM格式的公钥。
▮▮▮▮函数原型示例:EVP_PKEY *PEM_read_bio_PUBKEY(BIO *bp, EVP_PKEY **x, pem_password_cb *cb, void *arg);
int PEM_write_bio_PUBKEY(BIO *bp, EVP_PKEY *x);
⚝ PEM_read_bio_X509
/ PEM_write_bio_X509
:
▮▮▮▮从BIO读取/向BIO写入PEM格式的X.509证书。
▮▮▮▮函数原型示例:X509 *PEM_read_bio_X509(BIO *bp, X509 **x, pem_password_cb *cb, void *arg);
int PEM_write_bio_X509(BIO *bp, X509 *x);
⚝ d2i_PrivateKey_bio
/ i2d_PrivateKey_bio
:
▮▮▮▮从BIO读取/向BIO写入DER格式的私钥。前缀d2i
表示 "DER to internal",i2d
表示 "internal to DER"。
▮▮▮▮函数原型示例:EVP_PKEY *d2i_PrivateKey_bio(BIO *bp, EVP_PKEY **a);
int i2d_PrivateKey_bio(BIO *bp, EVP_PKEY *a);
⚝ d2i_PUBKEY_bio
/ i2d_PUBKEY_bio
:
▮▮▮▮从BIO读取/向BIO写入DER格式的公钥。
▮▮▮▮函数原型示例:EVP_PKEY *d2i_PUBKEY_bio(BIO *bp, EVP_PKEY **a);
int i2d_PUBKEY_bio(BIO *bp, EVP_PKEY *a);
⚝ d2i_X509_bio
/ i2d_X509_bio
:
▮▮▮▮从BIO读取/向BIO写入DER格式的X.509证书。
▮▮▮▮函数原型示例:X509 *d2i_X509_bio(BIO *bp, X509 **a);
int i2d_X509_bio(BIO *bp, X509 *a);
Appendix A8: TLS/SSL协议函数
这些函数是实现TLS/SSL客户端和服务器端通信的核心。
⚝ SSL_CTX
:
▮▮▮▮SSL/TLS连接上下文的数据结构。包含协议版本、密码套件、证书、私钥、会话选项、回调函数等配置信息。一个SSL_CTX
对象可以用于创建多个SSL
对象(连接)。
⚝ SSL_CTX_new
/ SSL_CTX_free
:
▮▮▮▮创建和释放SSL_CTX
对象。创建时需要指定SSL方法(如TLS_server_method()
, TLS_client_method()
)。
▮▮▮▮函数原型:SSL_CTX *SSL_CTX_new(const SSL_METHOD *method);
void SSL_CTX_free(SSL_CTX *a);
⚝ SSL_CTX_set_min_proto_version
/ SSL_CTX_set_max_proto_version
:
▮▮▮▮设置允许的最小/最大TLS协议版本,以禁用不安全的旧版本(如TLS 1.0/1.1)。
▮▮▮▮函数原型:int SSL_CTX_set_min_proto_version(SSL_CTX *ctx, int version);
int SSL_CTX_set_max_proto_version(SSL_CTX *ctx, int version);
(version 例如 TLS1_2_VERSION
, TLS1_3_VERSION
)
⚝ SSL_CTX_set_cipher_list
:
▮▮▮▮设置允许使用的密码套件列表。这是提高TLS安全性的关键步骤。
▮▮▮▮函数原型:int SSL_CTX_set_cipher_list(SSL_CTX *ctx, const char *str);
(str 是一个OpenSSL格式的密码套件字符串,例如 "HIGH:!aNULL:!MD5")
⚝ SSL_CTX_load_verify_locations
/ SSL_CTX_set_default_verify_paths
:
▮▮▮▮指定用于验证对端证书的信任根证书文件或目录。
▮▮▮▮函数原型:int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile, const char *CApath);
int SSL_CTX_set_default_verify_paths(SSL_CTX *ctx);
⚝ SSL_CTX_use_certificate_file
/ SSL_CTX_use_PrivateKey_file
:
▮▮▮▮加载服务器端(或需要客户端证书认证时的客户端)的证书和私钥文件。
▮▮▮▮函数原型:int SSL_CTX_use_certificate_file(SSL_CTX *ctx, const char *file, int type);
(type 例如 SSL_FILETYPE_PEM
) int SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx, const char *file, int type);
⚝ SSL_CTX_set_verify
:
▮▮▮▮设置对端证书的验证模式(如强制验证 SSL_VERIFY_PEER
)和可选的验证回调函数。
▮▮▮▮函数原型:void SSL_CTX_set_verify(SSL_CTX *ctx, int mode, SSL_verify_cb callback);
⚝ SSL
:
▮▮▮▮表示一个独立的TLS/SSL连接的数据结构。每个客户端连接在服务器端或每个客户端发起的连接都需要一个SSL
对象。
⚝ SSL_new
/ SSL_free
:
▮▮▮▮从一个SSL_CTX
对象创建/释放一个SSL
对象。
▮▮▮▮函数原型:SSL *SSL_new(SSL_CTX *ctx);
void SSL_free(SSL *ssl);
⚝ SSL_set_bio
:
▮▮▮▮将SSL
对象与底层传输BIO(通常是套接字BIO)关联起来。
▮▮▮▮函数原型:void SSL_set_bio(SSL *ssl, BIO *rbio, BIO *wbio);
(rbio和wbio通常是同一个套接字BIO)
⚝ SSL_connect
:
▮▮▮▮在客户端发起TLS/SSL握手。
▮▮▮▮函数原型:int SSL_connect(SSL *ssl);
⚝ SSL_accept
:
▮▮▮▮在服务器端接受客户端的TLS/SSL连接并进行握手。
▮▮▮▮函数原型:int SSL_accept(SSL *ssl);
⚝ SSL_read
:
▮▮▮▮从TLS/SSL连接中读取应用层数据。解密过程在此函数内部完成。
▮▮▮▮函数原型:int SSL_read(SSL *ssl, void *buf, int num);
⚝ SSL_write
:
▮▮▮▮向TLS/SSL连接中写入应用层数据。加密过程在此函数内部完成。
▮▮▮▮函数原型:int SSL_write(SSL *ssl, const void *buf, int num);
⚝ SSL_get_error
:
▮▮▮▮在SSL_connect
, SSL_accept
, SSL_read
, SSL_write
等函数返回非1(成功)或非负(阻塞)时,获取具体的错误类型,以便进行错误处理(例如:判断是致命错误还是需要重试的阻塞操作)。
▮▮▮▮函数原型:int SSL_get_error(const SSL *ssl, int ret);
(ret 是之前SSL函数调用的返回值)
⚝ SSL_shutdown
:
▮▮▮▮优雅地关闭TLS/SSL连接。涉及一个双向的关闭握手。
▮▮▮▮函数原型:int SSL_shutdown(SSL *ssl);
⚝ SSL_get_peer_certificate
:
▮▮▮▮获取对端(客户端在服务器端,或服务器端在客户端)的X.509证书。返回的证书需要调用X509_free
释放。
▮▮▮▮函数原型:X509 *SSL_get_peer_certificate(const SSL *ssl);
⚝ SSL_get_version
/ SSL_get_cipher
:
▮▮▮▮获取当前连接使用的TLS/SSL协议版本和密码套件。
▮▮▮▮函数原型示例:const char *SSL_get_version(const SSL *ssl);
const SSL_CIPHER *SSL_get_current_cipher(const SSL *ssl);
Appendix A9: 内存管理助手函数
OpenSSL有自己的内存管理函数,推荐在处理OpenSSL对象时使用它们,以便于内存追踪和可能存在的自定义内存分配器。
⚝ OPENSSL_malloc
/ OPENSSL_free
:
▮▮▮▮OpenSSL的内存分配和释放函数。
▮▮▮▮函数原型示例:void *OPENSSL_malloc(size_t num);
void OPENSSL_free(void *addr);
本附录仅列出了部分常用函数和数据结构,旨在提供快速参考。在实际开发中,读者应结合OpenSSL官方文档,深入理解每个函数的功能、参数、返回值和错误处理方式。
Appendix B: 常见错误码与排查
在使用 OpenSSL 库进行 C++ 开发时,遇到错误是不可避免的。理解 OpenSSL 的错误处理机制以及如何诊断和排查这些错误,对于构建稳定可靠的安全应用程序至关重要。本附录旨在深入解析 OpenSSL 的错误处理方式,并提供常见的错误码解释和实用的排查技巧。
Appendix B1: OpenSSL错误栈(Error Stack)机制
与其他许多库不同,OpenSSL 使用一个错误栈(Error Stack)来记录发生错误的上下文信息。当库内部发生错误时,相关的错误信息会被“压入”(push)到一个线程局部(thread-local)的错误栈中。一个错误通常包含以下几个关键组成部分:
① 函数库(Library):指示错误发生的函数库,例如 SSL 库(SSL)、加密库(Crypto,通常表示 libcrypto 内部的通用错误)等。
② 功能(Function):指示错误发生的具体功能或函数,例如 SSL_accept、EVP_EncryptUpdate 等。
③ 原因(Reason):一个数值码,表示错误发生的具体原因,例如 SSL_R_BAD_RSA_ENCRYPT、EVP_R_BAD_DECRYPT 等。
④ 文件(File)和行号(Line):指示错误发生的源代码文件和行号(如果编译时包含调试信息)。
一个错误栈可能包含多个错误条目,通常最上面的错误条目是最近发生的错误,而下面的条目则提供了导致该错误的更早的上下文信息。这种机制允许开发者追踪错误的根源,特别是在一个函数调用序列中发生错误时。
在使用 OpenSSL 时,一旦函数返回指示错误的特定值(通常是 0、-1 或空指针),就应该立即检查错误栈来获取详细信息。如果在同一个线程中进行了多次 OpenSSL 调用且没有及时检查错误栈,新的错误信息可能会覆盖或压入旧的错误信息,使得追溯困难。
Appendix B2: 获取和打印错误信息
OpenSSL 提供了一系列函数来访问和处理错误栈。最常用的函数包括:
① ERR_get_error()
:这个函数从当前线程的错误栈中“弹出”(pop)最上面的一个错误条目,并返回其错误码。如果错误栈为空,则返回 0。重复调用此函数可以遍历并清空错误栈中的所有错误。
② ERR_peek_error()
:与 ERR_get_error()
类似,但它只查看(peek)最上面的错误,而不将其从栈中移除。
③ ERR_peek_last_error()
:查看错误栈中最底部的(最早的)错误,也不从栈中移除。
④ ERR_error_string(unsigned long e, char *buf)
:将一个错误码 e
转换为可读的字符串。buf
参数如果非空,则用于存储字符串,否则函数内部会使用静态缓冲区(需要注意线程安全问题,尽管较新版本的 OpenSSL 在内部处理了)。
⑤ ERR_error_string_lnr(unsigned long e, char *buf, int len)
:这是一个更安全的版本,它将错误码 e
转换为字符串,包括函数库、功能和原因。它会将结果写入 buf
中,最多写入 len
字节。这个函数是推荐的错误字符串获取方式。
⑥ ERR_print_errors(BIO *bp)
:这个函数会清空当前线程的整个错误栈,并将每个错误条目以可读的格式打印到指定的 BIO 对象 bp
中。这是一个非常方便的调试函数,可以直接将错误信息打印到标准错误输出(使用 BIO_new_fp(stderr, BIO_FP_CACHES)
或 BIO_new_fd(STDERR_FILENO, BIO_NOCLOSE)
创建的 BIO)。
⑦ ERR_print_errors_fp(FILE *fp)
:类似于 ERR_print_errors
,但直接打印到 FILE
指针,例如 stderr
。
示例代码:获取并打印错误
1
#include <openssl/err.h>
2
#include <openssl/bio.h>
3
#include <cstdio> // For stderr
4
5
// ... some OpenSSL function calls that might fail ...
6
7
unsigned long errCode;
8
while ((errCode = ERR_get_error()) != 0) {
9
char errString[256];
10
ERR_error_string_lnr(errCode, errString, sizeof(errString));
11
fprintf(stderr, "OpenSSL Error: %s\n", errString);
12
}
13
14
// 或者使用 ERR_print_errors_fp
15
// BIO *bio_err = BIO_new_fp(stderr, BIO_FP_CACHES);
16
// if (bio_err) {
17
// ERR_print_errors(bio_err);
18
// BIO_free(bio_err);
19
// }
20
// 或者更简单的直接打印到 stderr
21
// ERR_print_errors_fp(stderr);
注意:ERR_get_error()
和 ERR_print_errors
/ERR_print_errors_fp
都会清空错误栈。如果在获取错误信息后不立即处理,后续的 OpenSSL 调用可能会将错误栈中的信息覆盖掉。因此,最佳实践是在一个可能失败的 OpenSSL 调用后立即检查并处理错误。
Appendix B3: 常见错误码分类与解释
OpenSSL 的错误码是一个 unsigned long
类型的值,它编码了函数库、功能和原因信息。虽然具体的错误码数值可能难以记忆,但通过 ERR_error_string_lnr
函数或查阅 OpenSSL 官方文档,可以获得详细的文本解释。
OpenSSL 的错误码通常具有固定的格式:ERR_PACK(lib, func, reason)
,其中 lib
是库 ID,func
是功能 ID,reason
是原因 ID。这些 ID 在 OpenSSL 的头文件中定义为宏,例如 SSL_F_SSL_ACCEPT
(SSL 库中的 SSL_accept 功能)、SSL_R_SSLV3_ALERT_HANDSHAKE_FAILURE
(SSL 库中的一个原因:SSLv3 握手失败警报)。
以下是一些在不同 OpenSSL 功能中常见的错误类别和示例错误码:
Appendix B3.1 SSL/TLS 错误
这些错误通常发生在 SSL/TLS 连接的建立(握手)、数据传输或关闭过程中。错误码通常以 SSL_R_
(Reason)开头。
⚝ SSL_ERROR_WANT_READ
/ SSL_ERROR_WANT_WRITE
:这不是一个真正的错误,而是指示底层的 BIO 需要进行读或写操作才能继续当前的 SSL 操作(例如握手或数据传输)。这在非阻塞(non-blocking)I/O 中非常常见,意味着应该等待套接字可读或可写后重试当前的 SSL 操作(如 SSL_accept
, SSL_connect
, SSL_read
, SSL_write
)。
⚝ SSL_ERROR_SSL
:一个通用的 SSL 错误。需要检查错误栈来获取更具体的原因码(SSL_R_*
)。
⚝ SSL_ERROR_SYSCALL
:指示发生了底层的系统调用错误(例如套接字操作失败)。需要检查错误栈以及全局的 errno
来确定具体的系统错误原因。
⚝ SSL_ERROR_ZERO_RETURN
:指示 TLS/SSL 连接已正常关闭。在使用 SSL_read
时返回 0 表示连接关闭,而不是错误。
⚝ SSL_R_SSLV3_ALERT_HANDSHAKE_FAILURE
:TLS/SSL 握手失败警报。可能的原因包括客户端和服务器无法协商共同支持的密码套件、协议版本、证书问题等。
⚝ SSL_R_PEER_CERTIFICATE_VERIFICATION_FAILED
:对等方(peer)的证书验证失败。可能的原因包括证书过期、颁发者不受信任、主机名不匹配、证书链不完整等。
⚝ SSL_R_NO_SHARED_CIPHER
:客户端和服务器没有共同支持的密码套件(cipher suite)。
⚝ SSL_R_PROTOCOL_IS_SHUTDOWN
:试图在一个已经关闭的连接上进行读写操作。
Appendix B3.2 Crypto 错误
这些错误通常发生在加密、解密、哈希、签名、密钥管理、证书解析等操作中。错误码可能以 EVP_R_
, RSA_R_
, X509_R_
, PEM_R_
等开头。
⚝ ERR_R_MALLOC_FAILURE
:内存分配失败。
⚝ EVP_R_BAD_DECRYPT
:解密操作失败。可能原因包括使用了错误的密钥、IV 或加密算法/模式,或者密文已被篡改。
⚝ EVP_R_UNSUPPORTED_CIPHER
/ EVP_R_UNSUPPORTED_DIGEST
:使用了 OpenSSL 版本不支持的加密算法或哈希算法。
⚝ RSA_R_PADDING_CHECK_FAILED
:RSA 解密或签名验证时,填充(padding)检查失败。通常意味着密文已被篡改或使用了错误的填充模式。
⚝ PEM_R_UNSUPPORTED_ENCRYPTION
:PEM 文件使用了不受支持的加密方式(例如私钥文件被加密)。
⚝ X509_R_CERTIFICATE_VERIFICATION_ERROR
:证书验证失败。通常需要结合错误栈中更详细的 X509_V_ERR_*
错误码来确定具体原因(例如 X509_V_ERR_CERT_HAS_EXPIRED
, X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY
)。
Appendix B3.3 BIO 和系统错误
与 I/O 操作或底层系统相关的错误。
⚝ BIO_R_CONNECT_ERROR
:BIO 连接错误,通常是底层网络连接失败。
⚝ BIO_R_READ_ERROR
/ BIO_R_WRITE_ERROR
:BIO 读/写错误,通常是底层文件或套接字读写失败。
⚝ SYS_R_OS_ERROR
:一个通用的系统级错误,需要检查全局 errno
。
Appendix B4: 错误排查建议与技巧
当 OpenSSL 函数返回错误时, following steps and techniques can help diagnose the issue:
① 立即检查错误栈(Check the Error Stack Immediately):这是最重要的一步。在任何可能返回错误的 OpenSSL 函数调用后,立即调用 ERR_get_error()
循环获取所有错误码,或直接使用 ERR_print_errors_fp(stderr)
将错误信息打印到标准错误输出。不要在错误发生后进行其他可能影响错误栈的 OpenSSL 调用。
② 分析错误信息(Analyze the Error Output):OpenSSL 错误字符串(通过 ERR_error_string_lnr
或 ERR_print_errors
获取)通常包含库、功能和原因。例如,“error:06065064:digital envelope routines:EVP_CIPHER_CTX_ctrl:BAD_LENGTH”。
⚝ 06
:库 ID(Crypto)。
⚝ 065
:功能 ID(digital envelope routines 下的 EVP_CIPHER_CTX_ctrl 函数)。
⚝ 064
:原因 ID(BAD_LENGTH)。
⚝ 文本部分提供了更直观的解释。对照 OpenSSL 官方文档(特别是 ERR_reason_codes(3)
手册页)可以获得更详细的错误原因说明。
③ 检查函数返回值(Check Function Return Values):OpenSSL 函数通常有明确的成功和失败返回值约定。例如,许多函数成功时返回 1,失败时返回 0;或者成功时返回有效指针,失败时返回 NULL
。在调用 OpenSSL 函数后,务必检查其返回值以确定是否发生了错误。
④ 理解 OpenSSL 对象生命周期(Understand OpenSSL Object Lifecycle):许多 OpenSSL 对象(如 EVP_CIPHER_CTX
, RSA
, X509
, SSL_CTX
, SSL
)需要通过特定的 _new
或 _init
函数创建,并在使用完毕后通过对应的 _free
函数释放。内存管理错误(如双重释放或未初始化使用)可能导致崩溃或难以诊断的错误。使用 RAII (Resource Acquisition Is Initialization) 封装 OpenSSL 对象是 C++ 开发中的推荐做法。
⑤ 注意线程安全(Pay Attention to Thread Safety):OpenSSL 库本身不是完全线程安全的,尤其是在多线程环境中进行全局初始化、错误处理或使用某些特定的非线程安全函数时。在 OpenSSL 1.1.0 版本之前,需要设置线程回调函数(如 CRYPTO_set_locking_callback
)。在 OpenSSL 1.1.0 及更高版本中,情况有所改善,但仍然需要注意并发访问共享对象的问题。错误的线程同步可能导致数据损坏或错误。
⑥ 检查输入参数(Verify Input Parameters):确保传递给 OpenSSL 函数的参数是有效的、正确的类型和长度。例如,密钥长度、IV 长度、缓冲区大小等。
⑦ 分阶段调试(Debug in Stages):将复杂的 OpenSSL 操作分解为更小的步骤。例如,对于 TLS/SSL 连接,先确认底层套接字连接成功,再逐步调试 SSL 上下文创建、证书加载、SSL 对象创建、握手(SSL_connect
/SSL_accept
)、数据读写(SSL_read
/SSL_write
)等过程。
⑧ 查阅文档和社区资源(Consult Documentation and Community Resources):OpenSSL 官方文档(manual pages)是获取详细函数信息、错误码解释和使用示例的最佳资源。此外,OpenSSL 邮件列表、Stack Overflow 等社区平台也有大量关于 OpenSSL 错误排查的讨论。
⑨ 使用调试构建(Use Debug Builds):如果可能,使用带有调试信息和更详细日志输出的 OpenSSL 库版本进行开发和测试。这有助于在崩溃或异常发生时获取更多上下文信息。
掌握 OpenSSL 的错误处理机制并熟练运用错误排查技巧,能够显著提高开发效率,并确保使用 OpenSSL 构建的应用程序的稳定性和安全性。
Appendix C: 代码示例与说明
本书在各个章节中穿插了大量的代码示例,旨在帮助读者理解 OpenSSL API 的用法,并将理论知识应用于实践。本附录将系统地整理本书中出现的关键代码示例,提供其列表、所在章节以及简要说明,方便读者查阅、理解和实践。
Appendix C.1: 引言与代码约定
🔑 代码是理论的最佳实践载体。本书中的每一个代码示例都经过精心设计,力求简洁、清晰,并突出所讲解的核心概念和 OpenSSL API。
📖 本附录的目的:
⚝ 提供本书代码示例的集中索引,方便读者快速定位。
⚝ 简要回顾每个示例所演示的核心功能和 OpenSSL API。
⚝ 强调代码示例在学习过程中的重要性,鼓励读者亲手实践。
📘 代码约定:
本书中的代码示例遵循以下约定:
① 使用标准的 C++ 语法和习惯。
② 注释(comment)会用于解释关键步骤和 OpenSSL 函数的用途。
③ 错误处理(error handling)是安全编程的关键部分,示例代码会包含 OpenSSL 特有的错误检查和报告机制(如使用 ERR_print_errors_fp
)。
④ 资源清理(resource cleanup)至关重要,示例中会演示如何正确释放 OpenSSL 分配的内存和结构体,避免内存泄漏。
⑤ 为了突出核心逻辑,某些示例可能会简化 I/O 操作或异常处理,但在实际项目中应使用更健壮(robust)的方法。
Appendix C.2: 开发环境与OpenSSL基础代码示例
这些示例主要出现在本书的第 2 章和第 3 章,帮助读者迈出 OpenSSL 开发的第一步。
① 示例名称: OpenSSL 初始化与清理
▮▮▮▮所在章节: 第 2 章 开发环境搭建与OpenSSL库初探 -> 2.4 OpenSSL的初始化与清理
▮▮▮▮核心功能: 演示 OpenSSL 全局库的初始化和清理过程。
▮▮▮▮关键API: OPENSSL_init_ssl
或 SSL_library_init
, SSL_load_error_strings
(旧版本), OPENSSL_cleanup
。
▮▮▮▮说明: 在使用 OpenSSL 的任何加密或 SSL/TLS 功能之前,通常需要先初始化库。此示例展示了如何在程序的生命周期内正确地执行初始化和清理操作,避免资源泄露或未定义行为。
② 示例名称: OpenSSL 错误信息获取与打印
▮▮▮▮所在章节: 第 3 章 核心加密概念与OpenSSL基础 -> 3.3 OpenSSL的错误处理机制
▮▮▮▮核心功能: 演示如何捕获 OpenSSL 内部产生的错误,并将其转换为可读的字符串。
▮▮▮▮关键API: ERR_get_error
, ERR_error_string
, ERR_print_errors_fp
。
▮▮▮▮说明: OpenSSL API 在执行失败时,会将错误信息压入一个线程本地的错误栈(error stack)。此示例讲解如何从栈中获取错误码,并利用 ERR_error_string
或更方便的 ERR_print_errors_fp
函数打印详细的错误原因,这对于调试(debugging) OpenSSL 应用至关重要。
③ 示例名称: 使用内存 BIO
▮▮▮▮所在章节: 第 3 章 核心加密概念与OpenSSL基础 -> 3.4 BIO I/O抽象层
▮▮▮▮核心功能: 演示如何使用内存 BIO (Memory BIO) 在内存中进行数据的读写操作,这常用于在不同的 OpenSSL 函数之间传递数据。
▮▮▮▮关键API: BIO_new
, BIO_s_mem
, BIO_write
, BIO_read
, BIO_free
。
▮▮▮▮说明: BIO 是 OpenSSL 提供的一个抽象 I/O 层。内存 BIO 是一种非常有用的 BIO 类型,允许开发者在内存缓冲区而不是文件或套接字(socket)上执行 I/O 操作。此示例展示了如何创建内存 BIO,向其中写入数据,然后从中读出数据。
Appendix C.3: 对称加密与哈希示例
这些示例集中在本书的第 4 章,涵盖了使用 OpenSSL 的 EVP 接口进行对称加密和哈希计算。
① 示例名称: 使用 EVP 接口进行 AES-256-CBC 加密与解密
▮▮▮▮所在章节: 第 4 章 对称加密与哈希函数 -> 4.2 对称加密算法与模式 -> 4.2.1 使用EVP接口进行对称加密
▮▮▮▮核心功能: 演示如何使用 EVP 接口、AES 算法和 CBC 工作模式对数据进行加密和解密。
▮▮▮▮关键API: EVP_CIPHER_CTX_new
, EVP_EncryptInit_ex
, EVP_EncryptUpdate
, EVP_EncryptFinal_ex
, EVP_DecryptInit_ex
, EVP_DecryptUpdate
, EVP_DecryptFinal_ex
, EVP_CIPHER_CTX_free
, EVP_aes_256_cbc
。
▮▮▮▮说明: EVP 接口提供了对多种加密算法和模式的统一访问。此示例是 EVP 对称加密的基础,演示了初始化上下文(context)、处理中间数据和完成最后一块数据的典型流程,以及如何正确处理填充(padding)。
② 示例名称: 密钥与 IV 生成与管理
▮▮▮▮所在章节: 第 4 章 对称加密与哈希函数 -> 4.2 对称加密算法与模式 -> 4.2.2 密钥(Key)与初始化向量(IV)管理
▮▮▮▮核心功能: 演示如何生成安全的随机密钥和初始化向量(IV)。
▮▮▮▮关键API: RAND_bytes
。
▮▮▮▮说明: 对称加密的安全取决于密钥的保密性和 IV 的不可预测性(对于某些模式)。此示例展示了如何使用 OpenSSL 的随机数生成器(Random Number Generator, RNG)生成满足加密要求的密钥和 IV。
③ 示例名称: 使用 EVP 接口计算 SHA-256 哈希值
▮▮▮▮所在章节: 第 4 章 对称加密与哈希函数 -> 4.3 哈希函数与消息认证码(HMAC) -> 4.3.1 使用EVP接口进行哈希计算
▮▮▮▮核心功能: 演示如何使用 EVP 接口计算数据的 SHA-256 哈希值。
▮▮▮▮关键API: EVP_MD_CTX_new
, EVP_DigestInit_ex
, EVP_DigestUpdate
, EVP_DigestFinal_ex
, EVP_MD_CTX_free
, EVP_sha256
。
▮▮▮▮说明: 哈希函数用于生成数据的数字“指纹”。此示例展示了使用 EVP 接口进行哈希计算的标准流程,与对称加密类似,包括初始化上下文、更新数据和获取最终摘要(digest)。
④ 示例名称: 使用 EVP 接口计算 HMAC-SHA256
▮▮▮▮所在章节: 第 4 章 对称加密与哈希函数 -> 4.3 哈希函数与消息认证码(HMAC) -> 4.3.2 使用EVP接口实现HMAC
▮▮▮▮核心功能: 演示如何使用 EVP 接口和密钥计算数据的 HMAC-SHA256 值。
▮▮▮▮关键API: EVP_PKEY_new_mac_key
, EVP_DigestSignInit
, EVP_DigestSignUpdate
, EVP_DigestSignFinal
, EVP_PKEY_free
, EVP_sha256
。
▮▮▮▮说明: HMAC (Hash-based Message Authentication Code) 是一种使用哈希函数和密钥来验证消息完整性和来源的机制。此示例展示了如何使用 EVP 接口生成 HMAC,这涉及到 EVP_PKEY
对象来承载密钥。
Appendix C.4: 非对称加密与密钥管理示例
这些示例主要出现在本书的第 5 章,讲解了非对称密钥的生成、加载、保存以及加解密操作。
① 示例名称: 生成 RSA 密钥对
▮▮▮▮所在章节: 第 5 章 非对称加密与密钥管理 -> 5.2 生成非对称密钥对 -> 5.2.1 生成RSA密钥对
▮▮▮▮核心功能: 演示如何使用 OpenSSL 生成 RSA 公钥和私钥对。
▮▮▮▮关键API: RSA_new
, BN_new
, RSA_generate_key_ex
, BN_free
, RSA_free
。
▮▮▮▮说明: RSA 是最常用的非对称加密算法之一。此示例展示了如何创建 RSA 对象,指定密钥长度(key length)和公开指数(public exponent),然后生成密钥对。
② 示例名称: 生成 ECC 密钥对
▮▮▮▮所在章节: 第 5 章 非对称加密与密钥管理 -> 5.2 生成非对称密钥对 -> 5.2.2 生成ECC密钥对
▮▮▮▮核心功能: 演示如何使用 OpenSSL 生成 ECC(Elliptic Curve Cryptography)密钥对。
▮▮▮▮关键API: EC_KEY_new_by_curve_name
, EC_KEY_generate_key
, EC_KEY_free
, NID_X9_62_prime256v1
(或其他曲线 OID)。
▮▮▮▮说明: ECC 提供了与 RSA 相似的安全强度,但密钥长度更短,计算效率更高。此示例展示了如何选择特定的椭圆曲线(elliptic curve)并生成相应的密钥对。
③ 示例名称: 将 RSA 私钥保存到 PEM 文件
▮▮▮▮所在章节: 第 5 章 非对称加密与密钥管理 -> 5.3 加载和保存密钥对 -> 5.3.3 保存密钥到文件
▮▮▮▮核心功能: 演示如何将生成的私钥以 PEM (Privacy Enhanced Mail) 格式保存到文件。
▮▮▮▮关键API: BIO_new_file
, PEM_write_bio_PrivateKey
, BIO_free
。
▮▮▮▮说明: PEM 是存储密钥和证书的常用文本格式。此示例展示了如何结合 BIO 文件接口和 PEM 写入函数将密钥对象写入文件。
④ 示例名称: 从 PEM 文件加载 RSA 私钥
▮▮▮▮所在章节: 第 5 章 非对称加密与密钥管理 -> 5.3 加载和保存密钥对 -> 5.3.1 加载私钥
▮▮▮▮核心功能: 演示如何从 PEM 格式的文件中读取私钥。
▮▮▮▮关键API: BIO_new_file
, PEM_read_bio_PrivateKey
, BIO_free
, EVP_PKEY_free
。
▮▮▮▮说明: 在实际应用中,密钥通常存储在文件中。此示例演示了读取 PEM 私钥文件并将其加载到 EVP_PKEY
结构体中,EVP_PKEY
是 OpenSSL 用于统一表示各种密钥类型(RSA, ECC, DSA 等)的重要结构体。
⑤ 示例名称: 使用 RSA 公钥加密数据
▮▮▮▮所在章节: 第 5 章 非对称加密与密钥管理 -> 5.4 使用密钥对进行数据加解密 -> 5.4.1 RSA加解密
▮▮▮▮核心功能: 演示如何使用 RSA 公钥对小块数据进行加密。
▮▮▮▮关键API: RSA_public_encrypt
, RSA_size
。
▮▮▮▮说明: RSA 加密通常用于加密对称密钥,而不是直接加密大量数据。此示例展示了调用 RSA_public_encrypt
函数所需的步骤,包括输入数据、公钥和填充模式(padding mode)。
⑥ 示例名称: 使用 RSA 私钥解密数据
▮▮▮▮所在章节: 第 5 章 非对称加密与密钥管理 -> 5.4 使用密钥对进行数据加解密 -> 5.4.1 RSA加解密
▮▮▮▮核心功能: 演示如何使用 RSA 私钥解密由对应公钥加密的数据。
▮▮▮▮关键API: RSA_private_decrypt
, RSA_size
。
▮▮▮▮说明: 与加密示例对应,此示例展示了解密过程,强调输入数据是固定长度(等于密钥模长,modulus size)的密文。
Appendix C.5: 数字签名与验证示例
这些示例位于本书的第 6 章,重点展示了如何使用 OpenSSL API 实现数字签名和验证功能。
① 示例名称: 使用 EVP 接口对数据进行签名 (RSA-SHA256)
▮▮▮▮所在章节: 第 6 章 数字签名与验证 -> 6.2 使用EVP接口进行签名
▮▮▮▮核心功能: 演示如何使用 EVP 接口、RSA 私钥和 SHA256 哈希算法对数据进行签名。
▮▮▮▮关键API: EVP_MD_CTX_new
, EVP_DigestSignInit
, EVP_DigestSignUpdate
, EVP_DigestSignFinal
, EVP_MD_CTX_free
, EVP_PKEY_new
, EVP_PKEY_assign_RSA
(或使用 EVP_PKEY_set1_RSA
)。
▮▮▮▮说明: 数字签名使用私钥对数据的哈希值进行加密(或更准确地说,是签名算法特有的操作),以验证数据的完整性和来源。此示例展示了 EVP 签名流程,该流程与 EVP 哈希计算或加密的流程类似,但需要一个私钥对象。
② 示例名称: 使用 EVP 接口验证签名 (RSA-SHA256)
▮▮▮▮所在章节: 第 6 章 数字签名与验证 -> 6.3 使用EVP接口进行签名验证
▮▮▮▮核心功能: 演示如何使用 EVP 接口、RSA 公钥、SHA256 哈希算法和原始数据来验证数字签名。
▮▮▮▮关键API: EVP_MD_CTX_new
, EVP_DigestVerifyInit
, EVP_DigestVerifyUpdate
, EVP_DigestVerifyFinal
, EVP_MD_CTX_free
, EVP_PKEY_new
, EVP_PKEY_assign_RSA
(或使用 EVP_PKEY_set1_RSA
)。
▮▮▮▮说明: 签名验证是签名过程的逆过程,使用公钥对签名值进行解密(或验证算法特有的操作),并与原始数据的哈希值进行比较。此示例展示了 EVP 验证流程,返回验证结果(成功、失败或错误)。
③ 示例名称: 使用 RSA_sign/verify 进行签名验证
▮▮▮▮所在章节: 第 6 章 数字签名与验证 -> 6.4 RSA签名与验证
▮▮▮▮核心功能: 演示使用更底层(但也更直接)的 RSA 函数进行签名和验证。
▮▮▮▮关键API: RSA_sign
, RSA_verify
, RSA_private_key_check
, RSA_public_key_check
。
▮▮▮▮说明: 虽然 EVP 接口是推荐使用的,但了解底层的 RSA 函数也很有益。此示例展示了直接使用 RSA_sign
对哈希值签名和 RSA_verify
验证签名。
④ 示例名称: 使用 ECDSA_sign/verify 进行签名验证
▮▮▮▮所在章节: 第 6 章 数字签名与验证 -> 6.5 ECC签名与验证(ECDSA)
▮▮▮▮核心功能: 演示使用 ECDSA 算法(ECC 的签名算法)进行签名和验证。
▮▮▮▮关键API: ECDSA_sign
, ECDSA_verify
, EC_KEY_check_key
。
▮▮▮▮说明: ECDSA 是 ECC 的标准数字签名算法。此示例展示了使用 ECDSA 函数对数据(通常是哈希值)进行签名和验证的过程。
Appendix C.6: 证书(X.509)管理示例
这些示例位于本书的第 7 章,涵盖了 X.509 证书的加载、解析、验证以及生成。
① 示例名称: 从 PEM 文件加载并解析 X.509 证书
▮▮▮▮所在章节: 第 7 章 证书(X.509)管理 -> 7.2 加载和解析证书
▮▮▮▮核心功能: 演示如何从文件读取 PEM 格式的 X.509 证书,并提取证书中的主题(subject)、颁发者(issuer)、有效期(validity)等信息。
▮▮▮▮关键API: BIO_new_file
, PEM_read_bio_X509
, X509_get_subject_name
, X509_get_issuer_name
, X509_NAME_print_ex
, X509_get_notBefore
, X509_get_notAfter
, ASN1_TIME_print
, X509_get_pubkey
, EVP_PKEY_free
, X509_free
, BIO_free
。
▮▮▮▮说明: X.509 证书是公钥基础设施(PKI)的核心组成部分。此示例展示了如何将证书文件加载到 X509
结构体中,并通过各种 X509_get_*
函数访问证书的属性。
② 示例名称: 验证 X.509 证书链
▮▮▮▮所在章节: 第 7 章 证书(X.509)管理 -> 7.3 证书链构建与验证
▮▮▮▮核心功能: 演示如何构建一个证书信任存储(certificate trust store),加载根证书或中间证书,然后验证给定的终端实体证书(end-entity certificate)是否有效且被信任。
▮▮▮▮关键API: X509_STORE_new
, X509_STORE_add_cert
, X509_STORE_load_locations
, X509_STORE_CTX_new
, X509_STORE_CTX_init
, X509_verify_cert
, X509_STORE_CTX_get_error
, X509_STORE_CTX_get_error_string
, X509_STORE_CTX_free
, X509_STORE_free
。
▮▮▮▮说明: 证书链验证是确认证书真实性和信任性的关键步骤。此示例展示了创建证书存储、添加信任锚(trust anchor)(如CA根证书),然后使用 X509_verify_cert
函数进行链验证和检查验证结果及错误信息。
③ 示例名称: 创建自签名 X.509 证书
▮▮▮▮所在章节: 第 7 章 证书(X.509)管理 -> 7.4 创建自签名证书
▮▮▮▮核心功能: 演示如何生成私钥并使用该私钥创建并签署一个自签名的 X.509 证书。
▮▮▮▮关键API: X509_new
, X509_set_version
, X509_set_subject_name
, X509_set_issuer_name
, X509_set_pubkey
, X509_set_notBefore
, X509_set_notAfter
, X509_sign
, X509_free
以及密钥生成和名称设置相关的 API。
▮▮▮▮说明: 自签名证书通常用于测试或内部应用,不被公共 CA 信任。此示例详细展示了创建 X509
对象,设置证书各个字段,关联公钥,设置有效期,并使用私钥进行签名的完整过程。
④ 示例名称: 生成证书签名请求 (CSR)
▮▮▮▮所在章节: 第 7 章 证书(X.509)管理 -> 7.5 生成证书签名请求(CSR)
▮▮▮▮核心功能: 演示如何生成私钥并创建证书签名请求(Certificate Signing Request),以便提交给 CA 颁发正式证书。
▮▮▮▮关键API: X509_REQ_new
, X509_REQ_set_version
, X509_REQ_set_subject_name
, X509_REQ_set_pubkey
, X509_REQ_sign
, X509_REQ_free
以及密钥生成和名称设置相关的 API。
▮▮▮▮说明: CSR 包含申请者的公钥和身份信息,并由申请者的私钥签名。此示例展示了创建 X509_REQ
对象,填充信息,关联公钥,并使用私钥签名的过程。
Appendix C.7: TLS/SSL协议客户端开发示例
这些示例是本书第 8 章的核心,展示了如何使用 OpenSSL 构建 TLS/SSL 客户端。
① 示例名称: 构建 SSL 上下文 (SSL_CTX)
▮▮▮▮所在章节: 第 8 章 TLS/SSL协议基础与客户端开发 -> 8.3 OpenSSL SSL上下文(SSL_CTX)
▮▮▮▮核心功能: 演示创建和配置 SSL_CTX
对象,它是 SSL/TLS 会话的配置模板。
▮▮▮▮关键API: SSL_CTX_new
, TLS_client_method
(或 SSLv23_client_method
- 旧版本), SSL_CTX_set_min_proto_version
, SSL_CTX_set_cipher_list
, SSL_CTX_load_verify_locations
, SSL_CTX_free
。
▮▮▮▮说明: SSL_CTX
存储了协议版本、密码套件(cipher suite)、证书信任存储、证书加载、各种选项和回调函数等配置信息。此示例展示了创建客户端上下文并进行一些基本配置。
② 示例名称: 创建 SSL 对象并关联 BIO
▮▮▮▮所在章节: 第 8 章 TLS/SSL协议基础与客户端开发 -> 8.4 OpenSSL SSL对象(SSL)
▮▮▮▮核心功能: 演示如何基于 SSL_CTX
创建 SSL
对象,并将其与底层网络连接的 BIO(通常是套接字 BIO)关联。
▮▮▮▮关键API: SSL_new
, SSL_set_bio
, BIO_new_socket
, SSL_free
, BIO_free
。
▮▮▮▮说明: SSL
对象代表一个独立的 TLS/SSL 连接会话。此示例展示了如何创建 SSL 对象,然后创建一个套接字 BIO 并将其绑定到 SSL 对象,使得 SSL 层可以通过 BIO 进行网络通信。
③ 示例名称: 实现基本 TLS/SSL 客户端连接与数据交换
▮▮▮▮所在章节: 第 8 章 TLS/SSL协议基础与客户端开发 -> 8.5 实现一个基本TLS/SSL客户端
▮▮▮▮核心功能: 提供一个完整的 TLS/SSL 客户端代码示例,包括建立 TCP 连接、SSL 握手、数据发送和接收以及连接关闭。
▮▮▮▮关键API: socket
, connect
(系统调用), SSL_connect
, SSL_write
, SSL_read
, SSL_shutdown
, close
(系统调用)。
▮▮▮▮说明: 这是客户端开发的核心示例。它整合了前面 SSL_CTX 和 SSL 对象的创建,并演示了 TLS/SSL 连接建立的关键步骤 (SSL_connect
),以及如何在安全通道上进行数据的双向通信 (SSL_write
, SSL_read
),最后安全地关闭连接 (SSL_shutdown
)。
④ 示例名称: 客户端证书验证配置与回调
▮▮▮▮所在章节: 第 8 章 TLS/SSL协议基础与客户端开发 -> 8.6 客户端证书验证
▮▮▮▮核心功能: 演示客户端如何配置信任锚(如 CA 证书文件或目录)以及如何设置证书验证回调函数来控制证书验证逻辑。
▮▮▮▮关键API: SSL_CTX_load_verify_locations
, SSL_CTX_set_default_verify_paths
, SSL_CTX_set_verify
, SSL_set_verify_depth
, SSL_CTX_set_cert_verify_callback
, X509_STORE_CTX_get_current_cert
, X509_STORE_CTX_get_error
, X509_STORE_CTX_get_error_depth
。
▮▮▮▮说明: 客户端验证服务器证书是防止中间人攻击(Man-in-the-Middle attack)的关键。此示例展示了加载信任根证书的方法,以及如何通过设置验证标志和回调函数来自定义证书链验证过程。
Appendix C.8: TLS/SSL服务器开发示例
这些示例构成本书第 9 章的主体,指导读者使用 OpenSSL 编写 TLS/SSL 服务器。
① 示例名称: 构建 SSL 服务器上下文 (SSL_CTX) 并加载证书/私钥
▮▮▮▮所在章节: 第 9 章 TLS/SSL服务器开发 -> 9.1 OpenSSL SSL服务器上下文配置
▮▮▮▮核心功能: 演示创建和配置服务器端 SSL_CTX
,特别是加载服务器的证书和对应的私钥。
▮▮▮▮关键API: SSL_CTX_new
, TLS_server_method
(或 SSLv23_server_method
- 旧版本), SSL_CTX_use_certificate_file
, SSL_CTX_use_PrivateKey_file
, SSL_CTX_check_private_key
, SSL_CTX_set_cipher_list
, SSL_CTX_free
。
▮▮▮▮说明: 服务器端 SSL_CTX 的配置与客户端有所不同,需要加载服务器自己的公钥证书和私钥,并配置支持的密码套件。此示例展示了这些关键配置步骤。
② 示例名称: 实现基本多线程 TLS/SSL 服务器
▮▮▮▮所在章节: 第 9 章 TLS/SSL服务器开发 -> 9.3 实现一个基本TLS/SSL服务器
▮▮▮▮核心功能: 提供一个基本的 TLS/SSL 服务器代码示例,能够监听端口、接受客户端连接、执行 SSL 握手并在单独的线程中处理每个客户端连接。
▮▮▮▮关键API: socket
, bind
, listen
, accept
(系统调用), SSL_new
, SSL_set_bio
, SSL_accept
, SSL_read
, SSL_write
, SSL_shutdown
, SSL_free
, BIO_free
, close
(系统调用), pthread_create
(或其他线程库 API)。
▮▮▮▮说明: 这是服务器端开发的核心示例。它演示了服务器如何等待传入连接,为每个连接创建一个新的 SSL
对象和套接字 BIO,执行服务器端握手 (SSL_accept
),并在握手成功后通过安全通道进行通信。此处通常会引入多线程或多进程来处理并发连接。
③ 示例名称: 配置和实现客户端证书认证 (Mutual Authentication)
▮▮▮▮所在章节: 第 9 章 TLS/SSL服务器开发 -> 9.4 客户端证书认证(Mutual Authentication)
▮▮▮▮核心功能: 演示服务器端如何配置以要求客户端提供证书,并验证客户端证书的有效性。
▮▮▮▮关键API: SSL_CTX_set_verify
, SSL_VERIFY_PEER
, SSL_VERIFY_FAIL_IF_NO_PEER_CERT
, SSL_CTX_load_verify_locations
, SSL_get_peer_certificate
。
▮▮▮▮说明: 相互认证(Mutual Authentication)增强了安全性,要求客户端也提供有效证书。此示例展示了服务器 SSL_CTX 的关键配置,包括设置验证标志(verify flags)以及如何在握手后获取和检查客户端证书。
Appendix C.9: 高级TLS/SSL特性示例
这些示例出现在本书的第 10 章,探讨了 TLS/SSL 的一些高级用法和优化技巧。
① 示例名称: 配置 OpenSSL Engine 进行硬件加速
▮▮▮▮所在章节: 第 10 章 高级TLS/SSL特性与实践 -> 10.1 性能优化
▮▮▮▮核心功能: 演示如何加载和使用 OpenSSL Engine,以便利用硬件加速或其他第三方加密库。
▮▮▮▮关键API: ENGINE_load_builtin_engines
, ENGINE_init
, ENGINE_by_id
, ENGINE_set_default
, ENGINE_ctrl_cmd_string
, ENGINE_free
。
▮▮▮▮说明: Engine 机制允许 OpenSSL 将某些加密操作(如私钥操作)委托给硬件模块或外部库执行,从而提高性能。此示例展示了加载和设置 Engine 的基本步骤。
② 示例名称: 配置和实现 TLS/SSL 会话复用 (Session Reuse)
▮▮▮▮所在章节: 第 10 章 高级TLS/SSL特性与实践 -> 10.2 会话复用
▮▮▮▮核心功能: 演示客户端和服务器端如何配置和利用会话 ID (Session ID) 或会话票据 (Session Ticket) 来实现 TLS/SSL 会话复用,减少握手开销。
▮▮▮▮关键API: SSL_CTX_set_session_cache_mode
, SSL_CTX_set_timeout
, SSL_CTX_sess_number
, SSL_get1_session
, SSL_set_session
, SSL_SESSION_free
。
▮▮▮▮说明: 会话复用是提高 TLS/SSL 性能的重要手段,避免了每次连接都进行完整的握手过程。此示例展示了在 SSL_CTX 层面开启会话缓存以及在 SSL 对象层面设置和获取会话对象的方法。
③ 示例名称: 服务器端处理 SNI (Server Name Indication)
▮▮▮▮所在章节: 第 10 章 高级TLS/SSL特性与实践 -> 10.3 SNI (Server Name Indication)
▮▮▮▮核心功能: 演示服务器端如何使用 SNI 回调函数,根据客户端请求的域名选择不同的证书。
▮▮▮▮关键API: SSL_CTX_set_tlsext_servername_callback
, SSL_get_servername
, SSL_set_SSL_CTX
。
▮▮▮▮说明: SNI 允许服务器在同一个 IP 地址和端口上托管多个域名的 TLS 证书。此示例展示了设置 SNI 回调,在回调中获取客户端请求的域名,并动态切换到对应域名的 SSL_CTX。
④ 示例名称: 使用自定义证书验证回调函数
▮▮▮▮所在章节: 第 10 章 高级TLS/SSL特性与实践 -> 10.5 自定义证书验证
▮▮▮▮核心功能: 演示如何编写一个自定义回调函数,取代 OpenSSL 默认的证书链验证逻辑。
▮▮▮▮关键API: SSL_CTX_set_verify
, SSL_VERIFY_PEER
, X509_STORE_CTX_get_ex_data
, X509_STORE_CTX_set_ex_data
, X509_STORE_CTX_get_current_cert
, X509_STORE_CTX_get_error
, X509_STORE_CTX_set_error
。
▮▮▮▮说明: 有时需要更灵活的证书验证策略,例如根据自定义的信任列表或业务规则判断证书是否可信。此示例展示了如何设置验证回调,并在回调函数中访问验证上下文 X509_STORE_CTX
来实现自定义逻辑。
Appendix C.10: OpenSSL线程安全示例
此示例位于本书的第 11 章,专注于在多线程环境中使用 OpenSSL。
① 示例名称: 配置 OpenSSL 的多线程锁回调 (OpenSSL 1.0.2 及更早版本)
▮▮▮▮所在章节: 第 11 章 OpenSSL线程安全 -> 11.2 配置线程回调函数
▮▮▮▮核心功能: 演示在 OpenSSL 1.0.2 及更早版本中,如何通过设置全局的锁定和 ID 回调函数来实现线程安全。
▮▮▮▮关键API: CRYPTO_set_id_callback
, CRYPTO_set_locking_callback
, CRYPTO_num_locks
, CRYPTO_set_dynlock_create_callback
, CRYPTO_set_dynlock_lock_callback
, CRYPTO_set_dynlock_destroy_callback
。
▮▮▮▮说明: OpenSSL 在 1.1.0 版本之前并非完全线程安全,需要用户提供锁机制。此示例详细展示了如何创建锁数组、实现锁定和解锁函数,并将其注册到 OpenSSL。
② 示例名称: OpenSSL 1.1.0+ 线程安全示例
▮▮▮▮所在章节: 第 11 章 OpenSSL线程安全 -> 11.3 新的线程安全API (OpenSSL 1.1.0+)
▮▮▮▮核心功能: 演示在 OpenSSL 1.1.0 及更高版本中,由于库内部实现了线程管理,用户代码通常无需再设置全局锁回调。
▮▮▮▮关键API: 无需特殊的全局锁回调 API。
▮▮▮▮说明: 从 OpenSSL 1.1.0 开始,库自身变得线程安全,极大地简化了多线程应用中的使用。此示例会展示一个简单的多线程场景,说明在这种新模型下如何安全地使用 OpenSSL 对象(注意:对象本身的生命周期管理仍然需要用户负责)。
Appendix C.11: 实战案例代码概览
本书第 12 章的实战案例整合了前面章节的知识,这些案例的代码通常更复杂,功能更完整。
① 示例名称: 安全文件传输工具 (客户端与服务器)
▮▮▮▮所在章节: 第 12 章 实战案例分析 -> 12.1 安全文件传输工具
▮▮▮▮核心功能: 实现一个命令行工具,支持使用对称加密(如 AES)、非对称加密(用于密钥交换)、哈希和数字签名来安全地传输文件。
▮▮▮▮说明: 此案例整合了第 4、5、6 章的内容,演示了如何在实际应用中组合使用多种加密原语(cryptographic primitives)来实现文件安全。代码将包括密钥生成、文件分块读写、加密解密、签名验证等模块。
② 示例名称: 简易 HTTPS 服务器
▮▮▮▮所在章节: 第 12 章 实战案例分析 -> 12.2 构建简易HTTPS服务器
▮▮▮▮核心功能: 基于 OpenSSL 和套接字编程,实现一个能够处理简单 HTTP 请求并使用 TLS/SSL 保护通信的 Web 服务器。
▮▮▮▮说明: 此案例整合了第 8、9 章的内容,演示了如何在服务器端配置 TLS/SSL,接受 HTTPS 连接,执行握手,并在加密通道上读写 HTTP 数据。代码将涉及网络编程(socket API)、SSL_CTX 和 SSL 对象的管理以及多线程处理。
③ 示例名称: 安全 API 调用示例
▮▮▮▮所在章节: 第 12 章 实战案例分析 -> 12.3 安全API通信
▮▮▮▮核心功能: 演示两个 C++ 应用程序如何通过 TLS/SSL 建立安全连接,进行 API 调用(例如发送结构化数据并接收响应)。
▮▮▮▮说明: 此案例侧重于应用程序间的安全通信,与 HTTPS 服务器类似,但更抽象化,不限于 HTTP 协议。它将展示客户端发起连接、双向握手、数据序列化/反序列化以及通过安全通道发送/接收数据的过程。
Appendix C.12: 如何获取和使用代码示例
📘 获取代码:
本书的所有代码示例都将托管在一个在线代码仓库(例如 GitHub)中。读者可以从指定地址克隆(clone)或下载(download)整个代码库。仓库结构会按照本书的章节组织,方便读者查找对应示例。
🔗 代码仓库地址:[此处预留代码仓库链接]
⚙️ 构建与运行:
每个代码示例通常是一个独立的 C++ 源文件,或者是一个小型项目。代码仓库会提供详细的构建说明,通常会使用 CMake。读者需要确保已经正确安装了 OpenSSL 开发库和 CMake 工具。
① 克隆代码仓库:
1
git clone [代码仓库链接]
② 进入示例目录:
1
cd openssl-cpp-book-examples
③ 创建构建目录并配置 CMake:
1
mkdir build
2
cd build
3
cmake ..
④ 编译代码:
1
make
⑤ 运行示例:
1
./chapterX/exampleY
(具体的构建和运行命令会因示例结构和章节而异,代码仓库中会有详细的 README
文件说明。)
💡 学习建议:
⚝ 动手实践: 强烈建议读者亲自编译和运行每一个代码示例,观察其输出和行为。
⚝ 阅读注释: 仔细阅读代码中的注释,理解每一步的目的和所使用的 OpenSSL 函数。
⚝ 修改尝试: 在理解示例的基础上,尝试修改代码,例如更改加密算法、密钥长度、填充模式等,观察结果的变化,加深理解。
⚝ 结合章节内容: 代码示例是章节理论知识的补充,应结合章节内容一同学习。在遇到不理解的 API 或概念时,回查对应章节的讲解。
⚝ 查阅文档: OpenSSL 官方文档是学习的重要资源。当对某个函数有疑问时,积极查阅其 man
页面(例如:man SSL_connect
)。
希望这些代码示例能成为您学习 OpenSSL C++ 开发的有力工具! 💪
Appendix D: 参考资料与拓展阅读
本附录旨在为读者提供进一步学习 OpenSSL 库以及相关密码学概念的资源。通过这些参考资料和拓展阅读,您可以加深对书中内容的理解,探索更高级的主题,并及时了解 OpenSSL 的最新发展和安全动态。
Appendix D1: 经典密码学书籍(Classic Cryptography Books)
理解 OpenSSL 底层的密码学原理对于高效且安全地使用该库至关重要。以下是一些经典的密码学书籍,它们提供了坚实的理论基础:
① 《应用密码学》(Applied Cryptography) 🔐
▮▮▮▮由 Bruce Schneier 所著。这本书被认为是现代密码学领域的经典入门读物,详细介绍了各种加密算法、协议及其在实际应用中的安全性分析。它不是一本数学公式堆砌的书,更侧重于密码学在系统设计中的应用和潜在的安全风险。
▮▮▮▮⚝ 重点:广泛的算法覆盖、协议分析、实际应用安全考量。
② 《密码学与网络安全:原理与实践》(Cryptography and Network Security: Principles and Practice) 📚
▮▮▮▮William Stallings 的这本教材内容全面,涵盖了密码学基础、常用算法(对称加密算法(symmetric encryption algorithms)、非对称加密算法(asymmetric encryption algorithms)、哈希函数(hash functions)、数字签名(digital signatures))、认证协议(authentication protocols)以及网络安全协议(network security protocols)如 SSL/TLS, IPsec 等。适合作为系统的学习教材。
▮▮▮▮⚝ 重点:系统性的原理介绍、算法细节、网络安全协议应用。
③ 《应用密码学手册》(Handbook of Applied Cryptography) 📘
▮▮▮▮由 Alfred J. Menezes, Paul C. van Oorschot, Scott A. Vanstone 合著。这是一本更为学术和全面的参考手册,内容极为详尽,包含了大量的数学背景和算法细节。这本书的完整版本可以在其官方网站上免费获取。
▮▮▮▮⚝ 重点:全面、深入、包含大量数学和算法细节,适合深入研究。
Appendix D2: OpenSSL 官方文档(Official OpenSSL Documentation)
OpenSSL 官方文档是学习和使用 OpenSSL 库最权威、最准确的资源。无论是查找特定的函数用法、理解数据结构的含义,还是了解不同版本的变化,官方文档都是首选。
① OpenSSL 手册页(Man Pages) 📖
▮▮▮▮这是 OpenSSL 库函数(library functions)和命令行工具(command line tools)的详细参考。您可以通过命令行 man <function_name>
或在线文档浏览器(online documentation browser)访问。手册页通常包含函数的功能描述、参数说明、返回值、可能的错误以及示例代码。
▮▮▮▮⚝ 重点:最详细的 API 参考,查找函数用法必备。
② OpenSSL Wiki 🌐
▮▮▮▮Wiki 包含了许多有用的信息,如构建指南、常见问题解答(FAQ)、教程(tutorials)以及关于特定主题的更深入讨论。它是社区贡献和维护的重要知识库。
▮▮▮▮⚝ 重点:构建信息、FAQ、社区贡献的补充资料。
③ 版本发布说明(Release Notes) 📜
▮▮▮▮每次 OpenSSL 新版本发布时,都会有详细的发布说明,列出了新特性、bug 修复、API 变化(包括废弃和新增)以及安全相关的更新。关注发布说明对于使用最新版本和迁移代码非常重要。
▮▮▮▮⚝ 重点:了解版本变化、新特性和潜在的兼容性问题。
④ 在线文档浏览器(Online Documentation Browser) 💻
▮▮▮▮OpenSSL 官方网站(openssl.org)提供了所有版本的在线文档,方便读者通过浏览器进行搜索和查阅。强烈建议始终查阅与您使用的 OpenSSL 版本相对应的文档。
▮▮▮▮⚝ 重点:便捷的在线访问和搜索。
Appendix D3: 在线资源与教程(Online Resources and Tutorials)
除了官方文档,互联网上还有许多非官方但非常有价值的资源,它们通常以教程、博客文章或示例代码的形式存在,可以帮助您更快地理解和应用 OpenSSL。
① 各类技术博客和文章 ✍️
▮▮▮▮许多安全专家和开发者会在他们的博客上分享使用 OpenSSL 的经验、解决问题的技巧或解释复杂的概念。通过搜索引擎查找特定主题(例如:“OpenSSL C++ AES GCM”)通常能找到有用的文章。
▮▮▮▮⚝ 重点:实战经验、问题解决方案、特定概念解释。
② Stack Overflow ❓
▮▮▮▮Stack Overflow 是一个广受欢迎的程序开发问答网站。搜索带有 openssl
和 c++
标签的问题和答案,可以找到大量关于 OpenSSL 使用中遇到的具体问题的解决方案和代码示例。
▮▮▮▮⚝ 重点:解决具体编程问题、查找代码示例。
③ GitHub 或其他代码托管平台上的示例项目 📂
▮▮▮▮许多开源项目使用 OpenSSL,阅读它们的源代码是学习 OpenSSL 在实际应用中如何使用的好方法。此外,也有一些专门为演示 OpenSSL API 用法而创建的小型示例仓库。
▮▮▮▮⚝ 重点:学习实际项目中的集成方式、参考可工作的示例代码。
Appendix D4: 密码学标准与规范(Cryptography Standards and Specifications)
密码学和安全协议的实现往往需要遵循国际标准和行业规范。了解这些标准有助于您正确使用 OpenSSL 并确保互操作性和安全性。
① NIST 出版物(NIST Publications) 🏛️
▮▮▮▮美国国家标准与技术研究院(National Institute of Standards and Technology)发布了大量的密码学标准(如 FIPS 140-2/3 关于加密模块安全要求)和推荐标准(如 SP 800 系列,涵盖随机数生成、密钥管理、算法选择等)。OpenSSL 的许多实现都遵循 NIST 的规范。
▮▮▮▮⚝ 重点:加密模块认证、算法推荐、安全指南。
② RFC 文档(Request for Comments) 📄
▮▮▮▮互联网工程任务组(IETF)发布的 RFC 文档定义了许多网络协议,包括 TLS/SSL 的各个版本(例如 RFC 5246 定义 TLS 1.2,RFC 8446 定义 TLS 1.3)、各种密码学算法的使用方式、PKI 结构等。阅读相关的 RFC 文档可以帮助您深入理解协议细节。
▮▮▮▮⚝ 重点:协议定义(如 TLS/SSL)、密码学算法在协议中的应用。
③ ISO/IEC 标准 🌍
▮▮▮▮国际标准化组织(ISO)和国际电工委员会(IEC)也发布了与信息安全和密码学相关的标准,例如 ISO/IEC 27000 系列关于信息安全管理系统。虽然不像 NIST 和 RFC 那样直接指导 OpenSSL 的具体 API 使用,但提供了更广泛的安全背景和要求。
▮▮▮▮⚝ 重点:信息安全管理、安全技术标准。
Appendix D5: 社区与论坛(Community and Forums)
当您在使用 OpenSSL 过程中遇到困难或有疑问时,社区是寻求帮助和交流经验的好地方。
① OpenSSL 邮件列表(Mailing Lists) 📧
▮▮▮▮OpenSSL 项目维护了几个邮件列表,用于用户提问、开发者讨论和接收安全公告。这是与 OpenSSL 核心开发者和资深用户交流的直接渠道。
▮▮▮▮⚝ 重点:官方支持渠道、获取安全更新、深度技术讨论。
② 专业技术论坛 🗣️
▮▮▮▮除了 Stack Overflow,还有其他专注于网络安全、C++ 开发或特定操作系统平台的论坛,您可以在这些地方提问或搜索相关信息。
Appendix D6: 进阶主题与研究论文(Advanced Topics and Research Papers)
对于希望深入研究或从事相关工作的读者,可以探索一些更高级的主题,并查阅学术界的最新研究成果。
① 安全攻击与防御技术 🛡️
▮▮▮▮研究针对 TLS/SSL 协议、特定加密算法或 OpenSSL 实现的各种攻击(例如:心脏出血(Heartbleed)、降级攻击(downgrade attacks)、侧信道攻击(side-channel attacks))以及相应的防御措施。
▮▮▮▮⚝ 重点:了解安全漏洞、提升代码安全性。
② 形式化验证(Formal Verification) ✅
▮▮▮▮一些研究致力于使用形式化方法来证明密码学协议或其实现的安全性。了解这方面的工作可以提升对安全协议严谨性的认识。
▮▮▮▮⚝ 重点:协议安全性证明、提高信任度。
③ 后量子密码学(Post-Quantum Cryptography) ⚛️
▮▮▮▮量子计算(quantum computing)对现有非对称加密算法构成了潜在威胁。后量子密码学研究旨在开发在量子计算机时代仍然安全的算法。OpenSSL 也在逐步集成一些后量子算法的实验性实现。
▮▮▮▮⚝ 重点:未来密码学发展趋势。
通过以上各类资源的组合学习,相信您可以不断提升自己在 OpenSSL C++ 开发及信息安全领域的知识和技能。祝您学习愉快!