开源之夏02:SDF接口规范与软件实现设计
进入第三周后,核心工作转向深入理解 SDF 标准规范,并设计一套纯软件模拟实现的架构方案。
理解 SDF 规范:从文档到代码
密标委 发布的 密码设备应用接口规范 (GM/T 0018) 定义了密码卡设备的标准调用接口。通过将规范文档与 GmSSL 源码对照阅读,可以清晰地看到代码结构与规范的一一对应关系。
sdf_int.h 与规范的关系
GmSSL 的 sdf_int.h 定义了实现 SDF 接口所需的所有数据结构和函数接口。关键设计点:
函数指针表(SDF_METHOD) 是整套架构的核心。每种密码设备操作(OpenDevice、Encrypt、Sign 等)都定义为函数指针,运行时通过填充函数指针表来选择具体实现:
1 | |
为什么用函数指针?因为 C 语言没有虚函数和接口——这就是 C 语言中手工实现的”虚函数表(vtable)”。输入参数用一级指针(只读),返回参数用二级指针(需要修改调用者的指针指向),这是 C 语言中表示”输出参数”的标准惯用法。
单包与多包加密
SDF 规范中区分单包(Single-Part)和多包(Multi-Part)操作,这是一个需要理解的重要概念:
- 单包:数据以单个完整块的形式加密/解密。适用于小数据块(一个消息、一个文件)。每次操作独立,不依赖前后文。
- 多包:数据被分成多个部分分批处理。每个部分可能依赖前一个部分的状态(如 CBC 模式的 IV 链)。适用于大数据流或网络传输。
简单说——单包适合”一次性处理”的场景,多包适合”流式处理”的场景。
SDF_VENDOR 结构体:厂商适配的”翻译层”
SDF_VENDOR 结构体本质上是厂商差异的适配层,负责:
- 算法 ID 映射:将标准算法标识映射到厂商自定义 ID
- 能力位映射:将标准能力位图转译为厂商的能力定义
- ECCCipher 编解码:处理 SM2 加密输出格式(C1C3C2 vs C1C2C3)
- 错误码解释:将厂商错误码标准化
它和 SDF_METHOD 的关系是:METHOD 定义”能做什么”(接口契约),VENDOR 定义”这家厂商怎么做”(差异映射)。
sdf_lib.c vs sdf.c
GmSSL 中两个文件的职责不同:
- sdf_lib.c:标准 SDF_* 的统一实现与分发层。适配 vendor,调用厂商库,是 SDF 标准的直接映射。
- sdf.c:面向应用的易用封装层。隐藏算法 ID、结构体细节,提供默认安全参数、自动资源管理和一站式组合操作。
为什么要分层?降低使用复杂度。应用开发者不需要理解设备句柄、会话管理、算法映射等细节——sdf.c 处理这些,暴露更简洁的接口。
为什么不走 EVP/Engine/Provider?
GmSSL 的 SDF 完全独立于 EVP/provider/engine 体系,原因有三:
- 依赖收敛与可移植性:engine 已被 OpenSSL 弃用(3.0+),provider 接口演进成本高且 SDF 的设备/会话/索引语义与 EVP 不完全匹配。
- 性能与可控性:直连 SDF 性能更直接,不经过 EVP 层的额外抽象。
- 中国密码生态优先:国密硬件厂商更习惯于 SDF 标准接口,而非 OpenSSL 的 EVP/provider 范式。
换句话说:SDF 是业务侧协议/接口层,EVP 是算法执行层。它们不是竞争关系,而是适配关系。软实现 SDF 时调用 EVP 属于”用成熟算法引擎填充规范接口”的标准适配模式。
软件实现 SDF 的架构设计
分层思路
1 | |
软件实现的目标:不用真实硬件,用 OpenSSL/Tongsuo 已有的 EVP/随机/ECC 功能,在内存中模拟设备→会话→密钥句柄→操作。
分阶段实现路线
| 阶段 | 目标 | 涉及函数 |
|---|---|---|
| 1. MVP | 打通生命周期 | OpenDevice, OpenSession, GenerateRandom, Close |
| 2. 对称密钥 | 加解密 + MAC | ImportKeyWithKEK, DestroyKey, Encrypt, Decrypt, CalculateMAC |
| 3. ECC 公钥导出+签名 | 国密签名能力 | ExportSignPublicKey, InternalSign_ECC |
| 4. ECC 加解密 | SM2 Encrypt/Decrypt | InternalEncrypt_ECC, InternalDecrypt_ECC |
| 5. 扩展 | 动态密钥生成 | GenerateKey, GenerateKeyPair |
| 6. 访问控制 | 私钥权限管理 | Get/ReleasePrivateKeyAccessRight |
| 7. 完善 | 错误码、线程安全、资源清理 | 全部接口补全 |
每个阶段完成后写极简自测验证,再进入下阶段。
核心数据结构
设备(Device):代表一个逻辑加密设备。软件实现中实际就一个全局实例,包含引用计数、读写锁和预置的内部 ECC 私钥数组。
会话(Session):提供临时操作上下文。引用 Device,维护自己的对称密钥列表(会话范围内的密钥句柄),有独立锁。
密钥句柄(Key Handle):会话内有效的对称密钥对象。包含算法 ID、密钥数据、是否来自 KEK 包封等状态。
为什么强烈建议用 EVP 而非手写算法?
| 方案 | 优点 | 风险 |
|---|---|---|
| 全部用 EVP | 快速、安全、维护轻 | 需理解 EVP 抽象与参数映射 |
| 混合封装 | 80% EVP + 格式适配 | 需要理解底层格式 |
| 完全自写 SM2/SM3/SM4 | 理论上最大自主 | 极高的安全与时间成本、侧信道漏洞 |
对于 SDF 的软实现,90% 的函数内部可以直接调用 EVP:RAND_bytes(随机数)、EVP_Cipher*(对称加密)、EVP_DigestSign*(SM2 签名)、EVP_PKEY_encrypt(SM2 加密)。只有设备/句柄管理是你自己的逻辑。
如果换真实硬件,只需替换方法表中的函数指针,不再调 EVP 而调硬件驱动函数,上层代码不需要修改。
两种绑定策略:DSO vs 静态链接
DSO(运行时动态绑定)
1 | |
- DSO_load ≈ dlopen / LoadLibrary:把
libsdf.so映射进进程地址空间 - DSO_bind_func ≈ dlsym / GetProcAddress:查找符号地址并填入函数指针表
优点:可插拔、多厂商/热替换。缺点:运行时查找有微小开销。
编译期静态链接
直接把实现编译进库,函数指针直接赋值为本地函数。
优点:性能最优(编译期类型检查、零间接调用开销)。缺点:缺少运行时灵活性。
实际工程中两条路径并存——构建脚本决定走哪条。
SoftSDF 迁移
GmSSL 的子项目 SoftSDF 提供了一套软件模拟 SDF 的实现(libsoftsdf.so)。要把它集成到 Tongsuo 体系,需要理解两个配套组件:
- softsdfinit:初始化和密钥生成工具,用于生成 KEK、SM2 密钥对等文件——模拟硬件设备的初始化过程,只负责”造钥匙”。
- libsoftsdf.so:实现 SDF 标准接口的动态库,负责”用钥匙办事”——读取 softsdfinit 生成的密钥文件,提供加解密、签名、密钥管理等接口。
两者配合模拟真实 SDF 设备的完整流程:先初始化设备(生成密钥文件),再加载动态库执行业务操作。
实现思路关键节点(8月18日)
到 8 月中旬,总体思路明确:
- 把 SDF 与 TSAPI 部分移出来,新建独立项目
- 用 EVP 接口在编译期静态绑定,手动实现 SDF 软实现
- 同时学习 SoftSDF(CMake 构建),尝试动态加载到 Tongsuo
- 软实现调通后,开发命令行应用
- 最终完成 69 个接口的全面实现
这个位置处于 Tongsuo 库的腰部:向上打通命令行应用,向下完成接口标准适配与动态库解析。