开源之夏-4

第五周工作进展与疑问

第五周主要围绕gmssl库中sdf_init.h熟悉代码。

通读了密标委的SDF接口文件,找到了数据结构和函数接口的一一对应关系。

下载了Gmssl的子项目SoftSDF,进行了连接测试。

sdf_init.h定义了实现SDF接口的所有数据接口和函数接口。

1
2
3
4
5
6
7
8
9
10
11
12
/home/ddd/GmSSL/build/bin/gmssl sdftest -lib /home/ddd/SoftSDF-main/SoftSDF-main/build/libsoftsdf.so -kek 1 -key 1 -pass P@ssw0rd
test_SDF_GenerateRandom() ok
test_SDF_Hash() ok
test_SDF_Hash_Z() ok
test_SDF_GenerateKeyWithKEK() ok
test_SDF_CalculateMAC() ok
test_SDF_Encrypt() ok
test_SDF_Encrypt_SM4_CBC() ok
/home/ddd/SoftSDF-main/SoftSDF-main/softsdf.c:1817:SDF_Encrypt():
sdfutil: /home/ddd/GmSSL/src/sdf/sdf_lib.c 1192: SDF_Encrypt SDR_INARGERR
/home/ddd/GmSSL/tools/sdftest.c:1121:test_SDF_Encrypt_SM4_ECB():
/home/ddd/GmSSL/tools/sdftest.c:2255:sdftest_main():
1

现有问题

1.GMSSL 没有 provider/engine,如何支持硬件?

GMSSL 通过 SDF(GM/T 0018)对接硬件:运行时装载厂商库为 SDF_METHOD(函数指针表),用 SDF_VENDOR 适配算法 ID/能力位/ECCCipher 编解码/错误码;对外统一 SDF_* API(src/sdf/sdf_lib.c),上层还有易用封装(src/sdf/sdf.c)。

2.sdf_init.h里边的SDF_VENDOR 结构体“在做什么?”

厂商适配“虚函数表”:把标准与厂商差异做映射(cipher/digest/pkey 标准↔厂商、cap 位图映射、ECCCipher encode/decode、错误码解释),实现“统一 API,厂商差异可插拔”。

3.“隔离”“VENDOR”“函数指针表”再解释

隔离:把厂商差异集中在适配层,避免污染上层业务。
VENDOR:具体设备厂商(HSM/密码卡)的适配策略对象。
函数指针表:运行时决定调用哪个实现(类似 vtable)。

4.sdf_lib.c 和 sdf.c 的区别

sdf_lib.c:标准 SDF_* 的统一实现与分发(适配 vendor,调用厂商库)。
sdf.c:面向应用的易用封装(简化参数/上下文/流程),基于 SDF_*。

5.为什么要有“易用封装层”

降低使用复杂度,隐藏算法 ID/结构体细节,提供默认安全、资源生命周期管理与更统一的错误处理,支持一站式组合操作。

6.如何测试 SoftSDF 的函数(你已编出 libsoftsdf.so、softsdfinit、softsdftest)

用 gmssl 工具链通过 -lib 指向 libsoftsdf.so 做端到端验证:sdfinfo、sdftest、sdfexport(导出公钥)、sdfsign+sm2verify、sdfencrypt+sdfdecrypt、sdfdigest、sdfutil -rand。建议先最小化排查再跑 sdftest 并收集日志。
sdftest 退出码 1 的诊断

先跑 sdfinfo、sdfutil -rand、sdfexport、sdfsign/verify、sdfencrypt/decrypt、sdfdigest 定位失败点;用绝对路径 -lib;必要时 ldd 检查依赖;将 sdftest 首个错误输出贴出定位。

7.为什么 GmSSL 不用 provider/engine

依赖收敛与可移植性;engine 已弃用、provider 接口演进成本高;SDF 的设备/会话/索引语义与 EVP 不完全匹配;直连 SDF 性能更直接、控制更强;中国密码生态优先。若必须融入 OpenSSL 生态,可另外实现一个 provider,但工程量大。

8.我现在要实现的 SDF 接口已经脱离了 EVP 体系吗?

是。GmSSL 的 SDF 完全独立于 EVP/provider/engine。实现/调用标准 SDF_* 即可,由 SDF_METHOD/SDF_VENDOR 连接厂商库。

9.GMSSL 的 SDF 与当前 sdf_Tongsuo 的差异与指导

GMSSL:完整方法表+厂商适配层+工具链;TongSuo:已有 TSAPI_SDF_* 封装与方法表,但部分 API(如 Hash)和厂商适配层缺失。建议先补 HashInit/Update/Final 的方法表/DSO 绑定/包装,再完善对称/ECC/对象 API,逐步引入轻量 vendor 适配与命令行/单测。
什么是 TSAPI、什么是 DSO

TSAPI:TongSuo 对外 API 的前缀/接口层(做参数校验/错误规范/调度方法表)。
DSO:动态装载外部库的抽象(dlopen/dlsym/LoadLibrary),运行时绑定厂商 libsdf.so 的符号。
还是没懂,为什么要在链接时绑定

链接时绑定:依赖强可用、代码简单、编译期类型检查、调用开销低、合规易控、版本/ABI 锁定。运行时绑定(DSO):可插拔、多厂商/热替换。建议两条路径并存:默认 DSO,可选强链接(构建选项)。

10.如何将软实现的SDF集成到Tongsuo?

我尝试添加编译选项,一直跑不通。

GPT说:

  • 不用“只加一个编译选项”硬连就完事。最佳做法是“DSO 默认 + 可选强链接”,补齐 Hash 等方法表与绑定,提供环境变量 TS_SDF_LIB 指向 SoftSDF。按上面的最小链打通后,你的 sdf_Tongsuo 即可用 SoftSDF 做端到端测试。

可是我还是没看懂,这部分求老师给我看看该怎么办好。

小结(精炼版)
方法表 = C 里实现“可插拔后端 + 统一调用接口”的常规手段,解决多实现选择、动态加载、测试 mock、扩展演进、错误集中处理、最小可链接占位等问题;代价是极小的间接调用开销和一些结构体样板。对库级 / 可扩展 / 可能接第三方硬件的场景几乎是标准解。

C,但是先给我讲讲软件实现思路,我基础较差,第一次接触这种工程项目,请告诉我该如何做

你要做 C)——写一个“纯软件实现”后端,但你是第一次接触这种工程。我先不改代码,先把整体思路、分步骤路线、关键数据结构与每个函数该干什么详尽讲清楚。看完你就知道“先做哪一点、做到什么算完成、下一步如何扩展”。之后你说“可以动手了”,我再帮你生成骨架文件和占位实现。

总体目标一句话

实现一套 SDF_METHOD(比如命名 ts_sdf_sw_meth),不用真实硬件,用 OpenSSL/Tongsuo 已有的 EVP/随机/ECC 功能,把各个 SDF API 的功能在内存里模拟出来:设备 → 会话 → 密钥句柄 → 操作。


分阶段路线图(建议按阶段逐步提交)

  1. 最小可用阶段 (MVP):能成功 OpenDevice/OpenSession/GenerateRandom/Close。
  2. 对称密钥阶段:ImportKeyWithKEK / DestroyKey / Encrypt / Decrypt / MAC。
  3. 内部 ECC 公钥导出、内部签名(简单用软件生成固定索引私钥数组)。
  4. 内部 ECC 加解密(SM2 Encrypt/Decrypt)。
  5. 扩展 GenerateKey(动态生成对称密钥并返回句柄)。
  6. 访问控制(Get/ReleasePrivateKeyAccessRight)+ 密钥权限管理。
  7. 错误码、线程安全、资源清理、能力查询扩展。

每完成一个阶段,写一个极简自测(小 main 或单元测试)验证,再进入下阶段。


概念与数据结构设计

1. 设备 (Device)

代表“一个逻辑加密设备”。软件实现里实际就一个实例。

结构示例:

1
2
3
4
5
6
7
typedef struct {
int opened;
unsigned int refcnt;
CRYPTO_RWLOCK *lock; // 线程安全
// 预置内部私钥(按索引)
EVP_PKEY *isk_ecc[FIXED_MAX_ISK]; // e.g. 16
} SW_SDF_DEVICE;

OpenDevice:

  • 如果已存在全局 device,则增加引用 + 返回指针。
  • 否则分配、初始化锁、生成若干内部 ECC 私钥(或懒生成:第一次用到某 index 再生成)。
    CloseDevice:
  • 减引用;为 0 时释放所有密钥、锁、结构。

2. 会话 (Session)

提供临时操作上下文、随机、已导入会话密钥列表。

1
2
3
4
5
typedef struct {
SW_SDF_DEVICE *device;
STACK_OF(SW_SYMKEY) *sym_keys; // 存导入/生成的对称密钥句柄
CRYPTO_RWLOCK *lock;
} SW_SDF_SESSION;

OpenSession:

  • 分配 session,引用 device(refcnt++)。
    CloseSession:
  • 释放所有会话密钥,device refcnt–,释放 session。

3. 会话对称密钥句柄 (Key Handle)

1
2
3
4
5
6
7
typedef struct {
int alg_id;
size_t key_len;
unsigned char key[64]; // 支持最大 512 bit 对称密钥(如 SM4 16, AES 32)
int wrapped; // 是否来自 KEK 包封
int in_use;
} SW_SYMKEY;

DestroyKey:

  • 从 session 的栈/数组中找到该指针,清零内存后释放。

4. 内部私钥 (Internal SK / ISK)

  • 存在 device 里:isk_ecc[index]
  • 索引方式:uiISKIndex
  • 生成方法:EVP_PKEY_keygen 用 SM2(或 prime256v1,如果 SM2 不可用)算法。
  • ExportSign/EncPublicKey:读取对应私钥的公钥参数,填充 OSSL_ECCrefPublicKey(需要坐标转大端定长缓冲;先简化:只要长度 bits + x,y 的 bn2bin 填入)。

5. 算法 ID 映射

你需要一张表把 uiAlgID → EVP 层描述:

1
2
3
4
5
6
7
typedef struct {
unsigned int alg_id;
const char *cipher_name; // "SM4-CBC", "AES-128-CBC" ...
size_t key_len; // bytes expected
size_t iv_len; // bytes required
int is_block;
} ALG_MAP_ENTRY;

在 Encrypt/Decrypt:

  • 按 alg_id 找映射;检查 key 长度、IV 长度;调用 EVP_CipherInit_ex + Update + Final。

MAC:

  • 如果 SDF 语义是对称算法的 CBC-MAC 或 GMAC,需要再拆;
  • 简化第一版:用 HMAC(SM3) 产生 MAC(只为让接口通,后续再换成标准 SDF 语义)。
  • 或者:如果 alg_id 指 SM3,直接 EVP_DigestInit / Update。

6. 随机数

x_GenerateRandom

  • 直接调用 RAND_bytes(pucRandom, uiLength);返回 0 表示成功(和你的错误码约定一致)。

7. ImportKeyWithKEK / ImportKeyWithISK_ECC

第一版可以“假实现”,即:

  • 忽略 KEK/ISK 真实解包流程,直接把输入的密钥材料(假定是明文)复制到 SW_SYMKEY,返回句柄。
  • 同时在注释里写:TODO:实现真实 KEK 包封解密流程。
    益处:先让上层流程跑通,之后再补安全细节。

8. InternalEncrypt/Decrypt_ECC (SM2)

  • 使用 EVP_PKEY_encrypt / EVP_PKEY_decrypt(SM2 需要设置 SM2 ID 或者用 EVP_PKEY_CTX_set1_id),不行的话采用 OpenSSL SM2 封装函数。
  • 填充/解析 OSSL_ECCCipher:包含 C1(x,y) + C3(M) + C2(密文) 的 SM2 标准格式。第一版可直接返回 NOTSUPPORT,先实现 Sign,之后再做 Encrypt/Decrypt。

9. InternalSign_ECC

  • EVP_DigestSign* (SM3 + SM2)。流程:
    1. EVP_MD_CTX_new
    2. EVP_DigestSignInit with SM3/SM2 key
    3. EVP_DigestSignUpdate(data)
    4. EVP_DigestSignFinal 拿 (r||s) DER;解析 DER 里的 r,s BIGNUM,再写入 OSSL_ECCSignature 固定缓冲(大端填充到 MAX_LEN)。
      (第一版也可以只返回 NOTSUPPORT;但签名逻辑相对简单,早实现有价值。)

10. GenerateKey (扩展)

  • 根据 type(自定义:比如 1=对称 SM4, 2=AES-128, …)和 len
    • 分配 SW_SYMKEY
    • RAND_bytes 填充密钥
    • 推入 session
    • 返回句柄
  • no_kek 若含义是“不要外部包封形式”,第一版可忽略;后续支持时再增加一个输出缓冲参数(如果标准需要)。

11. 私钥访问控制

初版可直接返回成功(假装已授权)。以后:

  • 保存某索引是否“已授权”状态 bitset,在 GetPrivateKeyAccessRight 校验密码(可用硬编码)后设置 bit;ReleasePrivateKeyAccessRight 清除 bit,没有权限的内部操作返回错误。

12. 错误码策略

统一宏:

1
2
3
4
5
6
#define SDF_OK 0
#define SDF_ERR_NOTSUPPORT 0x2001
#define SDF_ERR_BAD_PARAM 0x2002
#define SDF_ERR_NO_MEMORY 0x2003
#define SDF_ERR_NOT_FOUND 0x2004
#define SDF_ERR_INTERNAL 0x20FF

每个函数最前做参数检查;失败路径 goto cleanup。

13. 线程安全

  • 设备结构有读写锁;操作内部私钥/生成新私钥需加写锁。
  • 会话结构有锁;增删 sym_keys 要加锁。
  • 若第一版只在单线程测试,可先不加锁,后续补。

14. 资源生命周期

函数分配/释放
OpenDevice分配 device(或复用),refcnt++
CloseDevicerefcnt–,为 0 才真正释放
OpenSession分配 session + ref device
CloseSession释放其持有 key 句柄,ref device
Import / GenerateKey分配 SW_SYMKEY(挂在 session)
DestroyKey从 session 移除、清零、free

确保任何返回错误的路径不泄露内存:使用统一 cleanup。

15. 日志/调试(可选)

加一个简单宏:

1
2
3
4
5
#ifdef SDF_SW_DEBUG
# define SW_LOG(...) fprintf(stderr, "[SDF_SW] " __VA_ARGS__)
#else
# define SW_LOG(...)
#endif

开发时开,发版时关。


从 0 到 MVP 的具体步骤(最小工作量版本)

第一轮只做 5 个函数:

  1. x_OpenDevice
  2. x_CloseDevice
  3. x_OpenSession
  4. x_CloseSession
  5. x_GenerateRandom

详细操作:

(1) 新文件:sdf_sw.c
(2) 定义内部结构 SW_SDF_DEVICE / SW_SDF_SESSION
(3) 全局静态指针:static SW_SDF_DEVICE *g_sw_device = NULL;
(4) 实现 5 个函数逻辑(如上)
(5) 组装 SDF_METHOD ts_sdf_sw_meth = { ... (填只实现的函数,其余指向占位或本文件里的 not_support 函数) }
(6) 在 loader 里(你还没贴 sdf_lib.c,可以稍后)加:

  • 如果未加载硬件库,fallback 到 ts_sdf_sw_meth,再不行退到 ts_sdf_meth
    (7) 写一个简易测试 main(或暂时用现有对外 API 调用)验证随机数与句柄生命周期。

逐步增加对称加密

添加:

  • ALG_ID 常量(假设你在 <openssl/sdf.h> 里已有;如果没有先临时定义)。
  • static const ALG_MAP_ENTRY alg_map[] = { ... }
  • x_ImportKeyWithKEK: 复制输入 pucKey → 新 SW_SYMKEY
  • x_Encrypt/x_Decrypt:
    • 查 key handle → alg_id → cipher_name
    • EVP_CIPHER *cipher = EVP_CIPHER_fetch(NULL, cipher_name, NULL);
    • EVP_CipherInit_ex(ctx, cipher, NULL, key, iv, enc=1/0);
    • Update + Final
  • 处理输入/输出长度(需要上层提供足够缓冲;第一版不做分片,假设一次全部)

MAC:

  • 先用 HMAC(SM3)(或 SM3 digest 直接当 MAC):
    • EVP_DigestInit_ex(ctx, EVP_sm3(), NULL);
    • Update(data)
    • Final(macbuf)

ECC 签名实现亮点(稍后阶段)

  1. 预置/懒生成私钥:
    1
    2
    3
    4
    static EVP_PKEY *get_isk(int idx) {
    if (!device->isk_ecc[idx]) generate_sm2_key();
    return device->isk_ecc[idx];
    }
  2. 签名:
    1
    2
    3
    4
    5
    6
    EVP_MD_CTX *mctx = EVP_MD_CTX_new();
    EVP_DigestSignInit(mctx, &pctx, EVP_sm3(), NULL, pkey);
    EVP_DigestSignUpdate(mctx, pucData, uiDataLength);
    EVP_DigestSignFinal(mctx, der, &derlen);
    解析 DER -> r,s
    填充 OSSL_ECCSignature
  3. 加解密参考 SM2 标准(可以推迟)。

判断“一个阶段完成”的验收标准(Checklist)

阶段 1 (MVP):

  • OpenDevice 返回 0
  • OpenSession 返回非空句柄
  • GenerateRandom 返回不同随机数据
  • CloseSession 不泄露(运行 valgrind/ASAN 若可)
  • CloseDevice 后再次 Open 仍能工作

阶段 2 (对称):

  • ImportKeyWithKEK 后返回句柄
  • Encrypt + Decrypt roundtrip 等于原文
  • DestroyKey 释放后再用返回 NOT_FOUND

阶段 3 (签名):

  • ExportSignPublicKey 返回 bits 和坐标非零
  • InternalSign_ECC 返回固定长度 r,s
  • (可选)使用 EVP_PKEY_verify 复核签名正确性(需要再写一个 verify 辅助)

常见初学坑提示

说明避免方式
句柄直接用数组下标删除时挪动导致悬挂用指针 + list/stack 保存
未清零密钥内存安全风险OPENSSL_cleansememset_s
EVP_CTX 复用不当加密/解密错乱每次操作新建+free 或正确 Reset
忘记 Final 导致少尾块CBC/PKCS7 出错Update 后必须 Final
SM2 签名 DER 解析错r,s 有前导 0x00使用 BIGNUM 读取后再 bn2binpad 定长填充
多线程竞态refcnt 不准加锁或暂时单线程

给你的一份“最小软件实现框架”脑图(文字版)

设备生命周期
→ OpenDevice: alloc/ref
→ OpenSession: alloc session (ref device)
→ GenerateRandom: RAND_bytes
→ ImportKey: store SW_SYMKEY
→ Encrypt/Decrypt: EVP cipher
→ CalculateMAC: EVP digest/HMAC
→ InternalSign: SM2 + SM3
→ DestroyKey: erase & free
→ CloseSession: free keys & deref device
→ CloseDevice: free all when refcnt=0


你现在可以怎么做

选项:
A. 先让我生成“阶段 1 + 阶段 2 的骨架代码”(全部函数写好结构 + TODO + 基本对称加密)。
B. 只生成阶段 1 最小 5 个函数,自己熟悉后再扩展。
C. 先生成数据结构头文件(sdf_sw_local.h)+ 空实现文件。

告诉我你选哪种(或自定义),我就直接在仓库里加文件并写初版代码。也可以先问任何一块你还没弄懂的,我再展开。

回复一个选项或继续提问就行。

这个软实现骨架必须调用evp吗?还是我可以不用其它库实现
你问:软实现骨架“必须”用 EVP 吗?还是可以完全不用其它库自己写?下面给你一个清晰决策框架:什么时候强烈建议用 EVP,什么时候可以(或不得不)绕过,完全自写要面对哪些成本与坑,并给出混合方案。

核心结论先说

  • 不是“必须”,理论上你可以不用 EVP,自己实现所有算法。
  • 实际工程里强烈建议:90% 功能用 EVP/Provider 层现成实现,只在极少数 SDF 规范与 EVP 语义不完全吻合的地方做“格式适配/封装”。
  • 彻底不用 EVP 重新实现算法,风险极高(正确性、安全性、时间成本、侧信道防护、FIPS/国密一致性),除非有硬性约束(比如严禁依赖 OpenSSL/Tongsuo 算法实现、做学术演示、练习用途、或要通过某些合规流程要求算法独立验证)。

选项对比一览

方案描述优点缺点 / 风险适用场景
A: 全部用 EVPSDF 函数内部直接调用 EVP_* / RAND_bytes / EVP_PKEY_*快速、安全、维护轻需理解 EVP 抽象与参数映射标准工程首选
B: 混合封装80% 用 EVP,某些结构(SM2 Cipher 格式、特殊 MAC)自己拼装折中,减少重复仍需理解底层格式SDF 规范格式与 EVP 输出略有差异
C: 低层 provider 调度使用 provider dispatch(OSSL_DISPATCH)或 EVP_*_fetch 做更细粒度控制可选特定实现(软/硬)复杂度高需要多实现策略/性能调优
D: 直接调用低层算法内部函数(非公开 API)引用库内部私有符号可能性能略高升级易崩,API 不稳定不推荐
E: 自己实现全部 SM2/SM3/SM4/AES/HMAC/RNG全部手写理论上最大自主极高安全与时间成本;易有侧信道漏洞只适合研究或无第三方依赖硬约束
F: 部分自写(比如仅 SM2 封装),其余 EVP自写少量特殊算法在特殊需求下减依赖仍有安全负担SM2 特殊行为需求

为什么优先用 EVP(具体价值)

  1. 安全性:已有的常量时间实现、边界检查、随机数质量保障。
  2. 算法演进与补丁:上游修复(比如 side-channel)自动受益。
  3. 多算法统一接口:减少你 SDF 层分支复杂度,只维护“ID→EVP 名称的映射”。
  4. 性能优化:硬件加速(AES-NI、SM4 指令、国密指令扩展)由 EVP 层透明启用。
  5. FIPS / 国密模式:将来若要进入受控模式,只需设置加载合规 provider。
  6. 测试覆盖:上游已有大量测试;你只需补 SDF 语义对应的小测试。
  7. 代码量减少:节省大量调试时间,把精力放在句柄/生命周期/错误码/规范兼容。

不用 EVP 自己实现要面对的清单

类别需要做的事
对称加密SM4/AES 模式实现 + PKCS#7 填充 + IV 管理 + 分组对齐
MACHMAC 或 CBC-MAC/GMAC 算法实现,注意 padding 与验证
随机数真随机源熵聚合,或至少系统调用 (/dev/urandom/WinCrypt/getrandom);DRBG 构建
ECC/SM2椭圆曲线点运算(大数库、自带曲线参数)、签名/加解密、KDF、C1C3C2 编解码
哈希SM3 / SHA 系列迭代函数、长度/位数填充
常量时间比较避免早停差异导致侧信道
内存清零安全清零函数(避免被优化掉)
测试向量覆盖全部算法官方/公开测试向量验证正确性
侧信道防护避免分支、避免 secret-dependent array index
维护 & 升级跟进规范/安全公告,自测回归

这对“第一次接触这种工程”的你来说几乎不可取。


混合封装的典型场景(你后面很可能碰到)

SDF 规范里一些结构化格式(举例 SM2 Encrypt 输出 C1 || C3 || C2,内部字段长固定)和 EVP 原生输出(可能是整体 DER 或者不同字段分离)不 1:1 对应。做法:

  1. 用 EVP 生成或解析“核心密码材料”(例如 SM2 加密输出的点坐标、密文、hash)。
  2. 自己按照 SDF 定义打包进 OSSL_ECCCipher(x,y,M,L,C[…])。
  3. 反向解密时再拆开。

这样只写“格式转换”代码,不写“曲线点乘 / KDF”。


如果仍想验证“手写最小实现”可行(学习用)

你可以挑一个最简单环节单独练练手,比如:

  • 只写一个伪随机函数(调用 RAND_bytes 作为后备);
  • 或只手写一个 XOR“加密”算法 ID,用来感受句柄/生命周期,而真实算法用 EVP。

但最终产出还是要替换成真实安全算法。


推荐分层结构(使用 EVP 的优雅方式)

逻辑分两层:

层 A:SDF 接口适配层

  • 实现 x_Encrypt/x_Decrypt/x_CalculateMAC/x_InternalSign_ECC
  • 做参数检查、句柄查找、错误码转换、结构打包

层 B:算法调度层(极薄)

1
2
3
4
5
6
7
typedef struct {
unsigned int alg_id;
const char *cipher_name;
size_t key_len;
size_t iv_len;
int mode; // 标识需要特殊处理,比如 STREAM / AEAD
} SW_ALG_DESC;

lookup_alg(alg_id) 返回描述,然后使用:

1
2
3
EVP_CIPHER *cipher = EVP_CIPHER_fetch(libctx, desc->cipher_name, NULL);
EVP_CipherInit_ex(ctx, cipher, NULL, key, iv, enc);
EVP_CipherUpdate/Final

SM2 签名:

1
2
3
4
5
6
7
EVP_PKEY *pkey = ... (内部索引)
EVP_MD_CTX *m = EVP_MD_CTX_new();
EVP_DigestSignInit(m, &pctx, EVP_sm3(), NULL, pkey);
EVP_DigestSignUpdate(m, data, len);
EVP_DigestSignFinal(m, NULL, &needed);
buf...
解析DER r,s → 填 OSSL_ECCSignature

何时可能“临时不用 EVP”

  1. 想先做一个极简演示:加密 = 明文复制;MAC = 全 0;只验证调用链。这种阶段性 stub 可以帮助调通生命周期,不涉及真实算法。完成后必须替换。
  2. 某特定厂商给了自带闭源静态库,只暴露低层函数,你需要包装它们填进方法表(仍然是“非 EVP”,但逻辑等价)。
  3. 某算法 EVP 尚未支持(极少数冷门或最新算法)——此时你自己实现后,未来可以再迁移到官方 provider。

决策指导流程(给你一个简单判断树)

  1. 目标是交付可用安全版本? → 用 EVP (YES)
  2. 只是演练接口生命周期? → 可以先写假实现,但打 TODO
  3. 有监管 / 合规 / FIPS 可能? → 必须用受控 provider (EVP)
  4. 是否有算法 EVP 不支持? → 仅该算法局部自写,其余用 EVP
  5. 你有资深密码实现经验?(常量时间 & 大数)→ 如果没有,请不要全栈自写

落地建议(对你这种“第一次接触”)

第一阶段(MVP):

  • 所有加密/随机/签名都用 EVP
  • Import/GenerateKey 直接存明文 key(后续再做 KEK 包封)
  • InternalSign 实现,InternalEncrypt 可以暂时 NOTSUPPORT

第二阶段:

  • 引入算法映射表
  • 增加 DestroyKey / Encrypt / Decrypt / MAC

第三阶段:

  • 实现 SM2 Encrypt/Decrypt(借助 EVP_PKEY_encrypt/ decrypt 或 provider 专用接口)
  • 结构格式转换

第四阶段:

  • 访问控制 / 权限 / 错误码清晰化
  • 安全清零 / 销毁顺序

你如果“坚持不用 EVP”最小工作量方向(不推荐但给你参考)

挑一个强约束最短路径:

  1. 只支持 SM4-CBC PKCS7:手写 SM4 轮函数 + CBC 模式 + padding。
  2. 只支持伪随机:读取 /dev/urandom / Windows RtlGenRandom。
  3. 只支持“签名”= 哈希(SM3)后截断前 32 字节(伪签名,危险!)——仅调通流程。
  4. 所有其它函数返回 NOTSUPPORT。

然后再逐个替换为正式实现。这种方式风险是容易“忘记”替换伪实现进入产线。


结论重申 + 建议下一步

  • 绝大多数情况下:用 EVP。你当前经验水平下这是正确路线。
  • 不用 EVP 只有“演示 stub”或“外部闭源库”两类必要场景。
  • 建议你让我们进入“生成阶段 1 + 阶段 2(对称)”的 EVP 骨架,这样你能看到具体代码模式,然后再继续扩展。

如果你同意,就回复:“生成 A(或 A+B)骨架”,我会:

  • 新增 sdf_sw.c
  • 定义内部结构
  • 填写 Open/CloseDevice/Session/GenerateRandom
  • 加对称 Encrypt/Decrypt(用 EVP)
  • 其它函数留 TODO 注释

或者你想先只看最小 5 个函数也可以。告诉我你的选择。

我不明白,evp和sdf应该是两套体系,那么我现在软实现sdf接口的“基座”,还需要调用evp接口?我说的对吗?

你现在困惑的点抓得很准:EVP 和 SDF 是两套“体系 / 抽象层”。问题是:在做一个“纯软件版”的 SDF 接口实现时,是否应该在内部调用 EVP?下面把概念分开、再说明为什么“调用 EVP”其实是一种非常典型、合理的“适配层”做法,并不是混淆体系。

✅ 你的问题拆解(Checklist)

  • SDF 与 EVP 是否同一层 / 同一体系?(否,是不同抽象)
  • 软实现 SDF“基座”是否必须依赖 EVP?(不是必须,但强烈推荐)
  • 为什么可以 / 值得在 SDF 实现里调用 EVP?(作为下层密码原语提供者)
  • 如果不用 EVP,有什么代价?(安全、时间、正确性成本)
  • 什么时候不调用 EVP?(已有硬件、闭源驱动或学习 stub)

1. 两套体系的定位(垂直分层,而不是平行冲突)

可以把它们想成三层:

1
2
3
4
5
[ 应用 / 框架 / 你的业务 ]
↓ 调用
[ SDF API (对外暴露的接口语义:设备/会话/句柄/内部索引) ]
↓ 由你实现,内部需要“做真正的密码”
[ 密码原语提供者:EVP(软件) / 硬件驱动 / 专有库 / 自写算法 ]
  • SDF:面向“设备 + 会话 + 句柄 + 内部索引 + 国密接口规范”语义的一组函数集合,强调“硬件/受控资源管理”。
  • EVP:通用的密码算法高层 API(抽象各种算法:对称、杂凑、签名、KDF、随机),不关心“设备句柄/密钥索引”这些 SDF 设备概念。
  • 你的软实现:用软件模拟“设备/会话/密钥管理”;真正做加密、签名时,需要调用一个“密码引擎”——EVP 正好就是现成引擎。

所以:SDF 是业务侧协议/接口层;EVP 是算法执行层。它们不是“竞争”关系,是“适配”关系。

2. 为什么在 SDF 实现里调用 EVP 是常规模式

把每个 SDF 函数拆开看:

SDF 函数语义内部需要的底层能力直接用 EVP 是否合适
OpenDevice / OpenSession分配结构、引用计数与算法无关,不用 EVP
GenerateRandom真随机 / DRBGRAND_bytes()
ImportKey / DestroyKey存储 & 清零自己写;不需要 EVP
Encrypt / Decrypt对称算法 + 模式 + PaddingEVP_Cipher*
CalculateMACHMAC / HashEVP_Digest* / EVP_MAC*
ExportEnc/SignPublicKey公钥序列化EVP_PKEY 提取坐标 ✅
InternalSign_ECCHash+签名(SM3+SM2)EVP_DigestSign*
InternalEncrypt_ECCSM2 加解密EVP_PKEY_encrypt/decrypt
GenerateKey(扩展)随机字节RAND_bytes

只有“设备/句柄管理”是你自己逻辑;算法工作全部交给 EVP,减少大量重复劳动与安全风险。

3. 如果“不调用 EVP”意味着什么

你得自己提供:

  • SM3 / SM4 / SM2 全套算法实现(正确、高性能、抗侧信道)
  • CBC/CTR 模式、填充、MAC/HMAC / KDF
  • 椭圆曲线点运算(大数、逆元、随机私钥生成、参数检查)
  • 随机数来源质量控制
  • 后续安全补丁跟进
    这对初学者实现成本极高,而且容易出安全隐患。

4. 软实现调用 EVP 的准确说法

不是“把两套体系揉在一起变成一套”,而是:

  • 你在实现“一个 SDF provider / backend”,它的“算法执行层”选择“EVP 软件算法”。
  • 如果未来换成真实硬件,只要替换方法表,不再调用 EVP,而是调硬件驱动函数即可。

同理,在另一些场景:

  • TLS 库的高层 handshake → 调用 EVP 加解密
  • PKCS#11 适配层 → 也会在软实现里调用 OpenSSL EVP
    这是标准的“适配桥接”模式(Adapter / Bridge Pattern)。

5. 类比帮助直观理解

类比“操作系统文件 API”:

  • 你的 SDF 函数 = fopen/fread/fwrite/close 这一层的行为(统一接口)。
  • EVP = 磁盘驱动 / 存储控制器的“读写块”实现。
  • 软件模拟时直接用内存数组 + 系统调用。换硬件后,调用真正硬件 ioctls。

6. 你可以选择的 3 种具体路线

路线做法适用
纯 EVP 封装(推荐)SDF 方法表里:设备/句柄逻辑自写,算法全走 EVP快速、安全、维护小
分段:先写假算法再逐步替换第一版 Encrypt 就 memcpy,后面替换成 EVP学习/调通生命周期
最终想接硬件现在写 EVP 版;未来再添加硬件版 method;运行时选择可平滑迁移

7. 一个具体“内部调用 EVP”的最小示例(只示意架构)

伪代码(不需要你现在写,只看结构):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int sw_Encrypt(void *hSession, void *hKey,
unsigned int algId, unsigned char *iv,
unsigned char *in, unsigned int in_len,
unsigned char *out, unsigned int *out_len)
{
SW_SYMKEY *k = (SW_SYMKEY *)hKey;
const EVP_CIPHER *cipher = map_alg_to_evp(algId); // 例如返回 EVP_sm4_cbc()
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
int n1=0,n2=0;
EVP_EncryptInit_ex(ctx, cipher, NULL, k->key, iv);
if (!EVP_EncryptUpdate(ctx, out, &n1, in, (int)in_len)) goto err;
if (!EVP_EncryptFinal_ex(ctx, out + n1, &n2)) goto err;
*out_len = n1 + n2;
EVP_CIPHER_CTX_free(ctx);
return SDF_OK;
err:
EVP_CIPHER_CTX_free(ctx);
return SDF_ERR_INTERNAL;
}

SDF 对外 API 完全看不到 EVP,它只看到“我传了句柄进来得到了密文”。

8. 什么时候你会分离得更彻底

如果未来:

  • 需要根据配置选择“软件实现 / 硬件实现”
  • 需要统计每个算法耗时
    你可以再加一层抽象(算法策略表),但对初学阶段没必要。

9. 快速判断你是否理解到位(自测)

回答下面三个问题(自问):

  1. 如果我以后接硬件,SDF 接口代码是否要大改?——不,需要新增一个硬件 method 表,替换指针。
  2. EVP 与 SDF 是不是同一“规格”的 API?——不是,侧重不同层次。
  3. 我现在写的软件版 SDF 调 EVP,会不会污染或限制以后接硬件?——不会,它只是一个 backend 选择。

如果这三句都能解释给别人听,说明你理解到位。

10. 下一步建议

  • 确认采用“纯 EVP 封装路线”。
  • 先生成软件 method 骨架(Open/Close/Session/Random)。
  • 再加对称加密 + MAC。
  • 然后 ECC 公钥导出 + 签名。
    如果你准备好了,可以让我直接生成那套骨架文件。

总结一句话

SDF 是“规范+设备/句柄”语义层,EVP 是“密码算法执行”层;软实现 SDF 时调用 EVP 只是“用成熟算法引擎填充规范接口”,不是混淆体系,而是标准的适配模式——不是必须,但强烈建议。

告诉我:要不要现在就生成软件实现骨架(告诉我范围:仅阶段 1 或阶段 1+2)?我可以直接加文件。只需回复你的选择。


开源之夏-4
https://43.242.201.154/2025/08/10/开源之夏-4/
Author
Dong
Posted on
August 10, 2025
Licensed under