Zynq 实战 19|安全启动:FSBL 加密 + RSA 签名 + eFuse 编程
Zynq 实战 19|安全启动:FSBL 加密 + RSA 签名 + eFuse 编程
这是《Zynq FPGA 嵌入式系统设计实战》系列的第 19 篇。 板子:Pynq-Z2(XC7Z020)。工具链:Vivado / Vitis / PetaLinux 2023.2。 上一篇:《Zynq 实战 18|OpenAMP 多核:Linux + 裸机同时跑》
0. 这一篇要解决什么问题
前 18 篇我们把 Zynq 的功能做全了——PL 硬件加速、Linux 驱动、以太网通信、双核 AMP。现在问题来了:任何人拿到你的设备,插一张 SD 卡就能启动自己的固件,或者用 JTAG 把 DDR 里的内容 dump 出来。
本篇要做的事:
- 弄清楚 Zynq-7000 的 硬件信任根(HWROT) 是什么、能防什么
- 用 Bootgen 生成 AES-256 密钥和 RSA-2048 公私钥对
- 写
.bif文件,对 FSBL / bitstream / U-Boot 做加密 + RSA 签名 - 把 PPK hash 烧进 eFuse,锁定唯一信任根(不可逆,本篇会反复提醒)
- 验证完整 secure boot 链:BootROM → FSBL → U-Boot → kernel
- 调试踩坑:bbram 暂存测试、hash 算错救砖、JTAG 锁定
本篇不覆盖:TrustZone 隔离、Secure OS(OP-TEE)、固件更新(FWU)——那些是另外的体系。
🚧 高优先级警告:eFuse 是一次性熔断的,烧错了不可恢复。本篇所有 eFuse 操作步骤都有”先用 bbram 测试”的前置步骤,请认真对待。
1. 威胁模型:能防什么、不能防什么
在投入精力之前,先想清楚保护目标:
| 威胁 | Zynq Secure Boot 能防 | 备注 |
|---|---|---|
| 插 SD 卡启动第三方固件 | ✅ 签名验证失败,BootROM 拒绝启动 | 前提:eFuse 已烧 PPK hash |
| JTAG dump 固件(加密后) | ✅ 固件密文,无密钥无法解密 | 需要同时 disable JTAG(eFuse) |
| 运行时 JTAG 调试(生产) | ✅ 可通过 eFuse 禁用 JTAG | 开发阶段不要禁 |
| 固件篡改(中间人修改 flash) | ✅ RSA 签名检测到篡改,拒绝启动 | |
| 物理芯片解封取密钥 | ❌ 硬件攻击,eFuse 存储可被侧信道攻击 | 超出 Zynq 防护范围 |
| 运行时软件漏洞(提权) | ❌ Secure Boot 只管启动链,不管运行时 | 需要 SELinux / TrustZone |
| 供应链攻击(出厂前植入) | ❌ 需要安全生产流程,非芯片功能 |
结论:Zynq Secure Boot 的价值在于 “离线设备防物理攻击 + 防固件篡改”,不是全栈安全方案。
2. Zynq-7000 安全机制架构
3. 准备工作:生成密钥
3.1 工具链要求
本篇使用 Vitis 2023.2 自带的 bootgen 工具,无需额外安装:
# 确认 bootgen 可用
which bootgen
# 期望:/opt/Xilinx/Vitis/2023.2/bin/bootgen
bootgen -help | head -5
# Bootgen 2023.2
同时需要 OpenSSL(生成 RSA 密钥):
openssl version
# OpenSSL 3.0.x (Ubuntu 22.04 自带)
3.2 生成 RSA-2048 密钥对(PPK / SPK)
Zynq Secure Boot 使用两级 RSA 密钥体系:
- PPK(Primary Public Key):hash 烧进 eFuse,是信任根
- SPK(Secondary Public Key):用 PPK 签名,实际签 image 用的是 SPK
mkdir -p ~/secure_boot_keys && cd ~/secure_boot_keys
# 生成 PPK(Primary Public Key)私钥,4096 位,实际上 Zynq 用 2048/4096 均可
# Zynq-7000 支持 RSA-2048(Vivado 2023.2 推荐 RSA-4096 用于 UltraScale+,Zynq-7000 用 2048)
openssl genrsa -out ppk.pem 2048
openssl rsa -in ppk.pem -pubout -out ppk_pub.pem
# 生成 SPK 私钥
openssl genrsa -out spk.pem 2048
openssl rsa -in spk.pem -pubout -out spk_pub.pem
# 生成 AES-256 密钥(用于加密)
bootgen -generate_keys aes -arch zynq -o ./aes_key.nky
# aes_key.nky 文件格式:
# Device xc7z020
# Key 0 <32字节十六进制>
# IV 0 <12字节十六进制>
ls -la
# ppk.pem ppk_pub.pem spk.pem spk_pub.pem aes_key.nky
🚧 避坑:
ppk.pem(私钥)绝对不能放进 git 仓库或上传到任何地方。一旦泄露,攻击者可以用它签名任意固件并绕过你的 secure boot。建议存入 HSM 或加密后放到离线介质。开发阶段用的测试密钥和生产密钥必须分开——开发机上的密钥不能是生产密钥。
4. .bif 文件:加密 + 签名配置
.bif(Boot Image Format)文件是告诉 bootgen 如何打包和保护每个组件的配置文件。
4.1 仅签名(不加密,适合开发验证)
/* sign_only.bif — 仅 RSA 签名,不加密,用于开发验证 */
the_ROM_image:
{
/* BootROM header,包含 PPK 公钥 */
[ppkfile] /path/to/ppk_pub.pem
/* SPK(Secondary Public Key),由 PPK 签名 */
[spkfile] /path/to/spk_pub.pem
/* FSBL:RSA 签名 */
[bootloader, authentication=rsa]
/path/to/fsbl.elf
/* PL bitstream:RSA 签名 */
[destination_device=pl, authentication=rsa]
/path/to/design_1_wrapper.bit
/* U-Boot:RSA 签名 */
[destination_cpu=a9-0, authentication=rsa]
/path/to/u-boot.elf
}
生成命令:
bootgen -image sign_only.bif -arch zynq -o BOOT_signed.bin -w on
# 查看生成的 bin 里包含的组件
bootgen -image sign_only.bif -arch zynq -o BOOT_signed.bin -w on -log debug 2>&1 | \
grep -E "(ppk|spk|rsa|aes|Adding)"
4.2 加密 + 签名(生产用)
/* encrypt_sign.bif — AES-256 加密 + RSA 签名,生产级配置 */
the_ROM_image:
{
/* 密钥源:bbram(开发测试)或 efuse(生产) */
[keysrc_encryption] bbram_red_key /* 生产改为:efuse_red_key */
/* PPK / SPK */
[ppkfile] /path/to/ppk_pub.pem
[spkfile] /path/to/spk_pub.pem
/* AES 密钥文件 */
[aeskeyfile] /path/to/aes_key.nky
/* FSBL:AES 加密 + RSA 签名 */
[bootloader,
encryption=aes,
authentication=rsa]
/path/to/fsbl.elf
/* PL bitstream:AES 加密 + RSA 签名 */
[destination_device=pl,
encryption=aes,
authentication=rsa]
/path/to/design_1_wrapper.bit
/* U-Boot:AES 加密 + RSA 签名 */
[destination_cpu=a9-0,
encryption=aes,
authentication=rsa]
/path/to/u-boot.elf
}
# 生成加密 + 签名的 BOOT.bin
bootgen -image encrypt_sign.bif -arch zynq -o BOOT_secure.bin -w on
# 文件大小参考(含签名和加密开销)
ls -lh BOOT_secure.bin
# -rw-r--r-- 1 user user 7.2M Apr 28 08:00 BOOT_secure.bin
# (相比未加密 BOOT.bin 增加约 15%,主要是 RSA 签名数据)
🚧 避坑:
bootgen生成.bif时会把 PPK 公钥嵌入 BOOT.bin 头部——这是设计如此,BootROM 需要读取它来验签。但这同时意味着 BOOT.bin 里的 PPK 公钥是明文可读的,任何人都能提取出来(用binutils)。不要把安全假设建立在”攻击者不知道公钥”上——RSA 的安全性来自私钥保密,公钥可以公开。
4.3 关键的 bootgen 输出验证
# 检查 PPK hash(这个值将烧进 eFuse)
bootgen -image encrypt_sign.bif -arch zynq -efusefile ppk_hash.txt -w on
cat ppk_hash.txt
# 输出格式:
# ppk0_sha3_384 = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# (96 个十六进制字符 = 384 bit)
把这个 hash 值记录下来并备份三份——后面烧 eFuse 用的就是它。
5. 第一阶段测试:用 BBRAM 验证流程
eFuse 烧了不能擦,所以必须先用 BBRAM(Battery-Backed RAM) 做全量测试,验证整个 secure boot 链正常后,再考虑 eFuse。
BBRAM 是一块由纽扣电池供电的 256-bit SRAM,可以随时改写,断电/电池耗尽后密钥消失。
5.1 通过 JTAG 把 AES 密钥写入 BBRAM
# program_bbram.tcl — 用 Vitis XSCT 或 vivado -mode tcl 执行
# 把 aes_key.nky 里的密钥写进 BBRAM
# 连接目标(确保 JTAG 已连接)
connect
target 1
# 读取 .nky 里的 AES 密钥(32字节十六进制字符串)
set aes_key "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
# ↑ 替换为 aes_key.nky 里实际的 Key 0 值
# 写入 BBRAM
jtag targets
device program {BOOT_secure.bin}
# 使用 program_flash 或专用 BBRAM 编程命令
# 在 Vitis 2023.2 里用 Xilinx BBRAM 编程工具:
# Xilinx → Program Device → Program BBRAM
更简洁的方式是用 Vivado TCL:
# 在 Vivado TCL Console 里执行
open_hw_manager
connect_hw_server
open_hw_target
# 写 BBRAM(需要 Vivado 2023.2+)
set_property PROGRAM.AES_KEY_FILE {/path/to/aes_key.nky} [get_hw_devices xc7z020_1]
program_hw_bbram [get_hw_devices xc7z020_1]
5.2 SD 卡启动测试
把 BOOT_secure.bin 拷到 SD 卡根目录,板子 Boot Mode 拨码开关设为 SD(01):
# 开机串口输出(成功情况)
Xilinx First Stage Boot Loader
Release 2023.2 Dec 7 2023
Bootmode: SD_MODE
...
Authentication Enabled
Encryption Enabled
Authentication of Partition 0 succeeded
Decryption of Partition 0 succeeded
Loading bitstream...
Authentication of Partition 1 succeeded
Decryption of Partition 1 succeeded
...
U-Boot 2023.01 (Apr 28 2026)
Zynq>
如果出现 Authentication Failed:
# 常见原因 1:.bif 里的密钥路径写错,公钥不一致
# 验证:重新运行 bootgen,检查输出里 ppk hash 是否和 bbram 里一致
# 常见原因 2:keysrc_encryption 设置和实际密钥存储不一致
# 验证:bbram_red_key 对应 BBRAM,efuse_red_key 对应 eFuse AES key
# 常见原因 3:FSBL 没有开启 secure boot 支持
# 解决:在 Vitis BSP 配置里确认 FSBL 的 XFSBL_RSA 和 XFSBL_AES 宏已定义
6. 烧写 eFuse:把 PPK Hash 锁定为硬件信任根
再次确认:eFuse 烧写不可逆,BBRAM 测试通过后再继续。
6.1 生成 program_efuse.tcl
# program_efuse.tcl — 通过 JTAG 烧 PPK hash 到 eFuse
# 在 Vivado 2023.2 TCL Console 里执行
# 前提:JTAG 已连接,板子上电
open_hw_manager
connect_hw_server -host localhost -port 3121
open_hw_target
# 选择设备
set device [lindex [get_hw_devices] 0]
current_hw_device $device
refresh_hw_device $device
# ── 检查当前 eFuse 状态(只读操作,安全) ──
set fuse_obj [get_hw_cfgmem -of_objects $device]
# 读取 PPK0 hash fuse(如果已烧,值非 0)
read_hw_cfgmem -force $fuse_obj
# ── 写入 PPK hash(注意:这步不可逆) ──
# ppk_hash 来自 bootgen -efusefile 输出
set ppk_hash "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ↑ 替换为实际 96 字符 SHA-384 hash
# 创建临时 .jtag 文件
set efuse_data [open /tmp/efuse_prog.jtag w]
puts $efuse_data "ppk0_sha3_384 = ${ppk_hash}"
puts $efuse_data "rsa_enable = 1"
# 注意:rsa_enable=1 和 ppk_hash 同时烧,BootROM 才会强制验签
# 如果只烧 ppk_hash 不烧 rsa_enable,secure boot 不会启用!
close $efuse_data
# 执行 eFuse 编程
program_hw_cfgmem -force \
-file /tmp/efuse_prog.jtag \
[get_hw_cfgmem -of_objects [get_hw_devices xc7z020_1]]
puts "eFuse 编程完成。请重启验证。"
🚧 避坑:
rsa_enable = 1和ppk_hash必须一起烧,缺一不可。如果只烧了 ppk_hash 但没烧rsa_enable,BootROM 会忽略 PPK hash 继续启动,secure boot 等于没开。反过来,如果只烧rsa_enable = 1但没提供有效 ppk_hash,BootROM 会直接拒绝所有启动——设备彻底砖了。两个操作要在同一次program_hw_cfgmem里完成。
6.2 烧后验证
# 拔掉 JTAG,只用 SD 卡启动
# 期望:secure boot 链正常,和 BBRAM 测试结果一致
# 用 Vivado 读回 eFuse 确认(只读)
read_hw_cfgmem -force [get_hw_cfgmem -of_objects [get_hw_devices xc7z020_1]]
# 检查 ppk0_sha3_384 字段是否等于预期 hash
7. FSBL 配置:启用安全特性
PetaLinux 2023.2 生成的 FSBL 默认不开 secure boot,需要手动加宏。
在 Vitis BSP 设置里,找到 FSBL 工程的 xfsbl_config.h:
/* xfsbl_config.h — FSBL 安全特性开关 */
/* 启用 RSA 身份验证 */
#define XFSBL_RSA 1
/* 启用 AES 加密解密 */
#define XFSBL_AES 1
/* 启用 SHA3-384(用于 PPK hash 计算) */
#define XFSBL_SHA3 1
/* 启用 PPK/SPK 选择(默认 PPK0,多密钥轮换时使用) */
#define XFSBL_RSA_KEY_ROLLOVER 0 /* 1 = 支持 PPK0/PPK1 轮换 */
/* AES 密钥来源(必须和 .bif 里 keysrc_encryption 一致) */
/* XFSBL_AES_KEY_SRC_DEV_EFUSE 或 XFSBL_AES_KEY_SRC_DEV_BBRAM */
#define XFSBL_AES_KEY_SRC XFSBL_AES_KEY_SRC_DEV_BBRAM /* 开发用 bbram */
重新编译 FSBL,更新 .bif 里的 fsbl.elf 路径,重新 bootgen。
8. 调试踩坑手册
8.1 PPK hash 算错了会怎样?
如果你把错误的 PPK hash 烧进了 eFuse(比如 .pem 文件路径写错,或者手抄时漏了字符):
- BootROM 会用 eFuse 里的 hash 去验证 BOOT.bin 里的 PPK 公钥
- 对比失败,BootROM 停止启动
- JTAG 此时还没有被禁用(只烧了 ppk_hash 和 rsa_enable,未烧 jtag_disable)
- 救砖方法:用 JTAG 连接,烧写第二个 PPK(PPK1)并启用 ppk1_en eFuse
# 救砖:启用 PPK1
# 前提:jtag 未被锁(jtag_disable eFuse 未烧)
# 1. 重新生成一对 PPK/SPK 密钥对(ppk1.pem / spk1.pem)
# 2. 用新密钥生成 BOOT_v2.bin,ppkfile 改为 ppk1_pub.pem
# 3. 烧 PPK1 hash 和 ppk1_sha_en
set efuse_data [open /tmp/efuse_ppk1.jtag w]
puts $efuse_data "ppk1_sha3_384 = <ppk1的hash>"
puts $efuse_data "ppk1_sha_en = 1"
close $efuse_data
program_hw_cfgmem -force -file /tmp/efuse_ppk1.jtag ...
8.2 JTAG 锁定之后
如果同时烧了 jtag_disable = 1,JTAG 被永久禁用,无法通过 JTAG 调试或编程。此时:
- BootROM 层面的调试通道关闭
- 唯一可以”调试”的方式是串口 UART 输出的启动日志
- 如果 secure boot 失败 + JTAG 禁用,设备是真正意义上的砖
结论:生产流程里,jtag_disable 应该是所有 eFuse 操作的最后一步,在充分测试 secure boot 通过之后才烧。
8.3 加密后 U-Boot 提示 “Authentication Failed”
# 串口输出
FSBL: Loading U-Boot...
Authentication of Partition 2 FAILED
Partition authentication failed.
FSBL: Error
排查步骤:
# 1. 确认 FSBL 里 XFSBL_RSA=1 且 XFSBL_AES=1
grep -r "XFSBL_RSA\|XFSBL_AES" <vitis-workspace>/fsbl/src/xfsbl_config.h
# 2. 确认 .bif 里 u-boot.elf 使用的 aeskeyfile 和当前 BBRAM/eFuse 里的密钥一致
# 重新 bootgen -efusefile 生成 hash,对比 eFuse 实际值
# 3. 确认 U-Boot elf 没有被重新编译(重编后内容变了,旧签名失效)
sha256sum u-boot.elf # 和生成 BOOT.bin 时用的一致?
# 4. 如果 U-Boot 被 strip 过或 objcopy 过,签名覆盖的范围可能不对
# 建议直接签 Vitis/PetaLinux 输出的原始 elf,不要 strip
🚧 避坑:每次重新编译任何组件(FSBL / U-Boot / kernel)之后,必须重新运行 bootgen 生成新的 BOOT.bin。签名是对 elf 二进制内容的数字签名,只要内容改了,旧的签名就失效了。这个看起来显而易见,但在 CI/CD 流水线里很容易漏掉”重新签名”的步骤。
9. 产品化建议
9.1 密钥管理
| 阶段 | 建议 |
|---|---|
| 开发 | 使用独立的测试密钥对(绝不使用生产密钥),BBRAM 存 AES key |
| 预生产 | 用 HSM(Hardware Security Module,如 AWS CloudHSM / Thales)托管 PPK 私钥 |
| 量产 | 生产线上通过 JTAG 编程 eFuse(自动化,每块板子独立密钥更安全) |
| 密钥轮换 | 预留 PPK1 槽位(Zynq-7000 支持 PPK0 + PPK1),PPK0 泄露后切 PPK1 |
9.2 生产线烧录流程
1. 制造商生产 PCB(此时 eFuse 全 0,JTAG 打开)
2. 功能测试(JTAG 调试,无 secure boot)
3. 烧录生产固件(BOOT_prod.bin,已加密签名)
4. 通过 JTAG + TCL 脚本烧 eFuse:
a. 烧 PPK hash
b. 烧 AES key(或 BBRAM key)
c. 烧 rsa_enable = 1
d. 验证 secure boot 正常启动
e. (可选)烧 jtag_disable = 1
5. 出厂
9.3 关键 eFuse 字段速查表
| eFuse 字段 | 作用 | 默认值 | 不可逆 |
|---|---|---|---|
ppk0_sha3_384 | PPK0 公钥 hash(384 bit) | 全 0(未启用) | ✅ |
ppk1_sha3_384 | PPK1 公钥 hash(备用) | 全 0 | ✅ |
rsa_enable | 强制 RSA 签名验证 | 0(禁用) | ✅ |
aes_efuse_key | AES-256 加密密钥 | 全 0 | ✅ |
jtag_disable | 禁用 JTAG | 0(启用) | ✅ |
user_fuse[0:7] | 用户自定义 8 字节 | 全 0 | ✅ |
10. 本篇 Checklist
- 用 bbram 完整测试加密 + 签名启动链,确认串口输出 “Authentication … succeeded”
-
bootgen -efusefile生成的 PPK hash 备份三份(不同物理位置) -
rsa_enable = 1和ppk_hash在同一次program_hw_cfgmem里烧入 -
jtag_disable是最后一步,在所有测试通过之后才考虑 - 生产密钥和开发测试密钥严格分开,不共用
- CI/CD 流水线里每次重编后都重新
bootgen生成签名镜像
11. 下一篇预告
下一篇是系列 第 20 篇(收官篇)——《Zynq 实战 20|综合实战 Capstone:音频采集 → 实时 FFT → Web 频谱仪》:
- 整合前 19 篇:PL IP + AXI DMA + Linux 驱动 + WebSocket
- 完整系统拓扑:ADAU1761 codec → I2S RX → FFT IP → DMA → 用户态 → 浏览器频谱图
- 端到端延迟 < 30 ms 实测
- 系列回顾:每一篇解决了什么,整个系列读完你得到了什么能力
参考资料
| 文档号 | 名称 | 用途 |
|---|---|---|
| UG585 | Zynq-7000 SoC TRM | 第 6 章:安全机制,eFuse 存储映射,BBRAM 接口 |
| UG1283 | Bootgen User Guide 2023.2 | .bif 文件完整语法,authentication / encryption 属性 |
| XAPP1175 | Zynq-7000 AP SoC AES Decryption | AES key 烧写流程,keysrc_encryption 选项详解 |
| UG1099 | Zynq-7000 SoC Device Configuration | eFuse 编程 TCL 脚本参考,program_efuse 用法 |
| PG150 | AXI EthernetLite | 与安全启动集成时的注意事项(网络更新场景) |
| Xilinx Wiki | Secure Boot Zynq | 实战配置示例,常见错误码 |
这是《Zynq FPGA 嵌入式系统设计实战》系列第 19 篇。 如果你在 eFuse 烧录、PPK hash 计算或 FSBL 认证失败上踩了坑,欢迎留言——这类问题通常非常具体,评论区可能帮到后来人。