GLEP 78:Gentoo 二进制包容器格式

作者 Michał Górny <[email protected]>,Sheng Yu <[email protected]>
类型 标准跟踪
状态 最终
版本 1.1
创建时间 2018-11-15
最后修改时间 2023-05-14
发布历史 2018-11-17, 2019-07-08, 2021-09-13, 2021-09-22, 2022-05-28, 2022-09-21
GLEP 源代码 glep-0078.rst

摘要

本 GLEP 提出了一种新的 Gentoo 二进制包容器格式。简要描述了当前的 tbz2/XPAK 格式,并解释了其不足之处。因此,设定了对新格式的要求,并提出了一种满足这些要求的 gpkg 格式。提供了设计决策的基本原理。

动机

当前的 Portage 二进制包格式

历史上的.tbz2Portage 使用的二进制包格式是两种不同格式的串联:面向头的压缩 .tar 格式(用于存放包文件)和面向尾部的自定义 XPAK 格式(用于存放元数据)[1]。该格式已经两次不兼容地扩展。

第一次,添加了对存储单个 ebuild 版本的二进制包的多个连续构建的支持。此功能依赖于在包文件名后追加一个额外的连字符,后面跟着一个整数。它默认情况下被禁用(保留向后兼容性),并由binpkg-multi-instance功能控制。

第二次,添加了对附加压缩格式的支持。当使用除 bzip2 之外的格式时,.tbz2后缀被替换为.xpak,Portage 依赖于魔数字节来检测所使用的压缩。为了向后兼容,Portage 仍然默认使用 bzip2;可以使用BINPKG_COMPRESS配置变量切换压缩程序。

此外,存储的元数据和文件存储策略也有一些小的变化。特别是,关于INSTALL_MASK,可控的文件压缩和剥离随着时间的推移发生了变化。

tbz2/XPAK 格式的优势

Portage 使用的 tbz2/XPAK 格式具有三个有趣的特点

  1. 每个二进制包都完全包含在一个文件中。虽然这看起来没有必要,但它使用户更容易传输二进制包,而无需担心找到要传输的所有必要文件。
  2. 二进制包通常与常规压缩 tar 包兼容。除了 pbzip2 的历史版本和最近的 zstd 压缩器之外,tbz2/XPAK 包可以使用常规的 tar 工具进行解压缩,该工具使用一个忽略尾部垃圾的压缩器实现。
  3. 元数据未压缩,并且可以在不解压缩包内容的情况下有效地访问。这包括重写它的可能性(例如,由于包移动),而无需重新打包文件。

当前二进制包格式的透明度问题

尽管有其优点,但 tbz2/XPAK 格式有一个重大的设计缺陷,它包含两个问题

  1. XPAK 格式是一种自定义二进制格式,明确使用二进制编码的文件偏移量和字段长度。因此,如果没有专门的工具,读取或编辑它并不容易。此类工具目前与包管理器分开实现,作为 portage-utils 工具包的一部分,用 C 语言编写[2]
  2. tarball 兼容性功能依赖于忽略压缩文件尾部垃圾的模糊特性。虽然这在大多数压缩器中始终如一地实现,但此特性实际上并不属于规范的一部分,而是传统行为。考虑到最初的理由不再适用,新的压缩器实现很可能缺乏对它的支持。

这两个问题都使得该格式难以在没有专用工具的情况下使用,或者当工具出现故障时使用。这影响了以下场景

  1. 使用二进制包进行系统恢复。在严重故障的情况下,格式依赖于尽可能少的工具,尤其是不要依赖于 Gentoo 特定的工具,这确实更可取。
  2. 详细检查超出标准包管理器功能的二进制包。
  3. 以包管理器作者未预测的方式修改二进制包。这方面的一个实际例子是解决损坏的pkg_*阶段,这些阶段阻止了包的安装。

OpenPGP 可扩展性问题

至少有三种明显的方法可以扩展当前格式以支持 OpenPGP 签名,每种方法都有其自身独特的难题

  1. 添加分离签名。此选项是非侵入性的,但会导致格式不再包含在一个文件中。
  2. 将包包装在 OpenPGP 消息格式中。这将使用标准格式,并使验证和解压缩变得相对容易。但是,它会破坏向后兼容性,并增加对 OpenPGP 实现的明确依赖,以解压缩包。
  3. 将 OpenPGP 签名添加为额外的 XPAK 成员。这是明智的解决方案。这意味着加强对自定义工具的依赖,现在还需要额外的工具来提取签名并重建原始文件以适应验证。

新容器格式的目标

考虑到以上所有因素,新格式应结合现有格式的优点,并尽可能地解决其不足之处。此外,由于正在进行格式替换,因此值得考虑可以稍作修改就能满足的额外目标。

已为替换格式设定了以下必选目标

  1. 包必须保留在单个文件中。出于用户便利的考虑,应该能够传输二进制包,而无需使用多个文件,并且可以从任何位置安装它们。
  2. 文件格式必须完全基于通用文件格式,遵循最佳实践,并且尽可能少地进行定制以满足需求。该格式应足够透明,让用户能够在没有特殊工具或详细知识的情况下检查和操作它。
  3. 文件格式必须能够检测到自己的数据损坏。特别是,它需要包含自身数据的校验和,以便包管理器能够在不依赖于其他文件的情况下验证其完整性。
  4. 文件格式必须提供对 OpenPGP 签名的支持。最好是使用标准的 OpenPGP 消息格式。
  5. 文件格式必须允许有效地更新元数据。特别是,应该能够更新元数据,而无需重新压缩包文件。

此外,还注意到以下可选目标

  1. 文件格式应通过文件名和内容进行简单识别。最好是拥有通过 file(1) 检测的独特特征。
  2. 文件格式应提供对二进制包的局部获取。应该能够轻松获取和读取包元数据,而无需下载整个包。
  3. 文件格式应允许元数据压缩。
  4. 文件格式应使未来的扩展在不破坏向后兼容性的情况下变得容易。

规范

容器格式

gpkg 包容器是一个未压缩的 .tar 存档,其文件名应使用.gpkg.tar后缀。

该存档包含多个文件。所有与包相关的文件应存储在一个目录中,该目录的名称与包文件名在剥离.gpkg.tar后缀后的名称匹配。但是,实现必须能够处理目录名称不匹配的存档。该目录不应有明确的存档成员条目。

包目录按顺序包含以下成员

  1. 包格式标识文件gpkg-1(必需)。
  2. 元数据存档metadata.tar${comp},可选压缩(必需)。
  3. 元数据存档的签名metadata.tar${comp}.sig(可选)。
  4. 文件系统镜像存档image.tar${comp},可选压缩(必需)。
  5. 文件系统镜像存档的签名image.tar${comp}.sig(可选)。
  6. 包清单数据文件Manifest,可选明文签名(必需)。

建议保留存档成员的相对顺序。但是,实现必须支持成员顺序混乱的存档。

该容器将来可能会扩展添加更多成员。如果存在清单文件,则存档中包含的所有文件都必须列在其中并验证成功。包管理器应忽略未知文件,但在包更新时保留它们。

为了使二进制包被视为已签名并适合进行真实性验证,清单文件必须存在并包含有效签名。建议也包含存档成员的独立签名。

允许的 .tar 格式功能

tar 存档应使用 POSIX.1-2017 [3] 中定义的 POSIX ustar 格式,或使用 GNU tar 手册 [4] 中描述的与 ustar 兼容的 GNU tar 格式的子集,并使用以下(可选)扩展

  • 长路径名和长链接名,
  • 大型文件大小的 base-256 编码。

应尽可能避免其他扩展。

包标识文件

包标识文件用于识别二进制包格式及其版本。

实现必须包含名为gpkg-1的文件。文件名包含包格式版本;实现应拒绝不包含此文件的包,因为它不支持的格式。

该文件可以包含任何内容。通常,它应该是空的。

此外,此文件应作为第一个成员包含在 .tar 存档中。这使得可以将其用作固定位置的额外魔数,可以由 file(1) 等工具使用来轻松区分 Gentoo 二进制包和常规 .tar 存档。

元数据存档

元数据存档存储包管理器处理包所需的包元数据。该存档应包含在二进制包的开头,以便能够将其从部分获取的二进制包中读取出来,并在必要时避免获取包的剩余部分。

该存档包含一个名为metadata的单个目录。在这个目录中,各个元数据键被存储为文件。确切的键和元数据格式不在本规范的范围内。

包管理器可能需要修改包元数据。在这种情况下,它应该替换元数据存档,而无需更改其他包成员。

元数据存档可以选择压缩。它也可以用独立的 OpenPGP 签名进行补充。

镜像存档

映像存档存储二进制包要安装的所有文件。它应包含在二进制包容器中的最后一个文件。

该存档包含一个名为image。在这个目录中,所有包文件都以文件系统布局存储,相对于根目录。

映像存档可以选择压缩。它也可以用独立的 OpenPGP 签名进行补充。

存档成员压缩

上面概述的存档成员支持使用包管理器支持的压缩文件格式之一进行可选压缩。压缩类型列表在 GLEP 74 [5] 中维护。包管理器可以实现压缩文件格式的任意子集。但是,建议它能够解压缩所有未列为已弃用的格式。

实现必须支持解压缩存档成员,并且必须支持对不同文件使用不同的压缩类型。

压缩存档成员时,应使用 GLEP 74 中指定的特定压缩文件类型的后缀对成员文件名进行后缀。

包清单文件

清单文件必须包含二进制包容器中所有文件的摘要,除了它本身。此文件的目的是为包管理器提供在尝试读取内部存档内容之前检测二进制包损坏或更改的能力。如果使用 OpenPGP 签名,此文件还提供针对签名重用/替换攻击的保护。

实现遵循 GLEP 74 中的清单规范,并使用DATA标记表示容器中的文件。如果包使用 OpenPGP 签名,清单文件还必须包含 GLEP 74 [5] 中定义的明文 OpenPGP 签名。

实现应该能够检测校验和不匹配,以及容器中缺少、重复或多余的文件。如果验证失败,则不应执行对存档的后续操作。

OpenPGP 成员签名

存档成员和清单支持可选 OpenPGP 签名。实现必须允许用户指定是否在远程获取的包中期望 OpenPGP 签名。

如果期望签名并且存档成员未签名,包管理器必须拒绝处理它。如果签名未验证,包管理器必须拒绝处理相应的存档成员。特别是,它在这些情况下不得尝试解压缩压缩成员。

签名被创建为二进制独立 OpenPGP 签名文件,如 RFC 4880 § 11.4 或后续标准中定义,其文件名与成员文件名对应,并添加.sig后缀 [6]

关于创建和验证签名,以及维护和分发密钥的确切细节不在本规范的范围内。

基本原理

其他发行版使用的包格式

对新包格式的研究包括调查是否可以重用其他操作系统发行版的解决方案。虽然重用外部包格式会很有趣,但 Gentoo 元数据结构的差异会阻止任何真正的兼容性。可以通过调整 Gentoo 元数据来实现一定程度的兼容性,但这种解决方案的成本可能会超过其实用性。

Debian 及其衍生产品使用 .deb 包格式。这是一种嵌套的存档格式,外部存档是 ar 格式,包含控制信息(元数据)和数据的嵌套 tarballs [7]

Red Hat、其衍生产品以及一些关系较少的发行版使用 RPM 格式。它是一种自定义二进制格式,直接存储元数据,并使用尾部 cpio 存档来存储包文件。

Arch Linux 使用 xz 压缩的 tarballs(后缀为.pkg.tar.xz)作为其二进制包格式。tarballs 在顶层包含包文件,并使用特殊命名的点文件用于包元数据。OpenPGP 签名存储为独立.sig文件,与包并排。

Exherbo 使用 pbins 格式。在此格式中,二进制包元数据存储在类似于 ebuilds 的存储库中,而二进制包文件则单独存储并像源 tarballs 一样下载。

嵌套存档格式

设计新格式的基本问题是如何将多个数据流(元数据、映像)嵌入到单个文件中。传统上,这是通过使用两种不冲突的文件格式来实现的。然而,虽然这种解决方案很巧妙,但它在透明度方面存在缺陷。

因此,已经确定新格式应该真正由单个存档格式组成,所有必要的数据都可以在文件中透明地访问。因此,已经讨论了二进制包数据的不同部分应该如何存储在该存档中。

继续将映像数据作为包格式中的顶层数据存储,并将元数据作为该结构中的特殊目录存储的提议已被丢弃,因为它是一种带内信令。

最后,该提议被塑造成将不同类型的数据作为嵌套存档存储在外部二进制包容器中。除了提供一种访问不同类型信息的干净方法之外,它还使得可以向它们添加单独的 OpenPGP 签名。

内部压缩与外部压缩

新格式辩论中的一个要点是,二进制包作为一个整体是否应该被压缩,还是应该压缩单个成员。第一个选项可能看起来是一个显而易见的选择,尤其是在数据量更大时,压缩可能会更有效。但是,它有一个明显的缺点:压缩会阻止对二进制包成员的随机访问和操作。

虽然为了读取二进制包的目的,可以通过方便的成员排序和避免二进制包的不连续读取来规避这个问题,但元数据更新要么需要重新压缩整个包(对于大型包来说可能非常耗时),要么需要应用复杂的技术,例如将压缩存档拆分为多个压缩流。

考虑到这一点,最简单的解决方案是对单个包成员应用压缩,而将容器格式保持为未压缩状态。它提供对单个成员的快速随机访问,以及更新它们的能力,而无需重新压缩容器中的其他文件。

这也使得可以轻松地使用标准 OpenPGP 独立签名格式来保护压缩文件。所有这些相结合,包管理器可以执行二进制包的部分获取,验证其元数据成员的签名并处理它,而无需获取可能很大的映像部分。

容器和存档格式

在辩论过程中,考虑了要使用的实际存档格式。.tar 格式似乎是映像存档的显而易见的选择,因为它是 POSIX 系统上唯一广泛部署的存档格式,可以存储所有类型的文件元数据。但是,已经讨论了外部格式的多种选择。

首先,ZIP 格式已被提议作为唯一普遍支持的格式,它支持从 stdin 添加文件(即,使能够将内部存档直接管道传输到容器中,而无需使用临时文件)。但是,这种格式已被明确拒绝,因为它既不存在于系统集中,又因为它是基于尾部的,因此在没有获取整个文件的情况下无法使用。

其次,考虑了 ar 和 cpio 格式。前者由 Debian 及其衍生二进制包使用;后者由 Red Hat 衍生产品使用。这两种格式都具有比 .tar 更少的历史包袱,以及更少的开销的优势。但是,它们也相当晦涩(特别是考虑到 ar 实际上是由 GNU binutils 提供的,而不是作为独立的归档器),被 POSIX 认为已过时,并且它们的文件大小限制都小于 .tar。

第三,SquashFS 是另一个有趣的选择。它的主要优势是支持透明压缩和作为文件系统挂载的能力。但是,它具有显著的实现复杂性,包括挂载管理和需要回退到 unsquashfs。由于映像需要对预安装操作可写,因此通过挂载使用它还需要某种类型的覆盖文件系统。将其用作顶层格式与使用带有 tar 的管道相比没有真正的收益,而且肯定不太便携。因此,使用 SquashFS 看起来没有好处。

考虑到所有这些因素,已经决定,除非第二种存档格式对 .tar 具有显著优势,否则没有必要在规范中使用它。因此,.tar 也被用作外部包格式,即使它比其他格式(主要是由于填充)具有更大的开销。

.tar 可移植性问题

现代的 .tar 扩展可以被认为是原始 .tar 格式的“脏”扩展。三个变体可能值得关注:POSIX ustar、pax(较新的 POSIX 标准)和 GNU tar。所有三种格式都受 GNU tar 支持,而 GNU tar 在用于创建二进制包的系统中普遍存在,因此可以依赖其存在。因此,可移植性问题主要与在 GNU tar 不可用情况下,能否读取和修改二进制包相关。

为了满足本规范的要求,我们对各个 tar 特性的可移植性进行了详细研究。研究结果表明

根据测试结果,最佳可移植性可以通过以下方式实现:

  • 尽可能使用严格的 POSIX ustar 格式,
  • 对于无法放入 ustar 格式的长路径,使用 GNU 格式,
  • 对于大型文件,使用 base-256(如果已使用,则使用 pax)编码,
  • 对于高范围/精度的时间戳和用户/组标识符,使用 pax(+ 八进制或 base-256),
  • 对于扩展元数据和/或卷标,使用 pax 属性。 [8]

我们已经确定,对于二进制包,我们实际上只需要关注长路径和超大型文件。因此,上述内容仅限于前三点,并据此制定了一项指南。

Debian 对于其包格式的内部 tar 也有类似的指南 [7]

.tar 安全问题

一些 .tar 的原始特性在现代使用中已经过时。

首先,.tar 允许存在重复文件 [10]。在按顺序提取所有文件时,后面的重复文件会覆盖先前提取的文件。这对增量备份很有用。但是,通用归档工具可能会选择与路径名匹配的任意文件,从而导致校验和或签名绕过。为了防止这种情况,禁止存在重复文件。

其次,.tar 缺乏完整性检查,除了头部的自检。数据损坏通常可以通过附加压缩层的完整性检查来检测。但是,这无法提前验证压缩数据的完整性。为此,包含了额外的 Manifest 文件,其中提供了存档中其他文件的校验和。损坏的 Manifest 会使整个包失效。

第三,许多 .tar 实现存在各种安全问题,包括 Python tarfile 模块 [11]。它们提供了多种攻击媒介,例如允许使用特殊文件名、符号链接、硬链接或设备文件覆盖目标目录之外的文件。为此,容器内只允许使用普通文件。建议就地处理容器数据,而不是将其提取出来。

成员排序

明确指定成员排序是为了方便从部分获取的档案中读取元数据。通过要求元数据档案存储在镜像档案之前,包管理器可以在读取元数据后停止获取,从而节省带宽和/或空间。

分离的 OpenPGP 签名

使用分离的 OpenPGP 签名是为了对二进制包进行身份验证。使用签名覆盖所有成员,可以轻松验证所有元数据和镜像内容,而无需为组合它们发明自定义机制。覆盖压缩档案有助于防止 zipbomb 攻击。覆盖单个成员而不是整个包可以验证部分获取的二进制包。

但是,对单个文件进行签名不能保证所有成员都来自同一个二进制包。这打开了替换/重用攻击的可能性,例如,将 foo-1.1 的签名元数据与 foo-1.0 的签名镜像结合起来。新二进制包通过签名检查。为了防止此类攻击,我们需要额外的 Menifest 文件及其签名来验证完整二进制包的真实性。

格式版本控制

格式通过一个显式文件进行版本化,版本存储在文件名中。如果格式发生了不兼容的更改,文件名会发生变化,旧实现不会将其识别为有效包。

之前,该格式试图避免为此目的使用显式文件,而是使用卷标。但是,由于不可预见的可移植性问题,放弃了使用卷标。

向后兼容性

该格式不保留与 tbz2 包的向后兼容性。已经确定,在不使新格式比旧格式更糟糕的情况下,保留与旧格式的兼容性是不可能的。

例如,在 tarball 中添加任何可见成员会导致旧版本的 Portage 将它们安装到文件系统中。解决此问题需要一些可怕的黑客行为,这与使用简单透明的包格式的目标背道而驰。

参考实现

gpkg 格式从 Portage 3.0.36 版本开始受支持 [9]

参考文献

[1]xpak - Portage 二进制包使用的 XPAK 数据格式 (https://dev.gentoo.org/~zmedico/portage/doc/man/xpak.5.html)
[2]portage-utils: 用 C 编写的快速小型 Portage 辅助工具 (https://packages.gentoo.org/packages/app-portage/portage-utils)
[3]The Open Group Base Specifications Issue 7, 2018 版本,pax - 可移植档案交换,ustar 交换格式 (https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_06)
[4]GNU tar: 一个归档工具,附录 E Tar 内部 (https://www.gnu.org/software/tar/manual/html_node/Tar-Internals.html)
[5](1, 2) GLEP 74: 使用 Manifest 文件进行全树验证 (https://gentoolinux.cn/glep/glep-0074.html)
[6]RFC 4880: OpenPGP 消息格式 (https://www.rfc-editor.org/rfc/rfc4880)
[7](1, 2) deb(5) — Debian 二进制包格式 (https://manpages.debian.org/unstable/dpkg-dev/deb.5.en.html)
[8]Michał Górny,tar 特性的可移植性 (https://dev.gentoo.org/~mgorny/articles/portability-of-tar-features.html)
[9]Portage 3.0.36 版本 (https://gitweb.gentoo.org/proj/portage.git/commit/?h=portage-3.0.36)
[10]tar: 多个同名成员 (https://www.gnu.org/software/tar/manual/html_node/multiple.html)
[11]Python tarfile: 遍历攻击漏洞 (https://bugs.python.org/issue21109)