从文件格式到事务校验,完整还原 RPM 的安全机制
📌 疑难排查背景关于文章开头提到的具体错误场景(
rust-1.91.1-9.zncgsl6.aarch64包的 Payload SHA256 digest 校验失败),其常见的排查思路与解决方案如下:通常优先mock --scrub=all彻底清理构建环境,该操作可解决绝大多数的元数据不一致与缓存损坏问题,再通过更换镜像源和验证网络完整性进行针对性处理。
一、从文件格式出发:深入理解 Payload Digest 的技术本质
1.1 RPM 文件的四段式结构
RPM 包由四个相互独立的逻辑段按顺序拼接而成:
| 段名称 | 作用 |
|---|---|
| Lead | 文件标识与兼容性魔数,固定 96 字节,以 0xED 0xAB 0xEE 0xDB 魔数开头 |
| Signature | 签名与摘要集合,零填充至 8 字节倍数 |
| Header | 包元数据(名称、版本、依赖、文件列表等),采用 Tag-Value 结构 |
| Payload | 被压缩的文件归档,通常为 cpio 格式,经 gzip/xz/zstd 等压缩 |
理解这个四段式布局是后续所有内容的基础------摘要与签名分散存储在 Signature 段和 Header 段中,而非单一位置。
1.2 核心概念:Payload Digest 的精确定义
Payload Digest 是对 RPM 包中 Payload 层的压缩数据(即压缩后的 cpio 归档)进行哈希计算后得到的校验值。
RPM 的文档和规范给出了一个清晰的表格来描述所有摘要与签名的分布:
| 标签类型 | 版本引入 | 算法 | 存储位置 | 校验范围 |
|---|---|---|---|---|
| MD5(SIGMD5) | 3.0 | MD5 | Signature 段 | 整个包(HP) |
| SHA1(SHA1HEADER) | 4.0 | SHA1 | Signature 段 | Header 段 |
| PAYLOADDIGEST | 4.14 | SHA256 | Header 段 | 压缩后的 Payload |
| PAYLOADDIGESTALT | 4.16 | SHA256 | Header 段 | 解压后的 Payload |
| FILEDIGESTS | 4.6 | SHA256 | Header 段 | Payload 内单个文件 |
| PAYLOADSHA256 | 4.14 | SHA256 | Header 段 | 压缩后的 Payload |
| PAYLOADSHA512 | 6.0 | SHA512 | Header 段 | 压缩后的 Payload |
RPM 的官方文档将这种关系图示为:
S = Signature header - H = Main header - P = Payload - F = Files in the payload (uncompressed) - c = compressed content
核心结论:RPM 维护了多个层次的摘要,形成了一套纵深防御体系:
RPM 包 → Signature 段 → 验证整个包完整性(传统 MD5)
→ Header 段 → 验证 Header 完整性(SHA1HEADER / SHA256HEADER)
→ 验证 Payload 压缩数据(PAYLOADDIGEST / PAYLOADSHA256)
→ 验证 Payload 解压后数据(PAYLOADDIGESTALT)
→ 验证 Payload 内单个文件(FILEDIGESTS)
1.3 演进中的冲突:PAYLOADDIGEST 的硬编码问题与 PAYLOADSHA256 的引入
一个重要的技术细节是:现有版本的 RPM 中,PAYLOADDIGEST 标签的行为被硬编码 ,无法以可扩展的方式支持新算法。为了给新算法腾出空间,RPM 社区决定将 PAYLOADDIGEST 重命名为 PAYLOADSHA256。
虽然这种重命名保持向下兼容,但在某些场景下(主要是那些直接查询标签而不通过 RPM API 的工具)可能会造成解析上的问题------这是理解为什么某些老旧工具在处理新版 RPM 包时可能出现奇怪行为的关键背景。RPM 6.0 版本将提供更完善的解决方案,包括 PAYLOADSHA512 和 PAYLOADSHA3_256 等新摘要支持。
1.4 Payload Digest 与 GPG 签名的本质区别
两者在 RPM 安全体系中各司其职:
| 维度 | Payload Digest | GPG 签名 |
|---|---|---|
| 核心目的 | 完整性------数据没有被改过 | 真实性------数据是谁签发的 |
| 算法类型 | 哈希算法(SHA256、SHA512) | 非对称加密(RSA、DSA、EdDSA) |
| 能否被篡改且不被发现 | 修改数据 → 摘要不匹配 → 必被发现 | 无私钥 → 签名无效 → 必被发现 |
两者必须同时通过,包才算安全可信。
二、深入理解 sha256sum package.rpm 与 Payload Digest 的本质差异
2.1 计算范围完全不同------这就是差异的根源
| 对比项 | sha256sum package.rpm |
RPM Payload Digest |
|---|---|---|
| 计算对象 | 整个 .rpm 文件(Lead + Signature + Header + Payload) |
仅 Payload 段的压缩数据 |
| 包含哪些 | 文件头 + 签名段 + 元数据 + 压缩的 cpio 归档 | 仅压缩的 cpio 归档 |
| 能否验证 Header 完整性 | 能(因为整个文件被纳入了计算) | 不能(Header 由 SHA256HEADER 单独验证) |
| 典型用途 | 验证整个文件完整性、检查磁盘 I/O 损坏 | 专注于验证包内容数据本身的正确性 |
2.2 为什么两者计算出的哈希值不同
用一张表格来直观理解两者的涵盖范围差异:
| RPM 的一部分 | sha256sum package.rpm 会计算吗 |
Payload Digest 会计算吗 |
|---|---|---|
| Lead(96 字节魔数和历史信息) | ✅ 会 | ❌ 不会 |
| Signature 段(签名、摘要集合) | ✅ 会 | ❌ 不会 |
| Header 段(包名、版本、依赖列表等) | ✅ 会 | ❌ 不会 |
| Payload(压缩后的 cpio 归档) | ✅ 会 | ✅ 仅此部分 |
正是这种涵盖范围的差异,直接导致了两者的计算结果完全不同。
2.3 技术深究:为何摘要要作用在"压缩后"而非"解压前"
RPM 选择对压缩后的 Payload做摘要,而不是先做摘要再压缩,有着深层工程考量。
社区开发者 Jeff Johnson 在 2017 年的邮件列表讨论中对此给出了清晰的解释:
"Short answer: digesting compressed payload instead of compressing the digested payload is expedient." (译:对压缩后的 Payload 做摘要,比对摘要结果做压缩更高效。)
关键原因:
- 流式处理需求:RPM 需要支持在没有大量内存开销的情况下流式验证和提取;
- 降本增效:先压缩再做摘要,允许在解析 Payload 的真实内容(如 magic 头部)之前,就轻松设置摘要/压缩参数;
- 应用适配:那些不使用完整 RPM API 而直接操作 RPM 文件格式的第三方工具,可以显著降低实现复杂度。
如果没有这个机制,Verifying 一个 4GB 的大包时,可能需要在内存中完整解压全部内容(并重新压缩)后才能通过校验,这在内存受限的嵌入式系统中是不可接受的。
2.4 实践案例:提取 RPM Payload 并验证
使用 rpm2cpio 提取 Payload 内容:
bash
# 将 RPM 包中的 CPIO 归档提取到当前目录
rpm2cpio package.rpm | cpio -idmv
# 列出 RPM 包中的文件(不实际提取)
rpm2cpio package.rpm | cpio -it | less
rpm2cpio 只提取 Payload 中的 CPIO 归档部分 ,不包括 Lead、Signature 和 Header 段。提取后对得到的文件进行 sha256sum 计算,然后与 package.rpm 的 sha256sum 对比,两者一定不同,因为前者只包含文件数据,后者包含整个包。
核心结论 :
sha256sum package.rpm验证的是"文件传输/存储过程中是否损坏",而 RPM 的 Payload Digest 验证的是"这段压缩数据(Payload)是否与签名时一致"。两者服务于不同的安全目的,不能互相替代。
三、YUM/DNF 的事务校验机制与完整流程
3.1 事务的原子性保证
YUM/DNF 在设计上要求对一组软件包操作保证 "原子性" ------要么全部成功,要么全部回滚,绝不允许系统停在某个中间状态。
完整流程分为三步:
- 依赖解析:计算要安装/升级/删除的包集合;
- 事务测试(Transaction Test) :在不动真实文件系统的前提下完成所有校验 ;这里就是
Payload SHA256 digest: BAD和Transaction test error发生的关键阶段 - 事务执行:只有事务测试完全通过,才进入真正安装流程。
特别说明:关于文章开头提到的错误,涉及 YUM/DNF 的校验行为------正如 RPM 社区一项 PR(#3736)所讨论的,YUM/DNF 在正式安装前会执行一次"测试事务"(test-transaction),导致每个包的摘要被校验了两次。为了解决潜在的性能开销,PR 中提出在测试事务阶段跳过某些摘要计算。
3.2 校验流程分步详解
| 阶段 | 操作 | 失败时的典型错误 |
|---|---|---|
| 第 1 步:仓库元数据验证 | 下载 repodata/repomd.xml 及其校验和,对比本地计算值 |
metadata file does not match checksum |
| 第 2 步:Header 摘要验证 | 解析 RPM Signature 段,对比 SHA256HEADER / SHA1HEADER | Header SHA256 digest: BAD |
| 第 3 步:Payload 压缩数据验证 | 计算压缩后 Payload 的 SHA256,对比 PAYLOADDIGEST | Payload SHA256 digest: BAD |
| 第 4 步:Payload 解压后验证(若启用) | Payload 解压后重新计算 SHA256,对比 PAYLOADDIGESTALT | Payload SHA256 ALT digest: BAD |
| 第 5 步:GPG 签名验证 | 验证 OpenPGP 签名是否有效且来自可信公钥 | NOKEY / BAD |
RPM 的安全架构文档指出,在包验证时,RPM 检查:Header 摘以确保 Header 完整性,Payload 摘以确保包内容完整性。
验证输出示例:
/data/RPMS/hello-2.0-1.x86_64-signed.rpm:
Header OpenPGP V4 RSA/SHA256 signature, key fingerprint: 771b18d3...: OK
Header SHA256 digest: OK
Payload SHA256 digest: OK
Legacy OpenPGP V4 RSA/SHA256 signature, key fingerprint: 771b18d3...: OK
3.3 事务测试阶段的校验缓存与性能问题
RPM 社区的一项发现指出:客户端(如 YUM/DNF)喜欢在真实的安装之前进行一次测试事务,导致验证被执行两次,从而重复计算包摘要。
这意味着如果一个事务中包含大量软件包,摘要计算的开销会翻倍。虽然这不会影响最终结果(因为校验结果一致),但对于 CI/CD 流水线或具有更新大量包的环境来说,了解和规避这种性能开销非常重要。
四、校验失败的深度原因与场景化解决方案
4.1 诊断命令工具箱
| 命令 | 作用 |
|---|---|
rpmkeys -Kv package.rpm |
首选诊断命令,查看包的摘要和签名状态 |
sha256sum package.rpm |
计算整个 RPM 文件的 SHA256 |
| `sha256sum $(rpm2cpio package.rpm | cpio -i --to-stdout 2>/dev/null)` |
rpm -qp --qf "%{PAYLOADDIGEST}\n" package.rpm(实验性) |
查询 RPM 头中的 PAYLOADDIGEST 值(取决于 RPM 版本) |
dnf clean all |
清理所有缓存 |
dnf --refresh upgrade |
强制刷新仓库元数据 |
mock --scrub=all |
彻底清理 Mock 构建环境 |
4.2 场景化解决方案表
| 错误场景 | 根本原因 | 解决方案 |
|---|---|---|
Payload SHA256 digest: BAD,单次出现 |
网络传输损坏或镜像节点缓存不一致 | dnf clean packages && dnf --refresh 重新下载 |
Payload SHA256 digest: BAD,多个包持续出现 |
仓库镜像节点长时间不同步 | 切换 baseurl 到另一个稳定镜像;联系仓库维护者同步 repodata |
Payload SHA256 ALT digest: BAD |
压缩 Payload 校验通过但解压后数据不匹配 | 罕见,通常表示 Payload 内部损坏较深;重新下载且换镜像节点 |
Header SHA256 digest: BAD |
包元数据本身损坏 | 文件级损坏,重新下载 |
NOKEY |
GPG 公钥未导入 | rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-* |
mock 中的 Transaction test error |
Mock chroot 缓存损坏或仓库元数据不一致 | mock --scrub=all 彻底清理,让 mock 重新构建环境 |
4.3 镜像延迟导致的校验失败
错误信息 [MIRROR] ... Downloading successful, but checksum doesn't match. Expected: 667463e8... != actual 发生时,常见原因是:
- 部分镜像节点处于"元数据最新,但 RPM 文件还停留在旧版"的不一致状态
- 客户端根据
repomd.xml访问了某一个镜像节点,拿到旧版文件,而repomd.xml中的哈希对新版文件才正确 - 解决方案:修改
.repo文件中mirrorlist或baseurl,排除问题镜像,或使用权威主仓库 URL
4.4 Mock 构建环境中的 Transaction test error
在 Mock 构建环境中,Transaction test error: package XXX does not verify: Payload SHA256 digest: BAD 发生在事务测试阶段。Mock 在开始构建 chroot 环境时,会进行事务测试来验证依赖完整性。此时出现校验失败的原因包括:
- Mock 本地的包缓存目录(Cache)损坏
- Mock 引用的仓库
repodata与实际 RPM 不同步 - Mock 的 chroot 环境处于不一致状态
最佳应对方案 :mock --scrub=all --target=aarch64 <your.config>,该操作彻底清理目标 chroot 环境和包缓存,让 Mock 重新从仓库获取所有内容。
五、参考资源
- RPM 官方文档:Signatures and Digests
- RPM 格式规范:RPM V4 Package format
- DeepWiki:RPM 安全特性 Security Features
- RPM 社区讨论:#184 Payload Digest 计算时机
- RPM 社区讨论:#3736 避免重复摘要计算
rpmkeys(8)手册页:RPMKEYS(8)