BuildKit 镜像 Attestation 存储格式详解:从 OCI Artifact 到 in-toto 语句的完整剖析
【免费下载链接】buildkitconcurrent, cache-efficient, and Dockerfile-agnostic builder toolkit项目地址: https://gitcode.com/GitHub_Trending/bu/buildkit
BuildKit 在构建产物上创建并挂载 attestation(证明),为供应链安全提供 SBOM、SLSA Provenance、构建日志等可信信息。本文以 docs/attestations/attestation-storage.md 为骨架,深入剖析 attestation 在镜像中的存储格式(OCI Artifact 存储与 legacy manifest 格式)、image index → attestation manifest → attestation blob三级对象结构、贯穿其中的关键 annotation 约定,并结合仓库源码(exporter/containerimage/writer.go、exporter/attestation/make.go 等)验证每一条存储规则的底层实现。读完本文,你将能徒手解析任意 BuildKit 产出的镜像 attestation,理解oci-artifact导出选项的切换逻辑,并掌握 80 MiB 大小限制等边界行为。
背景:BuildKit 的 Attestation 是什么
Attestation 是附着在构建产物上的、来自构建过程的元数据,可用于证明产物的来源与内容。常见的 attestation 类型包括:
- SBOM(Software Bill of Materials):构建过程中使用的软件清单,通常以 SPDX 格式编码;
- SLSA Provenance:描述构建过程与供应链来源的可验证记录(SLSA Provenance 规范);
- 构建日志等其他过程信息。
在 BuildKit 中,attestation 的生成、传递与存储是一条完整链路:frontend(如 Dockerfile frontend)生成 attestation 文件 → 通过exporter.Attestation结构传递给 exporter → 由 exporter 组装为 OCI 对象写入镜像。本文聚焦链路末端:这些 attestation 最终以什么格式、什么结构、什么 annotation 被存储到镜像中。
核心结论先行:BuildKit 默认将 attestation 存储为 OCI artifact(OCI media types 启用时),并以manifest对象形式挂载在image index上;若设置镜像导出选项oci-artifact=false,则回退到 legacy attestation image manifest 格式。
存储架构总览:三级对象结构
BuildKit 的 attestation 存储遵循 OCI 规范,由三个层级组成:
- Image Index(根镜像索引):最顶层的 OCI image index,同时引用“可运行镜像 manifest”和“attestation manifest”两类描述符;
- Attestation Manifest(证明清单):一个独立的 OCI image manifest,通过
artifactType、subject、config、layers四个关键字段组织证明; - Attestation Blob(证明数据体):manifest 的每个 layer 中承载的实际 in-toto statement JSON。
其中每个 attestation manifest 可以包含多个 attestation blob,且一个 manifest 内的全部 attestation 都作用于同一个平台 manifest。这意味着:多平台构建时,每个平台的 attestation 会被分别组织为各自独立的 attestation manifest。
Attestation Manifest:证明的载体
字段语义与两种存储模式
Attestation manifest 附着在根 image index 下,作为一个独立的 OCI image manifest。它的全部属性遵循标准 OCI / Docker manifest 规范。两种存储模式的差异集中在config与artifactType字段上:
| 字段 | OCI Artifact 模式(默认) | Legacy 模式(oci-artifact=false) |
|---|---|---|
artifactType | application/vnd.docker.attestation.manifest.v1+json | 不设置 |
subject | 指向目标镜像 manifest 的 descriptor | 不设置 |
config | OCI 空 JSON descriptor(application/vnd.oci.empty.v1+json) | 指向一个合法的 image config |
layers | 每个 layer 承载一个 attestation blob | 同左 |
在 OCI artifact 模式下,manifest 的subject描述符直接指向目标镜像 manifest(digest、size、mediaType三要素齐全)。源码中该逻辑位于 exporter/containerimage/writer.go:
if ociArtifact { mfst.ArtifactType = attestationManifestArtifactType mfst.Subject = &ocispecs.Descriptor{ Digest: target.Digest, Size: target.Size, MediaType: target.MediaType, } }其中常量attestationManifestArtifactType = "application/vnd.docker.attestation.manifest.v1+json"定义于同文件 exporter/containerimage/writer.go。config使用 OCI 空 JSON descriptor(sha256:44136fa355b3...,size 为 2,data 为e30=),即ocispecs.DescriptorEmptyJSON。
在 legacy 模式下(ociArtifact为 false),config指向一个合法的 image config,由attestationsConfig()函数生成(exporter/containerimage/writer.go):其Architecture/OS固定为intotoPlatform(即unknown/unknown),RootFS.DiffIDs逐一对应每个 attestation layer 的LabelUncompressed注解。该 config不包含任何 attestation 专属信息,仅为了兼容性而存在,消费者应直接忽略其内容。
layers 与 mediaType 约定
manifest 的每个layers条目承载一个 attestation blob。layer 的mediaType依 blob 内容设定,当前唯一支持的值是:
application/vnd.in-toto+json:表示一个 in-toto attestation blob。
源码中以intoto.PayloadType作为 layer 的 mediaType(exporter/containerimage/writer.go):
desc := ocispecs.Descriptor{ MediaType: intoto.PayloadType, Digest: digest, Size: int64(len(data)), Annotations: map[string]string{ labels.LabelUncompressed: digest.String(), "in-toto.io/predicate-type": statement.PredicateType, }, }对于未知的 mediaType,规范要求忽略该 layer,这是向前兼容的关键设计——未来新增 attestation 类型不会破坏旧消费者。
层描述符上的in-toto.io/predicate-type注解
为帮助消费方在无需拉取全部内容的情况下快速定位目标 attestation,每个 layer 描述符可以携带如下注解:
in-toto.io/predicate-type:当 layer 是 in-toto attestation 时(当前唯一支持的场景)设置,其值与 attestation 内部predicateType字段完全相同。
消费方(如 source/containerimage/source.go、solver/llbsolver/history.go、vendor 中的 moby/policy-helpers/image/resolve.go)正是通过读取该注解来筛选特定 predicate 类型的证明,从而避免拉取无关 blob。
Attestation Blob:in-toto Statement 数据体
每个 layer 的内容是一个依赖mediaType的 blob。对于application/vnd.in-toto+json,blob 内容是完整的 in-toto attestation statement,结构如下:
{ "_type": "https://in-toto.io/Statement/v1", "subject": [ { "name": "<NAME>", "digest": {"<ALGORITHM>": "<HEX_VALUE>"} }, ... ], "predicateType": "<URI>", "predicate": { ... } }关键约束:statement 的subject必须与 Attestation Manifest Descriptor 中描述的目标 manifest 的 digest 一致(或者是目标 manifest 内部某个对象的 digest)。这建立了证明与产物之间的可验证绑定关系。
80 MiB 大小限制
BuildKit 在将 attestation 文件包装为 in-toto statement 之前,将每次从构建结果中读取的 attestation 文件限制为 80 MiB,以保护 exporter 免受 frontend 提供的超大 attestation 文件的无界读取。该限制的源码实现位于 exporter/attestation/make.go:
const maxAttestationBytes int64 = 80 << 20读取路径使用io.LimitedReader实现“读到 limit+1 字节即停”的检测逻辑(exporter/attestation/make.go):
func readAllLimited(r io.Reader, name string, limit int64) ([]byte, error) { limited := &io.LimitedReader{R: r, N: limit + 1} dt, err := io.ReadAll(limited) ... if limited.N == 0 { return nil, errors.Errorf("%s exceeds %d bytes", name, limit) } return dt, nil }即:若读取后limited.N == 0,说明文件长度超过 80 MiB,直接报错。解包(unbundle)方向同样施加了该限制(exporter/attestation/unbundle.go)。这一约束的实际意义在 docs/attestations/sbom.md 中有直观说明:大型 SPDX JSON 文档(含详细的 file、package、relationship 元数据)容易逼近该上限。
Attestation Manifest Descriptor:挂载与遍历约定
Attestation manifest 通过根 image index 的manifests键挂载,位置在所有原始可运行 manifest 之后。其描述符遵循标准 OCI / Docker manifest descriptor 规范,并额外附加两类关键信息:
防误拉取:platform设为unknown/unknown
为防止容器运行时意外拉取或运行 attestation manifest 所描述的镜像,其platform属性被强制设置为:
"platform": { "architecture": "unknown", "os": "unknown" }这一设计使得支持平台过滤的运行时(如 containerd、Docker)在按平台选择镜像时天然跳过这些 manifest。
辅助索引遍历:两个vnd.docker.reference.*注解
描述符上会设置以下注解,用于帮助遍历 image index、建立 attestation manifest 与目标镜像 manifest 的关联:
vnd.docker.reference.type:描述 artifact 类型,固定为attestation-manifest。若该值为其他任意值,整个 manifest 应被忽略。vnd.docker.reference.digest:包含该 attestation manifest 所指向的、image index 中目标对象的 digest。可用于为选中的镜像 manifest 反查匹配的 attestation manifest。
这两个注解的常量定义位于 util/attestation/types.go:
DockerAnnotationReferenceType = "vnd.docker.reference.type" DockerAnnotationReferenceDigest = "vnd.docker.reference.digest" DockerAnnotationReferenceTypeDefault = "attestation-manifest"写入逻辑见 exporter/containerimage/writer.go:返回的 attestation manifest 描述符同时携带DockerAnnotationReferenceType(值为attestation-manifest)与DockerAnnotationReferenceDigest(值为目标 digest 字符串)。
oci-artifact导出选项与默认值
文档明确:当 OCI media types 启用时,BuildKit 默认将 attestation 存储为 OCI artifact;设置oci-artifact=false才回退到 legacy 格式。
该选项在源码中定义于 exporter/containerimage/exptypes/keys.go:
OptKeyOCIArtifact ImageExporterOptKey = "oci-artifact"解析逻辑在 exporter/containerimage/opts.go(OCIArtifactEnabled()),并通过 opts.go 的Validate()强制校验一个冲突条件:
if c.OCIArtifactEnabled() && !c.OCITypesEnabled() { return errors.New("exporter option \"oci-artifact=true\" conflicts with \"oci-mediatypes=false\"") }即oci-artifact=true与oci-mediatypes=false互斥——legacy Docker media types 与 OCI artifact 语义无法共存。同理,oci-mediatypes=false还会与compression=zstd等仅支持 OCI media types 的压缩类型冲突(exporter/containerimage/opts.go)。
在 buildctl 或 Dockerfile frontend 中,可通过 exporter 参数传入,例如:
buildctl build \ --exporter=image \ --exporter-opt name=example.com/app:latest \ --exporter-opt oci-artifact=false(构建工具链层面还可通过--opt attest=等参数启用 SBOM/Provenance 生成;oci-artifact控制的是导出阶段的存储格式。)
实战示例:解析一个 SBOM Attestation
下面用文档中的完整示例演示三级结构的实际形态。该示例为一个附加了 SBOM attestation 的linux/amd64镜像。
第一步:Image Index(sha256:94acc2ca70c4...)
索引定义了两个描述符:AMD64 镜像sha256:23678f31..及其对应的 attestation manifestsha256:02cb9aa7..:
{ "mediaType": "application/vnd.oci.image.index.v1+json", "schemaVersion": 2, "manifests": [ { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:23678f31b3b3586c4fb318aecfe64a96a1f0916ba8faf9b2be2abee63fa9e827", "size": 1234, "platform": { "architecture": "amd64", "os": "linux" } }, { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:02cb9aa7600e73fcf41ee9f0f19cc03122b2d8be43d41ce4b21335118f5dd943", "size": 1234, "annotations": { "vnd.docker.reference.digest": "sha256:23678f31b3b3586c4fb318aecfe64a96a1f0916ba8faf9b2be2abee63fa9e827", "vnd.docker.reference.type": "attestation-manifest" }, "platform": { "architecture": "unknown", "os": "unknown" } } ] }注意观察:attestation manifest 描述符的platform为unknown/unknown,annotations 中的vnd.docker.reference.digest精确指向第一个可运行镜像描述符的 digest——这正是上一节所述“防误拉取 + 辅助遍历”两大约定的直接体现。
第二步:Attestation Manifest(sha256:02cb9aa7...)
该 manifest 包含一个 in-toto attestation,其 predicate 为https://spdx.dev/Document,表明这是镜像的 SBOM:
{ "mediaType": "application/vnd.oci.image.manifest.v1+json", "schemaVersion": 2, "artifactType": "application/vnd.docker.attestation.manifest.v1+json", "config": { "mediaType": "application/vnd.oci.empty.v1+json", "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", "size": 2, "data": "e30=" }, "layers": [ { "mediaType": "application/vnd.in-toto+json", "digest": "sha256:133ae3f9bcc385295b66c2d83b28c25a9f294ce20954d5cf922dda860429734a", "size": 1234, "annotations": { "in-toto.io/predicate-type": "https://spdx.dev/Document" } } ], "subject": { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:23678f31b3b3586c4fb318aecfe64a96a1f0916ba8faf9b2be2abee63fa9e827", "size": 1234 } }关键点逐一对应前文规则:artifactType为 attestation manifest 专用值;config是 OCI 空 JSON;layers只有一个application/vnd.in-toto+json层并带in-toto.io/predicate-type注解;subject.digest与 image index 中第一个描述符的 digest 完全一致。
第三步:Layer 内容(SBOM 数据体)
layer digest 为sha256:1ea07d5e55eb...(示例中与 manifest 内层描述符 digest 不同的展示口径不影响结构理解),其内容是包装为 in-toto statement 的 SPDX SBOM:
{ "_type": "https://in-toto.io/Statement/v1", "predicateType": "https://spdx.dev/Document", "subject": [ { "name": "_", "digest": { "sha256": "23678f31b3b3586c4fb318aecfe64a96a1f0916ba8faf9b2be2abee63fa9e827" } } ], "predicate": { "SPDXID": "SPDXRef-DOCUMENT", "spdxVersion": "SPDX-2.2", ... } }predicateType(https://spdx.dev/Document)与 manifest 层描述符上的in-toto.io/predicate-type注解一致——这使消费方仅凭 manifest 元数据即可完成筛选,无需下载任何 blob。subject[0].digest.sha256再次指向目标镜像 manifest,完成证明与产物的绑定。
消费侧视角:如何遍历与校验
理解了存储格式后,消费方(运行时、策略引擎、审计工具)的遍历算法可以归纳为三步:
- 遍历根 image index 的
manifests,筛选annotations["vnd.docker.reference.type"] == "attestation-manifest"的描述符(其他值一律忽略);通过vnd.docker.reference.digest匹配目标镜像 digest,定位对应的 attestation manifest; - 读取 attestation manifest,按需利用各 layer 描述符的
in-toto.io/predicate-type注解跳过无关层,仅拉取目标 predicate 类型的 blob; - 解析 blob 内容为 in-toto statement,校验
subject与目标镜像 digest 一致,再按predicateType解析predicate(如 SPDX SBOM、SLSA Provenance)。
仓库中的测试用例印证了这一消费路径。例如 client/client_export_metadata_test.go 断言导出的 attestation manifest 的ArtifactType恰为application/vnd.docker.attestation.manifest.v1+json;frontend/dockerfile/dockerfile_provenance_test.go 验证vnd.docker.reference.digest等于镜像 digest、vnd.docker.reference.type为attestation-manifest;client/client_export_metadata_test.go 与 client/compatibility_test.go 则检查 layer 注解in-toto.io/predicate-type是否为 SLSA/SPDX predicate 类型。这些测试同时充当了格式规范的“可执行文档”。
常见问题与边界行为
- 未知 mediaType 的 layer 如何处理?忽略。这是格式演进的前向兼容设计,未来新增 attestation 类型不影响旧消费者。
oci-artifact=true与oci-mediatypes=false能否同时使用?不能,exporter/containerimage/opts.go 会直接返回配置冲突错误。- Legacy 模式下为何还要写一个 image config?仅为兼容性——早期工具链期望 manifest 携带合法 config;其内容不含 attestation 信息,应被忽略。
- attestation 文件超过 80 MiB 会怎样?导出失败并报
exceeds 83886080 bytes类错误,这是有意为之的防护性限制(exporter/attestation/make.go)。 - 一个 manifest 能放多个 attestation 吗?能。多个 blob 共享同一个平台 manifest 与同一个
subject,各自以独立 layer 呈现。
延伸阅读
- SBOM 生成与格式约定:SPDX 文档如何生成、80 MiB 限制的实际影响;
- SLSA Provenance 定义 与 SLSA 定义:Provenance 谓词的字段语义;
- Attestation 协议说明:SBOM 在 frontend 与 exporter 之间的传递协议;
- 核心实现:exporter/containerimage/writer.go(attestation manifest 写入)、exporter/attestation/make.go(blob 读取与 80 MiB 限制)、util/attestation/types.go(annotation 常量);
- 消费侧参考:vendor 中 moby/policy-helpers/image/resolve.go 展示了如何基于本格式在策略验证中解析 attestation。
【免费下载链接】buildkitconcurrent, cache-efficient, and Dockerfile-agnostic builder toolkit项目地址: https://gitcode.com/GitHub_Trending/bu/buildkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考