资讯动态

mbedTLS嵌入式RSA签名验签与X.509证书解析实战

发布时间:2026/9/9 20:01:47 来源:尧图企业网站定制
简介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. 环境准备裁剪一个“够用”的 mbedTLS2.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_COMPILERarm-linux-gnueabihf-gcc \ -DCMAKE_C_FLAGS-O2 -mcpucortex-a7 \ -DENABLE_PROGRAMSOff \ -DENABLE_TESTINGOff \ . make -j4编译完会生成library/libmbedcrypto.a、libmbedx509.a、libmbedtls.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_C和MBEDTLS_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告诉验证方这个证书是谁用什么算法签的签名值signatureValueCA 对上面对应公钥的哈希值做的签名。对验签方来说我们要用的公钥其实就埋在证书本体的 SubjectPublicKeyInfo 字段里。mbedTLS 的mbedtls_x509_crt_parse函数会一并把证书的 TBS 和公钥解析好存进结构体里的pk字段不用自己手动去翻 ASN.1 的 tag。3.2 解析 PEM 证书的标准姿势PEM 格式是证书最常见的文本封装本质就是把 DER 二进制做了 Base64 编码再加上头和尾标记。mbedTLS 的解析函数对 PEM 和 DER 都能处理但调用方式有细微差别PEM 字符串必须带终止符\0所以传入长度时要加 1DER 是二进制长度就是实际字节数。#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_sign和mbedtls_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_sign和mbedtls_pk_verify里的参数从MBEDTLS_MD_SHA256换成对应的盐长设置——低层 API 支持通过mbedtls_rsa_set_padding设置MBEDTLS_RSA_PKCS_V21和盐长高层 API 则直接根据宏来选。如果你愿意更底层一点也可以用mbedtls_rsa_pkcs1_sign和mbedtls_rsa_pkcs1_verify直接操作mbedtls_rsa_context但建议非必要不这么干。高层的mbedtls_pk_*接口会帮你处理不同公钥算法RSA、ECC 等的差异将来如果业务要从 RSA 切到 ECDSA上层代码几乎不用改。这也是一种抽象带来的好处。4.4 签名数据的编码与传输RSA 签名出来后是一段二进制长度等于密钥位长除以 8。RSA-2048 就是 256 字节RSA-3072 是 384 字节。在日志、配置、网络传输的场景里二进制不方便直接处理习惯上会转成 Base64 或 Hexvoid 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_parse、pk_parse_keyfile、pk_sign、pk_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_entropymbedtls_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就是干这个的。一个完整的启动校验流程建议至少包含两步先用根 CA 公钥验证设备证书合法性再用设备证书里的公钥验证固件签名。只做第二步证书可能是一张自签名的任意证书攻击者自己生成一张证书就能绕过校验。这种坑在安全方案评审里几乎一定会被问到做 demo 时就要把证书链校验的架子搭好。这段代码里mbedtls_pk_verify与mbedtls_x509_crt_verify的分工很多人一开始分不清。前者是“给我公钥验证一段签名”后者是“给我证书链验证这张证书是谁签的”。两者的组合才是实际项目里完整的信任模型。我在 demo 里通常把证书链校验也一并写上虽然会多几十行代码但整个安全逻辑就闭环了。先分享到这儿。这套 mbedTLS 做 RSA 签名验签的路子我实际用下来最深刻的体会是加密库本身只是工具真正费时间的永远是两端参数的对齐和错误码的定位。如果你也在集成过程中卡在某个莫名其妙的“验签失败”不妨按上面那张排查表走一遍多半能在十分钟内找到问题。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价