4. 集成RSA签名验证

任务目标

在现有Bootloader的固件跳转执行前,增加RSA-2048签名验证环节。Bootloader使用预置的公钥对固件头部附带的SHA-256摘要签名进行验签,仅当验签通过后才允许跳转到应用固件,否则进入错误处理流程。

环境与依赖

  • 硬件:STM32F407系列(具有硬件CRC与充足Flash),本文以STM32F407VET6为例
  • 密码库:mbedTLS 2.28.x(需启用MBEDTLS_RSA_C、MBEDTLS_SHA256_C、MBEDTLS_PKCS1_V15)
  • 上位机工具:OpenSSL(用于生成密钥对与签名固件)

固件签名与验证流程

  1. 上位机:使用私钥对固件二进制(不含签名区)计算SHA-256摘要,再用RSA私钥签名,得到256字节签名数据。
  2. 固件布局:应用固件起始处存放签名结构体(固定偏移),随后才是实际代码。
  3. Bootloader:读取签名区与固件代码区,用内置公钥验签,通过后跳转。

关键完整代码

// bootloader_rsa_verify.h
#ifndef BOOTLOADER_RSA_VERIFY_H
#define BOOTLOADER_RSA_VERIFY_H

#include <stdint.h>
#include "mbedtls/rsa.h"
#include "mbedtls/md.h"
#include "mbedtls/pk.h"

#define APP_SIGN_OFFSET     0x0       // 签名存放在固件起始处
#define SIGN_LEN            256       // RSA-2048 签名长度
#define APP_CODE_OFFSET     (SIGN_LEN + 8)  // 跳过签名+长度字段
#define APP_FLASH_ADDR      0x08010000

// 固件签名结构(存放于固件起始处)
typedef struct {
    uint8_t  signature[SIGN_LEN];   // RSA签名
    uint32_t code_len;              // 实际固件代码长度
    uint32_t reserved;              // 保留
} app_signature_t;

// 内置RSA公钥(由私钥导出,此处为示例,实际应替换为真实公钥)
static const uint8_t rsa_pubkey_der[] = {
    0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
    0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00,
    0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00,
    // ... 实际公钥DER数据(308字节),此处省略
    0x02, 0x03, 0x01, 0x00, 0x01
};

// 验证固件签名
// 返回 0: 验证通过; -1: 参数错误; -2: 验签失败
int verify_app_signature(void) {
    app_signature_t *sig = (app_signature_t *)APP_FLASH_ADDR;
    uint8_t *app_code = (uint8_t *)(APP_FLASH_ADDR + APP_CODE_OFFSET);
    uint32_t code_len = sig->code_len;
    
    if (code_len == 0 || code_len > 0x40000) {  // 固件最大256KB
        return -1;
    }
    
    mbedtls_pk_context pk;
    mbedtls_pk_init(&pk);
    
    int ret = mbedtls_pk_parse_public_key(&pk, rsa_pubkey_der, sizeof(rsa_pubkey_der));
    if (ret != 0) {
        mbedtls_pk_free(&pk);
        return -1;
    }
    
    // 计算固件代码区SHA-256
    uint8_t hash[32];
    mbedtls_md_context_t md_ctx;
    mbedtls_md_init(&md_ctx);
    mbedtls_md_setup(&md_ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 0);
    mbedtls_md_starts(&md_ctx);
    mbedtls_md_update(&md_ctx, app_code, code_len);
    mbedtls_md_finish(&md_ctx, hash);
    mbedtls_md_free(&md_ctx);
    
    // RSA验签
    ret = mbedtls_pk_verify(&pk, MBEDTLS_MD_SHA256, hash, 0,
                            sig->signature, SIGN_LEN);
    
    mbedtls_pk_free(&pk);
    return (ret == 0) ? 0 : -2;
}

#endif // BOOTLOADER_RSA_VERIFY_H
// main.c 中跳转前调用
#include "bootloader_rsa_verify.h"

void jump_to_app(void) {
    int ret = verify_app_signature();
    if (ret != 0) {
        // 验签失败,进入错误处理(如LED闪烁、等待重新烧录)
        while (1) {
            // 错误处理代码
        }
    }
    
    // 验签通过,跳转
    uint32_t app_addr = APP_FLASH_ADDR + APP_CODE_OFFSET;
    void (*app_reset)(void) = (void (*)(void))(*(volatile uint32_t *)(app_addr + 4));
    __set_MSP(*(volatile uint32_t *)app_addr);
    app_reset();
}

上位机签名命令(OpenSSL)

# 1. 生成RSA-2048密钥对
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem

# 2. 导出公钥DER格式(供Bootloader嵌入)
openssl rsa -in public.pem -pubin -outform DER -out pubkey.der

# 3. 对固件签名(假设固件为app.bin,不含签名区)
openssl dgst -sha256 -sign private.pem -out signature.bin app.bin

# 4. 生成带签名的固件镜像(签名+长度+代码)
# 使用Python脚本拼接:
python3 -c "
import struct
with open('app.bin','rb') as f:
    code = f.read()
with open('signature.bin','rb') as f:
    sig = f.read()
with open('app_signed.bin','wb') as f:
    f.write(sig)
    f.write(struct.pack('<I', len(code)))
    f.write(b'\x00'*4)  # reserved
    f.write(code)
print('Signed image created, code length:', len(code))
"

验证方法与通过条件

  1. 编译烧录:将Bootloader编译烧录至0x08000000,应用固件烧录至0x08010000(使用app_signed.bin)。
  2. 正常路径验证:使用正确私钥签名的固件,上电后Bootloader验签通过,LED指示跳转成功,应用正常运行。
  3. 篡改检测验证:修改app_signed.bin中任意一个字节(模拟固件被篡改),重新烧录。上电后Bootloader验签失败,进入错误处理(如LED以1Hz闪烁),不跳转应用。
  4. 判定标准:篡改固件无法运行,且正确签名固件可正常启动,即证明RSA签名验证生效。

常见问题

  • 公钥不匹配:确保rsa_pubkey_der[]与签名所用私钥对应,否则验签必然失败。
  • 内存不足:mbedTLS RSA验签需约4KB栈空间,确认Bootloader链接脚本中栈大小足够。
  • 固件长度字段:code_len必须与实际代码长度一致,否则SHA-256计算范围错误导致验签失败。