3. 密码库源码结构解析
裁剪密码库之前,必须先回答一个更基础的问题:这个库到底由哪些部分组成,哪些部分可以拆掉,拆掉之后谁会受影响。mbedTLS、wolfSSL、OpenSSL 虽然都提供 TLS 与密码算法能力,但它们的源码组织哲学差异很大,直接决定了裁剪的难度和手段。本节从模块划分、依赖关系和可裁剪边界三个角度逐一拆解,并给出可操作的判断方法。
三种库的模块划分风格
mbedTLS 采用“算法层—X.509 层—TLS 层”三层结构,目录划分非常直白:library/ 下每个算法基本对应一个 .c 文件,如 aes.c、sha256.c、rsa.c、ecp.c;include/mbedtls/ 下是配套头文件;mbedtls_config.h 是唯一的裁剪开关中心。它的依赖关系是单向的:TLS 依赖 X.509,X.509 依赖算法层,算法层之间只有少量交叉(如 rsa.c 调用 bignum.c)。这种“扁平算法 + 集中配置”的设计,使 mbedTLS 成为最容易按功能裁剪的库。
wolfSSL 的源码更集中,核心算法与 TLS 逻辑大量放在 wolfcrypt/src/ 与 src/ 中,通过 user_settings.h 与 --enable-* 宏控制编译。它的特点是“宏驱动裁剪”:一个 WOLFSSL_AES_128 或 NO_RSA 宏就能决定对应代码是否参与编译。wolfSSL 的依赖耦合比 mbedTLS 略高,例如 TLS 层与密码层共享较多内部结构体,裁剪时需要同时关注 wolfssl/wolfcrypt/settings.h 的宏联动。
OpenSSL 则是“分层 + 引擎 + provider”的复杂体系。3.x 之后引入 provider 架构,算法实现被封装进 providers/,通过 OSSL_PROVIDER 在运行时加载;crypto/ 下是底层原语,ssl/ 是协议层。它的可裁剪边界不像前两者那样由单个头文件决定,而是通过 Configure 的 no-* 选项在构建期剔除模块,例如 no-rsa、no-ec、no-legacy。OpenSSL 的依赖图最复杂,裁剪时最容易出现“关掉一个算法却导致 TLS 握手路径编译失败”的连锁反应。
依赖关系的三种典型形态
理解依赖关系,关键看“谁调用谁”和“谁定义谁”。mbedTLS 的依赖是编译期单向依赖:ssl_tls.c 调用 x509_crt.c,后者调用 rsa.c 和 ecp.c,而算法层之间几乎不反向调用 TLS。这意味着只要保留被 TLS 路径实际引用的算法,其余算法可以整文件剔除。例如关闭 MBEDTLS_ECDSA_C 后,只要 TLS 配置中不再协商 ECDSA 套件,链接就不会报错。
wolfSSL 的依赖是宏条件依赖。同一个 ssl.c 中,不同函数体被 #ifdef HAVE_AESGCM、#ifndef NO_RSA 等宏包裹。裁剪时不是删文件,而是让宏组合成立。风险在于宏之间存在隐含约束:例如启用 HAVE_ECC 但关闭 HAVE_SUPPORTED_CURVES,可能在握手时找不到曲线列表。因此 wolfSSL 裁剪必须做“宏组合编译验证”,而不是只改一个开关。
OpenSSL 的依赖是构建期 + 运行期双重依赖。Configure no-ec 会在编译期移除 EC 相关目标文件,但 provider 架构下,某些算法实现可能仍被默认 provider 引用。若同时使用 OSSL_PROVIDER_load(NULL, "default"),运行期加载的 provider 会决定实际可用算法。裁剪 OpenSSL 时,必须同时检查 Configure 选项、openssl.cnf 中的 provider 配置,以及应用是否显式加载了某个 provider。
可裁剪边界的判断方法
判断一个模块能否裁剪,可以按以下顺序操作。第一步,确定目标功能集合:例如只需要 TLS 1.2 客户端、AES-128-GCM、SHA-256、ECDHE-P256、RSA 签名验证。第二步,从入口函数反向追踪调用链:以 mbedtls_ssl_handshake 为起点,沿 ssl_tls.c → ssl_cli.c → x509_crt.c → pk.c → rsa.c / ecp.c → bignum.c 逐层标记被引用的符号。第三步,对照配置宏,把未被引用的算法宏关闭,重新编译并观察链接错误。第四步,对链接错误逐个判断:是“确实不需要但被间接引用”,还是“目标功能实际需要但被误关”。
以 mbedTLS 为例,一个典型的裁剪配置片段如下:
// mbedtls_config.h 裁剪示例(目标:TLS1.2 客户端 + AES-GCM + ECDHE + RSA 验签)
#define MBEDTLS_SSL_CLI_C
#define MBEDTLS_SSL_TLS_C
#define MBEDTLS_SSL_PROTO_TLS1_2
#define MBEDTLS_AES_C
#define MBEDTLS_GCM_C
#define MBEDTLS_SHA256_C
#define MBEDTLS_ECDH_C
#define MBEDTLS_ECP_C
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
#define MBEDTLS_RSA_C
#define MBEDTLS_PKCS1_V15
#define MBEDTLS_BIGNUM_C
#define MBEDTLS_X509_CRT_PARSE_C
#define MBEDTLS_ENTROPY_C
#define MBEDTLS_CTR_DRBG_C
// 关闭项
#undef MBEDTLS_SSL_SRV_C
#undef MBEDTLS_SSL_PROTO_TLS1_3
#undef MBEDTLS_DES_C
#undef MBEDTLS_CAMELLIA_C
#undef MBEDTLS_BLOWFISH_C
#undef MBEDTLS_XTEA_C
#undef MBEDTLS_MD5_C
#undef MBEDTLS_SHA1_C
#undef MBEDTLS_ECP_DP_SECP384R1_ENABLED
#undef MBEDTLS_ECP_DP_SECP521R1_ENABLED
#undef MBEDTLS_ECP_DP_CURVE25519_ENABLED
#undef MBEDTLS_PKCS12_C
#undef MBEDTLS_PK_PARSE_C
#undef MBEDTLS_PEM_PARSE_C
编译后若出现 undefined reference to mbedtls_sha1_process,说明某条被保留的路径仍引用 SHA-1,常见于旧版 X.509 或 TLS 记录层兼容逻辑。此时应检查 MBEDTLS_X509_USE_C 与 MBEDTLS_SSL_PROTO_TLS1_2 的宏组合,而不是简单把 SHA-1 重新打开。裁剪的目标是“最小可用集合”,不是“能编过就行”。
三种库的裁剪策略对比
| 维度 | mbedTLS | wolfSSL | OpenSSL 3.x |
|---|---|---|---|
| 裁剪入口 | mbedtls_config.h | user_settings.h / --enable-* | Configure no-* / provider 配置 |
| 依赖方向 | 单向、编译期 | 宏条件、编译期 | 构建期 + 运行期 |
| 最小 TLS 客户端典型体积 | 约 60–120 KB | 约 80–150 KB | 通常 1 MB 以上 |
| 主要风险 | 间接符号引用 | 宏组合冲突 | provider 与 no-* 不一致 |
| 验证方式 | 链接错误 + 握手测试 | 宏组合编译 + 握手测试 | Configure 检查 + provider 加载测试 |
表中体积为常见嵌入式配置下的量级参考,实际值随编译器优化、是否启用硬件加速、是否保留调试符号而明显变化,不应作为验收指标直接使用。真正可靠的判断依据是:目标功能全部可用、链接无未定义符号、握手与数据收发测试通过、裁剪后二进制体积相比裁剪前有可解释的下降。