简介:基于STM32H743单片机生成AES加密固件的上位机软件源码,专为需要嵌入式安全通信与固件安全升级的开发者准备,适用于STM32H743平台的固件工程师、嵌入式安全研发及物联网产品验证场景。压缩包共808个文件,整体约20.24MB,以112个h头文件与109个c源文件为主体,同时包含pem/der/key等密钥证书文件、obj编译中间文件及txt/sh/py辅助脚本,便于直接阅读、编译与二次调整。源码功能覆盖上位机与单片机通信、AES密钥生成与加解密、固件上传升级、用户界面及错误日志处理,基本形成一套可运行的加密固件工具链。目前已有62人学习浏览,示例代码与目录组成对理解STM32H743上的AES应用有直接参考价值,可作为集成加密功能时的设计蓝本与排错依据。
1. 为什么上位机要承担AES固件加密这个活
很多刚接触STM32H743的工程师,会把固件加密理解成“在App里跑一段AES算法”。但真正做过量产和OTA的人都知道,风险从来不在单片机内部,而在固件文件本身:编译器生成的bin是明文,烧录器读到的是明文,OTA服务器下发的是明文,任何一个环节被人拿走,H743里的代码就完全暴露。标题里说的“基于stm32h743单片机开发生成AES加密固件的上位机软件”,核心是换一种分工:上位机把编译产物加密成自包含的密文固件包,H743的bootloader只负责解密、校验和引导。它解决的是“固件被拷贝了也跑不起来”和“传输途中被篡改会立刻被发现”这两件事。适合正在做IAP升级、需要产线统一发版,或者想把固件安全边界前移到PC侧的团队。
2. 选型定盘星:C#上位机加AES-256-GCM,为什么这么配
做上位机加密工具,最怕开头选型定错,后面MCU端联调时返工。这一章先把两个决定性的问题讲清楚:上位机技术栈选哪个,AES用哪种模式。这两个点对了,后面开发就是水到渠成。
2.1 上位机技术栈:C#和Python该怎么选
上位机这一步,常见做法是C#(WinForms或WPF)和Python二选一。C#在嵌入式工具链里占有率高,主要因为VS2019/VS2022调试方便,System.Security.Cryptography从.NET Core 3.0起原生提供AesGcm类,不需要额外引入第三方密码学库;串口联动、扫码枪输入、产线日志导出这些周边功能也有现成控件。Python的pycryptodome同样成熟,脚本写起来快,但打包成exe体积大,产线环境装依赖容易出问题。
| 对比项 | C#(WinForms/WPF) | Python(pycryptodome) |
|---|---|---|
| AES-GCM支持 | .NET Core 3.0+ 原生AesGcm | 成熟,接口直观 |
| 串口/产线集成 | SerialPort、窗体控件齐全 | 需要额外UI库 |
| 部署方式 | 单exe,目标机器装运行时 | pyinstaller打包,体积大 |
| 团队上手难度 | 嵌入式工程师基本都会VS | 需要维护脚本环境 |
还有一个现实问题:VS2019创建的上位机工程,如果目标框架是.NET 5/6,VS2015是打不开的。如果你的工作环境还在VS2015,那就得把目标框架降到.NET Framework 4.7.2,同时改用BouncyCastle里的GcmBlockCipher来实现GCM。这个坑在团队协作时特别容易爆发,建议一开始就把目标框架统一写在工程说明里。
2.2 AES加密模式选型:ECB、CBC、CTR、GCM怎么挑
AES本身只是分组密码原语,选哪种模式直接决定固件的保密性、完整性和H743端的解码难度。先看一张对比表,再解释结论。
| 模式 | 是否需要IV | 是否带认证 | 相同明文加密结果 | H743 CRYP硬件支持 |
|---|---|---|---|---|
| ECB | 否 | 无 | 完全一致 | 支持 |
| CBC | 是,16字节 | 无 | 随机IV时每次不同 | 支持 |
| CTR | 是,16字节 | 无 | 随机IV时每次不同 | 支持 |
| GCM | 是,推荐12字节 | 有,GHASH认证 | 随机IV时每次不同 | 支持,CRYPEx接口 |
选型结论:AES-256-GCM,没有悬念。第一,STM32H743内置CRYP外设原生支持GCM模式,加解密走硬件,480MHz主频下处理几百KB固件是毫秒级的事情,不需要软件算法库。第二,GCM自带认证标签,加密的同时把“密文是否被篡改”这件事一并解决了,不用再单独做HMAC。CBC要自己处理PKCS7填充和MAC校验,CTR连填充都不用考虑,但完全没有认证——攻击者翻转密文某个bit,解密出来的明文对应bit就会被改写,这种攻击对固件升级是致命的。ECB坚决不用:固件里大量连续相同字节,比如Flash擦除态填充的0xFF,ECB会把这些块映射成完全相同的密文块,相当于在密文里直接画出固件结构图给攻击者看。
2.3 IV随机性与“每次加密结果都不一样”的机制
很多人在网上搜“AES什么模式每次加密结果都不一样”,本质就是IV在起作用。ECB没有IV输入,结果完全确定。CBC、CTR、GCM把IV引入运算链,所以只要每次随机生成IV,同一份bin就能产出完全不同的密文文件。GCM的nonce推荐12字节,内部把它与计数器拼接成16字节初始计数器块,再用GHASH把认证数据关联进来,任意一个bit被改动,tag校验都会失败。
这里必须强调一个安全边界:同一个密钥下,GCM的nonce绝对不允许重复。nonce一旦重用,不仅两份密文的XOR关系会暴露明文结构,严重时还能还原出认证密钥H,整个加密体系直接崩掉。因此上位机代码里必须用RandomNumberGenerator.Fill这类密码学安全随机源,不要用Guid.NewGuid()截取,更不要用系统时间凑数。生成结果每次不同是预期行为,不是软件bug,联调时不要拿“两次密文不一样”当问题去排查。
3. 上位机核心实现:从bin到AES加密固件包的生成流程
这一章给出可以直接抄进VS2019工程的核心代码。整体结构很简单:界面只负责选文件、点按钮、写输出;真正的加密逻辑放在一个静态类里,这样以后做单元测试、批量产线脚本都能直接调用,不依赖UI。
3.1 工程结构与依赖安排
工程建议拆成两个文件。MainForm.cs负责文件选择、版本号输入、进度显示和输出路径;FirmwareEncryptor.cs承载读文件、对齐、加密、组装包头、写文件的全部逻辑。目标框架直接用.NET 6,VS2019安装.NET 6 SDK即可。如果条件受限,回退到.NET Framework 4.7.2加BouncyCastle,下面是.NET 6的原生写法。
using System; using System.IO; using System.Security.Cryptography; public static class FirmwareEncryptor { public const int KeySize = 32; // AES-256,32字节密钥 public const int NonceSize = 12; // GCM推荐的随机数长度 public const int TagSize = 16; // GCM认证标签长度 public const uint Magic = 0xA55A5AA5; public static byte[] EncryptFirmware(byte[] raw, byte[] key, ushort version) { if (key.Length != KeySize) throw new ArgumentException("AES-256需要32字节密钥"); // 1. 对原始明文固件算SHA-256,取前4字节作为完整性指纹 byte[] digest = SHA256.HashData(raw); byte[] fileCrc = new byte[4]; Array.Copy(digest, fileCrc, 4); // 2. 对齐到16字节整数倍,填充0xFF int cipherLen = (raw.Length + 15) & ~15; byte[] padded = new byte[cipherLen]; Array.Copy(raw, padded, raw.Length); for (int i = raw.Length; i < cipherLen; i++) padded[i] = 0xFF; // 3. 生成12字节随机nonce byte[] iv = new byte[NonceSize]; RandomNumberGenerator.Fill(iv); // 4. 构造16字节固定前缀头部(统一按大端字节序),先算CRC再整体作为AAD byte[] prefix = new byte[16]; WriteUInt32BE(prefix, 0, Magic); WriteUInt16BE(prefix, 4, version); WriteUInt32BE(prefix, 8, (uint)cipherLen); Array.Copy(fileCrc, 0, prefix, 12, 4); ushort crc = Crc16CCITT(prefix.AsSpan(0, 6)); WriteUInt16BE(prefix, 6, crc); // 5. GCM加密,AAD就是完整的16字节头部 byte[] tag = new byte[TagSize]; byte[] cipher = new byte[cipherLen]; using (var aes = new AesGcm(key, TagSize)) { aes.Encrypt(iv, padded, cipher, tag, prefix); } // 6. 组装最终输出:头部 + IV + Tag + 密文 using var ms = new MemoryStream(); ms.Write(prefix, 0, 16); ms.Write(iv, 0, NonceSize); ms.Write(tag, 0, TagSize); ms.Write(cipher, 0, cipherLen); return ms.ToArray(); } private static void WriteUInt32BE(byte[] dst, int off, uint value) { dst[off] = (byte)(value >> 24); dst[off + 1] = (byte)(value >> 16); dst[off + 2] = (byte)(value >> 8); dst[off + 3] = (byte)value; } private static void WriteUInt16BE(byte[] dst, int off, ushort value) { dst[off] = (byte)(value >> 8); dst[off + 1] = (byte)value; } private static ushort Crc16CCITT(ReadOnlySpan<byte> data) { ushort crc = 0xFFFF; foreach (byte b in data) { crc ^= (ushort)(b << 8); for (int i = 0; i < 8; i++) crc = (ushort)((crc & 0x8000) != 0 ? (crc << 1) ^ 0x1021 : crc << 1); } return crc; } }代码核心逻辑分六步:SHA-256取指纹、填充对齐、随机IV、构造头部、GCM加密、组装输出。AesGcm(key, TagSize)这个构造函数在.NET 6中使用;如果是.NET 5环境,去掉第二个参数即可,默认tag长度就是16字节。填充用0xFF而不是0x00,原因有二:Flash擦除态是全1,填充0xFF能让密文长度更接近物理擦除块,减少升级时的无效擦写;解密后如果看到尾部是0xFF,也容易和真正的数据边界区分开。注意cipherLen这个值在后续bootloader里要原样读取,两边必须完全一致。
3.2 加密固件包头格式定义
上位机和H743之间的约定全在这44字节头部里,定义必须一次性冻结。推荐布局如下。
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0x00 | 4 | magic | 固定0xA55A5AA5,bootloader先检查它,不对直接放弃 |
| 0x04 | 2 | version | 固件版本号,大端,范围0x0000~0xFFFF |
| 0x06 | 2 | headerCrc | 对偏移0x00~0x05共6字节做CRC16-CCITT |
| 0x08 | 4 | cipherLen | 对齐后密文长度,解密循环先读它 |
| 0x0C | 4 | fileCrc | 原始明文SHA-256前4字节 |
| 0x10 | 12 | iv | GCM随机nonce |
| 0x1C | 16 | tag | GCM认证标签 |
| 0x2C | 不定 | ciphertext | AES-256-GCM密文 |
这里有两个校验字段要区分清楚。headerCrc防的是用户在产线偷偷改版本号;fileCrc防的是加密链路本身出错,或者密文被部分篡改后解密出损坏固件。SHA-256只取前4字节是够用的,毕竟前面还有GCM的tag做完整性兜底;如果需要更强保障,可以改成8字节,甚至把整个SHA-256放进头部,代价只是多占28字节Flash。
3.3 生成调用与实际使用注意点
界面里的调用代码很短,核心就三行。密钥通过YourKeyLoader.Load()读入,我一般不建议把密钥写死在源码里,更稳妥的是把密钥文件单独放产线机器,与上位机exe分离部署。版本号遵循0xMMMM格式,主版本占高字节,次版本占低字节,升级判断直接比较数值大小。
string inPath = @"build\app.bin"; byte[] key = YourKeyLoader.Load(); byte[] raw = File.ReadAllBytes(inPath); byte[] pkg = FirmwareEncryptor.EncryptFirmware(raw, key, 0x0102); File.WriteAllBytes(@"build\app_enc.bin", pkg);生成之前建议先打印raw.Length和它的SHA-256,与编译日志里的固件大小对比一下。Keil和STM32CubeIDE生成的bin都有固定路径,一不留神加密了旧的中间产物,烧到板子上就是一两小时的莫名问题。
4. 与STM32H743端配合:bootloader里的解密与校验
上位机把密文包生成出来只是第一步,真正让这套方案闭环的是H743端bootloader。这一章从Flash分区、CRYP外设调用、密钥存储三个角度讲清楚怎么把密文包安全地变成可运行的App。
4.1 Flash分区规划与升级流程
STM32H743的Flash按具体型号有1MB或2MB,常见做法是划分三个区域:bootloader只占前128KB,APP区紧随其后,最后留一块暂存区接收升级包。
| 区域 | 地址范围 | 用途 |
|---|---|---|
| Bootloader | 0x08000000 ~ 0x0801FFFF | 上电解密、校验、跳转 |
| APP区 | 0x08020000 起 | 解密后的明文App |
| 暂存区 | 0x08040000 起 | 存放加密固件包,升级时先落这里 |
分区表确定后,APP的链接脚本要把中断向量表改到0x08020000,否则跳转后任何中断都会跑飞。升级流程是:先用Ymodem或U盘把app_enc.bin整个写到暂存区,然后跳回bootloader执行解密。写入Flash时,HAL的HAL_FLASH_Program按128位也就是16字节编程,这正好对应上位机那边把固件对齐到16字节的行为,两边严丝合缝。
4.2 CRYP外设的AES-GCM解密流程
解密部分的C代码骨架如下。注意不同版本的STM32CubeH7在stm32h7xx_hal_cryp_ex.c里的函数签名略有差异,以你当前头文件为准,但调用顺序一定不变:SetIV、FeedAAD、Decrypt、GetTag。
#include "stm32h7xx_hal.h" typedef struct { uint32_t magic; uint16_t version; uint16_t headerCrc; uint32_t cipherLen; uint32_t fileCrc; uint8_t iv[12]; uint8_t tag[16]; } fw_header_t; static int decrypt_to_flash(uint32_t pkgAddr, uint32_t appAddr, const uint8_t *key) { fw_header_t hdr; memcpy(&hdr, (void *)pkgAddr, sizeof(hdr)); // 大端转主机序,STM32H743是Cortex-M7,默认小端 hdr.magic = BE32((uint8_t *)&hdr.magic); hdr.version = BE16((uint8_t *)&hdr.version); hdr.cipherLen = BE32((uint8_t *)&hdr.cipherLen); hdr.fileCrc = BE32((uint8_t *)&hdr.fileCrc); if (hdr.magic != 0xA55A5AA5) return -1; if (hdr.cipherLen == 0 || hdr.cipherLen > 0x100000) return -2; // AAD就是包头的16字节,必须与上位机加密时用的一致 uint8_t aad[16]; memcpy(aad, (void *)pkgAddr, 16); CRYP_HandleTypeDef hcryp = {0}; hcryp.Instance = CRYP; hcryp.Init.Algorithm = CRYP_AES_GCM; hcryp.Init.KeySize = CRYP_KEYSIZE_256B; hcryp.Init.DataType = CRYP_DATATYPE_8B; hcryp.Init.pKey = (uint8_t *)key; if (HAL_CRYP_Init(&hcryp) != HAL_OK) return -3; HAL_CRYPEx_AESGCM_SetIV(&hcryp, hdr.iv); HAL_CRYPEx_AESGCM_FeedAAD(&hcryp, aad, 16); HAL_CRYPEx_AESGCM_Decrypt(&hcryp, (uint8_t *)(pkgAddr + 0x2C), temp_buf, hdr.cipherLen); uint8_t tag[16]; HAL_CRYPEx_AESGCM_GetTag(&hcryp, tag, 16); if (memcmp(tag, hdr.tag, 16) != 0) return -4; // SHA-256校验明文指纹,防止解密链路任何一步出错 uint8_t sha[32]; mbedtls_sha256(temp_buf, hdr.cipherLen, sha, 0); if (memcmp(sha, &hdr.fileCrc, 4) != 0) return -5; // 校验全部通过后,擦除APP区,按128位写入 FLASH_EraseInitTypeDef erase = {0}; erase.TypeErase = FLASH_TYPEERASE_SECTORS; erase.Banks = FLASH_BANK_1; erase.Sector = FLASH_SECTOR_8; erase.NbSectors = 1; erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; uint32_t badSector = 0; if (HAL_FLASHEx_Erase(&erase, &badSector) != HAL_OK) return -6; // 后续循环调用HAL_FLASH_Program写入temp_buf,略 return 0; }一个容易被忽略的细节:不要边解密边擦Flash。先把密文解到RAM缓冲区,tag和fileCrc都校验通过,再动手擦除APP区。否则解密到一半发现tag不对,APP区老固件已经被擦掉,板子就变砖了。STM32H743内部有1MB RAM,一个512KB的固件可以整包放进缓冲区,完全够用。如果固件特别大,H7的CRYP外设支持连续数据流处理,但分块调用时中间不能重新初始化算法状态,否则计数器状态断掉,GCM的tag必然对不上。
4.3 密钥存储与读保护策略
解密用的32字节主密钥是这套安全方案的命根子。密钥不能存在App区,因为App本身是明文存放的;也不能直接写死在上位机源码里,反编译工具能轻松提取字符串。常见做法是两种:一是把密钥放在专用Flash页,配合RDP级保护;二是利用STM32H743的OTP区奇偶校验位存储密钥指纹。量产板建议直接把RDP设置为Level 2,这个级别会永久关闭调试口和读保护降级通道,设置后无法回退。所以必须保证bootloader和密钥都烧录完,再锁定RDP Level 2,顺序反了就只能换芯片。
上位机侧也要匹配同样强度的保护。密钥文件不随exe发布,产线机器单独部署;Windows环境下可以用DPAPI按当前用户加密密钥文件,这样就算exe被拷走,没有原产线账号也拿不到明文密钥。还可以在头部预留一个keyID字节,bootloader根据keyID从密钥表里选不同的根密钥,密钥轮换时旧设备也能平滑过渡。
4.4 AAD一致性检查清单
联调时最常见的失败原因不是密码学问题,而是上下位机对AAD的切分不一致。C#侧EncryptFirmware里是把16字节头部作为associatedData,H7端也是把这16字节作为AAD喂给HAL_CRYPEx_AESGCM_FeedAAD。任何一端多一个字节或少一个字段,tag校验必挂。每次修改包头格式,都要同步更新三个地方:C#的prefix构造、C的结构体定义、AAD的字节长度。我在工程里加了一个编译期断言,直接static_assert(sizeof(fw_header_t) == 44),提醒任何人改动结构体都要重新对齐。
5. 验证方法与STM32H743 AES固件加密的高频踩坑点
加密工具写完,第一件事不是烧板联调,而是先在PC侧把往返验证跑通。把EncryptFirmware输出的整个固件包传回解密接口,能解密出和原始bin逐字节相同的内容,说明加密链路本身没有问题,再去查H7端联调问题。
5.1 用OpenSSL交叉验证加密结果
如果不想写C#解密函数,可以直接用OpenSSL命令交叉验证。把密文部分单独扣出,同一条密钥、同一个IV、同一个tag,就能解出原始固件:
openssl enc -d -aes-256-gcm \ -K 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF \ -iv 000102030405060708090A0B \ -tag 101112131415161718191A1B1C1D1E1F \ -in fw.cipher -out fw_plain.bin-K是32字节密钥的十六进制串,-iv是12字节nonce,-tag是GCM标签,全部不带冒号连续拼接。对比解密出来的fw_plain.bin与原始bin的SHA-256一致,就可以确认上位机加密参数与OpenSSL兼容。这里注意,OpenSSL命令行模式对GCM tag的校验在部分版本里只是警告,真正校验还是靠后面对比SHA-256。
5.2 五个高频踩坑点
第一,AAD不一致。这个占了联调问题的九成,排查方法很简单:在C#里把prefix每个字节打印出来,H7端也逐字节打印,再对比FeedAAD输入的前16字节是否完全相同。
第二,字节序。上位机按大端写头部,H743是小端CPU,必须在C里做BE32/BE16转换。忘记转换的话,magic是反的,cipherLen变成一个巨大数字,代码直接走return -2。
第三,IV复用。上位机每次启动都要调用RandomNumberGenerator.Fill生成新IV。如果因为调试方便写死IV,不但密文固定,同一个密钥下nonce重用还会导致GCM认证密钥泄露,整个方案失效。
第四,填充对齐不足16字节。H743的CRYP外设按固定块处理数据,cipherLen不是16的倍数时,解密输出和原明文长度对不上,fileCrc校验必挂。上位机已经做了对齐,注意不要在bootloader里再对齐一次。
第五,tag比较不完整。有些低功耗场景里有人只比较tag前4字节,这在安全上是绝对不行的,攻击者可以暴力碰撞前4字节伪造认证。GCM的tag是16字节,必须全量memcmp。
最后提醒一点:temporary buffer的大小、扇区擦除范围、RDP锁定顺序,这些配置在拿到具体板子之后要连同加密工具一起做一次产线演练,把升级失败、断电回滚的路径都跑一遍,再交付给产线使用。把AAD边界和字节序这两件事理顺,这套上位机加密加H743解密的工具链就算真正闭环了。
本文还有配套的精品资源,点击获取