diff --git a/cmd/convertor/builder/builder.go b/cmd/convertor/builder/builder.go index 3c59359c..39e06477 100644 --- a/cmd/convertor/builder/builder.go +++ b/cmd/convertor/builder/builder.go @@ -91,6 +91,20 @@ type graphBuilder struct { id atomic.Int32 } +const ( + attestationManifestAnnotation = "vnd.docker.reference.type" + attestationManifestType = "attestation-manifest" +) + +// isAttestationManifest reports whether desc is a BuildKit attestation manifest +// (provenance/SBOM). Detection uses only the explicit +// vnd.docker.reference.type=attestation-manifest annotation: the accompanying +// unknown/unknown platform is a BuildKit convention shared by other artifacts, +// so it is not a reliable identifier on its own. +func isAttestationManifest(desc v1.Descriptor) bool { + return desc.Annotations[attestationManifestAnnotation] == attestationManifestType +} + func (b *graphBuilder) Build(ctx context.Context) error { fetcher, err := b.Resolver.Fetcher(ctx, b.Ref) if err != nil { @@ -150,6 +164,17 @@ func (b *graphBuilder) process(ctx context.Context, src v1.Descriptor, tag bool) for _i, _m := range index.Manifests { i := _i m := _m + if isAttestationManifest(m) { + // BuildKit attestation manifests (provenance/SBOM) carry no + // rootfs layers and reference a source platform manifest by + // digest (vnd.docker.reference.digest). That digest changes on + // conversion, so the attestation cannot be re-associated and + // converting its zero-layer manifest panics. Drop it from the + // converted index (with a warning) rather than emitting a stale + // reference. + log.G(ctx).Warnf("skipping attestation manifest %s: cannot be re-associated after conversion", m.Digest) + continue + } wg.Add(1) b.group.Go(func() error { defer wg.Done() @@ -166,6 +191,16 @@ func (b *graphBuilder) process(ctx context.Context, src v1.Descriptor, tag bool) return v1.Descriptor{}, ctx.Err() } + // Drop attestation manifests from the converted index: their + // source-digest references no longer point at an image in the index. + filtered := index.Manifests[:0] + for _, m := range index.Manifests { + if !isAttestationManifest(m) { + filtered = append(filtered, m) + } + } + index.Manifests = filtered + // upload index if b.Referrer { index.ArtifactType = b.Engine.ArtifactType() diff --git a/cmd/convertor/builder/builder_attestation_test.go b/cmd/convertor/builder/builder_attestation_test.go new file mode 100644 index 00000000..4dd154cd --- /dev/null +++ b/cmd/convertor/builder/builder_attestation_test.go @@ -0,0 +1,78 @@ +/* + Copyright The Accelerated Container Image Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package builder + +import ( + "testing" + + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +func TestIsAttestationManifest(t *testing.T) { + tests := []struct { + name string + desc v1.Descriptor + want bool + }{ + { + name: "attestation annotation", + desc: v1.Descriptor{ + Annotations: map[string]string{ + "vnd.docker.reference.type": "attestation-manifest", + }, + }, + want: true, + }, + { + name: "attestation annotation with unknown platform", + desc: v1.Descriptor{ + Annotations: map[string]string{ + "vnd.docker.reference.type": "attestation-manifest", + }, + Platform: &v1.Platform{OS: "unknown", Architecture: "unknown"}, + }, + want: true, + }, + { + name: "unknown platform without attestation annotation is not an attestation", + desc: v1.Descriptor{ + Platform: &v1.Platform{OS: "unknown", Architecture: "unknown"}, + }, + want: false, + }, + { + name: "normal image manifest", + desc: v1.Descriptor{ + Platform: &v1.Platform{OS: "linux", Architecture: "amd64"}, + }, + want: false, + }, + { + name: "descriptor without attestation metadata", + desc: v1.Descriptor{}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isAttestationManifest(tt.desc); got != tt.want { + t.Errorf("isAttestationManifest() = %v, want %v", got, tt.want) + } + }) + } +}