CEP 19 - 计算目录内容的哈希值
标题 | 计算目录内容的哈希值 |
状态 | 已批准 |
作者 | Jaime Rodríguez-Guerra <jaime.rogue@gmail.com> |
创建于 | 2024 年 11 月 19 日 |
更新于 | 2024 年 12 月 19 日 |
讨论 | https://github.com/conda/ceps/pull/100 |
实现 | https://github.com/conda/conda-build/pull/5277 |
摘要
给定一个目录,提出一种跨平台计算其内容聚合哈希值的算法。这对于检查远程源的完整性非常有用,而无需考虑所使用的压缩方法。
本文档中使用的关键词 "MUST"、"MUST NOT"、"REQUIRED"、"SHALL"、"SHALL NOT"、"SHOULD"、"SHOULD NOT"、"RECOMMENDED"、"NOT RECOMMENDED"、"MAY" 和 "OPTIONAL" 应按照 [RFC2119][RFC2119] 中的描述进行解释,当且仅当它们全部以大写形式出现时,如此处所示。
规范
给定一个目录,递归扫描其所有内容(不跟随符号链接),并按照它们的完整路径作为 Unicode 字符串进行排序。更具体地说,它必须遵循使用字符的数值 Unicode 代码点(即 Python 内置函数 ord()
的结果)的升序词典比较1。
对于内容表中的每个条目,计算以下各项的串联的哈希值:
- 路径的 UTF-8 编码字节,相对于输入目录。反斜杠必须在编码前标准化为正斜杠。
- 然后,根据类型
- 对于常规文件
- 如果是文本文件,则为
F
分隔符的 UTF-8 编码字节,后跟其行尾标准化内容(\r\n
替换为\n
)的 UTF-8 编码字节。如果所有内容都可以进行 UTF-8 解码,则该文件被视为文本文件。否则,它被视为二进制文件。如果文件无法打开,则将其视为空文件。 - 如果是二进制文件,则为
F
分隔符的 UTF-8 编码字节,后跟其内容的字节。 - 如果无法读取,则报错。
- 如果是文本文件,则为
- 对于目录,
D
分隔符的 UTF-8 编码字节,仅此而已。 - 对于符号链接,
L
分隔符的 UTF-8 编码字节,后跟它指向的路径的 UTF-8 编码字节。反斜杠必须在编码前标准化为正斜杠。 - 对于任何其他类型,则报错。
- 对于常规文件
- 字符串
-
的 UTF-8 编码字节。
请注意,该算法必须在不可读的文件和未知文件类型上报错,因为我们无法验证其内容。攻击者可能会在已知为 "不可哈希" 的路径中隐藏恶意内容,然后在构建脚本中再次显示(例如,通过 chmod
将它们设置为可读)。
Python 中的示例实现
import hashlib
from pathlib import Path
def contents_hash(directory: str, algorithm: str) -> str:
hasher = hashlib.new(algorithm)
for path in sorted(Path(directory).rglob("*")):
hasher.update(path.relative_to(directory).replace("\\", "/").encode("utf-8"))
if path.is_symlink():
hasher.update(b"L")
hasher.update(str(path.readlink(path)).replace("\\", "/").encode("utf-8"))
elif path.is_dir():
hasher.update(b"D")
elif path.is_file():
hasher.update(b"F")
try:
# assume it's text
lines = []
with open(path) as fh:
for line in fh:
lines.append(line.replace("\r\n", "\n")
for line in lines:
hasher.update(line.encode("utf-8")))
except UnicodeDecodeError:
# file must be binary
with open(path, "rb") as fh:
for chunk in iter(partial(fh.read, 8192), b""):
hasher.update(chunk)
else:
raise RuntimeError(f"Unknown file type: {path}")
hasher.update(b"-")
return hasher.hexdigest()
动机
像 conda-build
和 rattler-build
这样的构建工具需要获取正在打包的项目的源代码。下载的完整性通过将其已知的哈希值(通常是 SHA256)与获取的文件进行比较来检查。如果它们不匹配,则会引发错误。
然而,压缩存档的哈希值对无关紧要的更改很敏感,例如使用了哪种压缩方法、存档工具的版本以及其他与存档内容无关的细节,而这才是构建工具真正关心的。这种情况经常发生在从 Github 仓库引用实时获取的存档中,例如。验证对分支名称等动态引用的 git clone
操作的完整性也很有用。
通过此提案,构建工具可以添加一系列新的哈希检查,这些检查对于内容可再现性更加健壮。
理由
所提出的算法可以简单地将所有字节连接在一起,一旦目录内容已排序。相反,它还编码相对路径和分隔符,以防止 前像攻击。
没有使用 Merkle 树是为了简单起见,因为它没有必要经常更新哈希值或指出哪个文件导致了哈希值更改。
此算法作为构建工具中特定选项的实现不是本 CEP 的目标。该目标推迟到进一步的 CEP,这些 CEP 可以简单地说类似这样的话:
source
部分是一个对象列表,带有键 [...]contents_sha256
和contents_md5
(它们分别实现了 CEP 19 用于 SHA256 和 MD5)。
参考文献
- 最初提出这个想法的问题是
conda-build#4762
。 - Nix 生态系统具有类似的功能,称为
fetchzip
。 - 有几个 Rust crates 和 Python 项目 使用 Merkle 树实现了类似的策略。这里的一些细节受到了
dasher
的启发。
版权
所有 CEP 均明确采用 CC0 1.0 Universal。