ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于RT-Thread与AB32VG1的嵌入式可信启动方案设计与实现

基于RT-Thread与AB32VG1的嵌入式可信启动方案设计与实现 1. 项目概述为什么要在嵌入式端折腾可信启动最近在做一个工业网关项目客户对设备启动安全的要求提到了一个新高度。他们担心设备在野外部署后如果有人物理接触并恶意刷入篡改过的固件整个网络的数据安全链条就会从源头断裂。这让我把目光投向了“可信启动”这个在服务器和PC领域已经成熟但在资源受限的嵌入式场景下仍颇具挑战的技术。简单来说可信启动的核心思想是“信任链”。它不是一个单一的功能而是一套机制确保设备从加电那一刻起每一段要执行的代码从Bootloader到操作系统内核再到应用程序都经过完整性验证且来自可信的源头。任何一环验证失败启动过程就会中止防止恶意代码获得执行权限。我这次选择的硬件平台是AB32VG1一颗基于RISC-V架构的MCU主频120MHz内置640KB SRAM外置4MB Flash。软件框架则是国内嵌入式领域非常流行的RT-Thread物联网操作系统。选择这个组合一方面是看中RISC-V的开放生态和RT-Thread在资源紧凑设备上的优秀表现另一方面也是想探索在这样一个中等配置的MCU上实现一套完整、轻量且实用的可信启动方案到底有多大的可行性。这不仅仅是技术验证更是为未来大量类似的物联网终端设备提供一个可落地的安全基线参考。2. 可信启动的核心原理与方案选型2.1 信任链的构建从Root of Trust开始一切可信的基础都源于一个绝对可信的起点即“信任根”。在嵌入式系统中这个根通常由硬件来保障。对于AB32VG1这类通用MCU硬件安全模块可能不像专用安全芯片那样强大但我们依然可以构建软硬件结合的信任根。我的方案核心是利用芯片内部一段受保护的存储区域例如OTP区域或Flash的特定扇区来存储一个至关重要的密钥——根公钥。这个密钥在芯片出厂或设备首次安全配置时写入之后在物理上通过写保护或逻辑上使其不可更改。整个信任链就从验证这个根公钥签名的有效性开始。信任链的传递过程可以这样理解第一阶段Bootloader芯片上电后首先运行固化在ROM中的代码这部分是绝对可信的。它的唯一职责就是验证存储在Flash固定位置的第二阶段Bootloader的数字签名。签名使用的公钥就是预先烧录的根公钥。验证通过才跳转执行否则进入安全失败处理如停机或进入恢复模式。第二阶段Bootloader它被信任后其任务之一是加载并验证RT-Thread操作系统镜像的完整性和真实性。这里可以使用另一个由根公钥派生的密钥或者由第二阶段Bootloader自身携带一个次级公钥来验证。RT-Thread内核内核启动后在挂载文件系统或加载关键应用程序时可以继续延伸这条信任链验证后续模块或应用的签名。注意对于AB32VG1需要仔细查阅数据手册确认其Flash是否支持分扇区保护以及是否存在OTP区域。如果没有硬件保护那么根密钥存储的安全性会大打折扣方案的安全等级就需要重新评估或考虑外置SE安全芯片。2.2 密码学基础与轻量化选型数字签名和验证是可信启动的基石。常见的算法有RSA和ECC。在资源受限的嵌入式环境中ECC相比RSA具有明显的优势。密钥长度与存储要达到相同的安全强度ECC的密钥长度远小于RSA。例如256位的ECC密钥安全强度相当于3072位的RSA密钥。更短的密钥意味着在宝贵的Flash或RAM中占用更少的空间。计算开销签名验证运算对MCU是一大负担。ECC的验证速度通常比同等安全强度的RSA快得多这对于启动时间有严格要求的系统至关重要。因此我选择了ECDSA椭圆曲线数字签名算法作为本次实现的签名算法曲线参数使用secp256r1也称为NIST P-256。这是一个被广泛支持和审计的标准曲线在安全性和性能上取得了很好的平衡。哈希算法我选用SHA-256。它能为固件生成一个32字节的唯一“指纹”摘要足够安全且计算效率在MCU上可以接受。整个验证过程就是使用SHA-256计算待启动固件的哈希值然后使用存储的ECC公钥对固件附带的ECDSA签名进行验证确认该哈希值确实是由对应的私钥持有者即固件发布者签署的。2.3 启动流程设计与RT-Thread的适配传统的嵌入式启动流程是线性的Bootloader - 直接跳转到应用。我们要在其中插入验证环节。我设计的流程如下ROM Code芯片固有不可变。一级Bootloader这是一个极简的引导程序可能只有几KB。它的全部代码就是初始化最基础的硬件如时钟、串口用于调试。从Flash的固定地址读取“二级Bootloader”的镜像头和附带的ECDSA签名。从受保护存储区读取根公钥。计算二级Bootloader镜像的SHA-256哈希。使用根公钥验证签名。验证成功则跳转到二级Bootloader验证失败则点亮错误LED并通过串口输出错误码然后进入死循环或尝试进入通过网络/串口的固件恢复模式。二级Bootloader功能更丰富如支持固件升级、备份、环境变量管理等。它自身已被信任因此它的任务是从Flash中读取RT-Thread内核镜像通常是rtthread.bin及其签名。使用一个预置的“系统公钥”这个公钥可以是一级Bootloader根公钥签名的也可以直接写死在二级Bootloader中因为它本身是可信的来验证RT-Thread内核的签名。验证通过则跳转到RT-Thread的入口函数否则启动失败回滚到上一个已知的好版本。RT-Thread内核启动内核在初始化后期可以增加一个“安全启动”组件。该组件的任务是验证将要挂载的文件系统如LittleFS中关键系统应用程序的签名或者验证保存在Flash特定分区中的安全配置数据的完整性从而将信任链延伸到应用层。这个设计的关键在于模块化和职责分离。一级Bootloader尽可能简单减少被攻击面。复杂的升级和管理逻辑放在已被验证的二级Bootloader中。RT-Thread则通过其组件化机制方便地集成安全启动检查功能。3. 基于AB32VG1与RT-Thread的具体实现3.1 开发环境与基础工程搭建首先需要准备好开发环境。我使用的是RT-Thread Studio它已经对AB32VG1提供了很好的支持。创建工程在RT-Thread Studio中新建一个基于AB32VG1开发板的工程。这会自动生成一个包含RT-Thread内核、驱动、FinSH控制台等基础组件的项目框架。理解内存布局这是至关重要的一步。我们需要修改链接脚本明确划分Flash的各个区域。通过查看board/linker_scripts/link.lds文件我规划了如下布局0x00000000 - 0x00003FFF:一级Bootloader区(16KB)。存放我们编写的、带验证功能的最小引导程序。0x00004000 - 0x0000BFFF:二级Bootloader区(32KB)。存放功能丰富的引导管理程序。0x0000C000 - 0x0007FFFF:主应用区(约456KB)。存放RT-Thread内核及主应用程序。0x00080000 - 0x000BFFFF:备份应用区(256KB)。用于固件升级时的回滚备份。0x000C0000 - 0x000FFFFF:参数存储区(256KB)。存放设备配置、密钥、版本信息等。注意以上地址和大小需根据AB32VG1实际的Flash大小调整4MB Flash的地址范围是0x00000000 - 0x003FFFFF准备密码学库在MCU上实现ECDSA和SHA-256强烈建议使用经过优化和测试的轻量级库而不是自己从头实现。我选择了mbedtls。RT-Thread的软件包中心提供了mbedtls的移植版本。通过RT-Thread Env工具或Studio的包管理器可以很方便地添加mbedtls软件包到工程中。配置时只勾选我们需要的模块MBEDTLS_SHA256_C,MBEDTLS_ECDSA_C,MBEDTLS_ASN1_PARSE_C等以节省代码空间。3.2 一级Bootloader的实现详解一级Bootloader需要独立编译生成一个单独的.bin文件。我们在主工程外新建一个目录。最小化代码仅包含必要的启动文件、时钟初始化、串口打印用于调试、Flash读取函数。实现验证函数核心是verify_signature函数。int verify_signature(const uint8_t *image, uint32_t image_len, const uint8_t *signature, uint32_t sig_len, const uint8_t *public_key) { int ret; mbedtls_sha256_context sha_ctx; unsigned char hash[32]; mbedtls_ecdsa_context ecdsa_ctx; mbedtls_ecp_keypair *pub_key NULL; // 1. 计算镜像的SHA-256哈希 mbedtls_sha256_init(sha_ctx); mbedtls_sha256_starts(sha_ctx, 0); // 0 表示 SHA-256 mbedtls_sha256_update(sha_ctx, image, image_len); mbedtls_sha256_finish(sha_ctx, hash); mbedtls_sha256_free(sha_ctx); // 2. 解析并设置公钥 mbedtls_ecdsa_init(ecdsa_ctx); // 假设公钥是PEM格式或裸的X, Y坐标。这里需要根据存储格式解析。 // 示例从固定数组加载公钥 ret mbedtls_ecp_group_load(ecdsa_ctx.grp, MBEDTLS_ECP_DP_SECP256R1); ret mbedtls_ecp_point_read_binary(ecdsa_ctx.grp, ecdsa_ctx.Q, public_key, 64); // 64字节未压缩公钥 // 3. 验证签名 (签名通常是ASN.1 DER格式) ret mbedtls_ecdsa_read_signature(ecdsa_ctx, hash, 32, signature, sig_len); mbedtls_ecdsa_free(ecdsa_ctx); if (ret 0) { return 0; // 验证成功 } else { // 打印错误码便于调试 debug_printf(Signature verification failed: -0x%04X\n, -ret); return -1; // 验证失败 } }固化根公钥将根公钥一个64字节的数组代表椭圆曲线点的X和Y坐标作为常量数组编译进一级Bootloader。为了模拟硬件保护可以将这个扇区在下载后设置为写保护。更严肃的做法是在生产时通过芯片厂商提供的工具将公钥哈希值烧录到OTP中Bootloader运行时计算公钥哈希并与OTP值比对确保公钥自身未被篡改。链接与烧录确保一级Bootloader的链接脚本将其代码定位到Flash起始地址如0x00000000。使用编程器或调试器先将一级Bootloader烧录到芯片。之后二级Bootloader和RT-Thread系统可以放在后续地址通过一级Bootloader来引导。3.3 二级Bootloader与安全升级二级Bootloader基于RT-Thread的Bootloader组件rt_bootloader进行开发但需要深度定制以加入安全验证。集成验证逻辑在rt_bootloader的固件加载函数中插入对RT-Thread镜像的验证步骤。验证逻辑与一级Bootloader类似但使用的公钥可以是另一个“系统公钥”。这个系统公钥可以存储在主应用区的头部或参数存储区并由一级Bootloader在跳转前传递给二级Bootloader通过寄存器或内存。实现安全升级流程下载新固件到备份区。在覆盖主应用区之前先使用系统公钥验证备份区中新固件的签名。只有验证通过才执行擦除-写入操作将新固件从备份区拷贝到主应用区。同时更新版本号。如果验证失败则丢弃备份区的数据报告升级失败设备继续从主应用区启动。增加回滚机制升级后如果主应用区启动失败例如连续重启多次二级Bootloader应能自动用备份区的旧版本覆盖主应用区恢复设备运行。与RT-Thread的协作二级Bootloader需要在跳转到RT-Thread前通过某种约定好的方式如特定的内存地址、RTC备份寄存器传递一个“启动标记”告知RT-Thread本次是正常启动还是升级后的首次启动。RT-Thread可以在启动初期检查这个标记并执行一些初始化或数据迁移工作。3.4 RT-Thread内的信任链延伸RT-Thread启动后我们还可以在应用层继续强化安全。启用内核模块签名验证如果使用RT-Thread的动态模块功能可以在加载.mo模块文件时验证其签名。文件系统完整性检查对于存储在Flash上的文件系统如LittleFS可以在挂载时检查关键系统文件如初始化脚本、配置文件的哈希值是否与安全存储中的预期值匹配。这可以防止关键配置被篡改。实现安全服务创建一个secure_boot软件包或组件提供统一的API供应用程序调用用于验证外部下载的数据、配置文件的完整性和真实性。4. 开发调试与生产部署中的关键问题4.1 调试技巧与常见陷阱在开发可信启动时最令人头疼的就是验证失败导致“变砖”。以下是我踩过坑后总结的调试方法分阶段验证不要试图一次性完成整个链条。首先在PC上使用OpenSSL或Python的cryptography库生成密钥对并对一个简单的测试文件签名、验证确保算法理解正确。然后在MCU上单独测试SHA-256和ECDSA验证函数使用已知正确的数据确保密码学库移植成功。丰富的调试输出在一级和二级Bootloader中务必保留串口调试输出功能。打印出关键步骤的结果如读取的镜像大小、计算出的哈希值以十六进制打印、验证函数的返回值等。mbedtls函数返回负的错误码可以通过其头文件error.h查找具体含义。签名格式问题这是最常见的坑。mbedtls_ecdsa_read_signature默认期望签名是ASN.1 DER编码格式。而很多工具如OpenSSL命令行默认生成或输出的可能是纯R|S格式。务必确认签名数据的格式与解析函数期望的格式一致。可以在代码中对比两者长度ASN.1 DER编码的签名长度通常是可变的70-72字节而裸的RS格式固定为64字节。公钥格式问题同样公钥的存储格式未压缩的04XY、压缩格式、PEM格式需要与mbedtls_ecp_point_read_binary等函数的输入要求匹配。最稳妥的方式是在代码中固定使用未压缩的64字节格式0x04 X坐标 Y坐标。内存地址对齐确保从Flash中读取的镜像数据、签名数据在内存中的缓冲区是字节对齐的某些MCU架构对非对齐访问不友好会导致数据读取错误。4.2 生产密钥管理与安全烧录实验室成功只是第一步量产才是真正的考验。密钥生成与存储根密钥对必须在离线、安全的环境中生成。私钥永远不能出现在设备或生产线上应由研发或安全团队严格保管用于为二级Bootloader和系统镜像签名。系统密钥对可以由根私钥签名生成一个证书链也可以独立生成。其公钥需要安全地烧录到设备中。设备唯一密钥对于更高要求可以为每台设备生成唯一的密钥对将设备公钥的哈希值烧录到OTP中实现设备的个体化身份认证。安全烧录流程在产线先烧录一级Bootloader内含根公钥或公钥哈希。立即通过工具锁定该Flash扇区或OTP防止后续被修改。烧录已由对应私钥签名的二级Bootloader和RT-Thread系统镜像。首次上电设备会自动完成验证并启动。恢复模式必须设计一个安全的恢复机制以防验证失败导致设备无法启动。例如可以通过按住某个按键上电强制进入一个极简的恢复Bootloader该恢复程序只能从特定的、带强认证的UART或USB接口接收并验证由根私钥签名的恢复固件。绝不能开放一个可以任意刷写的不验证接口。4.3 性能与资源开销评估在AB32VG1上实测使用mbedtls优化过的库验证一个256KB大小的RT-Thread镜像SHA-256哈希 ECDSA签名验证大约需要800ms - 1200ms。这个时间对于很多物联网设备如网关、控制器的启动时间来说是可接受的但对于需要极速启动的应用如某些传感器可能成为瓶颈。代码空间mbedtls仅包含SHA-256和ECDSA验证所需模块会增加约30KB - 50KB的Flash占用。一级Bootloader自身大小控制在20KB以内二级Bootloader含升级逻辑大约50-80KB。内存占用验证过程中的大数运算需要临时内存峰值RAM消耗可能在5-10KB左右需要确保Bootloader运行时有足够的栈和堆空间。优化建议如果启动时间非常敏感可以考虑只对镜像的头部例如前4KB包含向量表和关键初始化代码进行验证而不是整个镜像。但这会降低安全性需要权衡。探索使用更快的椭圆曲线实现或者利用芯片的硬件加速器如果AB32VG1有相关模块。5. 方案总结与扩展思考实现下来基于RT-Thread和AB32VG1构建一个基本的可信启动框架是完全可行的。它虽然无法达到专用安全芯片的等级但能有效抵御远程网络攻击和低成本的物理篡改为大量成本敏感的物联网设备提供了显著的安全提升。这个方案的核心价值在于提供了一个清晰的、可裁剪的框架。对于安全要求不高的设备可以只实现一级Bootloader验证对于要求高的可以延伸信任链到应用层。RT-Thread的组件化设计让这种集成变得相对容易。我个人在实践中的最大体会是安全是一个系统工程而非单一功能。可信启动是设备安全的第一道门但它需要与安全的固件升级、安全的存储如加密Flash、安全的通信TLS以及运行时保护如MPU内存保护相结合才能构成一个立体的防御体系。例如在RT-Thread中可以进一步结合其TLS软件包实现端到端的安全通信使用FALFlash抽象层组件管理受保护的密钥存储区。最后一个实用的建议在项目早期就将安全需求特别是启动安全的需求明确下来并据此规划Flash布局、选择硬件是否有OTP、硬件加解密。如果等项目后期再添加往往会发现资源捉襟见肘或者需要对架构进行伤筋动骨的调整。从AB32VG1这样的通用MCU开始实践可信启动是理解嵌入式安全底层逻辑的绝佳途径其经验可以平滑地迁移到更强大或更专业的平台上。
返回列表