1024 位 RSA 加 SHA-1:panda 的签名、刷写与扇区边界

作者:

在

结论: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 也与实际常量不符。新硬件应先定义清晰格式与测试向量,再决定算法与密钥长度。

延伸阅读

参考资料

  • 项目 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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注