OpenSSL3.0架构的面向对象设计

——所有的复杂性,都是在对“行为”和“数据”做排列组合,并用指针/引用把它们粘合起来

引言

ossl_hitls作为一个连接OpenSSL,OpenHiTLS以及 pqc provider的provider项目,其开发涉及对各个模块接口的调用。在这个过程中,各个模块的API本身具有很明显的面向对象特征多,这给我带来了很多启发与思考,在好奇心的驱使下,我去读了《Head First:设计模式》这本书,结合之前的一些项目经验,分享一些见解和讨论:

我们的项目在做什么?

provider architecture

从Provider最主要的目的,我们是在实现多态:让上层密码接口(EVP_)与底层实现解耦,使得基于OpenSSL开发的软件无需改动代码,就可以切换实际完成密码运算的算法实现。
进一步地,我们还希望代码能够变得易读,变得精简,我们让不同的部分能够继承共同的部分。我们希望代码能够易用且安全,只暴露必须的接口,保证类型安全——我们希望实现封装。
于是凑齐了向对象的核心三要素——封装、继承、多态——这次要在C语言中动手实现。
好消息是OpenSSL经历了20多年的维护与发展,为我们提供了非常好的范例,虽然这里没有类的概念,也就没法去说设计模式。但是这个库,或者说provider框架本身是按照设计模式中的思想来设计的,可以发现API是围绕着设计模式构造的。

案例:策略模式——SDF Integration

直接说Provider本身就是策略模式感觉太片面,Provider应该是各种模式的组合模式,但肯定用到了策略模式。
之前刚好接触过一个项目能体现普通的策略模式,这里拿来分析一下:
这个项目背景是这样:
在Tongsuo中的SDF功能,至少适配一种密码卡或密码机。
SDF是指《GM/T 0018-2023》,就是为上层接口提供硬件密码卡设备的实现,但不是通过Provider,而是单独的SDK集成。
实现方案就是加载厂商提供的动态库驱动,绑定函数指针,转换数据结构。但如何支持多厂商?
同样的场景,同样的问题,不做provider如何实现这样的多态需求?
(SDF集成)[https://github.com/Tongsuo-Project/Tongsuo/pull/768]

解决方案就是一个放着函数指针的结构体数组——通过一个_bind_init函数在编译期进行结构体数组指针的赋值,即绑定“方法表”,
将当前硬件设备加载到的函数绑定到库中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

DEFINE_RUN_ONCE_STATIC(ossl_sdf_lib_init)
{
# ifdef SDF_LIB_SHARED
# ifndef LIBSDF
// # define LIBSDF "sdf"
# define LIBSDF "swsds"
# endif

sdf_dso = DSO_load(NULL, LIBSDF, NULL, 0);
if (sdf_dso != NULL) {
#include "sdf_bind.h"
sdf_bind_init(&sdfm, sdf_dso);
printf("Debug1: This is a debug message.\n");
}
# else
printf("Debug: This is a debug message.\n");
# endif
return 1;
}
#endif
int sdf_bind_init(SDF_METHOD *sdfm,DSO *dso) {
if (sdfm == NULL) {
fprintf(stderr, "SDF_METHOD pointer is NULL\n");
return -1;
}
if (dso == NULL) {
fprintf(stderr, "DSO object is NULL\n");
return -1;
}
sdfm->OpenDevice = (SDF_OpenDevice_fn)DSO_bind_func(dso, "SDF_OpenDevice");
sdfm->CloseDevice = (SDF_CloseDevice_fn)DSO_bind_func(dso, "SDF_CloseDevice");
sdfm->OpenSession = (SDF_OpenSession_fn)DSO_bind_func(dso, "SDF_OpenSession");
sdfm->CloseSession = (SDF_CloseSession_fn)DSO_bind_func(dso, "SDF_CloseSession");
sdfm->GetDeviceInfo = (SDF_GetDeviceInfo_fn)DSO_bind_func(dso, "SDF_GetDeviceInfo");
sdfm->GenerateRandom = (SDF_GenerateRandom_fn)DSO_bind_func(dso, "SDF_GenerateRandom");
sdfm->GetPrivateKeyAccessRight = (SDF_GetPrivateKeyAccessRight_fn)DSO_bind_func(dso, "SDF_GetPrivateKeyAccessRight");
sdfm->ReleasePrivateKeyAccessRight = (SDF_ReleasePrivateKeyAccessRight_fn)DSO_bind_func(dso, "SDF_ReleasePrivateKeyAccessRight");
sdfm->ExportSignPublicKey_RSA = (SDF_ExportSignPublicKey_RSA_fn)DSO_bind_func(dso, "SDF_ExportSignPublicKey_RSA");
……
sdfm->ExternalKeyDecrypt = (SDF_ExternalKeyDecrypt_fn)DSO_bind_func(dso, "SDF_ExternalKeyDecrypt");
sdfm->ExternalKeyEncryptInit = (SDF_ExternalKeyEncryptInit_fn)DSO_bind_func(dso, "SDF_ExternalKeyEncryptInit");
sdfm->ExternalKeyDecryptInit = (SDF_ExternalKeyDecryptInit_fn)DSO_bind_func(dso, "SDF_ExternalKeyDecryptInit");
sdfm->ExternalKeyHMACInit = (SDF_ExternalKeyHMACInit_fn)DSO_bind_func(dso, "SDF_ExternalKeyHMACInit");
#endif
return 0;
}

最有意思的是你可以发现它非常类似策略模式的java实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 策略接口:方法表
interface MethodTable {
void bind();
}

// 抽象 Provider
abstract class Provider {
MethodTable methodTable;

public Provider() {
// 构造函数中绑定具体的方法表
methodTable = createMethodTable();
methodTable.bind();
}

protected abstract MethodTable createMethodTable();
}

// 具体策略:HSM 的方法表
class HSMMethodTable implements MethodTable {
public void bind() {
// 绑定 HSM 厂商的具体实现
System.out.println("绑定 HSM 硬件设备的方法");
// method_table.bind(hsm_methods);
}
}

// 具体策略:CPU 的方法表
class CPUMethodTable implements MethodTable {
public void bind() {
// 绑定 CPU 软件实现
System.out.println("绑定 CPU 软件实现的方法");
// method_table.bind(normal_methods);
}
}

// 具体 Provider:HSM 厂商
class HSM extends Provider {
protected MethodTable createMethodTable() {
return new HSMMethodTable();
}
}

// 具体 Provider:CPU 实现
class CPU extends Provider {
protected MethodTable createMethodTable() {
return new CPUMethodTable();
}
}

这样:

1
2
3
4
5
6
public class HSM{
public static void main(Stirng[] args){
Provider pctx = new HSM(); # 这会调用HSM集成来的绑定对应方法表的办法,进而委托给改对象进行处理。
pctx.do_calculate();
}
}

如果只用继承的话,给父类加一个方法就要求了每个子类必须有,要么就覆盖掉。这样当子类多了就很不方便。
这里体现的设计原则就是:多用组合,少用继承。
但在这个例子中我们也发现了缺陷:我们的C实现是编译期绑定,一旦编译完成,就无法在运行时切换。Java实现虽然在运行时绑定,
但一旦绑定,就无法在运行期修改,且无法在Provider和实例之间交换信息,无法根据配置文件动态选择……

这能实现Provider的基本功能要求,但对于一个在几乎所有系统中底层基础设施的OpenSSL Provider的存在,是远远不够的。
我们来看看如何处理真实世界中这样更加复杂的问题。

OpenSSL Provider的设计模式讨论

策略模式

Provider设计本身就是一个策略模式的完美实现,如前所述,provider希望替换底层实现,实现多态。
这就是策略模式的定义:

定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

基于回调的观察者模式

为什么是基于回调?
因为我们没有类这个概念,但回调可以当成具备类的一部分特征的结构,我们用
OpenSSL的出错机制就是一个典型的观察者模式实现:

1
2
3
4
5
6
7
8
9
10
11
// 定义回调类型
typedef void (*ERR_PrintCallback)(const char* str, size_t len, void* u);

// 注册观察者
void ERR_set_callback(ERR_PrintCallback cb, void* ctx);

// 错误发生时,通知所有观察者
static void err_print(const char* str, size_t len) {
ERR_PrintCallback cb = get_current_callback();
if (cb) cb(str, len, get_callback_ctx()); // ← 回调 + 上下文
}

错误系统是被观察者,回调函数是观察者。

工厂模式:_fetch 机制

OpenSSL 3.0 最大的架构革新是引入了Provider和_fetch系列函数:
这个非常经典,也很常见。

1
2
3
4
5
6
7
8
9
10
11
12
// 工厂方法:根据算法名称创建对象
EVP_MD* md = EVP_MD_fetch(NULL, "SHA3-256", NULL);
EVP_CIPHER* cipher = EVP_CIPHER_fetch(NULL, "AES-256-GCM", NULL);

// 内部实现(简化)
EVP_MD* EVP_MD_fetch(OSSL_LIB_CTX* ctx, const char* algorithm,
const char* properties) {
// 1. 查询已加载的Provider
// 2. 根据properties(如"fips=yes")筛选
// 3. 从Provider中获取算法实现
// 4. 缓存并返回
}

适配器模式:连接不同provider

在ossl_hitls项目中,需要同时调用OpenSSL和OpenHiTLS两套API。两者的设计哲学差异巨大:

维度 OpenSSL 3.0 OpenHiTLS
架构模式 Core + Provider 传统模块化
上下文传递 显式ctx参数 隐式全局状态
多态实现 函数指针表 条件编译 + 钩子

适配层的设计必须同时消化两种风格:
这个例子不引用具体代码,但是项目中有多处可以体现。比如在Encoder/Decoder讨论中,OpenSSL为这个操作提供的OBJECT_EXPORT函数,用统一接口包装不同的API。

副作用:

OpenSSL3.0这样的设计带来了两大副作用,性能下降和易用性降低。
你会发现使用的EVP接口成双成对,甚至一个及其简单的操作都需要初始化各种的上下文_CTX结构,调用重重接口才能实现。
这对于初学者是非常不友好的,但随着时间的推移,了解逐步深入:
你可以发现,所谓_CTX其实就是类,为了管理大量不同的算法,就会有父类,子类,子类的子类……
但是OSSL_LIB_CTX是你的工作台,它管理了一切,而EVP_PKEY_CTX等具体结构是你的操作对象,其中再初始化出具体的算法的CTX。
类之间如何传递信息?使用OSSL_PARAM,使用OSSL_DISPATCH,使用OSSL_ALGORITHM……

你无需记住各种各样的接口名,数据结构名,但通过对这里面向对象机制的了解,你知道在哪里需要做什么,为什么这样做,于是根据接口命名规范和文档查询,
就可以实现比较流畅的使用。时间长了就会不再觉得特别困难,还能被各种神奇的加密解密机制所吸引。
最关键的是,OpenSSL的接口设计也在潜移默化地规范你的编程习惯,锻炼实际的工程水平。

总结

上文中我们一直在尝试用面向对象的思考方式解释一个C库中的种种实现。但是这可能有点不太严谨,因为C中没有类的概念,自然也没有设计模式。
我们从中发现了不同编程语言的影子,发现了不同设计模式。
但我们发现了共同的目的:多态、继承、封装,共同的手段:抽象。共同的现实问题与业务场景。
那不同实现方式之间的联系是什么?

从C到面向对象

可以发现C语言中没有class关键字,但是各种各样的结构都有着类的特征,它们都具备一部分类的功能!
eg:
结构体嵌套:继承
函数指针数组:多态
不透明指针:封装
结构体+函数指针:类的简单模拟
回调函数 + 上下文指针:类的成员方法与this指针
……
我们发现共同点是不同结构的排列组合,实现一部分我们所需要的类的功能!

面向对象到C

类的各种特性就像是把各种C的基本结构的打包组合。
那我们如何用面向对象的方法实现一个C的函数指针?——一个没有成员变量的类!

回到开头的引言:——所有的复杂性,都是在对“行为”和“数据”做排列组合,并用指针/引用把它们粘合起来
把数据传递给行为,把行为传递给行为,把数据和行为传递给行为,以及之上的多层嵌套。这就是C语言中多级指针、泛函数、泛指针的实际行为,这也是所有
程序设计语言不断在重复的行为。

高级语言通用性、易用性换来的是性能降低,而C语言具有无可替代的灵活性与编程自由度。
这些设计都是不断的tradeoff。


OpenSSL3.0架构的面向对象设计
https://47.108.189.123/2026/08/12/实习经历/OpenHiTLS-2026/OpenSSL3.0架构的面向对象设计/
Author
Dong
Posted on
August 12, 2026
Licensed under