首页
/ BuildKit 镜像 Attestation 存储格式详解:从 OCI Artifact 到 in-toto 语句的完整剖析

BuildKit 镜像 Attestation 存储格式详解:从 OCI Artifact 到 in-toto 语句的完整剖析

2026-09-14 14:04:16作者:何举烈Damon

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.goexporter/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 规范,由三个层级组成:

  1. Image Index(根镜像索引):最顶层的 OCI image index,同时引用“可运行镜像 manifest”和“attestation manifest”两类描述符;
  2. Attestation Manifest(证明清单):一个独立的 OCI image manifest,通过 artifactTypesubjectconfiglayers 四个关键字段组织证明;
  3. 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 规范。两种存储模式的差异集中在 configartifactType 字段上:

字段 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(digestsizemediaType 三要素齐全)。源码中该逻辑位于 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.goconfig 使用 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.gosolver/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.goOCIArtifactEnabled()),并通过 opts.goValidate() 强制校验一个冲突条件:

if c.OCIArtifactEnabled() && !c.OCITypesEnabled() {
	return errors.New("exporter option \"oci-artifact=true\" conflicts with \"oci-mediatypes=false\"")
}

oci-artifact=trueoci-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 描述符的 platformunknown/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",
    ...
  }
}

predicateTypehttps://spdx.dev/Document)与 manifest 层描述符上的 in-toto.io/predicate-type 注解一致——这使消费方仅凭 manifest 元数据即可完成筛选,无需下载任何 blobsubject[0].digest.sha256 再次指向目标镜像 manifest,完成证明与产物的绑定。

消费侧视角:如何遍历与校验

理解了存储格式后,消费方(运行时、策略引擎、审计工具)的遍历算法可以归纳为三步:

  1. 遍历根 image index 的 manifests,筛选 annotations["vnd.docker.reference.type"] == "attestation-manifest" 的描述符(其他值一律忽略);通过 vnd.docker.reference.digest 匹配目标镜像 digest,定位对应的 attestation manifest;
  2. 读取 attestation manifest,按需利用各 layer 描述符的 in-toto.io/predicate-type 注解跳过无关层,仅拉取目标 predicate 类型的 blob;
  3. 解析 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+jsonfrontend/dockerfile/dockerfile_provenance_test.go 验证 vnd.docker.reference.digest 等于镜像 digest、vnd.docker.reference.typeattestation-manifestclient/client_export_metadata_test.goclient/compatibility_test.go 则检查 layer 注解 in-toto.io/predicate-type 是否为 SLSA/SPDX predicate 类型。这些测试同时充当了格式规范的“可执行文档”。

常见问题与边界行为

  • 未知 mediaType 的 layer 如何处理? 忽略。这是格式演进的前向兼容设计,未来新增 attestation 类型不影响旧消费者。
  • oci-artifact=trueoci-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 呈现。

延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐