
简介这份资源是面向嵌入式与信息安全开发者的 mbedtls RSA 签名验签实战示例聚焦数字证书场景下的非对称加密应用。它解决的是如何在资源受限环境中用轻量级 C 库完成密钥生成、哈希计算、私钥签名与公钥验证的完整链路问题适合具备一定 C 语言与密码学基础、希望将 RSA 签名验签落地到实际项目的开发者参考。压缩包共 111 个文件以 28 个 h 头文件、25 个 c 源文件为核心配合 obj 编译产物、cmake 构建脚本、make 与 txt 配置说明等整体约 640KB结构紧凑便于快速编译调试。目前已有 504 人学习下载。资源内含 main.c 主流程、CMakeLists.txt 构建配置及 crypto 库目录覆盖 mbedtls_rsa_gen_key、mbedtls_sha256、mbedtls_rsa_pkcs1_sign 与 verify 等关键接口调用并涉及 X.509 证书解析可帮助读者理解非对称加密在数字证书中的实际运用掌握签名验签的排错思路与工程组织方式。1. 从一次签名失败说起mbedtls 的 RSA 签名验签到底怎么落地嵌入式项目里做安全通信绕不开数字签名。我见过太多团队在 PC 上跑通了 OpenSSL 的 RSA 签名验签一移植到 MCU 上就翻车——要么是内存不够要么是随机数源没接对要么是填充模式选错导致验签永远返回失败。mbedtls 就是为这类场景准备的它足够小能在资源受限的环境里跑起来同时把 RSA、X.509、PKCS#1 这些密码学原语都封装好了。但封装好不等于用对RSA 签名验签这条链路上填充方式、哈希算法、密钥格式、随机数源任何一个环节对不上结果就是签名失败或者验签不通过。这篇笔记拆的是一个 mbedtls 实现 RSA 签名验签的 demo重点不在讲 RSA 数学原理而在把「怎么把签名验签跑通、跑对、跑稳」这件事说清楚。如果你正在做嵌入式安全、设备证书认证、固件签名校验这类活这份东西能帮你省掉不少翻文档的时间。2. RSA 签名验签在 mbedtls 里的实现骨架从密钥加载到结果校验2.1 为什么选 mbedtls 而不是 OpenSSL先说选型。在嵌入式场景里OpenSSL 的体积和依赖链基本劝退尤其是要跑在 Cortex-M 系列或者 RTOS 上的时候。mbedtls 的设计目标就是可裁剪、可移植编译出来可以只有几十 KB而且不依赖操作系统。它的 API 风格偏底层但换来的是对内存和行为的完全控制。RSA 签名验签这块mbedtls 提供了mbedtls_rsa_context这个结构体来承载密钥和运算上下文配合mbedtls_pk_*系列接口可以统一处理不同密钥类型。常见做法是用mbedtls_pk_parse_key加载私钥做签名用mbedtls_pk_parse_public_key加载公钥做验签中间通过mbedtls_pk_sign和mbedtls_pk_verify完成操作。这套接口的好处是如果你后面要换成 ECDSA上层调用逻辑几乎不用大改。但要注意mbedtls 的版本差异在这块挺明显。2.x 和 3.x 在mbedtls_rsa_*接口上有变动比如mbedtls_rsa_pkcs1_sign在 3.x 里参数和返回值处理都有调整。我一般会先确认工程里用的是哪个大版本再决定用哪套 API。如果你拿到的 demo 是基于 2.x 写的直接扔到 3.x 环境里编译大概率会报一堆 deprecated 警告甚至链接错误。2.2 签名流程的代码骨架下面这段是签名侧的核心逻辑基于 mbedtls 3.x 的接口风格。假设你已经把私钥读进了一个 buffer格式是 PEM。#include mbedtls/pk.h #include mbedtls/rsa.h #include mbedtls/sha256.h #include mbedtls/ctr_drbg.h #include mbedtls/entropy.h int rsa_sign_demo(const unsigned char *key_pem, size_t key_len, const unsigned char *msg, size_t msg_len, unsigned char *sig, size_t *sig_len) { int ret; mbedtls_pk_context pk; mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context ctr_drbg; unsigned char hash[32]; mbedtls_pk_init(pk); mbedtls_entropy_init(entropy); mbedtls_ctr_drbg_init(ctr_drbg); // 初始化随机数源签名本身不需要随机数但 mbedtls 内部可能用到 ret mbedtls_ctr_drbg_seed(ctr_drbg, mbedtls_entropy_func, entropy, NULL, 0); if (ret ! 0) goto cleanup; // 加载私钥 ret mbedtls_pk_parse_key(pk, key_pem, key_len, NULL, 0, mbedtls_ctr_drbg_random, ctr_drbg); if (ret ! 0) goto cleanup; // 先对消息做 SHA-256 ret mbedtls_sha256(msg, msg_len, hash, 0); if (ret ! 0) goto cleanup; // 用私钥签名指定 PKCS#1 v1.5 填充和 SHA-256 *sig_len mbedtls_pk_get_len(pk); ret mbedtls_pk_sign(pk, MBEDTLS_MD_SHA256, hash, 0, sig, sig_len, mbedtls_ctr_drbg_random, ctr_drbg); cleanup: mbedtls_pk_free(pk); mbedtls_ctr_drbg_free(ctr_drbg); mbedtls_entropy_free(entropy); return ret; }这段代码的逻辑链条是初始化上下文 → 播种随机数 → 解析私钥 → 对消息做哈希 → 用私钥对哈希值签名。几个关键参数说明一下。MBEDTLS_MD_SHA256指定了签名时使用的哈希算法这个必须和验签侧一致否则验签必挂。mbedtls_pk_sign的第四个参数是哈希值的长度传 0 表示让 mbedtls 根据算法自动判断。sig_len传入时是 buffer 大小返回时是实际签名长度RSA-2048 的话就是 256 字节。随机数源这块虽然 PKCS#1 v1.5 签名本身不需要随机数但 mbedtls 的 API 签名要求传入 RNG 回调所以还是得初始化一个。2.3 验签流程的代码骨架验签侧的逻辑和签名对称但用的是公钥。int rsa_verify_demo(const unsigned char *pub_pem, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len) { int ret; mbedtls_pk_context pk; unsigned char hash[32]; mbedtls_pk_init(pk); // 加载公钥 ret mbedtls_pk_parse_public_key(pk, pub_pem, pub_len); if (ret ! 0) goto cleanup; // 对原始消息做同样的哈希 ret mbedtls_sha256(msg, msg_len, hash, 0); if (ret ! 0) goto cleanup; // 验签 ret mbedtls_pk_verify(pk, MBEDTLS_MD_SHA256, hash, 0, sig, sig_len); cleanup: mbedtls_pk_free(pk); return ret; }验签的返回值很关键0 表示验签通过非 0 表示失败。常见的失败码包括MBEDTLS_ERR_RSA_VERIFY_FAILED这个通常意味着签名和公钥不匹配或者哈希算法不一致或者填充模式不对。调试的时候不要只看「失败」两个字把返回码打出来对着 mbedtls 的错误码表查能省很多时间。3. 把 demo 跑起来编译配置、密钥生成与端到端验证3.1 编译配置里必须打开的宏mbedtls 默认配置不一定包含 RSA 和 PKCS#1 相关的功能尤其是如果你用的是裁剪过的配置文件。下面这几个宏必须确认打开宏名称作用不开的后果MBEDTLS_RSA_C启用 RSA 算法所有 RSA 接口不可用MBEDTLS_PKCS1_V15启用 PKCS#1 v1.5 填充签名验签直接失败MBEDTLS_PK_C启用公钥抽象层mbedtls_pk_*接口不可用MBEDTLS_SHA256_C启用 SHA-256哈希计算失败MBEDTLS_CTR_DRBG_C启用 CTR_DRBG 随机数上下文初始化失败MBEDTLS_ENTROPY_C启用熵源随机数播种失败MBEDTLS_BIGNUM_C启用大数运算RSA 底层运算不可用MBEDTLS_ASN1_PARSE_C启用 ASN.1 解析PEM/DER 密钥解析失败如果你用的是mbedtls_config.h或者config.h直接搜这些宏确认没有被注释掉。有些 demo 为了减小体积会把MBEDTLS_PKCS1_V15关掉只留 PSS这时候你用 v1.5 的接口就会返回MBEDTLS_ERR_RSA_INVALID_PADDING。3.2 生成测试用的 RSA 密钥对跑 demo 之前得有一对密钥。用 openssl 命令行生成就行注意格式要和 mbedtls 的解析接口匹配。# 生成 2048 位私钥PEM 格式 openssl genrsa -out private.pem 2048 # 从私钥导出公钥 openssl rsa -in private.pem -pubout -out public.pem # 查看私钥内容确认是 PKCS#1 还是 PKCS#8 head -1 private.pemhead -1的输出如果是-----BEGIN RSA PRIVATE KEY-----说明是 PKCS#1 格式如果是-----BEGIN PRIVATE KEY-----说明是 PKCS#8 格式。mbedtls 的mbedtls_pk_parse_key两种都能解析但如果你用的是更底层的mbedtls_rsa_parse_key那就只认 PKCS#1。我一般建议统一用mbedtls_pk_*系列接口省得在格式上纠结。公钥这边mbedtls_pk_parse_public_key能处理-----BEGIN PUBLIC KEY-----这种 SubjectPublicKeyInfo 格式也能处理-----BEGIN RSA PUBLIC KEY-----这种 PKCS#1 格式。但如果你拿到的公钥是证书里的那就得先用mbedtls_x509_crt_parse解析证书再从证书结构里取公钥。3.3 端到端验证的完整流程把签名和验签串起来跑一遍确认整条链路是通的。下面是一个简化的 main 函数框架int main(void) { unsigned char sig[256]; size_t sig_len sizeof(sig); const char *msg hello mbedtls rsa; int ret; // 读取 private.pem 和 public.pem 到 buffer // 这里省略文件读取代码假设 key_buf 和 pub_buf 已经填好 ret rsa_sign_demo(key_buf, key_len, (const unsigned char *)msg, strlen(msg), sig, sig_len); if (ret ! 0) { printf(sign failed: -0x%04X\n, -ret); return -1; } printf(sign ok, sig_len%zu\n, sig_len); ret rsa_verify_demo(pub_buf, pub_len, (const unsigned char *)msg, strlen(msg), sig, sig_len); if (ret ! 0) { printf(verify failed: -0x%04X\n, -ret); return -1; } printf(verify ok\n); return 0; }跑通之后你会看到sign ok和verify ok。如果验签失败先把签名结果用openssl rsautl -verify或者openssl pkeyutl -verify交叉验证一下确认是 mbedtls 的问题还是密钥本身的问题。交叉验证这步很关键能帮你快速定位问题出在哪个环节。4. 避坑指南RSA 签名验签里最容易翻车的五个点4.1 哈希算法不一致导致验签永远失败现象签名返回成功验签返回MBEDTLS_ERR_RSA_VERIFY_FAILED换密钥、换消息都一样。原因签名时用了 SHA-256验签时用了 SHA-1或者签名时传的是原始消息验签时传的是哈希值。mbedtls 的mbedtls_pk_sign和mbedtls_pk_verify都要求传入哈希值而不是原始消息如果你在签名侧传了原始消息验签侧传了哈希值两边对不上。解决统一在调用mbedtls_pk_sign和mbedtls_pk_verify之前先做哈希并且确保两边的MBEDTLS_MD_*参数完全一致。建议把哈希算法定义成一个宏两边共用。4.2 密钥格式不匹配导致解析失败现象mbedtls_pk_parse_key返回MBEDTLS_ERR_PK_KEY_INVALID_FORMAT或MBEDTLS_ERR_PK_UNKNOWN_PK_ALG。原因私钥是 PKCS#8 格式但代码里用的是mbedtls_rsa_parse_key或者私钥是加密的但没提供密码或者 PEM 头尾有多余的空格或换行。解决优先用mbedtls_pk_parse_key而不是mbedtls_rsa_parse_key前者兼容性更好。如果是加密私钥第四个参数传密码长度传密码长度。另外注意 PEM 文件读取时不要漏掉末尾的换行有些解析器对格式很敏感。4.3 随机数源没接对导致初始化失败现象mbedtls_ctr_drbg_seed返回MBEDTLS_ERR_ENTROPY_SOURCE_FAILED或者整个签名流程在初始化阶段就挂了。原因嵌入式环境里没有默认的熵源mbedtls_entropy_func拿不到足够的随机性。或者mbedtls_entropy_init之后没有添加自定义熵源。解决在嵌入式平台上通常需要添加一个硬件随机数源作为熵源比如mbedtls_entropy_add_source配合芯片的 TRNG。如果平台没有 TRNG至少要用一个带时间抖动的软件熵源。PC 上跑 demo 一般不会遇到这个问题但移植到板子上就是高频翻车点。4.4 签名长度计算错误导致 buffer 溢出现象签名返回MBEDTLS_ERR_RSA_BAD_INPUT_DATA或者程序跑飞。原因sig_len传入时是 buffer 的实际大小如果 buffer 小于 RSA 密钥长度比如 RSA-2048 需要 256 字节mbedtls 会直接报错。或者签名成功后没有更新sig_len后续用错误的长度去验签。解决分配签名 buffer 时用mbedtls_pk_get_len(pk)获取密钥长度按这个长度分配。签名调用后sig_len会被更新为实际签名长度验签时用这个值。不要硬编码 256 或 128密钥长度变了代码就废了。4.5 填充模式选错导致兼容性问题现象mbedtls 自己签自己验能过但和 OpenSSL 或者其他库交叉验签就失败。原因mbedtls 默认用 PKCS#1 v1.5 填充但有些场景要求用 PSS。或者签名侧用了 v1.5验签侧配置里只开了 PSS。解决确认双方的填充模式一致。mbedtls 里 PKCS#1 v1.5 对应MBEDTLS_RSA_PKCS_V15PSS 对应MBEDTLS_RSA_PKCS_V21。如果要做跨库兼容建议用 v1.5兼容性最好。PSS 虽然更安全但参数配置更复杂容易在盐长度上翻车。5. 进阶技巧用 X.509 证书链做验签和批量校验5.1 从证书里提取公钥做验签实际项目里公钥很少单独传通常是放在 X.509 证书里。mbedtls 提供了完整的证书解析接口可以从证书里取出公钥再做验签。下面这段代码演示了从 PEM 证书里提取公钥并验签的流程#include mbedtls/x509_crt.h int verify_with_cert(const unsigned char *cert_pem, size_t cert_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len) { int ret; mbedtls_x509_crt crt; unsigned char hash[32]; mbedtls_x509_crt_init(crt); ret mbedtls_x509_crt_parse(crt, cert_pem, cert_len); if (ret ! 0) goto cleanup; ret mbedtls_sha256(msg, msg_len, hash, 0); if (ret ! 0) goto cleanup; // 直接用证书里的公钥验签 ret mbedtls_pk_verify(crt.pk, MBEDTLS_MD_SHA256, hash, 0, sig, sig_len); cleanup: mbedtls_x509_crt_free(crt); return ret; }这里的关键是crt.pk这个成员它直接承载了证书里的公钥上下文。mbedtls_x509_crt_parse支持 PEM 和 DER 两种格式PEM 的话传入的长度要包含末尾的\0DER 的话传实际字节数。这个细节很容易忽略导致解析返回MBEDTLS_ERR_X509_INVALID_FORMAT。5.2 证书链校验的配置要点如果你要做完整的证书链校验而不只是用证书里的公钥验签那就需要配置 CA 链和校验参数。mbedtls 的mbedtls_x509_crt_verify接口可以完成这个工作但配置项比较多。下面是一个典型的校验配置uint32_t flags; mbedtls_x509_crt ca_chain; mbedtls_x509_crt_init(ca_chain); // 加载 CA 证书链 ret mbedtls_x509_crt_parse(ca_chain, ca_pem, ca_len); if (ret ! 0) goto cleanup; // 校验设备证书 ret mbedtls_x509_crt_verify(device_crt, ca_chain, NULL, NULL, flags, NULL, NULL); if (ret ! 0) { printf(cert verify failed, flags0x%08X\n, flags); goto cleanup; }flags这个参数很重要校验失败时它会告诉你具体是哪一项没过——过期、签名不匹配、CA 不在链里都能从 flags 的位掩码里看出来。我一般会在调试阶段把 flags 打出来对着MBEDTLS_X509_BADCERT_*系列宏查比盲猜快得多。5.3 批量验签时的性能考量如果你要在设备上做批量签名校验比如固件更新时校验多个分片RSA 验签的性能会成为瓶颈。RSA-2048 在 Cortex-M4 上单次验签约 10-30ms签名更慢可能到 100ms 以上。优化方向有几个一是用公钥指数小的密钥比如 e65537 是默认值已经比较快了二是如果平台支持硬件加速把 RSA 运算卸载到硬件引擎三是考虑换 ECDSA同等安全强度下密钥更短、运算更快。但换算法意味着证书和签名格式都要改迁移成本不低得权衡。5.4 一个我踩过的坑早期做设备证书认证的时候我把证书链校验和签名验签混在一起做结果发现有些设备证书能通过链校验但签名验签失败。排查了半天才发现设备证书里的公钥和实际用来签名的私钥不是一对——产线烧录的时候把证书和私钥搞混了。从那以后我每次做证书相关的集成都会强制走一遍「用证书里的公钥验签、用私钥签名、交叉验证」的流程确认密钥对是匹配的再往下做。这个习惯帮我省了好几次返工。希望帮到你。本文还有配套的精品资源点击获取