BuildKit 镜像 Attestation 存储格式详解:从 OCI Artifact 到 in-toto 语句的完整剖析
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 manifest sha256: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。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.24 K638- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python650
SlideSCIPPT插件,支持素材库、AI助手、一键添加图片标题,复制粘贴位置、一键图片对齐、一键插入Markdown(加粗、超链接等行内样式、代码块、LaTeX等块级样式)、便捷导出图片!C#180
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python52774
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go22545
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java36351