首页 / 资讯中心 / 文章详情

mbedtls RSA签名验签与X.509证书固件校验Demo

mbedtls RSA签名验签与X.509证书固件校验Demo ★ FEATURED ARTICLE
简介这份面向嵌入式开发与信息安全学习者的mbedtls RSA签名验签数字证书示例工程聚焦资源受限环境下的非对称签名与验签实践适合具备C语言基础、希望理解数字证书底层机制的中级开发者帮助把密码学算法落到可编译运行的工程中。压缩包共111个文件、约640KB以28个头文件和25个C源文件为核心涵盖RSA运算、大数处理、AES、熵源采集与X.509证书解析等模块并配有cmake构建脚本、make文件及说明文本另有obj、exe等编译产物便于直接构建、运行和对照调试。目前已有504人学习下载。读者可完整梳理密钥对生成、SHA-256哈希、PKCS#1签名与公钥验签、证书公钥提取以及CMake工程链接的流程源码按算法与证书处理模块分层组织适合作为嵌入式密码学入门、课程实验与项目移植的参考样例。1. 从一次固件验签失败说起mbedtls 的 RSA 签名验签 demo 能给你什么固件包在设备上验签失败串口只吐出一个-0x4380别的什么都没有——这是不少人第一次用 mbedtls 做 RSA 签名验签时的现场。这份 demo 把签名、验签和 X.509 数字证书解析串成一条能编译、能单步调试、能拆开修改的最小链路构建机上用私钥对固件摘要签名设备端只烧一份证书启动时靠公钥把签名验回来。它适合两类人。一类是做嵌入式、网关、IoT 固件升级的开发者需要在 Cortex-M 上完成完整性校验但又不想把 OpenSSL 那一大堆 API 放进固件另一类是做插件、扩展包、配置下发的后端或客户端开发者想用最小依赖验证 RSA 签名加证书的流程。demo 的价值不在代码量而在于它把「密钥放哪台机器、用哪种填充、出错码对应哪一步」这些容易踩坑的地方写得很清楚。2. 架构与选型mbedtls 跑 RSA 需要的四层模块和一份裁剪清单2.1 从 mbedtls_mpi 到 mbedtls_x509_crt 的四层结构mbedtls 是分层库不是一堆平铺的函数。理解这四层调试时看到错误码才能立刻知道该往哪查。最底层是mbedtls_mpi负责大整数运算RSA 的模幂、素数检验全建在它上面。往上一层是mbedtls_rsa_context它是对 RSA 原语加密、解密、签名、验签的直接封装参数是裸的模数N、私钥指数D、公钥指数E。再往上是mbedtls_pk_context抽象层它把 RSA、EC、EdDSA 统一成一套pk_*接口业务代码不需要知道底层是哪种算法。最上层是mbedtls_x509_crt负责解析证书解析完之后它内部就挂着一个pk_context可以直接拿来验签。哈希是独立的分支走mbedtls_md_*。RSA 签名并不直接对原文做运算它对「摘要加一层 DigestInfo 的 ASN.1 包装」做运算所以mbedtls_pk_sign的参数里既要传md_alg又要传已经算好的hash。这两个参数必须一致传MBEDTLS_MD_SHA256却塞进去一个 MD5 的 16 字节摘要就是后面最常见的-0x4380。能力必须打开的宏关掉之后的典型症状大数与模幂MBEDTLS_BIGNUM_C链接期一片mbedtls_mpi_*undefinedRSA 原语MBEDTLS_RSA_Cmbedtls_rsa_init找不到定义pk 抽象层MBEDTLS_PK_CMBEDTLS_PK_PARSE_Cmbedtls_pk_parse_key返回-0x3D00PEM 文本密钥MBEDTLS_PEM_PARSE_CMBEDTLS_BASE64_C只认 DER读 PEM 直接报格式错证书解析MBEDTLS_X509_USE_CMBEDTLS_X509_CRT_PARSE_Ccrt.pk是空壳验签必失败随机源MBEDTLS_CTR_DRBG_CMBEDTLS_ENTROPY_C签名报-0x003C这张表是排查问题的第一现场。很多人遇到-0x3D00就去怀疑密钥文件坏了其实多半是MBEDTLS_PEM_PARSE_C没开库根本不认识-----BEGIN这一行。2.2 裁剪清单验签端能砍掉的三个模块签名的机器和验签的设备需要的宏集合不一样。签名需要私钥和随机数用于盲化因子验签只需要公钥做一次模幂不需要任何随机源。这一刀砍下去省的是 ROM 和 RAM不是一点点。/* verifier/mbedtls_config.h —— 只保留 RSA 验签所需的最小集合 */ #define MBEDTLS_BIGNUM_C #define MBEDTLS_ASN1_PARSE_C #define MBEDTLS_OID_C #define MBEDTLS_MD_C #define MBEDTLS_SHA256_C #define MBEDTLS_PK_C #define MBEDTLS_PK_PARSE_C #define MBEDTLS_RSA_C #define MBEDTLS_PEM_PARSE_C #define MBEDTLS_BASE64_C #define MBEDTLS_X509_USE_C #define MBEDTLS_X509_CRT_PARSE_C #define MBEDTLS_PLATFORM_C #define MBEDTLS_PLATFORM_MEMORY /* 接自己的 malloc/free方便统计堆水位 */ #define MBEDTLS_ERROR_C /* mbedtls_strerror调试期千万别关 */ /* 下面这三组是验签端主动砍掉的 */ /* #define MBEDTLS_ASN1_WRITE_C */ /* 只有构造签名数据时才需要 */ /* #define MBEDTLS_CTR_DRBG_C */ /* 验签不用随机数 */ /* #define MBEDTLS_ENTROPY_C */ /* 同上连带硬件熵源适配都能省 */这段配置的逻辑很直白验签是一次确定性运算输入是「摘要 签名 公钥」输出是非 0 即失败的比较结果中间没有任何需要随机性的步骤。MBEDTLS_ASN1_WRITE_C保留在签名端是因为要把DigestInfo那段 ASN.1 前缀拼出来验签端只需要解析MBEDTLS_ASN1_PARSE_C就够了。MBEDTLS_ERROR_C会增加几 KB 的字符串表但换来的是串口能打出人能看懂的错误名调试期不关、量产再关是我一贯的做法。按-Os编到 Cortex-M4 上这套配置的代码段大致在 4055KBRSA-2048 验签过程中的堆峰值在 24KB 之间栈上主要开销是mbedtls_x509_crt结构本身。具体数字跟编译器版本、是否开 CRT 加速关系很大用arm-none-eabi-size看自己的.text最准。2.3 demo 的目录划分与两端的职责边界这个 demo 最重要的一条设计原则是签名和验签不在同一台机器上私钥永远不进固件。目录结构直接体现这条边界。rsa_demo/ ├── configs/ │ ├── mbedtls_config_signer.h # 构建机用开 CTR_DRBG / ENTROPY │ └── mbedtls_config_verifier.h # 设备用砍掉随机源 ├── keys/ │ ├── device_priv.pem # 只在构建机禁止进仓库、禁止进固件 │ ├── device_pub.pem # 给不想拖证书的场景用 │ └── device_cert.pem # 自签证书烧到设备 ├── signer/ │ ├── main.c # 读镜像 → 摘要 → 签名 → 写 .sig │ └── file_io.c ├── verifier/ │ ├── main.c # 上电验签流程 │ └── cert_blob.c # xxd 生成的证书字节数组 ├── port/ │ └── entropy_port.c # mbedtls_hardware_poll 适配 └── CMakeLists.txtkeys/下的私钥在真实工程里应该放在构建机的密钥服务或者 CI 的加密变量里device_priv.pem这个文件本身就不该出现在代码仓库。设备侧只拿到device_cert.pem而公钥是公开信息泄露了也无所谓。这条边界如果一开始没划清后面把私钥烧进固件再想改就得做一次全量设备的密钥轮换成本翻好几倍。数据流是单向的CI 出包时算一次 SHA-256用私钥签名把.sig和镜像一起打进升级包设备收到包先算镜像摘要再拿本地证书里的公钥验签通过才写 flash。整个链路里设备不需要网络、不需要密钥协商是一个纯离线的完整性校验。3. 密钥与证书准备三条 openssl 命令产出的文件怎么塞进固件3.1 生成密钥、自签证书与一份纯公钥调试阶段最省事的做法是用 openssl 一次性把材料备齐别自己在代码里生成密钥——嵌入式上生成 2048 位 RSA 密钥慢到让人怀疑人生而且没必要。# 1. 私钥2048 位输出 PKCS#8 格式默认就是 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ -out device_priv.pem # 2. 自签证书有效期 10 年主题随便写验签不校验证书链 openssl req -new -x509 -key device_priv.pem -sha256 -days 3650 \ -subj /CNfw-sign-demo -out device_cert.pem # 3. 导出一份纯公钥给「不想拖证书」的场景用 openssl rsa -in device_priv.pem -pubout -out device_pub.pem # 4. 先用命令行自测一遍确认密钥本身没问题 openssl dgst -sha256 -sign device_priv.pem -out fw.bin.sig fw.bin openssl dgst -sha256 -verify device_pub.pem -signature fw.bin.sig fw.bin第 4 步非常关键。它能帮你把「密钥材料本身有问题」和「mbedtls 调用方式有问题」这两类故障彻底分开。命令行验得过、mbedtls 验不过问题一定在代码命令行都验不过那就是密钥或者摘要算法对不上跟 mbedtls 一点关系都没有。这一步能省掉大量无效排查。-days 3650和-subj里的内容对验签结果没有影响因为 demo 只做签名验证不做证书链校验和有效期检查。但如果后面要上完整的证书链验证mbedtls_x509_crt_verify会检查not_before/not_after设备 RTC 不准就会报证书过期那就是另一个问题了。3.2 PKCS#1 与 PKCS#8、PEM 与 DER 的取舍同一份密钥有四种常见落盘方式选错了mbedtls_pk_parse_key会直接返回格式错误。格式文件头mbedtls 支持情况建议PKCS#1 PEMBEGIN RSA PRIVATE KEY2.x/3.x 都支持老工程常见能用PKCS#8 PEMBEGIN PRIVATE KEY2.x/3.x 都支持推荐openssl 默认输出加密 PKCS#8BEGIN ENCRYPTED PRIVATE KEY需要MBEDTLS_PKCS5_C设备上别用解不开还得带口令DER 二进制无文件头直接是 ASN.1都支持省掉 base64 解码体积更小设备端我一般优先选 DER。PEM 是 base64 文本解析时要走MBEDTLS_BASE64_C凭空多几 KB 代码而且数组里还得留结尾的\0。证书本身用 PEM 也行因为只有一份体积差不了多少私钥这种绝不该出现在设备上的东西格式根本无所谓。需要注意的是mbedtls 3.x 的mbedtls_pk_parse_key多了两个参数f_rng和p_rng。这是为了支持加密私钥的解密。即便你传的是未加密的 PEM参数也得填通常直接给NULL, NULL就行——但如果传的密钥是加密的而这两个是NULL会返回MBEDTLS_ERR_PK_PASSWORD_REQUIRED。3.3 把 PEM 变成 C 数组注意结尾那个\0设备上没有文件系统证书一般是以字节数组的形式编进固件。最省事的转换工具是xxd。xxd -i device_cert.pem verifier/cert_blob.c # 产出形如 # unsigned char device_cert_pem[] { 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, ... }; # unsigned int device_cert_pem_len 1234;xxd -i生成的数组长度不包含结尾的\0。而mbedtls_x509_crt_parse对 PEM 输入的要求是buflen必须把结尾的空字符算进去否则解析会失败。这个坑非常隐蔽因为数组内容看着完全正确只是长度差了 1。/* verifier/cert_blob.c 里补一个包装把长度问题一次性解决 */ static int load_device_cert(mbedtls_x509_crt *crt) { /* 静态缓冲避免每次调用都在栈上开一份 */ static unsigned char pem_buf[sizeof(device_cert_pem) 1]; static int inited 0; if (!inited) { memcpy(pem_buf, device_cert_pem, sizeof(device_cert_pem)); pem_buf[sizeof(device_cert_pem)] \0; /* 补上结尾空字符 */ inited 1; } /* 此时 buflen sizeof(device_cert_pem) 1包含 \0 */ return mbedtls_x509_crt_parse(crt, pem_buf, sizeof(pem_buf)); }这段代码做了两件事。第一把xxd数组复制到一个多出一字节的静态缓冲区并在末尾补\0这样传给mbedtls_x509_crt_parse的长度就是「有效字符数 1」符合 PEM 分支的要求。第二用static加一次性初始化避免每次验签都在栈上开一份证书大小的缓冲——如果证书是 1.2KB放在一个 2KB 栈的任务里第二次调用就会栈溢出。如果改成 DER 格式这一段可以简化成直接把数组和长度传进去不需要补\0也不需要MBEDTLS_PEM_PARSE_C和MBEDTLS_BASE64_C。量产版本我更倾向 DER。3.4 parse_key 与 parse_public_key 的差别加载密钥有两个函数别用混。/* 签名端加载私钥3.x 需要传 rng 参数未加密密钥可传 NULL */ ret mbedtls_pk_parse_key(pk, key_buf, key_len, NULL, 0, mbedtls_ctr_drbg_random, ctr_drbg); /* 验签端加载纯公钥不需要任何随机源 */ ret mbedtls_pk_parse_public_key(pk, pub_buf, pub_len);mbedtls_pk_parse_key能读私钥也能读公钥mbedtls_pk_parse_public_key只读公钥。验签端用后者代码更小也不会因为误传私钥而埋下安全隐患。返回值的语义也不一样-0x3D00是格式不对-0x3C00是需要口令-0x3F80是内存分配失败这三个错误码对应的处理动作完全不同不要一律当成「密钥文件坏了」。4. 签名与验签链路mbedtls_pk_sign 与 mbedtls_pk_verify 逐行拆4.1 签名端随机源、上下文与一次完整的签名调用签名端跑在构建机上资源不受限可以把完整流程写清楚。/* signer/main.c —— 对一段内存数据做 RSA-2048 SHA-256 签名 */ #include mbedtls/pk.h #include mbedtls/md.h #include mbedtls/ctr_drbg.h #include mbedtls/entropy.h #define SIG_BUF_MAX 512 /* 2048 位只需 256留余量给 3072/4096 位密钥 */ int rsa_sign(const unsigned char *key_buf, size_t key_len, const unsigned char *data, size_t data_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]; /* SHA-256 固定 32 字节 */ const char *pers rsa_sign_demo; mbedtls_pk_init(pk); mbedtls_entropy_init(entropy); mbedtls_ctr_drbg_init(ctr_drbg); /* 1. 熵源喂给 CTR_DRBG签名盲化因子从这里取 */ ret mbedtls_ctr_drbg_seed(ctr_drbg, mbedtls_entropy_func, entropy, (const unsigned char *)pers, strlen(pers)); if (ret ! 0) goto exit; /* 2. 加载私钥3.x 的后两个参数是 rng2.x 没有 */ ret mbedtls_pk_parse_key(pk, key_buf, key_len, NULL, 0, mbedtls_ctr_drbg_random, ctr_drbg); if (ret ! 0) goto exit; /* 3. 先算摘要原文多长都只算这一次 */ ret mbedtls_md(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), data, data_len, hash); if (ret ! 0) goto exit; /* 4. 签名md_alg 必须和上一步算摘要用的算法一致 */ *sig_len SIG_BUF_MAX; ret mbedtls_pk_sign(pk, MBEDTLS_MD_SHA256, hash, sizeof(hash), sig, SIG_BUF_MAX, sig_len, mbedtls_ctr_drbg_random, ctr_drbg); exit: mbedtls_pk_free(pk); mbedtls_ctr_drbg_free(ctr_drbg); mbedtls_entropy_free(entropy); return ret; }逐个参数说明。pk是加载好私钥的上下文MBEDTLS_MD_SHA256告诉库用哪种 DigestInfo 前缀去包装摘要hash和sizeof(hash)是已经算好的摘要长度必须严格等于该摘要算法的输出长度sig是输出缓冲SIG_BUF_MAX是缓冲容量3.x 新增的参数防越界sig_len是传入传出参数函数返回时被写成实际签名长度最后两个是随机数回调签名时的盲化因子由它提供这是防侧信道攻击的必要步骤不能传NULL。hash这个缓冲区是栈上的 32 字节很安全。但如果你把data传成整包固件比如 8MBmbedtls_md会一次性算完构建机上没问题设备上就得改成流式mbedtls_md_starts/update/finish一边读 flash 一边喂。4.2 验签端从证书里直接取公钥验签端最省事的写法是直接把证书解析出来用里面的pk字段。/* verifier/main.c —— 用证书里的公钥验证签名 */ int rsa_verify(const unsigned char *cert_pem, size_t cert_len_with_nul, const unsigned char *data, size_t data_len, const unsigned char *sig, size_t sig_len) { int ret; mbedtls_x509_crt crt; unsigned char hash[32]; mbedtls_x509_crt_init(crt); /* 1. 解析证书cert_len_with_nul 必须包含结尾 \0 */ ret mbedtls_x509_crt_parse(crt, cert_pem, cert_len_with_nul); if (ret ! 0) goto exit; /* 2. 算摘要算法必须和签名端完全一致 */ ret mbedtls_md(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), data, data_len, hash); if (ret ! 0) goto exit; /* 3. crt.pk 就是证书里解出来的公钥上下文不需要再 parse */ ret mbedtls_pk_verify(crt.pk, MBEDTLS_MD_SHA256, hash, sizeof(hash), sig, sig_len); exit: mbedtls_x509_crt_free(crt); /* 必须释放内部有多个堆分配 */ return ret; }mbedtls_x509_crt_parse支持一次传多张证书PEM 里连着写多段BEGIN CERTIFICATE会解析成链表这里只传一张crt.pk就直接指向第一张证书的公钥。如果证书是 DER 格式把MBEDTLS_PEM_PARSE_C关掉也一样能走通只是长度参数不需要 1。mbedtls_pk_verify的返回值语义要记牢0 表示验签通过MBEDTLS_ERR_RSA_VERIFY_FAILED-0x4380表示签名和内容对不上其他负值表示环境或参数有问题。第三类错误和「签名不匹配」必须分开处理——前者说明代码有 bug 或者配置没开不能当成「固件被篡改」上报否则线上会天天报安全告警却查不出原因。4.3 PSS 与 v1.5 的切换点PKCS#1 v1.5 是默认填充mbedtls_pk_sign/mbedtls_pk_verify走的都是它。如果对端要求 PSS新的协议规范里越来越常见签名和验签都要换 API。/* 签名端改用 mbedtls_rsa_set_padding或者直接用 pk_sign_ext */ mbedtls_pk_rsassa_pss_options pss_opts { .mgf1_hash_id MBEDTLS_MD_SHA256, .expected_salt_len MBEDTLS_RSA_SALT_LEN_ANY, }; /* 验签端普通 verify 换成 verify_ext多传一个 options 结构 */ ret mbedtls_pk_verify_ext(MBEDTLS_PK_RSASSA_PSS, pss_opts, crt.pk, MBEDTLS_MD_SHA256, hash, sizeof(hash), sig, sig_len);mgf1_hash_id是掩码生成函数用的哈希通常和主摘要算法保持一致expected_salt_len表示接受任意长度的盐如果对端固定了盐长比如 32这里就填 32。PSS 和 v1.5 在字节层面完全不同一端用 PSS、另一端用 v1.5验签必然失败而且错误码还是-0x4380本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站