1
0
mirror of synced 2026-10-29 01:59:38 +03:00
Files
cosign/pkg/oci/layout/write_test.go
Ali Asghar 43d83e8490 fix(layout): preserve manifest artifactType in index.json when saving (#4821)
When cosign save writes an OCI artifact to disk, partial.Descriptor falls
back to config.mediaType as the artifactType in the index.json descriptor
because go-containerregistry's v1.Manifest struct does not expose the
manifest-level artifactType field. The result is that config.mediaType
appears as the artifactType entry in index.json instead of the correct
manifest artifactType, violating the OCI Image Layout spec.

Fix appendImage to parse the raw manifest JSON and override the descriptor's
ArtifactType field when a top-level artifactType is present. The existing
config.mediaType fallback still applies when artifactType is absent.

Fixes #4694

Signed-off-by: Ali <alliasgher123@gmail.com>
2026-09-14 16:24:02 -07:00

200 lines
5.0 KiB
Go

//
// Copyright 2021 The Sigstore 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 layout
import (
"bytes"
"fmt"
"runtime"
"testing"
"github.com/google/go-cmp/cmp"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/sigstore/cosign/v3/pkg/oci"
"github.com/sigstore/cosign/v3/pkg/oci/mutate"
"github.com/sigstore/cosign/v3/pkg/oci/signed"
"github.com/sigstore/cosign/v3/pkg/oci/static"
)
func TestReadWrite(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("test is flaky on windows, see https://github.com/sigstore/cosign/issues/1389")
}
// write random signed image to disk
si := randomSignedImage(t)
tmp := t.TempDir()
if err := WriteSignedImage(tmp, si); err != nil {
t.Fatal(err)
}
// read the image and make sure the signatures exist
imageIndex, err := SignedImageIndex(tmp)
if err != nil {
t.Fatal(err)
}
gotSignedImage, err := imageIndex.SignedImage(v1.Hash{})
if err != nil {
t.Fatal(err)
}
// compare the image we read with the one we wrote
compareDigests(t, si, gotSignedImage)
// make sure we have 5 attestations
attImg, err := imageIndex.Attestations()
if err != nil {
t.Fatal(err)
}
atts, err := attImg.Get()
if err != nil {
t.Fatal(err)
}
if len(atts) != 5 {
t.Fatal("expected 5 attestations")
}
// make sure signatures are correct
sigImage, err := imageIndex.Signatures()
if err != nil {
t.Fatal(err)
}
sigs, err := sigImage.Get()
if err != nil {
t.Fatal(err)
}
want := 6
if len(sigs) != want {
t.Fatal("didn't get the expected number of signatures")
}
// make sure the annotation is correct
for i, sig := range sigs {
annotations, err := sig.Annotations()
if err != nil {
t.Fatal(err)
}
val, ok := annotations["layer"]
if !ok {
t.Fatal("expected annotation doesn't exist on signature")
}
if val != fmt.Sprintf("%d", i) {
t.Fatal("expected annotation isn't correct")
}
}
}
func randomSignedImage(t *testing.T) oci.SignedImage {
i, err := random.Image(300 /* byteSize */, 7 /* layers */)
if err != nil {
t.Fatalf("random.Image() = %v", err)
}
si := signed.Image(i)
want := 6 // Add 6 signatures
for i := 0; i < want; i++ {
annotationOption := static.WithAnnotations(map[string]string{"layer": fmt.Sprintf("%d", i)})
sig, err := static.NewSignature(nil, fmt.Sprintf("%d", i), annotationOption)
if err != nil {
t.Fatalf("static.NewSignature() = %v", err)
}
si, err = mutate.AttachSignatureToImage(si, sig)
if err != nil {
t.Fatalf("SignEntity() = %v", err)
}
}
want = 5 // Add 5 attestations
for i := 0; i < want; i++ {
sig, err := static.NewAttestation([]byte(fmt.Sprintf("%d", i)))
if err != nil {
t.Fatalf("static.NewSignature() = %v", err)
}
si, err = mutate.AttachAttestationToImage(si, sig)
if err != nil {
t.Fatalf("SignEntity() = %v", err)
}
}
return si
}
func compareDigests(t *testing.T, img1 oci.SignedImage, img2 oci.SignedImage) {
d1, err := img1.Digest()
if err != nil {
t.Fatal(err)
}
d2, err := img2.Digest()
if err != nil {
t.Fatal(err)
}
if d := cmp.Diff(d1, d2); d != "" {
t.Fatalf("digests are different: %s", d)
}
}
// artifactTypeImage wraps a v1.Image and injects an artifactType field into
// the raw manifest so we can test that cosign save preserves it in index.json.
type artifactTypeImage struct {
v1.Image
artifactType string
}
func (a *artifactTypeImage) RawManifest() ([]byte, error) {
raw, err := a.Image.RawManifest()
if err != nil {
return nil, err
}
// Inject the artifactType field before the closing brace.
insert := fmt.Sprintf(`, "artifactType": %q}`, a.artifactType)
raw = append(bytes.TrimRight(raw, " \t\r\n}"), []byte(insert)...)
return raw, nil
}
func TestAppendImagePreservesArtifactType(t *testing.T) {
const wantArtifactType = "application/vnd.example.test+json"
base, err := random.Image(1024, 1)
if err != nil {
t.Fatal(err)
}
img := &artifactTypeImage{Image: base, artifactType: wantArtifactType}
dir := t.TempDir()
if err := WriteSignedImage(dir, signed.Image(img)); err != nil {
t.Fatalf("WriteSignedImage: %v", err)
}
sii, err := SignedImageIndex(dir)
if err != nil {
t.Fatalf("SignedImageIndex: %v", err)
}
idx, err := sii.IndexManifest()
if err != nil {
t.Fatal(err)
}
found := false
for _, m := range idx.Manifests {
if m.ArtifactType == wantArtifactType {
found = true
break
}
}
if !found {
t.Errorf("artifactType %q not found in index.json manifests: %+v", wantArtifactType, idx.Manifests)
}
}