跳到主要内容

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-buildrattler-build 这样的构建工具需要获取正在打包的项目的源代码。下载的完整性通过将其已知的哈希值(通常是 SHA256)与获取的文件进行比较来检查。如果它们不匹配,则会引发错误。

然而,压缩存档的哈希值对无关紧要的更改很敏感,例如使用了哪种压缩方法、存档工具的版本以及其他与存档内容无关的细节,而这才是构建工具真正关心的。这种情况经常发生在从 Github 仓库引用实时获取的存档中,例如。验证对分支名称等动态引用的 git clone 操作的完整性也很有用。

通过此提案,构建工具可以添加一系列新的哈希检查,这些检查对于内容可再现性更加健壮。

理由

所提出的算法可以简单地将所有字节连接在一起,一旦目录内容已排序。相反,它还编码相对路径和分隔符,以防止 前像攻击

没有使用 Merkle 树是为了简单起见,因为它没有必要经常更新哈希值或指出哪个文件导致了哈希值更改。

此算法作为构建工具中特定选项的实现不是本 CEP 的目标。该目标推迟到进一步的 CEP,这些 CEP 可以简单地说类似这样的话:

source 部分是一个对象列表,带有键 [...] contents_sha256contents_md5(它们分别实现了 CEP 19 用于 SHA256 和 MD5)。

参考文献

所有 CEP 均明确采用 CC0 1.0 Universal

脚注

  1. 这就是 Python 的做法。请参阅 值比较 中的 "strings"。