开源之夏07:Provider框架与PKCS#11探索

十月的主要任务是探索如何为 SDF/SKF 制作 OpenSSL Provider,这需要先理解 PKCS#11 标准(国际上的 SDF/SKF 对应物),再深入 OpenSSL 3.x 的 Provider 框架。

PKCS#11 入门

PKCS#11(又名 Cryptoki,Cryptographic Token Interface)是 OASIS 维护的密码设备接口标准,最新版本为 3.1(2023年7月)。

OASIS 标准的版本管理

理解两个概念:

  • This Stage(固定版本):用于开发、引用、合规认证的精确快照,十年后还是同一个版本。
  • Latest Stage(智能书签):永远指向最新版本,用于调研和追踪进展。

核心概念

  • Slot:硬件插槽,一个物理设备可以有多个 slot。SoftHSM 中每次初始化一个 Token 就会创建一个新 slot。
  • Token:插在 slot 里的”密码设备”,可以是硬件也可以是软件模拟。
  • Session:应用与 Token 之间的逻辑连接,几乎所有 PKCS#11 函数都需要。应用可以打开多个 session。
  • Session Object vs Token Object:会话对象(临时,session 关闭后消失)和令牌对象(持久,存储在 token 上)。

国外对 PKCS#11 的实现非常丰富:

  • NSS softokn3:Mozilla NSS 自带的软件 PKCS#11 模块
  • SoftHSM:基于软件的 HSM 实现,用于开发、测试和学习环境

SoftHSM 实操

1
2
3
4
5
6
7
8
9
10
11
softhsm2-util --show-slots
softhsm2-util --init-token --slot <id> --label <TokenName>

# 查看 token 信息
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --show-info

# 列出所有 slot
pkcs11-tool --list-slots --module /usr/lib/softhsm/libsofthsm2.so

# 列出 token 中的对象
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --list-objects

PKI 基础知识

PKI(Public Key Infrastructure,公钥基础设施)是 PKCS#11 所依赖的信任框架:

组件 职责
CA(证书颁发机构) 验证身份,签发数字证书
RA(注册机构) CA 的合作伙伴,处理身份验证
CRL(证书吊销列表) 维护被吊销的证书信息
数字证书 包含公钥+身份信息+CA 签名
PKID(公钥目录) 全局目录服务,存储分发公钥和证书

PKCS#12(.p12/.pfx)是存储和传输私钥、公钥和数字证书的二进制格式,广泛用于 TLS 客户端和服务器证书管理。TLS 利用 PKI 的数字证书进行身份验证,使用公钥和私钥进行加密通信。

OpenSSL Provider 框架

核心数据结构

OpenSSL 3.x 用 Provider 替代了旧的 Engine 机制。核心是两张表:

OSSL_DISPATCH(dispatch 表):function_id → 函数指针的映射,定义”我能做什么”。

1
2
3
4
5
static const OSSL_DISPATCH p11prov_rand_functions[] = {
{ OSSL_FUNC_RAND_INSTANTIATE, (void (*)(void))p11prov_rand_instantiate },
{ OSSL_FUNC_RAND_GENERATE, (void (*)(void))p11prov_rand_generate },
{ 0, NULL } // 终止标志
};

OSSL_ALGORITHM(算法表):算法名 → dispatch 表的映射,定义”我有哪些算法”。

1
2
3
4
static const OSSL_ALGORITHM p11prov_op_random[] = {
{ "PKCS11-RAND", "provider=p11prov", p11prov_rand_functions, "PKCS#11 RNG" },
{ NULL, NULL, NULL, NULL }
};

完整调用链

以随机数生成为例,GDB 堆栈验证了完整的调用路径:

1
2
3
4
5
应用调用 EVP_RAND_fetch("CTR-DRBG", ...)
→ Core 向 provider 查询: ossl_provider_query_operation(prov, OSSL_OP_RAND, &no_cache)
→ Provider 返回 OSSL_ALGORITHM 数组 (含 dispatch 表指针)
→ Core 遍历 dispatch 表,提取函数指针填入 EVP_RAND 结构
→ 应用调用 EVP_RAND_generate(...) 时直接跳转到 provider 的实现函数

query_operation 是核心调度函数,no_cache 参数控制 Core 是否缓存构造结果——cache 命中后不需要重复向 provider 查询,提高性能。

回调:Provider 模型的灵魂

回调(函数指针)是 OpenSSL Core 与可插拔 Provider 之间实现动态绑定与职责分离的核心机制。存在两类回调:

  1. core → provider(下行):core 通过 provider 提供的函数指针调用 provider 功能(query_operation、random_bytes 等)。这些是 provider 提供给 core 的”实现回调”。
  2. provider → core(上行/upcalls):provider 在初始化时被传入一组 core 的函数指针(core_dispatch),可以在实现中调用 core 的功能(申请内存、报告错误等)。

一旦绑定后,调用是直接的函数指针调用,比字符串查找/反射快得多——这也是为什么 Provider 机制比 Engine 的字符串匹配更高效。

一个深刻的规律:**”初始化就是在为回调做准备”**。无论是 Web 服务器注册路由、GUI 绑定事件监听器,还是 Provider 填充 dispatch 表——程序的初始化过程很大程度上就是一个”配置回调函数,为未来发生的事件做好准备”的过程。

这体现了两个重要的软件工程原则:

  • 控制反转(IoC):框架控制流程,你在初始化时”注册”回调,框架在合适时机调用你——“Don’t call us, we’ll call you.”
  • 间接层原则:计算机科学的大部分问题都可以通过增加一个间接层来解决。回调函数本身就是”事件”和”处理逻辑”之间的间接层。

实战调试:全局析构顺序问题

在开发 pkcs11-provider 时遇到一个经典的崩溃问题。

现象

openssl list -providers 程序退出时发生 Segmentation Fault,调用栈显示崩溃在 SoftHSM 的 C_CloseSession 内部。

诊断

通过 Valgrind 确认:

1
2
3
==12897== Invalid read of size 4
==12897== at 0x57DA7DC: ??? (in libsofthsm2.so)
==12897== by 0x57AD91C: C_CloseSession

这是 C/C++ 全局析构顺序问题(Static Destruction Order Fiasco):程序退出时,C/C++ 全局对象的析构顺序是不确定的。OpenSSL libcrypto 开始清理 → 调用 provider teardown → provider 试图调用 C_CloseSession → 但此时 SoftHSM 内部的某些全局对象可能已经被析构 → 访问已析构内存 → Segmentation Fault。

解决

pkcs11-provider 提供了 no-deinit quirk 配置:

1
pkcs11-module-quirks = no-deinit

原理:让 provider 在 teardown 时跳过 PKCS#11 模块的清理调用。这是安全的,因为程序退出时 OS 会自动回收所有资源。不调用 C_CloseSession 和 C_Finalize 避免了访问已析构内存。

这背后有个编程教训:你无法控制全局对象的析构顺序,所以依赖全局对象的”礼貌性清理代码”在程序退出时可能成为定时炸弹。

Provider 中的宏展开

OpenSSL 使用宏大量生成 dispatch 表。以 IMPLEMENT_generic_cipher(sm4, SM4, cbc, CBC, 0, 128, 128, 128, block) 为例,展开后生成:

  • sm4_128_cbc_get_params:填充算法参数(mode, flags, key/block/iv bits)
  • sm4_128_cbc_newctx:分配并初始化 provider 上下文
  • ossl_sm4128cbc_functions[]:完整的 OSSL_DISPATCH 表,包含 NEWCTX、FREECTX、ENCRYPT_INIT、UPDATE、FINAL 等十几个函数指针

宏的命名规则:ossl_ + 算法名(去下划线) + 密钥长度 + 模式 + _functions。这种命名约定让 Core 可以通过构造名字来查找符号,而不需要手工维护注册表。


开源之夏07:Provider框架与PKCS#11探索
https://47.108.189.123/2025/10/24/开源之夏/07-Provider框架与PKCS11探索/
Author
Dong
Posted on
October 24, 2025
Licensed under