TT Lab
开始
学习 学习路径 课程

CI/CD 流水线

用哈希命名并不等于构建可复现

在 TT Lab 中继续学习

一句话总结

用哈希给制品命名,与让同一份源码产出同样的字节,是两回事。前者是把标签贴准确,后者是把构建变成“输入相同则输出相同的函数”。只贴了标签就宣称可复现的流水线非常多。

为什么需要它

“用源码哈希打了标签,所以可复现”这种话经常听到,却很少被证明过。把同一个提交构建两次,直接比较两个制品的 SHA-256,往往是不同的。然而名字却是一样的。名字相同而内容不同的制品,会同时破坏三样东西:缓存在说谎,晋级模型崩塌,事故复盘时没有人能回答“当时发出去的到底是不是这个”。

可复现性实际换来的有三点。第一,缓存变得可信。 只有“相同的键就得到相同的结果”这个前提成立,才能把缓存命中当作“可以跳过构建”的依据。第二,可以重新构建来核实制品的来源。 用同样的源码和流程重新构建,字节一致的话,不需要签名就能核对该制品确实出自那份源码。第三,出事故时能把当时的制品重新造出来。 即使它已从仓库中被删除,只要源码和流程还在,就能再得到同样的字节。

非确定性从哪里进来

来源几乎是固定的。遇到新语言、新工具,这份清单也不会有太大变化。

如何固定

核心约定是 SOURCE_DATE_EPOCH。官方文档把这个变量定义为“某个对象(通常是源代码)的最后修改时间,以自 Unix 纪元以来的秒数表示”。也就是说,构建工具在需要写入时间时,约定用这个值取代当前时间。这个值通常取自该提交的时间。

归档尤其要小心。官方文档推荐的形式如下。

export SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)"
tar --sort=name \
    --mtime="@${SOURCE_DATE_EPOCH}" \
    --owner=0 --group=0 --numeric-owner \
    --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime \
    -cf product.tar build
gzip -6 -n < product.tar > product.tar.gz   # -n 은 원본 이름과 시각을 빼고 압축한다

--sort=name 固定文件顺序,--mtime 固定时间,--owner/--group/--numeric-owner 固定所有者,--pax-option 去掉混入 PAX 头的 atime/ctime。这个组合需要 GNU tar 1.28 及以上。并且判定不靠眼睛,而靠哈希。构建两次,看 sha256sum 是否相同——除此之外,没有别的方法能确认可复现性。

摘要保证什么,不保证什么

OCI 镜像规范把摘要定义为“针对字节的抗碰撞哈希”,并写明它使内容寻址成为可能。也就是说,只要通过安全的途径拿到摘要,即使内容来自不可信的地方,也可以重新计算哈希并比对,从而确认它没有被篡改。格式是 알고리즘:인코딩된값(占位符依次为算法与编码后的值),规范建议来自不可信来源的内容在使用之前必须用摘要验证。

这一特性也直接固化在存储格式里。OCI 镜像布局规范要求,放在 blobs/<알고리즘>/<인코딩된값>(占位符依次为算法与编码后的值)的内容,必须与摘要 알고리즘:인코딩된값 一致。也就是说,文件名指向的不是位置,而是内容本身。布局中还必须同时存在 oci-layout 以及充当入口点的 index.json。

这里必须把界线划准确。摘要所保证的,只到“我收到的字节确实就是那些字节”为止。至于这些字节出自哪份源码、经过什么流程,它完全没有说明。 要主张这种联系,就必须另外记录构建以什么为输入、经历了什么流程,这正是来源证明(provenance)所做的事。可复现构建与它相互配对,因为它使这个主张任何人都可以重新构建来验证。

在现场相遇的样子

参考

下一项实验要做什么

只用 shell、tar、sha256sum,亲手核对同一份源码是否会产出相同的字节。先在不采取任何措施的情况下把归档构建两次,确认哈希出现分歧,然后逐一固定时间、顺序、所有者和 gzip 头,同时用眼睛追踪每一项让多少个字节发生了变动。最后整理成一个从提交时间提取 SOURCE_DATE_EPOCH 的构建脚本,并以运行两次的结果哈希是否相同来判定。这个 Pod 中无法启动容器,所以镜像方面的做法是用 skopeo 读取现成的 oci-archive 的摘要来进行核对。