mbedTLS嵌入式RSA签名验签与X.509证书解析实战
2026/9/9 20:01:45 网站建设 项目流程

简介:mbedtls实现RSA签名验签(数字证书)demo,面向嵌入式开发者与信息安全初学者,演示如何基于mbedtls轻量级库完成RSA密钥生成、SHA-256哈希、PKCS#1签名与验证,并结合X.509证书解析掌握数字证书的可信验证流程。资源压缩包共111个文件,约640KB,主体为C源码与头文件(含mbedtls库及示例),辅以CMake构建配置、Makefile、可执行文件等,便于直接编译运行与二次学习。已有504人学习查看,适合在资源受限环境中实践非对称加密与证书处理。通过阅读main.c可完整理解签名验签的实现步骤,参考CMakeLists可掌握mbedtls库的链接方式,结合crypto等目录能快速定位底层密码算法实现,为后续嵌入TLS通信或证书校验功能提供可复用的代码基底。 最近在做嵌入式安全启动的方案,需要在资源受限的板子上完成固件合法性校验。手里没有现成的加密库,评估了一圈,最终选了 mbedTLS。这个标题看起来很常规,但真正做一遍会发现,RSA 签名验签配合 X.509 数字证书的 demo 涉及不少容易踩的细节:证书解析、公钥提取、padding 模式、哈希对齐、错误码定位,任何一个环节对不上,验签结果不是一句“rsa public key not found”,就是干脆静默失败。这篇文章把我实际踩过的坑和可复用的代码片段整理出来,给要在 MCU、Linux 板子或者其他嵌入式环境里用 mbedTLS 做证书验签的人一份可以直接参考的作业。

1. 为什么验签选 mbedTLS 而不是直接用 OpenSSL

先说结论:能用 OpenSSL 的地方,大部分人不会问这个问题。真正逼着你选 mbedTLS 的是嵌入式环境——内存就那么几百 KB,闪存也紧张,OpenSSL 那套依赖链根本塞不进去。

mbedTLS 的前身是 PolarSSL,被 ARM 收购后改名为 mbedTLS,后来独立出来交给 Trusted Firmware 项目维护。它的核心优势其实就两个字:可裁剪。库本身按模块组织,你用 RSA 就编 RSA,用 X.509 就编 X.509,不需要的协议栈可以直接关掉,最终链接出来的体积可以做得很小。在 Cortex-M 级别的 MCU 上,如果只保留 RSA 验签和证书解析相关代码,静态库的体积能压到几十 KB 量级,这个量级在嵌入式场景里是完全可以接受的。

拿它和 OpenSSL 对比一下,差别就更明显了:

对比项mbedTLSOpenSSL
目标场景嵌入式、IoT、资源受限设备服务器、桌面、通用平台
库体积裁剪后几十 KB 到几百 KB通常数 MB
配置方式宏裁剪,config.h 统一控制编译选项,依赖较复杂
API 风格分层清晰,init/free 成对出现结构复杂,回调多
证书解析内建 X.509 解析,接口简单完整但繁重
底层依赖几乎无外部依赖依赖 zlib、crypto 等

还有一点很重要,mbedTLS 的 API 设计比较“直白”。解析证书就是一个mbedtls_x509_crt_parse,解析完公钥就在crt.pk里,调用mbedtls_pk_verify就能验签。不需要像 OpenSSL 那样先BIO读文件、再PEM_read_bio_X509、再X509_get_pubkey、再EVP_PKEY_get1_RSA绕一大圈。对嵌入式工程师来说,这种接口风格友好太多。

所以我的选型结论很直接:如果你的目标平台不是跑完整 Linux 的大服务器,而是单片机、路由器、摄像头、网关这类设备,mbedTLS 就是目前最省事的 RSA 验签方案。

2. 环境准备:裁剪一个“够用”的 mbedTLS

2.1 获取源码与编译

mbedTLS 源码可以直接从 GitHub 拉,或者去官网下发布包。推荐直接用 tag 版本,不要用 master 分支,因为主分支在持续开发,接口可能有变动。

git clone --branch v3.6.0 --depth 1 https://github.com/Mbed-TLS/mbedtls.git cd mbedtls

编译方式有两种:CMake 或者直接编库文件。嵌入式交叉编译我建议用 CMake,通过工具链文件指定编译器就行:

# 以 ARM 交叉编译器为例 cmake -DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc \ -DCMAKE_C_FLAGS="-O2 -mcpu=cortex-a7" \ -DENABLE_PROGRAMS=Off \ -DENABLE_TESTING=Off \ . make -j4

编译完会生成library/libmbedcrypto.alibmbedx509.alibmbedtls.a三个静态库。注意这三个库有依赖关系:libmbedtls.a依赖libmbedx509.a,而libmbedx509.a又依赖libmbedcrypto.a。自己项目链接的时候,顺序一般是-lmbedtls -lmbedx509 -lmbedcrypto,顺序反了会出现一堆未定义引用。

2.2 config.h 里必须打开的宏

mbedTLS 的裁剪全靠include/mbedtls/mbedtls_config.h里的宏。默认配置是开了一大堆功能的,嵌入式场景下建议显式确认下面这几个宏处于打开状态,否则编出来的库没有对应功能:

// 证书解析与公钥操作必需 #define MBEDTLS_X509_CRT_PARSE_C #define MBEDTLS_PK_PARSE_C #define MBEDTLS_PK_C #define MBEDTLS_RSA_C #define MBEDTLS_BIGNUM_C #define MBEDTLS_MD_C #define MBEDTLS_SHA256_C // RSA 填充模式,按需选择 #define MBEDTLS_PKCS1_V15 // 传统 PKCS#1 v1.5 padding #define MBEDTLS_PKCS1_V21 // 需要 PSS 时开启

如果确认项目里用不到 TLS 握手,可以关掉MBEDTLS_SSL_TLS_CMBEDTLS_SSL_CLI_C,能再省掉一截体积。这里踩过的一个坑是:在旧版本里配置文件叫config.h,新版本改成了mbedtls_config.h,网上很多老文章写的路径是过时的,照抄会找不到文件。

2.3 作为子模块引入项目

如果是自己的嵌入式工程,直接把 mbedTLS 源码以子模块方式放进工程目录,然后在自己的 Makefile 里加入三行编译目标,再头文件搜索路径指到mbedtls/include即可,不用先单独编一次库。这种方式编译时可读性更好,出错了能直接定位到具体.c文件。

3. 证书解析:从数字证书里“抠”出 RSA 公钥

3.1 数字证书的本质

X.509 数字证书本质上是一段 DER 编码的 ASN.1 结构,里面包含了三块核心内容:

  • 证书本体(TBSCertificate):持有者、颁发者、有效期、扩展字段,以及最重要的——持有者公钥信息;
  • 签名算法(signatureAlgorithm):告诉验证方这个证书是谁用什么算法签的;
  • 签名值(signatureValue):CA 对上面对应公钥的哈希值做的签名。

对验签方来说,我们要用的公钥其实就埋在证书本体的 SubjectPublicKeyInfo 字段里。mbedTLS 的mbedtls_x509_crt_parse函数会一并把证书的 TBS 和公钥解析好,存进结构体里的pk字段,不用自己手动去翻 ASN.1 的 tag。

3.2 解析 PEM 证书的标准姿势

PEM 格式是证书最常见的文本封装,本质就是把 DER 二进制做了 Base64 编码,再加上头和尾标记。mbedTLS 的解析函数对 PEM 和 DER 都能处理,但调用方式有细微差别:PEM 字符串必须带终止符\0,所以传入长度时要加 1;DER 是二进制,长度就是实际字节数。

#include "mbedtls/x509_crt.h" #include "mbedtls/pk.h" #include "mbedtls/error.h" static int load_cert_and_get_rsa_pubkey(const char *cert_path, mbedtls_x509_crt *crt); int load_cert_and_get_rsa_pubkey(const char *cert_path, mbedtls_x509_crt *crt) { unsigned char buf[2048]; FILE *fp; size_t len; int ret; mbedtls_x509_crt_init(crt); fp = fopen(cert_path, "rb"); if (fp == NULL) { return -1; } len = fread(buf, 1, sizeof(buf), fp); fclose(fp); if (len == 0 || len >= sizeof(buf)) { return -2; } /* PEM 格式需要再多读一个字节的 '\0' */ ret = mbedtls_x509_crt_parse(crt, buf, len + 1); if (ret != 0) { char err[128]; mbedtls_strerror(ret, err, sizeof(err)); printf("x509 parse failed: -0x%04x %s\n", (unsigned int)-ret, err); return ret; } /* 确认里面装的是 RSA 公钥 */ if (mbedtls_pk_get_type(&crt->pk) != MBEDTLS_PK_RSA) { printf("cert key is not RSA\n"); return -3; } printf("cert subject : %s\n", crt->subject.val.p); printf("cert issuer : %s\n", crt->issuer.val.p); printf("key bits : %d\n", (int)mbedtls_pk_get_bitlen(&crt->pk)); return 0; }

这里补一个很容易被忽略的点:mbedtls_pk_get_bitlen返回的是公钥位长,RSA-2048 返回 2048,这个值不仅在打印日志时有意义,后续判断签名缓冲区大小也用得上。解析完成后,crt->pk里就保存着一份完整的 RSA 公钥,可以直接交给验签接口,不需要再额外导出、转换。

如果项目里证书是以二进制 DER 形式存储的,注意传给mbedtls_x509_crt_parse的长度不能加 1,要用len而不是len + 1。我在一个项目里就是直接把解析函数包了一层,统一传len + 1,结果 DER 证书解析偶尔失败,排查半天才意识到是这个 +1 导致的。PEM 和 DER 的差异,建议在封装层就处理掉,别让上层代码去记这些细节。

4. 签名与验签:从 PKCS#1 v1.5 到 PSS 的完整调用链

4.1 底层原理:RSA 签名到底在签什么

RSA 签名并不是对原始数据直接做模幂运算,那样既不安全也无意义。标准流程是:先对原始消息做摘要(比如 SHA-256,得到 32 字节哈希),再对哈希做一轮编码(填充),最后用私钥对编码结果做 RSA 私钥运算。验签则是用公钥解出编码结果,对比摘要是否一致。

所以签名验签的两个关键参数必须对齐,缺一不可:

  • 哈希算法:签名方用 SHA-256 算摘要,验签方也必须用 SHA-256;
  • 填充模式:签名方用 PKCS#1 v1.5,验签方也必须用 v1.5;用 PSS,双方就必须都是 PSS。

这两个参数只要有一个对不上,验签结果百分之百失败。实际项目里最常见的失败原因也就是这两处不匹配。

4.2 用高层 API 实现签名

mbedTLS 提供了高层封装mbedtls_pk_signmbedtls_pk_verify,不需要关心底层 RSA 运算细节。签名前需要先加载私钥,私钥可以是 PEM 编码的 PKCS#1 或 PKCS#8 格式:

#include "mbedtls/pk.h" #include "mbedtls/rsa.h" #include "mbedtls/md.h" #include "mbedtls/ctr_drbg.h" #include "mbedtls/entropy.h" #define SIGNATURE_LEN 512 /* RSA-2048 签名固定 256 字节,留出余量 */ static mbedtls_entropy_context entropy; static mbedtls_ctr_drbg_context ctr_drbg; int rsa_sign_data(const unsigned char *data, size_t data_len, const char *privkey_path, unsigned char *sig, size_t *sig_len) { mbedtls_pk_context pk; unsigned char hash[32]; int ret; mbedtls_pk_init(&pk); mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_init(&ctr_drbg); mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, NULL, 0); ret = mbedtls_pk_parse_keyfile(&pk, privkey_path, NULL); if (ret != 0) { printf("parse private key failed: -0x%04x\n", (unsigned int)-ret); goto out; } /* 第一步:对数据做 SHA-256 摘要 */ mbedtls_md(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), data, data_len, hash); /* 第二步:用私钥对摘要签名,默认走 PKCS#1 v1.5 */ ret = mbedtls_pk_sign(&pk, MBEDTLS_MD_SHA256, hash, sizeof(hash), sig, SIGNATURE_LEN, sig_len, mbedtls_ctr_drbg_random, &ctr_drbg); if (ret != 0) { printf("sign failed: -0x%04x\n", (unsigned int)-ret); } out: mbedtls_pk_free(&pk); mbedtls_ctr_drbg_free(&ctr_drbg); mbedtls_entropy_free(&entropy); return ret; }

注意mbedtls_pk_sign的入参里需要传入摘要(hash),不是原始数据。很多第一次用的同学在这里会犯错,直接把原始数据往下扔,结果验签时怎么都对不上。

4.3 用证书里的公钥完成验签

验签使用的公钥直接来自前面解析出来的证书结构体,整个流程比签名还简单:

int rsa_verify_with_cert(mbedtls_x509_crt *crt, const unsigned char *data, size_t data_len, const unsigned char *sig, size_t sig_len) { unsigned char hash[32]; int ret; mbedtls_md(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), data, data_len, hash); ret = mbedtls_pk_verify(&crt->pk, MBEDTLS_MD_SHA256, hash, sizeof(hash), sig, sig_len); if (ret != 0) { printf("verify failed: -0x%04x\n", (unsigned int)-ret); return ret; } printf("verify OK\n"); return 0; }

这段代码里有两个值得留意的地方。第一,mbedtls_pk_verify自动判断哈希算法并完成 PET 编码,不需要自己手动拼接 DigestInfo;第二,如果后续要用 PSS 模式,只需把mbedtls_pk_signmbedtls_pk_verify里的参数从MBEDTLS_MD_SHA256换成对应的盐长设置——低层 API 支持通过mbedtls_rsa_set_padding设置MBEDTLS_RSA_PKCS_V21和盐长,高层 API 则直接根据宏来选。

如果你愿意更底层一点,也可以用mbedtls_rsa_pkcs1_signmbedtls_rsa_pkcs1_verify直接操作mbedtls_rsa_context,但建议非必要不这么干。高层的mbedtls_pk_*接口会帮你处理不同公钥算法(RSA、ECC 等)的差异,将来如果业务要从 RSA 切到 ECDSA,上层代码几乎不用改。这也是一种抽象带来的好处。

4.4 签名数据的编码与传输

RSA 签名出来后是一段二进制,长度等于密钥位长除以 8。RSA-2048 就是 256 字节,RSA-3072 是 384 字节。在日志、配置、网络传输的场景里,二进制不方便直接处理,习惯上会转成 Base64 或 Hex:

void bytes_to_hex(const unsigned char *in, size_t len, char *out) { static const char hex[] = "0123456789abcdef"; size_t i; for (i = 0; i < len; i++) { out[i * 2] = hex[(in[i] >> 4) & 0x0F]; out[i * 2 + 1] = hex[in[i] & 0x0F]; } out[len * 2] = '\0'; }

要注意的是,转成 Hex 再传,接收方必须做逆转换,且逆转换后长度必须是密钥位长的一半。这个环节如果用了sprintf("%s")这类字符串函数处理二进制,很容易因为中间的\0被截断,签名数据残缺导致验签失败。

5. 实测中遇到的那些“签名失败”:错误码、编码和公钥的连环坑

5.1 先学会看错误码,而不是看“not found”

网上搜“rsa public key not found”这种报错,搜索结果五花八门,但 mbedTLS 本身几乎不会返回这个错误。mbedTLS 的接口返回值是一串负数,比如MBEDTLS_ERR_RSA_VERIFY_FAILED-0x4180MBEDTLS_ERR_X509_BAD_INPUT_DATA-0x0080。你看到的“not found”很可能是上层签名工具、烧录软件或者自己封装代码打印的文案,而不是 mbedTLS 库的原始输出。

所以排查的第一步,永远是看底层接口实际返回的错误码。把mbedtls_strerror打印出来,能定位到具体是解析阶段挂了还是验签运算阶段挂了。不要对着一个包装过的“not found”瞎猜。

5.2 一轮典型的验签失败排查过程

之前在一台 Linux 板子上集成证书验签,用 PEM 证书解析很顺利,打印 subject 也正常,但mbedtls_pk_verify就是持续返回MBEDTLS_ERR_RSA_VERIFY_FAILED。整个排查链路如下:

检查项排查方法结果
证书能否解析mbedtls_x509_crt_parse返回值正常
公钥类型mbedtls_pk_get_typeRSA,正常
签名数据长度打印 sig_len256,正常
哈希算法签名/验签均用 SHA-256逻辑上一致
原始数据打印关键数据 hex发现签名方多了个换行符
填充模式双方都用默认 v1.5一致

最后一查,问题出在签名方对原始数据做摘要时,把文件内容连同末尾的换行符一起算了进去,也就是实际签名的哈希跟验签方算出来的哈希根本不是同一个。这种情况只靠看代码很难发现,我一通打印 hex 之后就清楚了。

5.3 几个高频坑位总结

结合碰到的各种问题,我把高频坑位分成了下面几类,每一类都值得在做 demo 时就注意:

  • 哈希不对齐:签名方算摘要时对数据的处理跟验签方不一致,最常见的差异是换行符、\0、字符串长度计算错误。建议两边对原始数据的十六进制逐字节比对。
  • 填充模式不统一:一端用 v1.5,另一端用 PSS,必然失败。协商约定时要写清楚,特别是跨部门、跨厂商对接时,默认值往往是 v1.5,但新项目越来越多直接用 PSS,不确认就出事。
  • PEM/DER 混用:解析接口对 PEM 要传len + 1,对 DER 用len。封装层建议区分清晰,别传错长度。
  • 签名缓冲区不够:RSA-2048 签名是 256 字节,但如果你对底层签名缓冲区用了sizeof(unsigned char[128])之类的局部判断,很容易越界或截断。用MBEDTLS_MPI_MAX_SIZE不够严谨,直接根据mbedtls_pk_get_bitlen算出字节数最稳妥。
  • 公钥误用:有时候代码里同时存在多个证书,有的是根 CA,有的是设备证书,验签时拿错了对象,用根 CA 的公钥去验设备证书的签名,结果自然是失败。打印一下证书 subject,确认公钥归属。

5.4 打印错误的代码要好好写

排查过程中,错误码转换成字符串这一步太重要了。我习惯在项目里封装一个统一的打印函数,把所有 mbedTLS 返回的错误码转成可读文本:

static void print_mbedtls_error(const char *func, int ret) { char err_buf[128]; mbedtls_strerror(ret, err_buf, sizeof(err_buf)); printf("[%s] failed: -0x%04x (%s)\n", func, (unsigned int)-ret, err_buf); }

x509_crt_parsepk_parse_keyfilepk_signpk_verify每个关键节点都调用它,错误码可读性会大幅提升。这不只是 demo 阶段的习惯,生产环境也建议保留,省得线上板子出问题只能对着十六进制数翻头文件。

6. 沉淀下来的几条工程经验

6.1 证书解析结果尽量做缓存

如果系统启动时要校验多份签名,不要每次验签都重新解析一遍证书文件。证书解析涉及 ASN.1 编解码、大整数运算初始化,开销不小。我一般把证书解析放在启动阶段做一次,解析完成后用memcpy保存mbedtls_x509_crt结构体,后续所有验签直接复用里面的pk字段。在多线程环境里要注意:同一份mbedtls_x509_crt被多个线程同时调用mbedtls_pk_verify是否安全,取决于具体版本和编译选项,保险的做法是在封装层加一个互斥锁,或者为每个线程准备独立的上下文。

6.2 RNG 不能省,也不能偷懒

用私钥签名时mbedtls_pk_sign需要传入随机数生成回调。很多人为了省事,直接传NULL。对 RSA 的 PKCS#1 v1.5 签名来说,随机数可能不是必需的,但 PSS 签名必须要盐,没有随机源就没法生成盐值。更重要的是,签名私钥操作中随机数的强度直接关系到安全性。我在 demo 里用mbedtls_entropy+mbedtls_ctr_drbg是最标准的组合,照抄就行。

贴一段初始化的代码,不要图省事跳过这一步:

mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_init(&ctr_drbg); ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *)"rsa-demo", 8);

如果只是验签,不需要 RNG,但一旦涉及到签名,RNG 初始化就绕不开。

6.3 非对称签名只是“验签”,不是“认证”

最后说一个容易被忽视的点:RSA 签名验签只能证明“数据由持有对应私钥的一方签名”,并且数据没有被篡改。它本身不能证明对方是谁。要在实际系统里做身份认证,还需要验证证书链——也就是用根 CA 公钥去验证设备证书的签名,确认设备证书确实由可信 CA 签发。mbedTLS 里mbedtls_x509_crt_verify就是干这个的。一个完整的启动校验流程建议至少包含两步:

  1. 先用根 CA 公钥验证设备证书合法性;
  2. 再用设备证书里的公钥验证固件签名。

只做第二步,证书可能是一张自签名的任意证书,攻击者自己生成一张证书就能绕过校验。这种坑在安全方案评审里几乎一定会被问到,做 demo 时就要把证书链校验的架子搭好。

这段代码里mbedtls_pk_verifymbedtls_x509_crt_verify的分工,很多人一开始分不清。前者是“给我公钥,验证一段签名”,后者是“给我证书链,验证这张证书是谁签的”。两者的组合,才是实际项目里完整的信任模型。我在 demo 里通常把证书链校验也一并写上,虽然会多几十行代码,但整个安全逻辑就闭环了。

先分享到这儿。这套 mbedTLS 做 RSA 签名验签的路子,我实际用下来最深刻的体会是:加密库本身只是工具,真正费时间的永远是两端参数的对齐和错误码的定位。如果你也在集成过程中卡在某个莫名其妙的“验签失败”,不妨按上面那张排查表走一遍,多半能在十分钟内找到问题。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询