简介:这份文档面向需要理解 OpenSSL 扩展加密能力的开发者与安全工程师,聚焦 ENGINE 机制这一通用加密接口的设计与实现,帮助读者弄清 OpenSSL 如何透明调用第三方软件加密库或硬件加密设备,属于偏中高级的底层机制学习材料。资源包共 1 个 doc 文件,约 44KB,内容围绕概念与实现两条主线展开:既交代 ENGINE 自 0.9.6 引入、0.9.7 并入内核的演进脉络,也梳理 CryptoSwift、nCipher、Atalla、Nuron、UBSEC、Aep、SureWare、IBM 4758 CCA 等内嵌设备接口及 PKCS#11、CryptoAPI 的扩展方向。实现部分结合 openssl/crypto/engine/ 目录,剖析 ENGINE_load_XXXX 的加载流程与 ENGINE_set_id、ENGINE_set_ciphers 等注册接口,并给出 rtl8651b 引擎的示例代码,便于读者对照源码理解动态库加载与算法指针替换。目前已有 1219 人学习,适合作为研读 OpenSSL 源码、对接硬件加密卡的参考笔记。
1. 到底什么是 OpenSSL Engine:一个“换引擎不换车”的类比
很多人第一次看到OPENSSL_ENGINE这个词,是在某份配置、某段编译日志或者某个加密设备的接入文档里,然后就被一串ENGINE_load_XXXX、ENGINE_set_default弄懵了。其实它没那么玄乎。打个比方:OpenSSL 像一辆车,方向盘、油门、刹车这些操作方式(也就是对外的加密接口)都固定不变,而 Engine 机制就是允许你把车头盖打开,把原厂发动机换成第三方发动机——车还是那辆车,开法也没变,但动力来源换了。
通俗地说,OpenSSL Engine 是一套“可插拔的加密后端”机制。它让 OpenSSL 在调用加解密算法时,不必死守自己内置的那套软件实现,而是可以透明地转交给第三方软件加密库或者硬件加密设备去算。这里的“透明”是关键:上层应用调用的还是EVP_系列那套老接口,代码几乎不用改,底下真正干活的却可能是一块加密卡。搞懂它,你就能明白为什么有些系统明明用的是 OpenSSL,加解密速度却快得不像纯软件,也能看懂那些“接入加密机”的文档到底在说什么。
2. Engine 为什么存在:从“加密库”到“通用加密接口”的转身
2.1 纯软件加密的天花板在哪里
OpenSSL 本身是一个开放源代码的安全项目,目标是做出健壮、商业级的工具包,用加密算法实现安全套接层和传输层安全,里面包含完整的加密算法、数字签名算法和证书算法,能保证数据的完整、保密和正确。这套东西在纯软件层面已经相当能打,但软件算力总有上限:当一台服务器要扛住海量 TLS 握手,或者某个场景要求密钥必须待在专用硬件里不能落到内存,纯软件实现就顶不住了。
这时候有两条路。一条是把业务代码整个改写成调用某个硬件厂商的 SDK,代价是代码和厂商绑死,换设备就得重写;另一条是让 OpenSSL 自己留一个“插座”,第三方把加密能力做成“插头”插进来。Engine 机制走的就是第二条路。它的目的很明确:使 OpenSSL 能够透明地使用第三方提供的软件加密库或者硬件加密设备进行加密。正是这个设计,让 OpenSSL 不再只是一个加密库,而变成了一个通用的加密接口,能够与绝大部分加密库或者加密设备协调工作。
2.2 一段版本演进史说明了它的定位
Engine 机制不是一开始就长在 OpenSSL 身体里的。它出现在 0.9.6 版本,那时候普通版本和支持 Engine 的版本是分开编译的,相当于“选装件”;到了 0.9.7 版本,Engine 机制被集成进 OpenSSL 内核,成了不可缺少的一部分,从“选装”变成了“标配”。这个演进本身就说明了一件事:社区认可了“加密后端应该可替换”这个方向,把它当成基础设施而不是附加功能。
从功能目标上看,Engine 机制和 Windows 提供的 CSP 功能基本是同一类东西——都是给上层提供统一的加密调用入口,底层接谁由配置决定。理解了这个定位,你就不会把 Engine 当成某个具体算法,它是一层“调度层”。
2.3 它到底“调度”了些什么
Engine 机制在实现上主要包含两块:动态库加载的代码,以及加密函数指针管理的一系列接口。所谓函数指针管理,就是 OpenSSL 内部原本指向自己算法实现的那些指针,可以被重新指向你加载进来的动态库里的函数。于是应用程序调用加解密算法时,实际执行的是你加载的动态库里的实现,而不是原先 OpenSSL 库里的实现。
这里要澄清一个常见误解:Engine 不是“再包一层壳做转发”那么简单,它替换的是算法实现层面的函数指针,所以性能损耗很小,硬件加速的收益能真正落到业务上。当然,要让特定加密库或设备与 OpenSSL 协调工作,还是需要写少量接口代码,工作量不算大,但确实需要一点密码学知识——这也是很多人卡住的地方。
3. Engine 是怎么工作的:加载、注册、接管三步走
3.1 第一步:把 Engine 加载进来
使用 Engine 的前提是先把它加载进系统。每个ENGINE_load_XXXX函数本质上就是一个加载过程:新建一个 ENGINE 对象,然后逐项设定它的身份和算法能力,最后注册到 OpenSSL 的 Engine 列表里。下面这段结构是理解整个机制的钥匙(示意,非完整可编译代码):
void ENGINE_load_demo(void) { ENGINE *engine = ENGINE_new(); /* 创建一个空的 Engine 对象 */ if (engine == NULL) return; /* 设定身份:id 是内部标识,name 是给人看的描述 */ if (!ENGINE_set_id(engine, "demo") || !ENGINE_set_name(engine, "demo hardware engine") || /* 挂上各类算法的实现函数表 */ !ENGINE_set_ciphers(engine, demo_engine_ciphers) || !ENGINE_set_digests(engine, demo_engine_digests) || !ENGINE_set_ctrl_function(engine, demo_ctrl) || !ENGINE_set_cmd_defns(engine, demo_defns)) { ENGINE_free(engine); /* 任一步失败就释放,避免泄漏 */ return; } /* 设为默认摘要算法提供者;失败则退而求其次只加入列表 */ if (!ENGINE_set_default(engine, ENGINE_METHOD_DIGESTS)) ENGINE_add(engine); ENGINE_free(engine); ERR_clear_error(); }这段代码里几个参数值得说清楚。ENGINE_set_id设的是大小写敏感的简要名称,编程或执行命令时经常要用到,目前一般都采用小写字符;ENGINE_set_name只是描述性文字。ENGINE_set_ciphers和ENGINE_set_digests分别挂上对称加解密和摘要算法的函数表,这是“接管”的实质。ENGINE_set_ctrl_function和ENGINE_set_cmd_defns负责处理控制命令,比如查询设备状态、设置参数。最后的ENGINE_set_default才是真正让这个 Engine 成为默认后端的关键一步,失败时用ENGINE_add至少把它登记进列表备用。
3.2 第二步:用 set 系列函数注册能力
ENGINE_set_xxxx这一族函数,是对 Engine 机制各个方面的设定注册。除了上面出现的 id、name、ciphers、digests、ctrl、cmd_defns,还有一整套针对具体算法的挂载点:ENGINE_set_RSA、ENGINE_set_DSA、ENGINE_set_DH、ENGINE_set_RAND,以及生命周期相关的ENGINE_set_destroy_function、ENGINE_set_init_function、ENGINE_set_finish_function,还有密钥加载用的ENGINE_set_load_privkey_function和ENGINE_set_load_pubkey_function。
可以看出,具体的 RSA、DSA、DH、RAND、CIPHERS、DIGESTS 算法实现,主要就是通过这些函数挂进去的。换句话说,Engine 对象本身是个“空壳登记表”,你往里填什么,它就具备什么能力。填得全,这个 Engine 就能接管全部算法;只填摘要,那它就只管摘要。
3.3 第三步:算法实现长什么样
以摘要算法 DIGESTS 为例,接管一个 MD5 需要先声明一个对应的数据结构,把算法标识、输出长度、初始化/更新/收尾三个函数指针、分组长度、上下文大小等信息填好:
const EVP_MD demo_hash_md5 = { NID_md5, /* 算法数字标识 */ NID_md5WithRSAEncryption, /* 关联的签名算法标识 */ MD5_DIGEST_LENGTH, /* 摘要输出字节数 */ NULL, demo_digest_init, /* 初始化:准备上下文 */ demo_digest_update, /* 更新:喂入数据 */ demo_digest_final, /* 收尾:输出摘要 */ NULL, NULL, EVP_PKEY_RSA_method, MD5_CBLOCK, /* 分组长度 */ sizeof(DEMO_DIGEST_CTX) /* 上下文需要多大空间 */ };其中demo_digest_init、demo_digest_update、demo_digest_final就是具体的算法实现,也是你对接硬件或第三方库时真正要写的那部分代码。各项字段的准确含义要参考EVP_MD数据结构,这里不展开。填完这张表,再通过ENGINE_set_digests挂到 Engine 上,整条链路就通了。
3.4 接管范围:ENGINE_set_default的 Flag 怎么选
ENGINE_set_default(ENGINE *e, int Flag)决定这个 Engine 接管哪些算法类别,Flag 的取值直接对应不同的接管范围:
| Flag 取值 | 接管范围 |
|---|---|
ENGINE_METHOD_ALL | 所有存在的算法(默认) |
ENGINE_METHOD_RSA | 仅 RSA |
ENGINE_METHOD_DSA | 仅 DSA |
ENGINE_METHOD_DH | 仅 DH |
ENGINE_METHOD_RAND | 仅随机数算法 |
ENGINE_METHOD_CIPHERS | 仅对称加解密算法 |
ENGINE_METHOD_DIGESTS | 仅摘要算法 |
选 Flag 的原则是“只接管你确实实现好的那部分”。比如你的设备只加速了摘要运算,就老老实实传ENGINE_METHOD_DIGESTS,别图省事传ENGINE_METHOD_ALL——否则 RSA 等算法也会被指到你的实现上,一旦没实现全,握手阶段就会翻车。这个特性还有个额外好处:可以用它把 OpenSSL 里原本没有的加解密算法加进来,让开发加解密算法的应用程序更简便,相比传统开发方法更稳妥。
4. 常见误区与易混概念:别把 Engine 和这些东西搞混
4.1 易混点一:Engine 和具体加密算法
初学者常把OPENSSL_ENGINE当成一种算法名字,看到文档里写“Engine 支持 RSA”就以为 Engine 是 RSA 的某种变体。混淆的原因在于两者总是一起出现。区分方法很简单:算法是“算什么”,Engine 是“由谁来算”。RSA 是算法,Engine 是决定 RSA 交给软件库还是硬件卡去执行的那层调度。你换 Engine 不会改变算法本身的数学定义,只会改变执行它的后端。
4.2 易混点二:Engine 和动态库
有人觉得加载 Engine 就是加载一个.so动态库,两者是一回事。实际上动态库只是载体,Engine 是库里面注册出来的一套函数指针集合。dynamic这个内嵌接口就是专门用来动态加载 Engine 设备的,它负责把库加载进来并找到注册入口,但库本身不等于 Engine。区分点在于:库可以加载失败、可以加载了但里面没有合法 Engine,这时 OpenSSL 依然正常工作,只是没接管任何算法。
4.3 易混点三:ENGINE_set_default和ENGINE_add
这两个函数经常被连着写,导致很多人以为它们作用一样。ENGINE_add只是把 Engine 登记进全局列表,让它“存在”;ENGINE_set_default才是把它设为某类算法的默认提供者,让它“上岗”。只 add 不 set_default,你的 Engine 就在列表里躺着,业务调用根本不会走到它。反过来,set_default 失败时再 add 作为兜底,是常见写法,但要知道这两步语义完全不同。
4.4 易混点四:Engine 和 Windows CSP
原文明确说 Engine 机制的功能跟 Windows 提供的 CSP 功能目标基本相同,于是有人以为两者可以互换使用。目标相同不等于实现相同:CSP 是 Windows 平台的一套加密服务提供者接口,Engine 是 OpenSSL 自己的可插拔后端机制,跨平台、跟着 OpenSSL 走。你在 Linux 上谈 Engine,在 Windows 上谈 CSP,别把两套接口的调用方式混着记。
4.5 易混点五:内嵌支持等于完整支持
OpenSSL 0.9.7 版本内嵌支持的第三方加密设备有 8 种,包括 CryptoSwift、nCipher、Atalla、Nuron、UBSEC、Aep、SureWare 以及 IBM 4758 CCA 的硬件加密设备,后来还出现了支持 PKCS#11 接口的 Engine 接口,支持微软 CryptoAPI 的接口也有人开发。但要注意,这些 Engine 接口的支持不一定很全面,比如可能只支持其中一两种公开密钥算法。看到“支持某设备”就默认全算法可用,是典型的踩坑点,接入前务必确认它到底接管了哪几类算法。
5. 什么时候该用 Engine,什么时候别碰
先说该用的场景。第一种是性能瓶颈明确落在加解密上:比如某台服务器 TLS 握手压力大,而手头有能加速摘要或对称运算的硬件,用 Engine 接管对应算法,业务代码几乎不动就能吃到加速。第二种是合规或安全要求密钥不出硬件:私钥必须待在加密设备里,这时通过ENGINE_set_load_privkey_function这类挂载点让签名操作在设备内完成,是正路。第三种是引入 OpenSSL 原生没有的算法:借助 Engine 把新算法挂进来,比改 OpenSSL 源码稳妥得多。
再说别碰的场景。如果你的系统只是普通 Web 服务,加解密开销占比很低,为了“看起来专业”硬接一个 Engine,只会增加部署复杂度和故障面——多一个动态库、多一个初始化步骤、多一个可能加载失败的环节。还有一种情况是设备厂商的 Engine 只实现了部分算法,你却用ENGINE_METHOD_ALL一把梭,结果某些握手路径走到未实现的函数上直接崩,这种翻车在现场排查起来相当费劲。判断标准其实就一条:Engine 带来的收益(性能、合规、新算法)是否明显大于它引入的运维成本。
我自己的习惯是,每次要接一个 Engine 之前,先写一个最小验证:只加载、只ENGINE_set_default到单一算法类别,跑一遍对应的加解密和摘要,确认函数指针真的被接管了,再逐步扩大 Flag 范围。这个“后悔药”式的步骤帮我省过好几次通宵排查。从那以后我每次接入新的加密后端,都强制先走一遍单算法验证再放开范围。希望帮到你。
本文还有配套的精品资源,点击获取