结论:panda 的启动真实性由 bootstub 用「长度门 + SHA-1 + 版本门 + 1024 位 RSA 校验」完成,这是一道启动镜像真实性检查,而不是给运行期 USB/SPI 控制请求做逐请求鉴权。更关键的是:主机端「不许擦 sector 7」只是高层约束,设备端 flash_erase_sector() 实际上接受 sector 7;长度门允许接近 16 MiB,而该 H7 链接脚本声明的 flash 只有 1 MiB。以下基于源码快照 c8fb906 的静态阅读与宿主隔离探针,未运行刷写工具、未做实板测试。
构建制品与签名字节契约
SConscript 分别构建 bootstub 与 app,app 入口放在 0x08020000,随后以 SETLEN=1 调用 sign.py。签名脚本在原始 app 尾部追加 VERS 与版本 2,把首 4 字节重写为「签名覆盖区长度」,再以 SHA-1 对首 4 字节之后的内容计算摘要,最后附加 128 字节 RSA 签名。普通构建使用 debug 私钥并启用 ALLOW_DEBUG;RELEASE 构建需提供证书/密钥路径。生成的 cert.h 含 debug/release 公钥,但构建期未见核对 CERT 私钥是否与 release.pub 配对。
| 环节 | 字节/常量 | 说明 |
|---|---|---|
| 签名长度 | RSANUMBYTES=128 |
公钥模数在构建时须为 1024 位 |
| 填充前缀 | 00 01 + 105 字节 FF + 00 + 20 字节摘要 |
PKCS#1 v1.5 风格,但无标准 SHA-1 DigestInfo/ASN.1 编码 |
| 十六进制补齐 | rjust(0x100,'0') = 256 字符 |
写入镜像时转换为 128 字节 |
| 摘要长度 | 20 字节 | 验签只接受 128 字节签名与 20 字节摘要 |
验签函数在做完公钥运算后,把末尾摘要与预期值异或归零,并逐字节比较整个 128 字节固定模板。因此它只处理这一种签名格式,不应直接当作通用 RSA-SHA1 校验;源码注释里出现的「2048-bit」和 SHA-256 与当前启用的常量及检查不符。
上电到跳转:bootstub 做哪几道检查
early_initialization() 先初始化寄存器、浮置早期 GPIO、识别板型;遇到 ROM 模式标志或芯片 ID 不符会走 jump_to_bootloader()。此后 bootstub.main() 先处理 softloader 标志,再读 app 长度、计算 SHA-1、检查 VERS/最小版本,最后用发布公钥验签;仅 ALLOW_DEBUG 构建额外接受调试公钥。失败进入 soft_flasher_start(),成功则调用 app 向量。
摘要实现是标准 SHA-1:SHA_hash() 依次调用 SHA_init()、SHA_update()、SHA_final(),64 字节分组经 SHA1_Transform() 处理,末尾补 0x80、零填充及 64 位比特长度,输出 20 字节大端摘要。bootstub 只哈希首字长度之后的 len-4 字节,签名本身与长度字都不在哈希区;长度字虽不参与摘要,却控制被读取的范围和签名位置。SHA-1 原实现通过空串、abc、分块 abc、56 字节填充边界与百万个 a 五类宿主标准向量。
扇区与刷写边界:主机保护不等于设备保护
存储布局上,sector0 放 bootstub,sector1 至 6 放 app,sector7 放设备序列与制造数据。主机端的 Panda.flash_static() 会用 last_sector < 7 阻止正常客户端擦除 sector 7,但设备端 flash_erase_sector() 只拒绝 sector 0,允许在解锁状态下擦除 sector 1 至 7。也就是说,直接发 EP0/EP2 的主机可以绕过这一层。EP0 控制请求 0xb1 解锁、0xb2 擦除、0xd1 进 ROM/softloader、0xd8 刷新并复位;comms_endpoint2_write() 从 app 起点顺序写 32 位字,源码未见对写指针、镜像长度或 provisioning 区域做边界检查。
| 边界 | 源码事实 | 需要台架确认的点 |
|---|---|---|
| 扇区保护 | 主机禁擦 sector 7;设备端仅拒绝 sector 0 | 经原始维护接口能否破坏制造数据 |
| 长度门 | bootstub 允许镜像长度接近 16 MiB | 超出 1 MiB flash 后读/故障表现 |
| 版本门 | 只要求 vers[1] >= 2 |
是否另设单调计数器防降级 |
关键函数与地址契约
下表整理了引导路径上与签名、刷写、身份相关的实现及其边界。它们是理解「哪一层保证什么」的最小清单:
| 函数/定义 | 实际作用 | 边界 |
|---|---|---|
bootstub.main() |
用首字长确定签名位置,哈希 app 主体,校验版本与 RSA-SHA1 签名 | 长度上界接近 16 MiB,超出 1 MiB flash 声明 |
RSA_verify() |
检查公钥结构、128 字节签名、20 字节摘要及固定填充模板 | 只处理该格式,不宜泛化为新硬件的完整安全启动方案 |
SHA_hash() |
对镜像被签区做 SHA-1 分组计算并输出 20 字节摘要 | 只覆盖调用者传入的 len-4 字节;memcpy 宿主声明有 ABI 警告 |
get_key_header() |
从固定公钥文件生成固件公钥常量 | 公钥与 CERT 私钥的配对关系未见构建期检查 |
flash_erase_sector() |
拒绝 sector 0,已解锁时擦除 sector 1 至 7 | sector 7 含制造数据,设备端并未排除 |
五项隔离探针把原版 rsa.c、sha.c 编译为临时共享库,按 debug 私钥构造无 DigestInfo 固定填充签名后经 ctypes 调用原版验签:有效签名返回 1,摘要/签名字节被改、标准 SHA-1 DigestInfo 填充、错误签名或摘要长度、错误公钥字数、不支持的指数均返回 0。合成镜像另核对首字长度、末尾 VERS/版本 2 与 128 字节签名,改动被签主体后验签失败。
常见问题
bootstub 验签通过,是不是就代表设备运行期安全?
不是。它校验的是启动镜像的真实性与完整性,并没有给运行期的 USB/SPI 控制请求做逐请求鉴权。EP0 解锁、擦除、EP2 写入等维护命令仍会被接受,因此需要把签名密钥与刷写权隔离到受控维护进程。
为什么主机端禁止擦 sector 7 还不够?
因为那是主机库的约束。flash_erase_sector() 只拒绝 sector 0,允许已解锁时擦除 sector 1 至 7,而 sector 7 含设备序列与制造数据。直接发 EP0/EP2 的主机不受「last_sector < 7」限制。
版本号能防止固件降级吗?
不能作为单调防回滚证明。启动检查只要求 vers[1] >= 2,源码中未见与设备单调计数器或持久最小版本比较。硬件改版若需防降级,应另行设计并保留救援路径。
这套 1024 位 RSA 加 SHA-1 能直接搬到新硬件吗?
不宜照搬。该验签只处理当前这种非标准固定填充格式,源码注释里的「2048-bit」与 SHA-256 也与实际常量不符。新硬件应先定义清晰格式与测试向量,再决定算法与密钥长度。
延伸阅读
- Ed25519 签名加 96 字节事务记录:让 MCU 升级不怕中途断电 —— 同类签名升级与掉电回滚的工程取舍
- EdgeDeploy:BLE 与 USB 两条通道怎么把签名包送到 ESP32-S3 —— 签名包从主机到设备的传输链
- UDS 诊断工具为什么不能随手调:只读、会话与写 ECU 的分权设计 —— 维护命令为什么要独立鉴权
- panda 板卡与固件生态 —— 本文所述 bootstub/soft flasher 所在的硬件载体
参考资料
- 项目 02 ——
openpilot-research/PANDA_BOOTSTUB_SIGNATURE_FLASH_BOUNDARY_DEEP_DIVE.md(bootstub 签名、刷写与制造数据边界,含源码行号索引) - 项目 02 ——
openpilot-research/probes/test_panda_rsa_signature_contract.py(RSA 与签名脚本五项宿主隔离探针) - 项目 02 ——
openpilot-research/probes/panda_sha1_vectors_contract.c(SHA-1 标准向量宿主探针)
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复