简介:这份资源是面向嵌入式与信息安全开发者的 mbedtls RSA 签名验签实战示例,基于轻量级 C 语言密码库 mbedtls,演示非对称加密在数字证书场景中的落地方式。内容围绕密钥对生成、SHA-256 哈希、私钥签名、公钥验签以及 X.509 证书解析等核心环节展开,并配套 CMake 工程配置,帮助读者理解证书链中公钥与身份信息的可信验证逻辑。压缩包共 111 个文件,以 28 个 h 头文件、25 个 c 源文件为主,另有 obj 编译产物、cmake 构建脚本、txt 说明及少量可执行文件,整体约 640KB,结构紧凑,便于在资源受限环境中直接编译调试。目前已有 504 人学习下载。通过该示例,读者可掌握 mbedtls 中 rsa、bignum、pkparse、oid、ctr_drbg 等模块的调用方式,获得一套可复用的签名验签代码骨架与工程组织思路,适合需要快速上手数字证书与安全通信开发的工程师参考。
1. 从一次证书链校验失败说起:这个 demo 到底能干什么
去年帮一个做工业网关的朋友排查 TLS 握手失败,日志里只留下一句x509_verify_cert() failed,没有更多信息。抓包看证书链完整,时间也没过期,最后定位到是自签根证书的 RSA 签名在 mbedtls 里验不过——原因很朴素:签名时用的是 SHA-256,验签时配置里却挂了个 SHA-1 的md_info。这种问题在真实项目里非常常见,因为 RSA 签名验签这条链路横跨了密钥生成、摘要计算、填充模式、证书编码四个环节,任何一个环节对不上,报错信息都不会告诉你具体是哪一步。
这份 mbedtls 实现 RSA 签名验签的 demo,价值就在于把这条链路完整地摊开给你看。它不是那种只调一个mbedtls_rsa_pkcs1_sign()就结束的示例,而是把密钥对生成、PEM/DER 编解码、摘要上下文、签名验签、以及数字证书的签发与校验串成了一条可运行的流程。适合两类人:一类是正在用 mbedtls 做设备端证书认证、需要搞清楚每个 API 参数含义的嵌入式开发者;另一类是想理解数字证书底层签名机制、但不想一上来就啃 RFC 5280 的后端或安全方向开发者。跑通它,你对「证书为什么能证明身份」这件事会有完全不一样的体感。
2. 环境搭建与首次编译:把 demo 跑起来需要几步
2.1 依赖清单与获取方式
这个 demo 的核心依赖只有 mbedtls 本身,但版本选择有讲究。mbedtls 在 2.x 和 3.x 之间做了不小的 API 调整,尤其是 RSA 模块,3.x 把很多mbedtls_rsa_*函数的参数做了精简,去掉了mode和f_rng这类冗余入参。如果你拿到的 demo 代码里还有mbedtls_rsa_pkcs1_sign(rsa, rng, NULL, MBEDTLS_RSA_PRIVATE, ...)这种六七个参数的写法,那基本是 2.x 的代码,直接配 3.x 编译会报参数数量不匹配。
常见做法是锁定 2.28 LTS 版本,这个分支维护时间长、文档全、社区示例多,踩坑成本最低。获取方式有两种:一是直接下载源码包解压,二是用包管理器装。我一般倾向于源码编译,因为 demo 里往往需要打开MBEDTLS_PEM_PARSE_C、MBEDTLS_X509_CRT_PARSE_C这些默认关闭的宏,用系统包管理器装的版本不一定带。
# 下载并解压 mbedtls 2.28 LTS wget https://github.com/Mbed-TLS/mbedtls/archive/refs/tags/v2.28.8.tar.gz tar -xzf v2.28.8.tar.gz cd mbedtls-2.28.8 # 编译静态库,关闭测试程序加快速度 make lib -j4编译完成后,library/目录下会生成libmbedtls.a、libmbedcrypto.a、libmbedx509.a三个静态库。demo 链接时这三个都要带上,顺序也有讲究:-lmbedtls -lmbedx509 -lmbedcrypto,因为 tls 依赖 x509,x509 依赖 crypto,链接器从左到右解析符号,顺序反了会报 undefined reference。
2.2 配置文件裁剪:别让默认配置坑了你
mbedtls 的include/mbedtls/config.h是个大而全的开关集合,默认配置为了兼容性打开了很多用不上的模块。做 RSA 签名验签 demo,至少需要确认这几个宏是打开的:
// include/mbedtls/config.h 中需要确认的宏 #define MBEDTLS_RSA_C // RSA 算法本体 #define MBEDTLS_PKCS1_V15 // PKCS#1 v1.5 填充,签名最常用 #define MBEDTLS_PKCS1_V21 // PKCS#1 v2.1 (PSS) 填充,可选 #define MBEDTLS_BIGNUM_C // 大数运算,RSA 的底座 #define MBEDTLS_MD_C // 摘要框架 #define MBEDTLS_SHA256_C // SHA-256 摘要算法 #define MBEDTLS_PEM_PARSE_C // 解析 PEM 格式密钥/证书 #define MBEDTLS_PEM_WRITE_C // 写出 PEM 格式 #define MBEDTLS_X509_CRT_PARSE_C // 解析 X.509 证书 #define MBEDTLS_X509_CRT_WRITE_C // 生成 X.509 证书 #define MBEDTLS_BASE64_C // PEM 底层是 Base64 #define MBEDTLS_CTR_DRBG_C // 随机数生成器,生成密钥要用 #define MBEDTLS_ENTROPY_C // 熵源这里有个血泪经验:MBEDTLS_CTR_DRBG_C和MBEDTLS_ENTROPY_C如果没开,mbedtls_rsa_gen_key()会直接返回MBEDTLS_ERR_RSA_BAD_INPUT_DATA,但错误码不会告诉你缺的是随机数模块。我第一次遇到时对着 RSA 源码翻了半天,最后才发现是熵源没配。裁剪配置时建议用scripts/config.py脚本操作,比手改config.h靠谱:
# 用官方脚本打开需要的宏 python3 scripts/config.py set MBEDTLS_PKCS1_V15 python3 scripts/config.py set MBEDTLS_PEM_PARSE_C python3 scripts/config.py set MBEDTLS_X509_CRT_WRITE_C2.3 编译运行与预期输出
demo 的编译命令不复杂,关键是头文件和库路径要对:
gcc -o rsa_demo rsa_demo.c \ -I./mbedtls-2.28.8/include \ -L./mbedtls-2.28.8/library \ -lmbedtls -lmbedx509 -lmbedcrypto跑起来后,正常输出会分几段:先生成一对 2048 位 RSA 密钥并打印 PEM 格式的公私钥,然后对一段固定字符串做 SHA-256 摘要并签名,打印签名的十六进制,接着验签并输出verify success,最后生成一张自签证书并做证书链校验。如果卡在某一步,先看返回值,mbedtls 的错误码都是负数,-0x4080这类值去include/mbedtls/rsa.h里查宏定义,比盲猜快得多。
3. 核心模块拆解:密钥、摘要、签名、证书四段链路
3.1 RSA 密钥对生成与 PEM 编解码
密钥生成是整个流程的起点。mbedtls 把 RSA 上下文和随机数生成器分开管理,mbedtls_rsa_context只存密钥数据,随机数由mbedtls_ctr_drbg_context提供。这种设计的好处是密钥上下文可以序列化、可以跨线程传递,而随机数生成器通常每个线程一个。
#include "mbedtls/rsa.h" #include "mbedtls/ctr_drbg.h" #include "mbedtls/entropy.h" #include "mbedtls/pk.h" mbedtls_rsa_context rsa; mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context ctr_drbg; int ret; mbedtls_rsa_init(&rsa, MBEDTLS_RSA_PKCS_V15, 0); mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_init(&ctr_drbg); // 用固定种子初始化 DRBG,生产环境应使用真实熵源 const char *pers = "rsa_demo"; ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *)pers, strlen(pers)); if (ret != 0) { printf("drbg seed failed: -0x%04X\n", -ret); return -1; } // 生成 2048 位密钥,指数固定 65537 ret = mbedtls_rsa_gen_key(&rsa, mbedtls_ctr_drbg_random, &ctr_drbg, 2048, 65537); if (ret != 0) { printf("key gen failed: -0x%04X\n", -ret); return -1; }mbedtls_rsa_init的第二个参数指定填充模式,签名场景下MBEDTLS_RSA_PKCS_V15是兼容性最好的选择,几乎所有证书都用它。第三个参数是哈希 ID,传 0 表示不绑定,后续签名时再指定。mbedtls_rsa_gen_key的第四个参数是密钥位数,2048 是当前最低安全线,再低会被现代浏览器拒绝;第五个参数是指数,65537 是事实标准,用 3 虽然快但安全性有争议。
生成完密钥后,通常要导出成 PEM 格式存文件。mbedtls 提供了mbedtls_pk_write_key_pem和mbedtls_pk_write_pubkey_pem,但注意它们操作的是mbedtls_pk_context而不是mbedtls_rsa_context。所以实际代码里往往先用mbedtls_pk_setup把 RSA 上下文包进 PK 上下文,再调写出函数。这个包装步骤容易被忽略,直接拿 RSA 上下文去调 PEM 写出会编译不过。
3.2 摘要计算与签名:参数对齐是关键
签名本质是「先摘要、再加密摘要」。mbedtls 的mbedtls_rsa_pkcs1_sign内部会自动做摘要,你只需要把原始数据和摘要类型传进去。但这里有个容易翻车的点:签名和验签用的md_info必须完全一致,包括算法类型和内部状态。
const char *msg = "hello mbedtls rsa"; unsigned char hash[32]; unsigned char sig[256]; // 2048 位密钥签名长度固定 256 字节 size_t sig_len = sizeof(sig); // 先算 SHA-256 摘要 mbedtls_md(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), (const unsigned char *)msg, strlen(msg), hash); // 用私钥签名 ret = mbedtls_rsa_pkcs1_sign(&rsa, mbedtls_ctr_drbg_random, &ctr_drbg, MBEDTLS_RSA_PRIVATE, MBEDTLS_MD_SHA256, 32, hash, sig); if (ret != 0) { printf("sign failed: -0x%04X\n", -ret); return -1; } printf("signature length: %zu\n", sig_len);mbedtls_rsa_pkcs1_sign的参数依次是:RSA 上下文、随机数函数、随机数上下文、公钥/私钥模式、摘要类型、摘要长度、摘要值、输出缓冲区。注意这里传的是摘要值而不是原始消息,如果你直接把msg传进去,签名结果会错得莫名其妙。摘要长度对 SHA-256 固定是 32,写死没问题,但如果换成 SHA-512 就要改成 64。
验签是签名的逆操作,用公钥:
ret = mbedtls_rsa_pkcs1_verify(&rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, 32, hash, sig); if (ret == 0) { printf("verify success\n"); } else { printf("verify failed: -0x%04X\n", -ret); }验签时随机数参数传 NULL 即可,因为公钥操作不需要随机性。mbedtls_rsa_pkcs1_verify返回 0 表示验签通过,返回MBEDTLS_ERR_RSA_VERIFY_FAILED(-0x4380)表示签名不匹配。这个错误码不区分是摘要不对、填充不对还是密钥不对,排查时只能逐个排除。
3.3 数字证书的签发与校验
证书的本质是「用 CA 私钥对持有者公钥和身份信息做签名」。mbedtls 的mbedtls_x509write_crt模块封装了证书生成流程,但需要手动填一堆字段。
mbedtls_x509write_cert crt; mbedtls_mpi serial; unsigned char cert_buf[4096]; mbedtls_x509write_crt_init(&crt); mbedtls_mpi_init(&serial); mbedtls_mpi_read_string(&serial, 10, "1"); // 序列号设为 1 mbedtls_x509write_crt_set_version(&crt, MBEDTLS_X509_CRT_VERSION_3); mbedtls_x509write_crt_set_serial(&crt, &serial); mbedtls_x509write_crt_set_validity(&crt, "20250101000000", "20350101000000"); mbedtls_x509write_crt_set_subject_name(&crt, "CN=demo,O=test"); mbedtls_x509write_crt_set_issuer_name(&crt, "CN=demo,O=test"); // 自签 mbedtls_x509write_crt_set_subject_key(&crt, &pk); mbedtls_x509write_crt_set_issuer_key(&crt, &pk); mbedtls_x509write_crt_set_md_alg(&crt, MBEDTLS_MD_SHA256); ret = mbedtls_x509write_crt_der(&crt, cert_buf, sizeof(cert_buf), mbedtls_ctr_drbg_random, &ctr_drbg);set_validity的时间格式是YYYYMMDDHHMMSS,必须严格 14 位,少一位都会返回MBEDTLS_ERR_X509_INVALID_DATE。set_subject_name和set_issuer_name的字符串格式遵循 RFC 2253,多个字段用逗号分隔,字段名和值之间用等号。自签证书这两个名字要一致,否则校验时会报MBEDTLS_X509_BADCERT_NOT_TRUSTED。
证书校验用mbedtls_x509_crt_verify,需要传入 CA 证书链和可选的 CRL:
uint32_t flags; mbedtls_x509_crt ca, cert; mbedtls_x509_crt_init(&ca); mbedtls_x509_crt_init(&cert); mbedtls_x509_crt_parse(&ca, ca_pem, ca_pem_len); mbedtls_x509_crt_parse(&cert, cert_pem, cert_pem_len); ret = mbedtls_x509_crt_verify(&cert, &ca, NULL, NULL, &flags, NULL, NULL); if (ret == 0) { printf("cert verify success\n"); } else { printf("cert verify failed, flags: 0x%08X\n", flags); }flags是个位掩码,每一位对应一种失败原因。MBEDTLS_X509_BADCERT_EXPIRED是过期,MBEDTLS_X509_BADCERT_NOT_TRUSTED是签发者不在信任链里,MBEDTLS_X509_BADCERT_BAD_MD是摘要算法太弱。拿到 flags 后去x509_crt.h里对照宏定义,比看返回码精确得多。
4. 避坑与排查:那些让 demo 跑不起来的细节
4.1 签名长度不是密钥长度除以 8 那么简单
现象:用 2048 位密钥签名,sig_len打印出来是 256,但验签总是失败。原因:mbedtls_rsa_pkcs1_sign的sig_len是入参也是出参,传入时必须等于密钥模长(2048/8=256),如果传了个更大的值,函数会按你传的长度去读缓冲区,读到脏数据。解决:签名前用mbedtls_rsa_get_len(&rsa)获取准确长度,不要手算。
4.2 PEM 解析失败但文件明明存在
现象:mbedtls_x509_crt_parse_file返回MBEDTLS_ERR_X509_INVALID_FORMAT,但用cat看文件内容正常。原因:PEM 文件末尾缺少换行符,或者 Windows 下编辑产生了\r\n行尾,mbedtls 的 Base64 解码器对行尾敏感。解决:用mbedtls_x509_crt_parse直接传内存缓冲区,并在解析前把\r过滤掉;或者用dos2unix转换文件。
4.3 自签证书校验时 flags 报 NOT_TRUSTED
现象:证书是自己签的,CA 证书也是自己,但mbedtls_x509_crt_verify返回MBEDTLS_X509_BADCERT_NOT_TRUSTED。原因:mbedtls_x509_crt_verify的第二个参数是 CA 链,如果传的是单张 CA 证书而不是mbedtls_x509_crt链表,且这张 CA 的 subject 和证书的 issuer 字符串不完全一致(比如空格、大小写差异),就会判定为不受信。解决:确保set_subject_name和set_issuer_name用的是完全相同的字符串,并且 CA 证书确实被mbedtls_x509_crt_parse成功加载。
4.4 编译时报 undefined reference tombedtls_rsa_pkcs1_sign
现象:头文件都包含了,链接时却找不到符号。原因:mbedtls 3.x 里这个函数签名变了,或者你链接的库是 2.x 编译的但头文件用的是 3.x。解决:用nm libmbedcrypto.a | grep rsa_pkcs1_sign确认库里到底有没有这个符号,以及符号的完整签名。版本混用是嵌入式项目里最常见的翻车点。
4.5 随机数生成器未播种导致密钥生成卡死
现象:mbedtls_rsa_gen_key长时间不返回,CPU 占用高。原因:mbedtls_ctr_drbg_seed失败但没检查返回值,DRBG 处于未初始化状态,gen_key内部循环等随机数等不到。解决:每次调用mbedtls_ctr_drbg_seed后必须检查返回值,非 0 就打印错误码并退出。熵源在嵌入式环境里可能因为硬件 RNG 未就绪而失败,加个重试或降级到软件熵源。
5. 二次开发:从 demo 到可用模块的改造思路
把 demo 变成项目里能用的模块,第一件事是封装。demo 里所有操作都堆在main里,实际项目里应该拆成keygen、sign、verify、cert_issue、cert_verify五个独立函数,每个函数只做一件事,错误码统一用int返回,调用方决定怎么处理。我一般会再包一层rsa_ctx_t结构体,把mbedtls_rsa_context、mbedtls_ctr_drbg_context、mbedtls_entropy_context打包在一起,初始化和释放各一个函数,避免到处写init和free。
第二件事是密钥存储。demo 里密钥生成完就打印到终端,实际项目里私钥必须落盘加密。常见做法是用mbedtls_pk_write_key_pem导出后,再用 AES 加密存储,密钥从设备唯一标识派生。如果设备有安全芯片,私钥应该直接生成在芯片内,永远不出芯片,签名操作通过芯片的密码学接口完成,mbedtls 只负责上层协议。
第三件事是证书链管理。demo 只验一张自签证书,实际场景往往是「设备证书 → 中间 CA → 根 CA」三级链。mbedtls_x509_crt_verify支持传 CA 链,但要求链中证书顺序正确,且每张证书的 issuer 和上一张的 subject 匹配。我一般会写个辅助函数,把 PEM 文件里的多张证书依次 parse 进链表,再传给 verify。
验证改造是否成功,最直接的方法是构造一组「正确签名 + 篡改数据 + 错误密钥」的测试用例,确保正确用例通过、错误用例全部失败。mbedtls 自带的programs/test/目录下有selftest和benchmark可以参考,但不要直接改官方测试代码,复制一份出来改更安全。
从那以后我每次集成 mbedtls 的 RSA 模块,都会先跑一遍「密钥生成 → 签名 → 验签 → 证书签发 → 证书校验」的完整链路,确认每个环节的返回值都是 0,再往业务代码里接。这个习惯帮我省掉了至少三次因为配置宏没开导致的深夜排查。希望帮到你。
本文还有配套的精品资源,点击获取