RPMC 串行闪存防回滚与重放:JESD260 命令及固件实现
2026/9/17 4:54:12 网站建设 项目流程

简介:这份JEDEC JESD260标准文档面向存储安全工程师、固件与驱动开发者以及芯片验证人员,聚焦串行闪存设备中重放保护单调计数器(RPMC)的设计与实现问题,用于理解如何借助密码学手段防止计数器被重放攻击篡改。正文涵盖RPMC的适用范围与术语定义、计数器与随机数生成器的结构设计、身份验证码的生成与更新流程、与主机端的命令交互,以及相应的技术要求和测试验证方法,并给出固件集成与安全评估时可参照的规范依据。资源共1个PDF文件,为完整英文电子版,全27页,压缩包约508KB,属标准原文类资料,适合直接查阅条款与图表。目前已有338人学习下载,便于工程师在flash存储卡、固态驱动器等场景中对照规范落地实现、排查认证失败问题。

1. RPMC 到底防什么:从串行闪存的硬件攻击面说起

PC 主板上的串行闪存装的是 BIOS 代码,同时还得给平台上若干颗负责安全和电源管理的 MCU 提供持久存储。这类芯片过去靠主板上的串行闪存控制器做读/写保护,粒度到扇区或子扇区——某块只读,某块运行时可写。但这套机制有个默认前提:攻击者摸不到板子。

一旦物理访问成立,局面就变了。攻击者可以把芯片吹下来重新烧录再焊回去,可以用逻辑分析仪在总线上抓一段合法事务、换个时间点原样重放,还可以串一块 FPGA 在中间做在线改写,读写看起来都正常。这三种手法的共同点是:它们操作的都是"内容",而内容本身没有自证能力。

真正需要保住的其实不是某个字节,而是"这个值只增不减"这个语义。云侧把平台上的持久内容当授权凭证,平台侧把计数器当许可证锚点,两边的信任链都压在"值不能回退"上。JESD260 定的就是把这层语义下沉进 flash 硅片的命令接口:一段读不出来的根密钥、一组 HMAC-SHA-256 签名的命令、一个只能加不能减的 32 位计数器。做 BIOS/UEFI 的、做 flash 控制器 IP 的、以及需要在嵌入式设备上做防回滚计数的固件工程师,是这个标准的主要读者。

2. RPMC 的威胁模型与两级密钥体系

2.1 三类硬件攻击与 RPMC 的防护边界

先划清楚 RPMC 能做什么、不能做什么,否则很容易在方案评审时被问住。

  • 拆焊重编程/换片:攻击者把 flash 拆下,用编程器直接改内容再装回。RPMC 的应对是让关键状态(计数器值、根密钥)不以明文形式落在可编程的普通扇区里,并且计数器递增必须携带合法 HMAC 签名。
  • 总线抓包重放:抓一段"递增计数器"的合法事务,在别的时间点重放。RPMC 用 32 位单调计数器本身来识别——重放的指令对应的 CounterData 与器件内部当前值不一致时会被拒绝。
  • FPGA 中间人:在控制器与 flash 之间插入逻辑做在线篡改。这类攻击者能改数据流,但改不出签名,因为没有根密钥。

边界也要说清楚:RPMC 防的是"回退"和"重放",不是侧信道,也不是物理破坏性攻击。探测功耗、故障注入这类手段,标准里没有覆盖。评审时主动承认这个边界,比事后被追问要好。

2.2 Root Key 寄存器:一次写入、终生不可读出

根密钥是整条信任链的起点,256 位,存在 flash 内部,从外部读不出来,包括测试模式。标准明确规定:非0FF..FFH的根密钥只能写一次,时机是系统制造阶段。

它跟一个 32 位单调计数器绑定。只要一次合法的 256 位写根密钥操作成功,关联计数器就清零——不管写进去的值是不是0FF..FFH。这个细节容易踩:产线上如果误发了一次写根密钥命令,计数器会被悄悄清零,事后再想恢复原值已经来不及。

还有个只在制造测试模式下才开放的口子:根密钥寄存器可以被擦除,擦除过程不能读出其值。擦完之后寄存器可以重编程,方便做全片测试。擦除时关联计数器是被初始化还是保留原值,标准交给厂商自己定——换句话说,换一家 flash 供应商,产线脚本可能就要改。

2.3 HMAC Key 的动态派生:为什么一次命令做两次 HMAC

HMAC Key 同样存在 flash 内部,同样读不出来,包括测试模式。它和根密钥的关键区别在于:它不是烧一次就完事,而是每次上电由控制器下发命令重建

派生过程是标准的 HMAC-SHA-256。伪代码大致如下:

import hmac, hashlib def derive_hmac_key(root_key: bytes, key_data: bytes) -> bytes: # root_key: 32 字节,烧录后永久驻留在 flash 内部,固件侧拿不到 # key_data: 4 字节,随 Update HMAC Key 命令的 payload 一起下发 # 返回值 32 字节,即为 HMAC Key Register 的新值 return hmac.new(root_key, key_data, hashlib.sha256).digest() # 控制器侧验签时用同一把派生出的 key def verify_response(hmac_key: bytes, msg: bytes, sig: bytes) -> bool: expect = hmac.new(hmac_key, msg, hashlib.sha256).digest() return hmac.compare_digest(expect, sig)

参数含义:root_key是器件内部值,固件侧永远拿不到,所以派生只能在 flash 内部完成;key_data是控制器提供的 32 位扰动数据,每次上电可以不同,这样即使某次上电过程被完整抓包,下次上电的 HMAC Key 也是新值,抓到的签名无法跨上电周期复用——这是标准里用 4 字节 KeyData 而不是固定常量的用意。

compare_digest而不是==是常规做法,避免比较耗时随匹配前缀长度变化。虽然这条路径通常在硬件里做,写参考模型时仍然值得保留。

值得注意的是,Update HMAC Key 这条命令本身也要验签,所以它内部会做两次 HMAC-SHA-256:一次用于派生新 key,一次用于验证命令携带的 256 位签名。实测时如果用软件模型模拟,一次命令的 CPU 开销是两倍,别按单次 HMAC 去估性能预算。

2.4 资源下限:至少四组计数器

标准要求器件最少支持 4 个计数器,每组配有独立的根密钥寄存器和 HMAC Key 寄存器。实际设计里,这 4 组资源通常对应 4 个不同的使用方:BIOS 度量、电源管理固件的版本回滚保护、许可证计数、以及一条留给 OEM 自定义用途。固件在划分时最好把 CounterAddr 的使用规则写进接口文档,否则多个模块共用一个计数器,任何一方递增都会让其他人的基线失效。

3. OP1/OP2 命令帧逐字节拆解与 SPI 时序

3.1 无地址相位:为什么 CmdType 紧跟在 OP1 后面

常规 SPI flash 命令的骨架是「opcode + 地址相位 + 数据相位」。RPMC 的命令不走这套:OP1 之后没有地址相位,第一个字节直接就是 CmdType。

原因不难理解——计数器寻址用的是 payload 里的CounterAddr单字节,跟 flash 的线性地址空间完全无关。如果强行套用 24 位或 32 位地址相位,只会白占三个字节的时钟,还会让控制器状态机多一层解析分支。省掉之后,控制器可以在发完 opcode 的同一段 CS 拉低周期里连续推 payload,硬件实现更干净。

OP2 是读侧命令,走另一条路径:opcode 之后是 8 个 dummy clock,然后是 ExtendedStatus[7:0]、Tag[95:0]、CounterData[31:0]、Signature[255:0]。8 个 dummy bit 的延时与 Fast Read 命令一致,控制器侧可以复用同一套采样时序参数。另外,OP1 和 OP2只定义在 1-0-1 模式下,四线或双线模式下这套命令不成立,切换模式的固件要特别注意别在 Quad 使能状态下误发。

3.2 五种 CmdType 的 payload 布局对照

把所有命令的字节偏移放在一张表里,写驱动时对着改最省事。表里所有多字节字段都是 MSB 在前。

CmdType命令Byte1Byte2后续载荷签名
00HWrite Root Key RegisterCmdTypeCounterAddrReserved[7:0] @B3、RootKey[255:0] @B4–B35TruncatedSign[223:0] @B36–B63
01HUpdate HMAC Key RegisterCmdTypeCounterAddrReserved[7:0] @B3、KeyData[31:0] @B4–B7Signature[255:0] @B8–B39
02HIncrement Monotonic CounterCmdTypeCounterAddrReserved[7:0] @B3、CounterData[31:0] @B4–B7Signature[255:0] @B8–B39
03HRequest Monotonic CounterCmdTypeCounterAddrReserved[7:0] @B3、Tag[95:0] @B4–B15Signature[255:0] @B16–B47
04H–FFHReserved保留,不可使用

OP2 侧(Read Data)的布局是:Byte2 为 ExtendedStatus[7:0],Byte3–B14 为 Tag[95:0],Byte15–B18 为 CounterData[31:0],Byte19–B50 为 Signature[255:0]。

payload 上限是 512 位,也就是 64 字节。Write Root Key 正好把这个额度用满:1 字节 CmdType + 1 字节 CounterAddr + 1 字节 Reserved + 32 字节 RootKey = 35 字节,剩下的 29 字节里塞 28 字节签名,留 1 字节余量。这就是TruncatedSign 只有 224 位而不是 256 位的原因——纯粹是帧长限制下的工程折衷,不是密码学上的设计选择。写验签逻辑时,要按"取完整 HMAC-SHA-256 结果的高 224 位再比较",而不是拿 224 位去截断密钥。

3.3 字节序、位序与构造报文的参考实现

规则很直白:多字节字段 MSB 先出,字节内 MSB 先出。CmdType 永远是 OP1 之后的第一个字节。下面用 C 拼一个 Write Root Key 的 payload,方便对照偏移:

#include <stdint.h> #include <string.h> #define RPMC_PAYLOAD_MAX 64 /* 拼装 Write Root Key Register (CmdType = 00H) 的 payload。 * 注意:本函数只负责组包,RootKey 由安全环境提供, * 截断签名由内部 HMAC 引擎算好后回填。 */ int build_write_root_key(uint8_t counter_addr, const uint8_t root_key[32], const uint8_t trunc_sig[28], uint8_t out[RPMC_PAYLOAD_MAX]) { memset(out, 0, RPMC_PAYLOAD_MAX); out[0] = 0x00; /* B1: CmdType = 00H */ out[1] = counter_addr; /* B2: CounterAddr, 目标计数器编号 */ out[2] = 0x00; /* B3: Reserved, 固定填 0 */ /* B4..B35: RootKey[255:0],MSB 在前 */ memcpy(&out[3], root_key, 32); /* B36..B63: TruncatedSign[223:0],完整 HMAC 结果的高 224 位 */ memcpy(&out[35], trunc_sig, 28); return 64; /* 返回实际发送字节数 */ }

逻辑说明:函数先把 64 字节缓冲区清零,避免栈上残留数据被当成有效载荷发出去。偏移out[1]是 CounterAddr,标准里是单字节,所以一个器件最多 256 个计数器地址空间,但资源下限只要求 4 个,实现时通常只解码低几位、其余地址报"越界"。

参数说明:counter_addr选目标计数器组;root_key32 字节由产线工具或 HSM 提供,不能从器件读回;trunc_sig28 字节,必须是 HMAC-SHA-256 全量输出的前 28 字节,少一位对不上。out[2]的 Reserved 必须写 0,某些器件会把它当长度校验的一部分,填别的值直接触发 payload 长度错误位。

4. 五条命令的固件实现与扩展状态寄存器排错

4.1 上电初始化:先 Update HMAC Key,再谈其他

上电顺序不能乱。器件刚上电时 HMAC Key Register 是未初始化状态,此时任何需要验签的命令都会被打回。所以流程固定为:

  1. 控制器拉低 CS,发 OP2 读 ExtendedStatus,确认读到00000000(Power On State)。
  2. 对每个需要使用的计数器组,发 Update HMAC Key Register(CmdType=01H),KeyData 每次上电可以换新。
  3. 收到 ExtendedStatus[7]=1,说明 OP1 成功完成,HMAC Key Register 已就绪。
  4. 之后才能发 Increment / Request。

第 2 步里,控制器本地也要同步派生一份 HMAC Key 用于后续验签。如果控制器把 KeyData 交给安全引擎、由引擎返回派生结果,注意这条路径的时延可能比 SPI 事务本身还长,别把它放在对实时性敏感的中断上下文里。

4.2 Increment 与 Request:签名覆盖哪些字段

Increment(02H)的 payload 里,控制器提供 32 位 CounterData 和 256 位签名。器件侧做两件事:验签,以及比对 CounterData 与内部当前值。CounterData 不匹配会置起 ExtendedStatus[4],对应标准里的 Counter Data Mismatch。这解释了为什么递增必须"读—算—写"三步走:先 Request 拿当前值,本地加一后签名,再 Increment。

Request(03H)带 96 位 Tag,器件在 OP2 响应里把这 96 位原样回显,配合响应里的 CounterData 和签名一起验。Tag 的作用是把请求和响应绑定起来——如果控制器同时挂多个计数器组、或者有并发事务,靠 Tag 区分响应归属比靠时序更稳。常见做法是用单调递增的事务序号填 Tag,回显不一致直接丢弃。

4.3 扩展状态寄存器:位域定义与错误组合

排错基本靠这张表。扩展状态是 8 位,定义如下:

置 1 条件适用 CmdType含义
[7]=100–03OP1 无错误完成
[6:5]保留标准表中未展开
[4]=102Counter Data Mismatch,且 payload 长度正确
[3]=102, 03HMAC Key Register 或计数器未初始化,且 payload 长度正确
[2]=100–03签名不匹配 / 计数器地址越界 / CmdType 越界 / payload 长度错误
[1]=100, 0100 时:根密钥覆盖、地址越界或截断签名不匹配;01 时:对应计数器未初始化
[0]=100–0FF器件正忙,仅当 SFDP 的 Busy_Polling_Method 位为 0 时有效

00000000是上电初始态,此时直接发 OP2 就能读到。

实战里最容易搞混的是 [1] 和 [2]:两者都可能在"签名对不上"时置位,区别在于 [2] 覆盖的是通用验签失败和长度/类型错误,[1] 在 CmdType=00H 时专门指向根密钥覆盖和截断签名不匹配。调驱动时如果只盯 [2],一次误发的重复写根密钥操作会被误判成"签名算法写错了",方向就跑偏了。

Request 和 Increment 还有个共性:正确收到 payload 长度之后才会去置 [3]。如果上位机把 payload 少发了几个字节,器件先置的是 [2],这时候去看 [3] 会一头雾水。

4.4 Busy 轮询与 SFDP 的 Busy_Polling_Method

器件执行 OP1 期间是忙的。忙状态怎么表示,取决于 SFDP 表里的Busy_Polling_Method

  • 该位为 0:控制器需要轮询 ExtendedStatus[0],置 1 表示忙,执行完毕自动清 0。
  • 该位为 1:ExtendedStatus[0] 被忽略,控制器走另一套忙判断机制(通常是状态寄存器)。

固件启动阶段应该先读 SFDP 把这一位取出来,再决定轮询策略。写死一种方式的产品,换颗 flash 就可能卡在轮询循环里出不来。轮询超时值也别照抄别的项目——先测一遍最长命令(Write Root Key,64 字节 payload)的耗时,再留 3 倍余量。

5. 产线烧录与验证:几个容易翻车的地方

5.1 写根密钥的顺序必须和不可逆性对齐

写根密钥是整条链上唯一不可逆的动作,产线脚本要把它当成"最后一步"来设计。合理的顺序是:烧普通固件 → 跑功能自检 → 确认目标 CounterAddr → 写根密钥 → 立即 Request 一次读回计数器确认已清零 → 打标记录。

关键在于根密钥写下去之后计数器必然清零,如果产线在写根密钥之前就已经把某些状态种进了计数器,那些状态全没了。我一般会在写之前先 Request 一次,把返回值记进产线数据库,写之后再 Request 一次一并落库,两个值都能对上才算这颗芯片合格。

5.2 根密钥擦除只在制造测试模式下做

标准允许在制造测试模式下擦除根密钥寄存器以便重编程,但擦除时关联计数器是初始化还是保留,由厂商决定——这一条在选型阶段就要问清楚,不然同一套产线脚本换料就会行为不一致。擦除过程不能读出根密钥值,所以不要指望"读出来备份再写回去"这种流程。

另外,用测试模式擦除不等于能给已经出厂的产品做返修。测试模式的口子在量产件上通常是熔断关闭的。

5.3 用错误注入验证状态机

状态位表只是文档,真正确认驱动写对了,得靠注入错误。三种注入性价比最高:

  • 改 Tag:在 Request 的 96 位 Tag 里翻一位,验响应回显是否被正确比对、是否被丢弃。
  • 改签名:把 Increment 的 256 位签名最后一字节翻转,确认 [2] 置位且命令未生效——重点是"未生效",光看状态位不够,要读回计数器确认值没变。
  • 改 payload 长度:把 Increment 的 payload 从 40 字节改成 39 字节再发,确认器件置的是 [2] 而不是 [3],用来验证长度校验在签名校验之前。

最后加一条时序验证:在 OP1 执行期间主动读 ExtendedStatus,确认 [0] 位的行为与 SFDP 里读到的Busy_Polling_Method一致。这一步能把"控制器和器件对忙定义理解不同"这类问题在上线前就摁住。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询