通过几个具体的例子来说明哈希算法在单片机(MCU)物联网产品开发过程中的应用场景。这些场景紧密围绕实际开发中会遇到的问题,特别是安全性和可靠性问题。

 场景一:安全 Bootloader 与固件完整性校验(最核心的应用)

这是单片机开发中最重要和最常见的哈希应用场景,用于确保设备不会运行被篡改或损坏的固件。

背景:你开发了一个智能家居设备,使用STM32单片机。为了防止攻击者通过漏洞上传恶意固件,或者确保固件在存储/传输过程中没有出现位错误,你需要一个验证机制。

应用流程:

1.  编译后处理(开发端):

在IDE(如Keil或IAR)中,编写一个后处理脚本(Post-build script)。

脚本使用电脑上的工具(如 sha256sum)计算刚刚编译生成的 firmware.bin 文件的SHA-256哈希值。

脚本将这个哈希值转换为C语言数组的形式,并生成一个头文件 firmware_hash.h,内容类似于:

#ifndef  FIRMWARE_HASH_H

#define  FIRMWARE_HASH_H

const uint8_t expected_firmware_hash[32]  =  {0xA5, 0x91, 0xA6, ... };

#endif

将这个头文件编译到Bootloader工程中。

2.  Bootloader 设计(单片机端):

Bootloader是单片机上电后运行的第一段代码。它的主要任务不是实现功能,而是负责验证和跳转到主应用程序(App)。

在跳转前,Bootloader会执行以下操作:

// 1. 从主Flash应用程序区读取固件数据

uint8_t *app_address  = (uint8_t*)0x08010000; // App起始地址

uint32_t app_size = ...; // 知道App的大小

// 2. 计算当前App的哈希值

uint8_t calculated_hash[32];

SHA256_Calculate(app_address, app_size, calculated_hash); // 使用哈希库

// 3. 与预存的正确哈希值对比

if(memcmp(calculated_hash, expected_firmware_hash, 32) == 0) {

// 验证通过,跳转到应用程序

jump_to_application();

} else {

// 验证失败!固件可能被篡改或损坏

LOG("Firmware Integrity Check Failed!\n");

// 采取安全措施:如关机、进入DFU模式等待更新、点亮错误灯等

halt_system();}

好处:

有效防止运行被篡改的恶意固件。

防止因Flash读写错误或下载不完整导致的设备“变砖”。

 场景二:安全OTA(空中升级)更新

OTA是单片机联网设备的必备功能,哈希是保证OTA安全的关键一环。

背景:你的物联网设备需要通过Wi-Fi从服务器下载新的固件包并进行更新。

应用流程:

1.  服务器端准备:

服务器对新的固件文件 firmware_v2.bin 计算其SHA-256哈希值 H1。

服务器用自身的私钥对哈希值 H1 进行签名,得到数字签名(这引入了非对称加密,哈希是其核心)。

将 firmware_v2.bin + 数字签名 一起下发设备。

2.  设备端(单片机)处理:

单片机收到整个升级包后,将其存入临时Flash区域(不能直接覆盖当前运行的程序)。

在安装前,进行验证:

// 1. 使用预烧录在单片机中的服务器公钥,对收到的“数字签名”进行解密,得到服务器计算出的哈希值 H1。

// 2. 单片机自己对收到的 firmware_v2.bin 数据计算SHA-256哈希值,得到 H2。

// 3. 比较 H1 和 H2。

if (H1 == H2) {

// 验证成功!说明两个问题:

//   a. 固件包来自合法的服务器(身份认证)。

//   b. 固件包在传输过程中没有被篡改(完整性)。

start_application_update(); // 开始更新

} else {

// 验证失败,删除错误的升级包

delete_download_file();

send_error_report_to_server();

}

好处:

避免了“中间人”攻击:即使攻击者截获了数据包并注入恶意固件,也无法伪造签名,设备会验证失败。

保证了升级文件的完整性。

 场景三:通信数据完整性校验

单片机与外部设备(如传感器、另一个MCU、手机App)通信时,需要确保数据没有被干扰出错。

背景:你的单片机通过串口(UART)接收来自PC的上传指令,指令格式为 [命令头][数据长度][数据内容]。由于串口易受电磁干扰,数据可能出错。

应用流程:

1.  发送端(PC):

在组织好 [命令头][数据长度][数据内容] 后,计算这部分数据的CRC32(一种速度快、专门用于检错的哈希算法)值。

将CRC32值附加在数据包末尾,格式变为:[命令头][数据长度][数据内容][CRC32值]。

发送整个数据包。

2.  接收端(单片机):

单片机接收完数据包后,提取出前面的数据部分。

使用同样的CRC32算法对数据部分进行计算,得到一个本地CRC值。

将计算出的CRC值与数据包中发送过来的CRC值进行比较。

if (received_crc32 != calculated_crc32) {

// 数据在传输中出错了

uart_send_nak(); // 发送否定应答,请求重传

} else {

// 数据正确,处理指令

uart_send_ack(); // 发送肯定应答

process_command();

}

好处:

简单高效地实现了数据链路层的差错检测,比简单的奇偶校验可靠得多。

CRC32计算量非常小,对低端单片机没有压力。

 场景四:系统配置参数的完整性检查

背景:单片机需要将一些用户配置参数(如IP地址、阈值、校准数据)保存在EEPROM或Flash中。这些参数可能因电压不稳定等原因导致存储区数据损坏。

应用流程:

1.  保存时:

将配置参数的结构体 config_t 序列化为一段二进制数据。

计算这段数据的CRC32值。

将 数据本身 + CRC32值 一同写入EEPROM。

2.  读取时:

从EEPROM中读出数据本身和存储的CRC32值。

对读出的数据计算新的CRC32值。

比较新旧两个CRC32值。

if (stored_crc != calculated_crc) {

// 配置数据已损坏,使用默认配置

load_default_config();

} else {

// 数据完好,正常使用

use_stored_config();

}

好处:防止设备因配置数据错误而出现不可预测的行为,提高了系统的鲁棒性。

 总结

在单片机开发中,哈希算法的应用无处不在:

场景

主要目的

常用算法

资源消耗

固件验证

防篡改,保安全

SHA-256, SHA-3

高(建议用带硬件加速的MCU)

安全OTA

身份认证与完整性

SHA-256 with RSA/ECC

高

通信校验

检错,抗干扰

CRC32, Adler-32

极低

数据存储

检错,防损坏

CRC16, CRC32

低

开发者需要根据具体场景对安全性和资源开销的要求,选择合适的哈希算法。对于安全场景,必须使用加密强哈希(如SHA-256);对于简单的错误检测,使用轻量级的CRC足矣。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐