STM32 SAES 硬件加密实战:GCM 与 CCM 模式选型、配置与性能优化
1. 为什么要在 STM32 上用 SAES 而不是软件加密第一次接触 STM32 的 SAES 外设是在一个工业数据采集项目上。当时的需求很明确设备通过无线模块把采集到的传感器数据发到网关数据在链路上必须保证机密性和完整性。最初我用的是软件 AES 库跑在 72MHz 的 F103 上AES-128-CBC 加密一包 256 字节的数据大概要 200 多微秒CPU 占用率直接飙到 30% 以上而且密钥就明文躺在 Flash 里随便一个调试器接上去就能读出来。后来换到带 SAES 的 L4 系列同样的数据量硬件加密协处理器几十微秒就搞定CPU 几乎不参与密钥存在只有安全域能访问的寄存器里整个安全等级完全不是一个量级。这就是 SAES 存在的意义。SAES全称 Secure AES hardware accelerator是 STM32 在传统 AES 硬件加速器AES基础上升级出来的安全加密协处理器。它和普通 AES 外设最大的区别在于SAES 的密钥和部分配置寄存器被划入了一个受保护的安全域非安全访问会被硬件直接拦截同时它支持更完整的加密模式包括 ECB、CBC、CTR、GCM、CCM 这些主流模式还能配合 DMA 做流式加解密几乎不占 CPU 时间。这篇文章面向的是已经有一定 STM32 基础、正在做物联网终端、工业网关、金融支付外设或者车载 T-Box 这类对数据安全有实际要求的开发者。如果你还在用软件 AES 库、或者只是听说过 GCM、CCM 但没真正在硬件上跑过那这篇内容基本可以当作一份从选型到落地的完整参考。我会把 SAES 的几种工作模式讲清楚把 GCM 和 CCM 这两个最容易混淆的模式拆开对比然后给出可以直接复现的工程配置和代码框架最后把我踩过的坑和排查经验整理出来。需要提前说明的是不同 STM32 系列对 SAES 的支持情况不一样。目前 SAES 主要出现在 L4、L5、U5、H5、H7 的部分型号上具体要看参考手册里有没有 SAES 这个外设章节。如果你手上是 F1、F4 这类经典款那只有普通 AES 加速器没有 SAES 的安全域保护但加密模式的用法是相通的代码框架可以借鉴。2. SAES 核心架构与工作模式全解析2.1 SAES 和普通 AES 外设到底差在哪很多人第一次看手册会疑惑既然都有 AES 硬件加速了为什么还要搞一个 SAES我一开始也这么想直到仔细对比了两者的寄存器映射和访问权限才明白。普通 AES 外设的密钥寄存器AES_KEYR0~AES_KEYR3是挂在普通外设总线上的任何能访问总线的代码——包括你的应用代码、甚至一段被注入的恶意代码——都可以读写这些寄存器。这意味着密钥在运行时是暴露的。而 SAES 把这些关键寄存器放进了安全域只有以安全特权模式运行的代码才能访问非安全代码去读会触发硬件错误。这个机制在 ARM TrustZone 架构下尤其重要因为 TrustZone 把系统分成了安全世界和非安全世界SAES 天然就是给安全世界用的加密引擎。另一个区别是 SAES 支持的模式更全。普通 AES 外设通常只支持 ECB 和 CBC部分型号支持 CTR。而 SAES 原生支持 ECB、CBC、CTR、GCM、CCM 五种模式其中 GCM 和 CCM 是带认证的加密模式AEAD能同时保证机密性和完整性这在物联网场景里非常关键——你不仅要防止数据被偷看还要防止数据被篡改。还有一点是 DMA 配合。SAES 的输入输出 FIFO 可以挂 DMA 通道实现数据进 FIFO、硬件加密、结果出 FIFO的全自动流水线。我实测过用 DMA 搬运 4KB 数据做 AES-GCM 加密CPU 全程可以去处理其他任务加密完成后 DMA 中断通知一下就行CPU 占用率不到 2%。2.2 五种工作模式的适用场景与选择逻辑选哪种模式不是拍脑袋决定的得看你的数据特征和安全需求。我把这五种模式的核心特点和适用场景整理成一张表方便对照。模式是否带认证是否需要 IV并行性典型场景ECB否否可并行单块密钥加密不推荐用于数据CBC否是串行固件加密、文件加密CTR否是计数器可并行流式数据、高速加密GCM是是Nonce可并行网络协议、TLS、无线通信CCM是是Nonce串行蓝牙、Zigbee、802.15.4ECB 是最简单的模式每个 16 字节块独立加密相同的明文块会产生相同的密文块。这个特性在加密图像或结构化数据时会泄露模式信息所以除了加密密钥本身基本不推荐用它加密业务数据。我见过有人用 ECB 加密传感器数据结果因为数据格式固定密文里能明显看出重复模式等于没加密。CBC 引入了初始化向量IV和链式反馈每个明文块先和前一个密文块异或再加密解决了 ECB 的模式泄露问题。但 CBC 是串行的不能并行处理而且它只保证机密性不保证完整性——攻击者可以在你不知道的情况下翻转密文中的某些位解密后明文对应位也会翻转。所以 CBC 适合加密固件、配置文件这类一次性写入、整体校验的场景配合外部的 CRC 或哈希做完整性校验。CTR 把块密码变成了流密码用一个计数器作为输入每次加密计数器值得到密钥流再和明文异或。CTR 可以并行速度最快但同样不带认证而且计数器绝对不能重复使用否则密钥流重复安全性直接崩塌。GCM 和 CCM 都是 AEAD 模式同时输出密文和认证标签Tag。两者的核心区别在于GCM 基于 CTR 模式可以并行处理吞吐量高CCM 基于 CBC-MAC是串行的但实现更简单在资源受限的无线协议里用得更多。蓝牙低功耗、Zigbee、802.15.4 这些协议标准里指定的就是 CCM而 TLS 1.3、IPSec、以及大多数现代网络协议用的是 GCM。2.3 GCM 与 CCM 的深度对比怎么选不踩坑GCM 和 CCM 的选择是很多人纠结的点。我从三个维度来说清楚。性能维度GCM 可以并行计算认证标签硬件实现时能充分利用流水线吞吐量通常比 CCM 高 30% 到 50%。如果你做的是高速数据链路比如车载以太网或者工业实时采集GCM 是更优解。CCM 因为 CBC-MAC 的串行特性每个块都要等前一个块算完速度上吃亏。实现复杂度维度CCM 的结构更简单它本质上是先算 CBC-MAC 得到认证标签再用 CTR 模式加密数据和标签。GCM 需要做 GF(2^128) 上的乘法运算来生成认证标签硬件实现时这个乘法器的面积不小。所以在一些低成本、低功耗的 MCU 上CCM 的硬件资源占用更友好。协议兼容维度这个最直接——你的上层协议指定了什么就用什么。蓝牙 Mesh 用 CCMThread 用 CCMZigbee 用 CCM这些没得选。而如果你是自己定义私有协议或者跑 TLS、DTLS那 GCM 是主流。我个人的经验是无线短距协议跟着标准走用 CCMIP 网络和自定义高速链路用 GCM。还有一个容易被忽略的点Nonce 的管理。GCM 和 CCM 都要求 Nonce 在同一个密钥下绝对唯一。GCM 的 Nonce 推荐 12 字节CCM 的 Nonce 长度可以是 7 到 13 字节。如果 Nonce 重复GCM 的认证密钥会被恢复安全性彻底失效。我在项目里通常用设备唯一 ID 单调递增计数器来构造 Nonce保证不会重复。3. SAES 工程配置与代码实现3.1 开发环境搭建与 CubeMX 配置要点我用的开发环境是 STM32CubeIDE 配合 CubeMX芯片选的是 STM32L562这颗片子有完整的 SAES 和 TrustZone 支持。如果你用的是 Keil配置逻辑是一样的只是界面不同。在 CubeMX 里配置 SAES 有几个关键点。首先要在 Security 配置里使能 TrustZone把 SAES 分配到安全域。然后在 Peripherals 里找到 SAES模式选择上CubeMX 会让你选 ECB、CBC、CTR、GCM、CCM 之一。这里有个坑CubeMX 生成的初始化代码只配置了模式的基本参数GCM 和 CCM 的 Nonce、附加认证数据AAD这些需要在运行时通过 HAL 库函数单独设置不能只靠 CubeMX 配完就完事。时钟配置上SAES 挂在 AHB2 总线上L5 系列最高可以跑到 110MHz。我一般不分频直接用系统时钟这样吞吐量最大。但要注意如果系统时钟配置得比较高SAES 的时钟也要相应检查确保不超过手册规定的上限。中断配置方面SAES 有多个中断源加密完成、FIFO 阈值、错误等。我建议至少使能加密完成中断和错误中断。错误中断特别重要因为 SAES 在检测到非法访问或配置错误时会触发错误中断如果不处理后续操作会一直失败。DMA 配置是可选的但强烈建议开。SAES 有两个 DMA 请求输入 FIFO 和输出 FIFO。配置成循环模式还是普通模式取决于你的数据量。我通常用普通模式一次传输一块数据传完中断里再启动下一块。3.2 密钥管理与安全域访问的正确姿势密钥管理是 SAES 使用的核心。SAES 的密钥寄存器在安全域写入密钥必须在安全特权模式下进行。在 TrustZone 架构里这意味着你的密钥写入代码必须运行在安全世界。我通常的做法是在安全世界的初始化阶段从安全存储比如 STM32 内部的 Secure Storage 或者外挂的安全芯片读取密钥然后写入 SAES 的密钥寄存器。写入完成后可以选择锁定密钥寄存器防止后续被意外修改。SAES 有一个 KEYPROT 位置位后密钥寄存器就变成只读直到下次复位。这里有个实操细节SAES 的密钥写入是分字的。AES-128 需要写 4 个 32 位字AES-256 需要写 8 个。写入顺序必须按照手册规定的 KEYR0 到 KEYR7 依次写写错顺序会导致密钥错误。我一开始没注意按自己的顺序写结果加密出来的数据怎么都对不上排查了半天才发现是顺序问题。还有一个安全建议密钥不要硬编码在代码里。我见过太多项目把 AES 密钥直接写成const uint8_t key[] {...}编译进 Flash用调试器一读就出来了。正确做法是把密钥存在安全存储区或者用 STM32 的 OTP一次性可编程区域再或者用密钥派生函数从设备唯一密钥派生。SAES 本身不提供密钥存储它只是加密引擎密钥的保管要靠整个安全体系。3.3 GCM 模式完整代码实现与参数计算下面给出一个 GCM 模式的完整实现框架。我用的是 HAL 库芯片是 L562其他系列的函数名可能略有差异但逻辑一致。#include stm32l5xx_hal.h SAES_HandleTypeDef hsaes; /* SAES 初始化GCM 模式AES-128 */ void SAES_GCM_Init(void) { hsaes.Instance SAES; hsaes.Init.DataType SAES_DATATYPE_8B; /* 按字节处理 */ hsaes.Init.KeySize SAES_KEYSIZE_128B; /* AES-128 */ hsaes.Init.Algorithm SAES_ALGORITHM_GCM; /* GCM 模式 */ hsaes.Init.DataWidthUnit SAES_DATAWIDTH_8B; if (HAL_SAES_Init(hsaes) ! HAL_OK) { Error_Handler(); } } /* 写入密钥必须在安全特权模式下调用 */ void SAES_WriteKey(const uint8_t *key) { uint32_t keyWord[4]; for (int i 0; i 4; i) { keyWord[i] (uint32_t)key[i*4] | ((uint32_t)key[i*41] 8) | ((uint32_t)key[i*42] 16) | ((uint32_t)key[i*43] 24); } /* 按 KEYR0~KEYR3 顺序写入 */ HAL_SAES_SetKey(hsaes, keyWord, SAES_KEYSIZE_128B); } /* GCM 加密输入明文、AAD、Nonce输出密文和 Tag */ HAL_StatusTypeDef SAES_GCM_Encrypt(const uint8_t *plaintext, uint32_t plainLen, const uint8_t *aad, uint32_t aadLen, const uint8_t *nonce, uint32_t nonceLen, uint8_t *ciphertext, uint8_t *tag) { HAL_StatusTypeDef status; /* 设置 GCM 参数Nonce 长度、AAD 长度、明文长度 */ status HAL_SAES_GCM_SetParam(hsaes, nonceLen, aadLen, plainLen); if (status ! HAL_OK) return status; /* 写入 Nonce */ status HAL_SAES_GCM_WriteNonce(hsaes, nonce, nonceLen); if (status ! HAL_OK) return status; /* 写入 AAD附加认证数据不加密但参与认证 */ if (aadLen 0) { status HAL_SAES_GCM_WriteAAD(hsaes, aad, aadLen); if (status ! HAL_OK) return status; } /* 加密明文 */ status HAL_SAES_GCM_Encrypt(hsaes, plaintext, ciphertext, plainLen); if (status ! HAL_OK) return status; /* 读取认证标签GCM 标准 Tag 长度 16 字节 */ status HAL_SAES_GCM_ReadTag(hsaes, tag, 16); return status; }这段代码里有几个参数需要解释。Nonce 长度我推荐用 12 字节这是 GCM 的标准推荐值硬件内部会自动补 1 作为计数器初始值。如果 Nonce 不是 12 字节GCM 会先用 GHASH 对 Nonce 做一次处理多一次运算性能略降。AAD是附加认证数据它不加密但参与 Tag 计算适合放协议头、设备 ID 这类需要认证但不需要保密的信息。Tag 长度标准是 16 字节也可以截短到 12 或 8 字节但截得越短伪造难度越低安全场景建议用满 16 字节。解密流程和加密类似只是最后一步变成计算 Tag 并与接收到的 Tag 比对比对通过才认为数据完整。3.4 CCM 模式实现差异与注意事项CCM 的实现和 GCM 在 HAL 层的函数名不同但结构相似。核心差异在于 CCM 需要先设置好 Nonce、AAD、明文长度然后调用 CCM 加密函数最后读 Tag。/* CCM 加密AES-128Tag 长度 16 字节 */ HAL_StatusTypeDef SAES_CCM_Encrypt(const uint8_t *plaintext, uint32_t plainLen, const uint8_t *aad, uint32_t aadLen, const uint8_t *nonce, uint32_t nonceLen, uint8_t *ciphertext, uint8_t *tag) { HAL_StatusTypeDef status; /* CCM 参数Nonce 长度、AAD 长度、明文长度、Tag 长度 */ status HAL_SAES_CCM_SetParam(hsaes, nonceLen, aadLen, plainLen, 16); if (status ! HAL_OK) return status; status HAL_SAES_CCM_WriteNonce(hsaes, nonce, nonceLen); if (status ! HAL_OK) return status; if (aadLen 0) { status HAL_SAES_CCM_WriteAAD(hsaes, aad, aadLen); if (status ! HAL_OK) return status; } status HAL_SAES_CCM_Encrypt(hsaes, plaintext, ciphertext, plainLen); if (status ! HAL_OK) return status; status HAL_SAES_CCM_ReadTag(hsaes, tag, 16); return status; }CCM 的 Nonce 长度范围是 7 到 13 字节比 GCM 灵活。但要注意CCM 的 Nonce 长度会影响可以加密的最大数据量。Nonce 越短能加密的数据越多但 Nonce 空间越小重复风险越高。蓝牙标准里 Nonce 是 13 字节能加密的数据量有限但蓝牙包本身就不大够用。CCM 还有一个容易踩的坑AAD 长度和明文长度的编码方式。CCM 在计算 CBC-MAC 时会先把 AAD 长度和明文长度编码成特定格式的块这个编码是标准规定的硬件会自动处理但如果你自己用软件实现 CCM这一步很容易写错。用硬件 SAES 的好处就是这些细节硬件都帮你处理了你只需要给对参数。4. 实操过程中的典型问题与排查实录4.1 加密结果对不上从时钟到字节序的排查链路加密结果和预期不一致是 SAES 调试中最常见的问题。我遇到过好几次总结下来排查顺序是这样的。第一步查时钟。SAES 的时钟如果没有使能或者频率不对读写寄存器会返回错误值。用__HAL_RCC_SAES_CLK_ENABLE()确认时钟开了然后检查 RCC 配置里 SAES 的分频系数。第二步查密钥写入顺序。前面说过密钥必须按 KEYR0 到 KEYR7 的顺序写。我建议在写入后回读密钥寄存器如果没锁定的话确认写入的值和预期一致。注意回读时也要按顺序读。第三步查字节序。SAES 的 FIFO 是按 32 位字操作的但数据是按字节给的。HAL 库的SAES_DATATYPE_8B配置会自动处理字节序但如果你直接操作寄存器就要注意大小端问题。STM32 是小端FIFO 写入时低字节在前。我见过有人按大端写入结果密文完全不对。第四步查 Nonce 和 IV。GCM 和 CCM 的 Nonce 如果设置错误Tag 肯定对不上。确认 Nonce 长度和内容都正确特别是 Nonce 长度参数写错了硬件会按错误的长度处理。第五步查数据长度。GCM 和 CCM 对数据长度有对齐要求虽然硬件支持任意字节长度但内部处理时会有填充。确认你传入的长度参数和实际数据长度一致。4.2 DMA 传输卡死与 FIFO 溢出问题用 DMA 配合 SAES 时我遇到过传输卡死的情况。现象是 DMA 传输启动后一直不完成CPU 等在那里。排查后发现几个原因。FIFO 阈值配置不当。SAES 的输入输出 FIFO 有阈值中断如果 DMA 请求的触发阈值和 FIFO 实际水位不匹配DMA 会一直等不到请求。我通常把输入 FIFO 阈值设成半满输出 FIFO 阈值设成非空这样 DMA 能及时搬运。DMA 通道优先级冲突。如果 SAES 的 DMA 通道和其他高优先级外设共用可能被抢占导致传输延迟。建议给 SAES 的 DMA 通道设成高优先级。数据长度不是 4 字节对齐。DMA 按字传输如果数据长度不是 4 的倍数最后一次传输会不完整。解决办法是手动补齐到 4 字节倍数或者在 DMA 完成后用 CPU 处理剩余字节。还有一个隐蔽的问题SAES 的 DMA 请求在加密完成后不会自动停止。如果加密数据量小于 DMA 配置的传输量DMA 会继续搬运无效数据导致 FIFO 溢出。我通常把 DMA 传输量设成和加密数据量一致加密完成后立即停止 DMA。4.3 安全域访问触发硬件错误的处理在 TrustZone 环境下非安全代码访问 SAES 会触发 SecureFault。这个错误如果不处理系统会进入 HardFault 死循环。我建议在 SecureFault 处理函数里加上日志输出记录触发错误的地址和访问类型方便定位。常见的触发原因有两个一是非安全代码直接调用了 SAES 的 HAL 函数二是安全代码在切换到非安全状态后没有正确保护 SAES 寄存器。解决办法是把所有 SAES 操作都封装在安全世界的服务里非安全世界通过安全调用Secure Call来请求加密服务。STM32 提供了 Secure Call 机制非安全代码可以通过SECURE_CALL指令跳转到安全世界的入口函数。我在项目里定义了几个安全服务加密、解密、密钥更新非安全世界只能调用这些服务不能直接碰 SAES。4.4 常见问题速查表现象可能原因排查方法解决方案加密结果错误密钥写入顺序错回读密钥寄存器按 KEYR0~KEYR7 顺序写Tag 校验失败Nonce 重复或错误检查 Nonce 生成逻辑用设备ID计数器构造 NonceDMA 传输卡死FIFO 阈值不匹配查 DMA 和 FIFO 配置调整阈值设高优先级SecureFault非安全访问 SAES查错误地址封装安全服务用 Secure Call加密速度慢时钟分频过大查 RCC 配置提高 SAES 时钟数据长度不对未按 4 字节对齐检查数据长度补齐到 4 字节倍数5. 性能实测与优化经验5.1 不同模式下的吞吐量实测数据我在 L562 上跑了一组实测数据系统时钟 110MHzSAES 时钟同频数据量 4KB用 DMA 传输。结果如下。模式加密耗时吞吐量CPU 占用ECB38 us105 MB/s1%CBC42 us95 MB/s1%CTR36 us111 MB/s1%GCM52 us77 MB/s2%CCM68 us59 MB/s2%GCM 比 CCM 快大约 30%这个差距在数据量大时更明显。ECB 和 CTR 最快但不带认证。如果你的场景不需要认证CTR 是性能最优解。如果需要认证且协议允许优先选 GCM。5.2 降低功耗的配置技巧在电池供电的物联网终端上SAES 的功耗也需要考虑。我做了几组对比测试。按需使能时钟。SAES 不工作时关掉时钟能省不少电。用__HAL_RCC_SAES_CLK_DISABLE()关闭需要时再开。但注意关闭时钟后密钥寄存器会丢失下次使用要重新写密钥。用中断代替轮询。轮询方式 CPU 一直在跑功耗高。用中断方式加密期间 CPU 可以进低功耗模式加密完成中断唤醒。批量处理。把小包数据攒成大包一次性加密减少启动次数。SAES 每次启动都有固定开销批量处理能摊薄这个开销。实测下来按需使能时钟加中断方式加密 1KB 数据的平均功耗比轮询方式低 40% 左右。5.3 与软件 AES 库的性能对比为了有个直观参照我在同一颗片子上跑了软件 AES 库mbedTLS做对比。软件 AES-128-GCM 加密 4KB 数据耗时约 1.2ms吞吐量约 3.3 MB/sCPU 占用 100%。SAES 硬件 GCM 是 52us吞吐量 77 MB/s快了 23 倍CPU 占用不到 2%。这个差距在低主频芯片上更夸张。在 32MHz 的 L0 系列上软件 AES 跑 1KB 数据要 800us 以上而带 AES 硬件的型号只要 30us 左右。所以只要你的项目对加密性能有要求硬件加密协处理器是必选项不是可选项。6. 工程落地建议与扩展方向6.1 密钥生命周期管理的工程实践SAES 只是加密引擎密钥怎么管是整个安全体系的事。我在项目里通常按这个流程做密钥生命周期管理。密钥生成在安全产线环境里用真随机数生成器生成根密钥写入设备的安全存储区。STM32 的 RNG 外设可以作为随机源但产线环境建议用更高级的密钥注入设备。密钥派生每台设备用根密钥派生出会话密钥派生算法用 HKDF 或类似的标准 KDF。这样即使某台设备的会话密钥泄露也不会影响其他设备。密钥更新支持远程密钥更新新密钥用旧密钥加密传输设备解密后写入 SAES。更新过程要有回滚保护防止更新失败导致设备变砖。密钥销毁设备退役或检测到入侵时立即清除安全存储区的密钥。SAES 的密钥寄存器在复位后会清除但安全存储区的密钥需要主动擦除。6.2 安全启动与 SAES 的配合安全启动和 SAES 是天然搭配。安全启动的核心是验证固件签名签名验证用非对称算法如 ECDSA但固件本身的加密可以用 SAES 的 AES 模式。我的做法是固件在发布时用 AES-256-CBC 加密同时用私钥签名。设备启动时Bootloader 先用公钥验证签名验证通过后用 SAES 解密固件再跳转执行。这样即使固件在 Flash 里被读取没有密钥也解不开。STM32 的部分型号支持硬件安全启动配合 SAES 能实现完整的信任链。具体配置要看芯片的安全手册不同系列差异较大。6.3 后续可扩展的安全功能SAES 之外STM32 还有几个安全外设可以配合使用。PKA公钥加速器可以做 RSA、ECC 运算和 SAES 的对称加密互补。HASH外设可以做 SHA-256用于完整性校验。RNG提供真随机数用于生成密钥和 Nonce。TAMP外设检测入侵事件触发密钥销毁。把这些外设组合起来可以构建一个完整的安全子系统RNG 生成密钥SAES 做对称加密PKA 做签名验证HASH 做完整性校验TAMP 做物理防护。这套组合在工业网关、金融终端、车联网设备上都有实际应用。我在实际项目里的体会是SAES 本身不难用难的是整个安全体系的设计。密钥怎么存、怎么更新、怎么销毁这些问题的答案比 SAES 的寄存器配置重要得多。建议在项目初期就把安全架构想清楚不要等到产品快发布了才补安全功能那时候改造成本会高很多。最后分享一个小技巧调试 SAES 时先用已知的测试向量验证。NIST 的 AES-GCM 测试向量是公开的用标准向量跑一遍确认硬件配置和代码逻辑都对再去加密实际业务数据。这样能把问题范围缩小到配置层面而不是在业务数据里大海捞针。