From 5867094e3b12491c5244d494150613dba33cfdb7 Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Tue, 2 Sep 2025 12:52:04 +0000 Subject: [PATCH 01/22] added signature support for oci layout Signed-off-by: Ayato Tokubi --- ...352621630bcfdb5cef147fa445f2e630e49f7a9910 | 1 + ...ea7ad4f8a5f0e23bc16068d612227507e54599c18a | 1 + ...b1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 | 1 + ...1769692e4669470de97718d2ffab83273ea57474cb | 43 +++++ .../delete_image_with_signature/index.json | 1 + .../delete_image_with_signature/oci-layout | 1 + image/oci/layout/oci_delete.go | 39 ++++- image/oci/layout/oci_delete_test.go | 23 +++ image/oci/layout/oci_dest.go | 159 +++++++++++++++++- image/oci/layout/oci_dest_test.go | 87 ++++++++++ image/oci/layout/oci_src.go | 56 +++++- image/oci/layout/oci_transport.go | 8 + 12 files changed, 411 insertions(+), 9 deletions(-) create mode 100644 image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/2558a560cb1cd192d12862352621630bcfdb5cef147fa445f2e630e49f7a9910 create mode 100644 image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a create mode 100644 image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 create mode 100644 image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb create mode 100644 image/oci/layout/fixtures/delete_image_with_signature/index.json create mode 100644 image/oci/layout/fixtures/delete_image_with_signature/oci-layout diff --git a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/2558a560cb1cd192d12862352621630bcfdb5cef147fa445f2e630e49f7a9910 b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/2558a560cb1cd192d12862352621630bcfdb5cef147fa445f2e630e49f7a9910 new file mode 100644 index 0000000000..a0990a01a7 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/2558a560cb1cd192d12862352621630bcfdb5cef147fa445f2e630e49f7a9910 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a","size":147},"layers":[{"mediaType":"application/vnd.dev.cosign.simplesigning.v1+json","digest":"sha256:6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273","size":12,"annotations":{"dev.cosignproject.cosign/signature":"test-signature"}}],"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb","size":1506,"annotations":{"org.opencontainers.image.ref.name":"imageValue"}}} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a new file mode 100644 index 0000000000..bf54928cb8 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a @@ -0,0 +1 @@ +{"architecture":"","os":"","config":{},"rootfs":{"type":"","diff_ids":["sha256:6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273"]}} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 new file mode 100644 index 0000000000..adb6ebe808 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 @@ -0,0 +1 @@ +test-payload \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb new file mode 100644 index 0000000000..26efc23db7 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb @@ -0,0 +1,43 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 5940, + "digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f", + "annotations": { + "test-annotation-1": "one" + } + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 51354364, + "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 150, + "digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 11739507, + "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + "urls": ["https://layer.url"] + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 8841833, + "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + "annotations": { + "test-annotation-2": "two" + } + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 291, + "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" + } + ] +} diff --git a/image/oci/layout/fixtures/delete_image_with_signature/index.json b/image/oci/layout/fixtures/delete_image_with_signature/index.json new file mode 100644 index 0000000000..266e2dbdee --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_with_signature/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb","size":1506,"annotations":{"org.opencontainers.image.ref.name":"imageValue"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:2558a560cb1cd192d12862352621630bcfdb5cef147fa445f2e630e49f7a9910","size":704,"annotations":{"org.opencontainers.image.ref.name":"sha256-bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb.sig"}}]} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_with_signature/oci-layout b/image/oci/layout/fixtures/delete_image_with_signature/oci-layout new file mode 100644 index 0000000000..1343d370fa --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_with_signature/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion":"1.0.0"} \ No newline at end of file diff --git a/image/oci/layout/oci_delete.go b/image/oci/layout/oci_delete.go index 7eaf6f0889..a852609538 100644 --- a/image/oci/layout/oci_delete.go +++ b/image/oci/layout/oci_delete.go @@ -3,10 +3,12 @@ package layout import ( "context" "encoding/json" + "errors" "fmt" "io/fs" "os" "slices" + "strings" digest "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -42,7 +44,15 @@ func (ref ociReference) DeleteImage(ctx context.Context, sys *types.SystemContex return err } - return ref.deleteReferenceFromIndex(descriptorIndex) + err = ref.deleteReferenceFromIndex(descriptorIndex) + if err != nil { + return err + } + + if isSigstoreTag(ref.image) { + return nil + } + return ref.deleteSignatures(ctx, sys, descriptor.Digest) } // countBlobsForDescriptor updates dest with usage counts of blobs required for descriptor, INCLUDING descriptor itself. @@ -187,3 +197,30 @@ func saveJSON(path string, content any) (retErr error) { return json.NewEncoder(file).Encode(content) } + +func (ref ociReference) deleteSignatures(ctx context.Context, sys *types.SystemContext, d digest.Digest) error { + signTag, err := sigstoreAttachmentTag(d) + if err != nil { + return err + } + + signRef, err := newReference(ref.dir, signTag, -1) + if err != nil { + return err + } + + err = signRef.DeleteImage(ctx, sys) + if err != nil && errors.As(err, &ImageNotFoundError{}) { + return nil + } + return err +} + +func isSigstoreTag(tag string) bool { + if !strings.HasSuffix(tag, ".sig") { + return false + } + digestPart := strings.TrimSuffix(tag, ".sig") + digestPart = strings.Replace(digestPart, "-", ":", 1) + return digest.Digest(digestPart).Validate() == nil +} diff --git a/image/oci/layout/oci_delete_test.go b/image/oci/layout/oci_delete_test.go index a80bf04177..f2e78e51c8 100644 --- a/image/oci/layout/oci_delete_test.go +++ b/image/oci/layout/oci_delete_test.go @@ -40,6 +40,29 @@ func TestReferenceDeleteImage_onlyOneImage(t *testing.T) { require.Equal(t, 0, len(index.Manifests)) } +func TestReferenceDeleteImage_onlyOneImageWithSignatures(t *testing.T) { + tmpDir := loadFixture(t, "delete_image_with_signature") + + ref, err := NewReference(tmpDir, "imageValue") + require.NoError(t, err) + + err = ref.DeleteImage(context.Background(), nil) + require.NoError(t, err) + + // Check that all blobs were deleted + blobsDir := filepath.Join(tmpDir, "blobs") + files, err := os.ReadDir(filepath.Join(blobsDir, "sha256")) + require.NoError(t, err) + require.Empty(t, files) + + // Check that the index is empty as there is only one image in the fixture + ociRef, ok := ref.(ociReference) + require.True(t, ok) + index, err := ociRef.getIndex() + require.NoError(t, err) + require.Equal(t, 0, len(index.Manifests)) +} + func TestReferenceDeleteImage_onlyOneImage_emptyImageName(t *testing.T) { tmpDir := loadFixture(t, "delete_image_only_one_image") diff --git a/image/oci/layout/oci_dest.go b/image/oci/layout/oci_dest.go index 48fe812df5..231acd3de4 100644 --- a/image/oci/layout/oci_dest.go +++ b/image/oci/layout/oci_dest.go @@ -1,6 +1,7 @@ package layout import ( + "bytes" "context" "encoding/json" "errors" @@ -12,14 +13,17 @@ import ( "runtime" "slices" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" imgspec "github.com/opencontainers/image-spec/specs-go" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/sirupsen/logrus" "go.podman.io/image/v5/internal/imagedestination/impl" "go.podman.io/image/v5/internal/imagedestination/stubs" - "go.podman.io/image/v5/internal/manifest" "go.podman.io/image/v5/internal/private" "go.podman.io/image/v5/internal/putblobdigest" + "go.podman.io/image/v5/internal/signature" + "go.podman.io/image/v5/manifest" + "go.podman.io/image/v5/pkg/blobinfocache/none" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/fileutils" ) @@ -29,11 +33,12 @@ type ociImageDestination struct { impl.PropertyMethodsInitialize stubs.IgnoresOriginalOCIConfig stubs.NoPutBlobPartialInitialize - stubs.NoSignaturesInitialize - ref ociReference - index imgspecv1.Index - sharedBlobDir string + ref ociReference + index imgspecv1.Index + sharedBlobDir string + sys *types.SystemContext + manifestDigest digest.Digest } // newImageDestination returns an ImageDestination for writing to an existing directory. @@ -75,10 +80,10 @@ func newImageDestination(sys *types.SystemContext, ref ociReference) (private.Im HasThreadSafePutBlob: true, }), NoPutBlobPartialInitialize: stubs.NoPutBlobPartial(ref), - NoSignaturesInitialize: stubs.NoSignatures("Pushing signatures for OCI images is not supported"), ref: ref, index: *index, + sys: sys, } d.Compat = impl.AddCompat(d) if sys != nil { @@ -251,6 +256,7 @@ func (d *ociImageDestination) PutManifest(ctx context.Context, m []byte, instanc if err := os.WriteFile(blobPath, m, 0644); err != nil { return err } + d.manifestDigest = digest if instanceDigest != nil { return nil @@ -322,6 +328,145 @@ func (d *ociImageDestination) CommitWithOptions(ctx context.Context, options pri return os.WriteFile(d.ref.indexPath(), indexJSON, 0644) } +func (d *ociImageDestination) PutSignaturesWithFormat(ctx context.Context, signatures []signature.Signature, instanceDigest *digest.Digest) error { + if instanceDigest == nil { + if d.manifestDigest == "" { + return errors.New("unknown manifest digest, can't add signatures") + } + instanceDigest = &d.manifestDigest + } + + sigstoreSignatures := []signature.Sigstore{} + for _, sig := range signatures { + if sigstoreSig, ok := sig.(signature.Sigstore); ok { + sigstoreSignatures = append(sigstoreSignatures, sigstoreSig) + } else { + return errors.New("OCI Layout only supports sigstoreSignatures") + } + } + + err := d.putSignaturesToSigstoreAttachment(ctx, sigstoreSignatures, *instanceDigest) + if err != nil { + return err + } + + return nil +} + +func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Context, signatures []signature.Sigstore, manifestDigest digest.Digest) error { + var signConfig imgspecv1.Image // Most fields empty by default + + signManifest := manifest.OCI1FromComponents(imgspecv1.Descriptor{ + MediaType: imgspecv1.MediaTypeImageConfig, + Digest: "", // We will fill this in later. + Size: 0, + }, nil) + + desc, err := d.getDescriptor(&manifestDigest) + if err != nil { + return err + } + signManifest.Subject = desc + + // To make sure we can safely append to the slices of signManifest, without adding a remote dependency on the code that creates it. + signManifest.Layers = slices.Clone(signManifest.Layers) + for _, sig := range signatures { + mimeType := sig.UntrustedMIMEType() + payloadBlob := sig.UntrustedPayload() + annotations := sig.UntrustedAnnotations() + + sigDesc, err := d.putBlobBytesAsOCI(ctx, payloadBlob, mimeType, private.PutBlobOptions{ + Cache: none.NoCache, + IsConfig: false, + EmptyLayer: false, + LayerIndex: nil, + }) + if err != nil { + return err + } + sigDesc.Annotations = annotations + signManifest.Layers = append(signManifest.Layers, sigDesc) + signConfig.RootFS.DiffIDs = append(signConfig.RootFS.DiffIDs, sigDesc.Digest) + logrus.Debugf("Adding new signature, digest %s", sigDesc.Digest.String()) + } + + configBlob, err := json.Marshal(signConfig) + if err != nil { + return err + } + logrus.Debugf("Creating updated sigstore attachment config") + configDesc, err := d.putBlobBytesAsOCI(ctx, configBlob, imgspecv1.MediaTypeImageConfig, private.PutBlobOptions{ + Cache: none.NoCache, + IsConfig: true, + EmptyLayer: false, + LayerIndex: nil, + }) + if err != nil { + return err + } + + signManifest.Config = configDesc + signManifestBlob, err := signManifest.Serialize() + if err != nil { + return err + } + logrus.Debugf("Creating sigstore attachment manifest") + signDigest := digest.FromBytes(signManifestBlob) + if err = d.PutManifest(ctx, signManifestBlob, &signDigest); err != nil { + return err + } + signTag, err := sigstoreAttachmentTag(manifestDigest) + if err != nil { + return err + } + d.addManifest(&imgspecv1.Descriptor{ + MediaType: signManifest.MediaType, + Digest: signDigest, + Size: int64(len(signManifestBlob)), + Annotations: map[string]string{ + imgspecv1.AnnotationRefName: signTag, + }, + }) + + return nil +} + +func (d *ociImageDestination) getDescriptor(digest *digest.Digest) (*imgspecv1.Descriptor, error) { + if digest == nil { + return nil, errors.New("digest is nil") + } + for _, desc := range d.index.Manifests { + if desc.Digest == *digest { + return &desc, nil + } + } + return nil, fmt.Errorf("manifest %s not found in index", digest.String()) +} + +// putBlobBytesAsOCI uploads a blob with the specified contents, and returns an appropriate +// OCI descriptor. +func (d *ociImageDestination) putBlobBytesAsOCI(ctx context.Context, contents []byte, mimeType string, options private.PutBlobOptions) (imgspecv1.Descriptor, error) { + blobDigest := digest.FromBytes(contents) + info, err := d.PutBlobWithOptions(ctx, bytes.NewReader(contents), + types.BlobInfo{ + Digest: blobDigest, + Size: int64(len(contents)), + MediaType: mimeType, + }, options) + if err != nil { + return imgspecv1.Descriptor{}, fmt.Errorf("writing blob %s: %w", blobDigest.String(), err) + } + return imgspecv1.Descriptor{ + MediaType: mimeType, + Digest: info.Digest, + Size: info.Size, + }, nil +} + +func (d *ociImageDestination) SupportsSignatures(ctx context.Context) error { + return nil +} + // PutBlobFromLocalFileOption is unused but may receive functionality in the future. type PutBlobFromLocalFileOption struct{} diff --git a/image/oci/layout/oci_dest_test.go b/image/oci/layout/oci_dest_test.go index 464fc32d3e..623fca58b5 100644 --- a/image/oci/layout/oci_dest_test.go +++ b/image/oci/layout/oci_dest_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.podman.io/image/v5/internal/private" + "go.podman.io/image/v5/internal/signature" "go.podman.io/image/v5/pkg/blobinfocache/memory" "go.podman.io/image/v5/types" ) @@ -216,3 +217,89 @@ func TestPutblobFromLocalFile(t *testing.T) { err = ociDest.CommitWithOptions(context.Background(), private.CommitOptions{}) require.NoError(t, err) } + +// TestPutSignaturesWithFormat tests that sigstore signatures are properly stored in OCI layout +func TestPutSignaturesWithFormat(t *testing.T) { + ref, tmpDir := refToTempOCI(t, false) + ociRef, ok := ref.(ociReference) + require.True(t, ok) + putTestManifest(t, ociRef, tmpDir) + + dest, err := ref.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dest.Close() + ociDest, ok := dest.(*ociImageDestination) + require.True(t, ok) + + desc, _, err := ociDest.ref.getManifestDescriptor() + require.NoError(t, err) + require.NotNil(t, desc) + + sigstoreSign := signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature"}, + ) + + err = ociDest.PutSignaturesWithFormat(context.Background(), []signature.Signature{sigstoreSign}, &desc.Digest) + require.NoError(t, err) + + err = ociDest.Commit(context.Background(), nil) + require.NoError(t, err) + + src, err := ref.NewImageSource(context.Background(), nil) + require.NoError(t, err) + ociSrc, ok := src.(*ociImageSource) + require.True(t, ok) + sign, err := ociSrc.GetSignaturesWithFormat(context.Background(), &desc.Digest) + require.NoError(t, err) + require.Len(t, sign, 1) + require.Equal(t, sigstoreSign, sign[0]) +} + +// TestPutSignaturesWithFormatNilDigest tests error handling when instanceDigest is nil +func TestPutSignaturesWithFormatNilDigest(t *testing.T) { + ref, _ := refToTempOCI(t, false) + + dest, err := ref.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dest.Close() + + // Cast to ociImageDestination to access PutSignaturesWithFormat + ociDest, ok := dest.(*ociImageDestination) + require.True(t, ok) + + // Create a test signature + testPayload := []byte(`{"test": "payload"}`) + testAnnotations := map[string]string{ + "dev.cosignproject.cosign/signature": "test-signature", + } + sig := signature.SigstoreFromComponents("application/vnd.dev.cosign.simplesigning.v1+json", testPayload, testAnnotations) + + // Test that PutSignaturesWithFormat fails when instanceDigest is nil + err = ociDest.PutSignaturesWithFormat(context.Background(), []signature.Signature{sig}, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "unknown manifest digest, can't add signatures") +} + +// TestPutSignaturesWithFormatNonSigstore tests error handling for non-sigstore signatures +func TestPutSignaturesWithFormatNonSigstore(t *testing.T) { + ref, _ := refToTempOCI(t, false) + + dest, err := ref.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dest.Close() + + // Cast to ociImageDestination to access PutSignaturesWithFormat + ociDest, ok := dest.(*ociImageDestination) + require.True(t, ok) + + // Create a non-sigstore signature (simple signing) + simpleSig := signature.SimpleSigningFromBlob([]byte("simple signature data")) + testDigest := digest.FromString("test-manifest") + + // Test that PutSignaturesWithFormat fails for non-sigstore signatures + err = ociDest.PutSignaturesWithFormat(context.Background(), []signature.Signature{simpleSig}, &testDigest) + require.Error(t, err) + require.Contains(t, err.Error(), "OCI Layout only supports sigstoreSignatures") +} diff --git a/image/oci/layout/oci_src.go b/image/oci/layout/oci_src.go index f265a21d70..81371e5c8c 100644 --- a/image/oci/layout/oci_src.go +++ b/image/oci/layout/oci_src.go @@ -16,8 +16,11 @@ import ( imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "go.podman.io/image/v5/internal/imagesource/impl" "go.podman.io/image/v5/internal/imagesource/stubs" - "go.podman.io/image/v5/internal/manifest" + "go.podman.io/image/v5/internal/iolimits" "go.podman.io/image/v5/internal/private" + "go.podman.io/image/v5/internal/signature" + "go.podman.io/image/v5/manifest" + "go.podman.io/image/v5/pkg/blobinfocache/none" "go.podman.io/image/v5/pkg/tlsclientconfig" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/fileutils" @@ -246,3 +249,54 @@ func GetLocalBlobPath(ctx context.Context, src types.ImageSource, digest digest. return path, nil } + +func (s *ociImageSource) GetSignaturesWithFormat(ctx context.Context, instanceDigest *digest.Digest) ([]signature.Signature, error) { + if instanceDigest == nil { + if s.descriptor.Digest == "" { + return nil, errors.New("unknown manifest digest, can't add signatures") + } + instanceDigest = &s.descriptor.Digest + } + signTag, err := sigstoreAttachmentTag(*instanceDigest) + if err != nil { + return nil, err + } + + var signDigest *digest.Digest + for _, m := range s.index.Manifests { + if m.Annotations[imgspecv1.AnnotationRefName] == signTag { + signDigest = &m.Digest + break + } + } + if signDigest == nil { + return nil, errors.New("no signature found for image") + } + signBlob, _, err := s.GetManifest(ctx, signDigest) + if err != nil { + return nil, err + } + ociManifest, err := manifest.OCI1FromManifest(signBlob) + if err != nil { + return nil, err + } + + signatures := make([]signature.Signature, 0, len(ociManifest.Layers)) + for _, layer := range ociManifest.Layers { + layerBlob, _, err := s.GetBlob(ctx, types.BlobInfo{Digest: layer.Digest}, none.NoCache) + if err != nil { + return nil, err + } + defer layerBlob.Close() + payload, err := iolimits.ReadAtMost(layerBlob, iolimits.MaxSignatureBodySize) + if err != nil { + return nil, fmt.Errorf("reading blob %s in %s: %w", layer.Digest.String(), signTag, err) + } + actualDigest := layer.Digest.Algorithm().FromBytes(payload) + if actualDigest != layer.Digest { + return nil, fmt.Errorf("digest mismatch, expected %q, got %q", layer.Digest.String(), actualDigest.String()) + } + signatures = append(signatures, signature.SigstoreFromComponents(layer.MediaType, payload, layer.Annotations)) + } + return signatures, nil +} diff --git a/image/oci/layout/oci_transport.go b/image/oci/layout/oci_transport.go index 7b5086cd88..2b670e1ae0 100644 --- a/image/oci/layout/oci_transport.go +++ b/image/oci/layout/oci_transport.go @@ -302,3 +302,11 @@ func (ref ociReference) blobPath(digest digest.Digest, sharedBlobDir string) (st } return filepath.Join(blobDir, digest.Algorithm().String(), digest.Encoded()), nil } + +// sigstoreAttachmentTag returns a sigstore attachment tag for the specified digest. +func sigstoreAttachmentTag(d digest.Digest) (string, error) { + if err := d.Validate(); err != nil { // Make sure d.String() doesn’t contain any unexpected characters + return "", err + } + return strings.Replace(d.String(), ":", "-", 1) + ".sig", nil +} From 2e50f7a12100b41415eb95cb5e2f7cc07172e61d Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 3 Sep 2025 12:45:17 +0000 Subject: [PATCH 02/22] Check pre-existing signatures Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_dest.go | 63 +++++++++++++++++++----- image/oci/layout/oci_transport.go | 80 ++++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 12 deletions(-) diff --git a/image/oci/layout/oci_dest.go b/image/oci/layout/oci_dest.go index 231acd3de4..4e094e9e9b 100644 --- a/image/oci/layout/oci_dest.go +++ b/image/oci/layout/oci_dest.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "io/fs" + "maps" "os" "path/filepath" "runtime" @@ -19,6 +20,7 @@ import ( "github.com/sirupsen/logrus" "go.podman.io/image/v5/internal/imagedestination/impl" "go.podman.io/image/v5/internal/imagedestination/stubs" + "go.podman.io/image/v5/internal/iolimits" "go.podman.io/image/v5/internal/private" "go.podman.io/image/v5/internal/putblobdigest" "go.podman.io/image/v5/internal/signature" @@ -336,7 +338,7 @@ func (d *ociImageDestination) PutSignaturesWithFormat(ctx context.Context, signa instanceDigest = &d.manifestDigest } - sigstoreSignatures := []signature.Sigstore{} + var sigstoreSignatures []signature.Sigstore for _, sig := range signatures { if sigstoreSig, ok := sig.(signature.Sigstore); ok { sigstoreSignatures = append(sigstoreSignatures, sigstoreSig) @@ -356,11 +358,28 @@ func (d *ociImageDestination) PutSignaturesWithFormat(ctx context.Context, signa func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Context, signatures []signature.Sigstore, manifestDigest digest.Digest) error { var signConfig imgspecv1.Image // Most fields empty by default - signManifest := manifest.OCI1FromComponents(imgspecv1.Descriptor{ - MediaType: imgspecv1.MediaTypeImageConfig, - Digest: "", // We will fill this in later. - Size: 0, - }, nil) + signManifest, err := d.ref.getSigstoreAttachmentManifest(manifestDigest, &d.index, d.sharedBlobDir) + if err != nil { + return err + } + if signManifest == nil { + signManifest = manifest.OCI1FromComponents(imgspecv1.Descriptor{ + MediaType: imgspecv1.MediaTypeImageConfig, + Digest: "", // We will fill this in later. + Size: 0, + }, nil) + signConfig.RootFS.Type = "layers" + } else { + logrus.Debugf("Fetching sigstore attachment config %s", signManifest.Config.Digest.String()) + configBlob, err := d.ref.getOCIDescriptorContents(signManifest.Config, iolimits.MaxConfigBodySize, d.sharedBlobDir) + if err != nil { + return err + } + if err := json.Unmarshal(configBlob, &signConfig); err != nil { + return fmt.Errorf("parsing sigstore attachment config %s in %s: %w", signManifest.Config.Digest.String(), + d.ref.StringWithinTransport(), err) + } + } desc, err := d.getDescriptor(&manifestDigest) if err != nil { @@ -375,7 +394,14 @@ func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Cont payloadBlob := sig.UntrustedPayload() annotations := sig.UntrustedAnnotations() - sigDesc, err := d.putBlobBytesAsOCI(ctx, payloadBlob, mimeType, private.PutBlobOptions{ + // Skip if the signature is already on the registry. + if slices.ContainsFunc(signManifest.Layers, func(layer imgspecv1.Descriptor) bool { + return layerMatchesSigstoreSignature(layer, mimeType, payloadBlob, annotations) + }) { + continue + } + + signDesc, err := d.putBlobBytesAsOCI(ctx, payloadBlob, mimeType, private.PutBlobOptions{ Cache: none.NoCache, IsConfig: false, EmptyLayer: false, @@ -384,10 +410,10 @@ func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Cont if err != nil { return err } - sigDesc.Annotations = annotations - signManifest.Layers = append(signManifest.Layers, sigDesc) - signConfig.RootFS.DiffIDs = append(signConfig.RootFS.DiffIDs, sigDesc.Digest) - logrus.Debugf("Adding new signature, digest %s", sigDesc.Digest.String()) + signDesc.Annotations = annotations + signManifest.Layers = append(signManifest.Layers, signDesc) + signConfig.RootFS.DiffIDs = append(signConfig.RootFS.DiffIDs, signDesc.Digest) + logrus.Debugf("Adding new signature, digest %s", signDesc.Digest.String()) } configBlob, err := json.Marshal(signConfig) @@ -557,3 +583,18 @@ func indexExists(ref ociReference) bool { } return true } + +func layerMatchesSigstoreSignature(layer imgspecv1.Descriptor, mimeType string, + payloadBlob []byte, annotations map[string]string) bool { + if layer.MediaType != mimeType || + layer.Size != int64(len(payloadBlob)) || + // This is not quite correct, we should use the layer’s digest algorithm. + // But right now we don’t want to deal with corner cases like bad digest formats + // or unavailable algorithms; in the worst case we end up with duplicate signature + // entries. + layer.Digest.String() != digest.FromBytes(payloadBlob).String() || + !maps.Equal(layer.Annotations, annotations) { + return false + } + return true +} diff --git a/image/oci/layout/oci_transport.go b/image/oci/layout/oci_transport.go index 2b670e1ae0..c999bc79df 100644 --- a/image/oci/layout/oci_transport.go +++ b/image/oci/layout/oci_transport.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "os" "path/filepath" "strings" @@ -14,7 +15,8 @@ import ( "go.podman.io/image/v5/directory/explicitfilepath" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/internal/image" - "go.podman.io/image/v5/internal/manifest" + "go.podman.io/image/v5/internal/iolimits" + "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/oci/internal" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/types" @@ -310,3 +312,79 @@ func sigstoreAttachmentTag(d digest.Digest) (string, error) { } return strings.Replace(d.String(), ":", "-", 1) + ".sig", nil } + +func (ref ociReference) getSigstoreAttachmentManifest(d digest.Digest, idx *imgspecv1.Index, sharedBlobDir string) (*manifest.OCI1, error) { + signTag, err := sigstoreAttachmentTag(d) + if err != nil { + return nil, err + } + var signDesc *imgspecv1.Descriptor + for _, m := range idx.Manifests { + if m.Annotations[imgspecv1.AnnotationRefName] == signTag { + signDesc = &m + break + } + } + if signDesc == nil { + // No signature found + return nil, nil + } + blobReader, _, err := ref.getBlob(signDesc.Digest, sharedBlobDir) + if err != nil { + return nil, fmt.Errorf("failed to get Blob %s: %w", signTag, err) + } + defer blobReader.Close() + signBlob, err := iolimits.ReadAtMost(blobReader, iolimits.MaxManifestBodySize) + mimeType := manifest.GuessMIMEType(signBlob) + if mimeType != imgspecv1.MediaTypeImageManifest { + return nil, fmt.Errorf("unexpected MIME type for sigstore attachment manifest %s: %q", + signTag, mimeType) + } + res, err := manifest.OCI1FromManifest(signBlob) + if err != nil { + return nil, fmt.Errorf("parsing manifest %s: %w", signDesc.Digest, err) + } + return res, nil +} + +func (ref ociReference) getBlob(d digest.Digest, sharedBlobDir string) (io.ReadCloser, int64, error) { + path, err := ref.blobPath(d, sharedBlobDir) + if err != nil { + return nil, 0, err + } + + r, err := os.Open(path) + if err != nil { + return nil, 0, err + } + fi, err := r.Stat() + if err != nil { + return nil, 0, err + } + return r, fi.Size(), nil +} + +func (ref ociReference) getOCIDescriptorContents(desc imgspecv1.Descriptor, maxSize int, sharedBlobDir string) ([]byte, error) { + if err := desc.Digest.Validate(); err != nil { // .Algorithm() might panic without this check + return nil, fmt.Errorf("invalid digest %q: %w", desc.Digest.String(), err) + } + digestAlgorithm := desc.Digest.Algorithm() + if !digestAlgorithm.Available() { + return nil, fmt.Errorf("invalid digest %q: unsupported digest algorithm %q", desc.Digest.String(), digestAlgorithm.String()) + } + + reader, _, err := ref.getBlob(desc.Digest, sharedBlobDir) + if err != nil { + return nil, err + } + defer reader.Close() + payload, err := iolimits.ReadAtMost(reader, maxSize) + if err != nil { + return nil, fmt.Errorf("reading blob %s in %s: %w", desc.Digest.String(), ref.image, err) + } + actualDigest := digestAlgorithm.FromBytes(payload) + if actualDigest != desc.Digest { + return nil, fmt.Errorf("digest mismatch, expected %q, got %q", desc.Digest.String(), actualDigest.String()) + } + return payload, nil +} From 938521354c581e586de86aa048f957c9003a4a6e Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 3 Sep 2025 12:47:14 +0000 Subject: [PATCH 03/22] Don't raise error when signature is not found Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_src.go | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/image/oci/layout/oci_src.go b/image/oci/layout/oci_src.go index 81371e5c8c..cf1f15b026 100644 --- a/image/oci/layout/oci_src.go +++ b/image/oci/layout/oci_src.go @@ -253,32 +253,18 @@ func GetLocalBlobPath(ctx context.Context, src types.ImageSource, digest digest. func (s *ociImageSource) GetSignaturesWithFormat(ctx context.Context, instanceDigest *digest.Digest) ([]signature.Signature, error) { if instanceDigest == nil { if s.descriptor.Digest == "" { - return nil, errors.New("unknown manifest digest, can't add signatures") + return nil, errors.New("unknown manifest digest, can't get signatures") } instanceDigest = &s.descriptor.Digest } - signTag, err := sigstoreAttachmentTag(*instanceDigest) - if err != nil { - return nil, err - } - var signDigest *digest.Digest - for _, m := range s.index.Manifests { - if m.Annotations[imgspecv1.AnnotationRefName] == signTag { - signDigest = &m.Digest - break - } - } - if signDigest == nil { - return nil, errors.New("no signature found for image") - } - signBlob, _, err := s.GetManifest(ctx, signDigest) + ociManifest, err := s.ref.getSigstoreAttachmentManifest(*instanceDigest, s.index, s.sharedBlobDir) if err != nil { return nil, err } - ociManifest, err := manifest.OCI1FromManifest(signBlob) - if err != nil { - return nil, err + if ociManifest == nil { + // No signature found + return nil, nil } signatures := make([]signature.Signature, 0, len(ociManifest.Layers)) @@ -290,7 +276,7 @@ func (s *ociImageSource) GetSignaturesWithFormat(ctx context.Context, instanceDi defer layerBlob.Close() payload, err := iolimits.ReadAtMost(layerBlob, iolimits.MaxSignatureBodySize) if err != nil { - return nil, fmt.Errorf("reading blob %s in %s: %w", layer.Digest.String(), signTag, err) + return nil, fmt.Errorf("reading blob %s in %s: %w", layer.Digest.String(), instanceDigest, err) } actualDigest := layer.Digest.Algorithm().FromBytes(payload) if actualDigest != layer.Digest { From d17b607e76781bf07a5b28f76d8cabc93b36511e Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 3 Sep 2025 13:14:51 +0000 Subject: [PATCH 04/22] Check whether algorithm is available Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_src.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/image/oci/layout/oci_src.go b/image/oci/layout/oci_src.go index cf1f15b026..ac7b7fc00e 100644 --- a/image/oci/layout/oci_src.go +++ b/image/oci/layout/oci_src.go @@ -278,7 +278,14 @@ func (s *ociImageSource) GetSignaturesWithFormat(ctx context.Context, instanceDi if err != nil { return nil, fmt.Errorf("reading blob %s in %s: %w", layer.Digest.String(), instanceDigest, err) } - actualDigest := layer.Digest.Algorithm().FromBytes(payload) + if err := layer.Digest.Validate(); err != nil { + return nil, fmt.Errorf("invalid digest %q: %w", layer.Digest, err) + } + digestAlgorithm := layer.Digest.Algorithm() + if !digestAlgorithm.Available() { + return nil, fmt.Errorf("invalid digest %q: unsupported digest algorithm %q", layer.Digest.String(), digestAlgorithm.String()) + } + actualDigest := digestAlgorithm.FromBytes(payload) if actualDigest != layer.Digest { return nil, fmt.Errorf("digest mismatch, expected %q, got %q", layer.Digest.String(), actualDigest.String()) } From 7fb500186ad6fadcaf845888607fc1ebbb88cbed Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 3 Sep 2025 13:15:53 +0000 Subject: [PATCH 05/22] Use getBlob Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_src.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/image/oci/layout/oci_src.go b/image/oci/layout/oci_src.go index ac7b7fc00e..c8c01c1db2 100644 --- a/image/oci/layout/oci_src.go +++ b/image/oci/layout/oci_src.go @@ -161,20 +161,7 @@ func (s *ociImageSource) GetBlob(ctx context.Context, info types.BlobInfo, cache } } - path, err := s.ref.blobPath(info.Digest, s.sharedBlobDir) - if err != nil { - return nil, 0, err - } - - r, err := os.Open(path) - if err != nil { - return nil, 0, err - } - fi, err := r.Stat() - if err != nil { - return nil, 0, err - } - return r, fi.Size(), nil + return s.ref.getBlob(info.Digest, s.sharedBlobDir) } // getExternalBlob returns the reader of the first available blob URL from urls, which must not be empty. From cfefbe36d999e2930147e15779767c13bf638424 Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 3 Sep 2025 14:25:38 +0000 Subject: [PATCH 06/22] Delete old signature manifest config Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_dest.go | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/image/oci/layout/oci_dest.go b/image/oci/layout/oci_dest.go index 4e094e9e9b..d8ed0042e6 100644 --- a/image/oci/layout/oci_dest.go +++ b/image/oci/layout/oci_dest.go @@ -23,6 +23,7 @@ import ( "go.podman.io/image/v5/internal/iolimits" "go.podman.io/image/v5/internal/private" "go.podman.io/image/v5/internal/putblobdigest" + "go.podman.io/image/v5/internal/set" "go.podman.io/image/v5/internal/signature" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/pkg/blobinfocache/none" @@ -41,6 +42,8 @@ type ociImageDestination struct { sharedBlobDir string sys *types.SystemContext manifestDigest digest.Digest + // blobsToDelete is a set of digests which may be deleted + blobsToDelete *set.Set[digest.Digest] } // newImageDestination returns an ImageDestination for writing to an existing directory. @@ -83,9 +86,10 @@ func newImageDestination(sys *types.SystemContext, ref ociReference) (private.Im }), NoPutBlobPartialInitialize: stubs.NoPutBlobPartial(ref), - ref: ref, - index: *index, - sys: sys, + ref: ref, + index: *index, + sys: sys, + blobsToDelete: set.New[digest.Digest](), } d.Compat = impl.AddCompat(d) if sys != nil { @@ -320,6 +324,26 @@ func (d *ociImageDestination) CommitWithOptions(ctx context.Context, options pri if err != nil { return err } + // Delete unreferenced blobs (e.g. old signature manifest config) + if !d.blobsToDelete.Empty() { + count := make(map[digest.Digest]int) + err = d.ref.countBlobsReferencedByIndex(count, &d.index, d.sharedBlobDir) + if err != nil { + return fmt.Errorf("error counting blobs to delete: %w", err) + } + // Don't delete blobs which are referenced + actualBlobsToDelete := set.New[digest.Digest]() + for dgst := range d.blobsToDelete.All() { + if count[dgst] == 0 { + actualBlobsToDelete.Add(dgst) + } + } + err := d.ref.deleteBlobs(actualBlobsToDelete) + if err != nil { + return fmt.Errorf("error deleting blobs: %w", err) + } + d.blobsToDelete = set.New[digest.Digest]() + } if err := os.WriteFile(d.ref.ociLayoutPath(), layoutBytes, 0644); err != nil { return err } @@ -379,6 +403,8 @@ func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Cont return fmt.Errorf("parsing sigstore attachment config %s in %s: %w", signManifest.Config.Digest.String(), d.ref.StringWithinTransport(), err) } + // The config of the signature manifest will be updated and unreferenced when a new config is created. + d.blobsToDelete.Add(signManifest.Config.Digest) } desc, err := d.getDescriptor(&manifestDigest) From 590fda63a891abc8a8a1f94f8938571cf657b7f0 Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 3 Sep 2025 14:49:50 +0000 Subject: [PATCH 07/22] Fix oci delete test Signed-off-by: Ayato Tokubi --- ...e402ae2e97119c6007b6e52146419985ec1f0092dc | 1 + ...352621630bcfdb5cef147fa445f2e630e49f7a9910 | 1 - ...2b8637b47ce96c838ba2aa0de66d14f45cedc11423 | 30 +++++++++++++ ...1769692e4669470de97718d2ffab83273ea57474cb | 43 ------------------- ...c8996fb92427ae41e4649b934ca495991b7852b855 | 27 ++++++++++++ ...b77c933269586ad0226c83405776be08547e4d2a18 | 16 +++++++ .../delete_image_with_signature/index.json | 22 +++++++++- .../delete_image_with_signature/oci-layout | 2 +- image/oci/layout/oci_delete.go | 2 + image/oci/layout/oci_delete_test.go | 12 ++++-- 10 files changed, 106 insertions(+), 50 deletions(-) create mode 100644 image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc delete mode 100644 image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/2558a560cb1cd192d12862352621630bcfdb5cef147fa445f2e630e49f7a9910 create mode 100644 image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 delete mode 100644 image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb create mode 100644 image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 create mode 100644 image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 diff --git a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc new file mode 100644 index 0000000000..e7e64ba41b --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc @@ -0,0 +1 @@ +insert binary content here #9671 diff --git a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/2558a560cb1cd192d12862352621630bcfdb5cef147fa445f2e630e49f7a9910 b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/2558a560cb1cd192d12862352621630bcfdb5cef147fa445f2e630e49f7a9910 deleted file mode 100644 index a0990a01a7..0000000000 --- a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/2558a560cb1cd192d12862352621630bcfdb5cef147fa445f2e630e49f7a9910 +++ /dev/null @@ -1 +0,0 @@ -{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a","size":147},"layers":[{"mediaType":"application/vnd.dev.cosign.simplesigning.v1+json","digest":"sha256:6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273","size":12,"annotations":{"dev.cosignproject.cosign/signature":"test-signature"}}],"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb","size":1506,"annotations":{"org.opencontainers.image.ref.name":"imageValue"}}} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 new file mode 100644 index 0000000000..f0f06201be --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 @@ -0,0 +1,30 @@ +{ + "created": "2019-08-20T20:19:55.211423266Z", + "architecture": "amd64", + "os": "linux", + "config": { + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "/bin/sh" + ] + }, + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0" + ] + }, + "history": [ + { + "created": "2019-08-20T20:19:55.062606894Z", + "created_by": "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / " + }, + { + "created": "2019-08-20T20:19:55.211423266Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + "empty_layer": true + } + ] +} diff --git a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb deleted file mode 100644 index 26efc23db7..0000000000 --- a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb +++ /dev/null @@ -1,43 +0,0 @@ -{ - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "config": { - "mediaType": "application/vnd.oci.image.config.v1+json", - "size": 5940, - "digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f", - "annotations": { - "test-annotation-1": "one" - } - }, - "layers": [ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "size": 51354364, - "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" - }, - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "size": 150, - "digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" - }, - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "size": 11739507, - "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", - "urls": ["https://layer.url"] - }, - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "size": 8841833, - "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", - "annotations": { - "test-annotation-2": "two" - } - }, - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "size": 291, - "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" - } - ] -} diff --git a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 new file mode 100644 index 0000000000..aa7a15becc --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -0,0 +1,27 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a", + "size": 147 + }, + "layers": [ + { + "mediaType": "application/vnd.dev.cosign.simplesigning.v1+json", + "digest": "sha256:6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273", + "size": 12, + "annotations": { + "dev.cosignproject.cosign/signature": "test-signature" + } + } + ], + "subject": { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18", + "size": 1506, + "annotations": { + "org.opencontainers.image.ref.name": "imageValue" + } + } +} diff --git a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 new file mode 100644 index 0000000000..1ff195d0f3 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423", + "size": 585 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc", + "size": 33 + } + ] +} diff --git a/image/oci/layout/fixtures/delete_image_with_signature/index.json b/image/oci/layout/fixtures/delete_image_with_signature/index.json index 266e2dbdee..94c28500b2 100644 --- a/image/oci/layout/fixtures/delete_image_with_signature/index.json +++ b/image/oci/layout/fixtures/delete_image_with_signature/index.json @@ -1 +1,21 @@ -{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb","size":1506,"annotations":{"org.opencontainers.image.ref.name":"imageValue"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:2558a560cb1cd192d12862352621630bcfdb5cef147fa445f2e630e49f7a9910","size":704,"annotations":{"org.opencontainers.image.ref.name":"sha256-bfd7a7b14cfc6349bfbc291769692e4669470de97718d2ffab83273ea57474cb.sig"}}]} \ No newline at end of file +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18", + "size": 476, + "annotations": { + "org.opencontainers.image.ref.name": "latest" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 704, + "annotations": { + "org.opencontainers.image.ref.name": "sha256-eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18.sig" + } + } + ] +} diff --git a/image/oci/layout/fixtures/delete_image_with_signature/oci-layout b/image/oci/layout/fixtures/delete_image_with_signature/oci-layout index 1343d370fa..21b1439d1c 100644 --- a/image/oci/layout/fixtures/delete_image_with_signature/oci-layout +++ b/image/oci/layout/fixtures/delete_image_with_signature/oci-layout @@ -1 +1 @@ -{"imageLayoutVersion":"1.0.0"} \ No newline at end of file +{"imageLayoutVersion": "1.0.0"} \ No newline at end of file diff --git a/image/oci/layout/oci_delete.go b/image/oci/layout/oci_delete.go index a852609538..faf121acb3 100644 --- a/image/oci/layout/oci_delete.go +++ b/image/oci/layout/oci_delete.go @@ -198,6 +198,7 @@ func saveJSON(path string, content any) (retErr error) { return json.NewEncoder(file).Encode(content) } +// deleteSignatures delete sigstore signatures of the given manifest digest. func (ref ociReference) deleteSignatures(ctx context.Context, sys *types.SystemContext, d digest.Digest) error { signTag, err := sigstoreAttachmentTag(d) if err != nil { @@ -216,6 +217,7 @@ func (ref ociReference) deleteSignatures(ctx context.Context, sys *types.SystemC return err } +// isSigstoreTag returns true if the tag is sigstore signature tag. func isSigstoreTag(tag string) bool { if !strings.HasSuffix(tag, ".sig") { return false diff --git a/image/oci/layout/oci_delete_test.go b/image/oci/layout/oci_delete_test.go index f2e78e51c8..9f2f54f0c9 100644 --- a/image/oci/layout/oci_delete_test.go +++ b/image/oci/layout/oci_delete_test.go @@ -43,9 +43,15 @@ func TestReferenceDeleteImage_onlyOneImage(t *testing.T) { func TestReferenceDeleteImage_onlyOneImageWithSignatures(t *testing.T) { tmpDir := loadFixture(t, "delete_image_with_signature") - ref, err := NewReference(tmpDir, "imageValue") + ref, err := NewReference(tmpDir, "latest") require.NoError(t, err) + ociRef, ok := ref.(ociReference) + require.True(t, ok) + index, err := ociRef.getIndex() + require.NoError(t, err) + require.Equal(t, 2, len(index.Manifests)) + err = ref.DeleteImage(context.Background(), nil) require.NoError(t, err) @@ -56,9 +62,7 @@ func TestReferenceDeleteImage_onlyOneImageWithSignatures(t *testing.T) { require.Empty(t, files) // Check that the index is empty as there is only one image in the fixture - ociRef, ok := ref.(ociReference) - require.True(t, ok) - index, err := ociRef.getIndex() + index, err = ociRef.getIndex() require.NoError(t, err) require.Equal(t, 0, len(index.Manifests)) } From 9b3cca231b1c255f1df92167347bc0d601dac1ce Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 3 Sep 2025 15:27:15 +0000 Subject: [PATCH 08/22] Fix oci dest test Signed-off-by: Ayato Tokubi --- ...e402ae2e97119c6007b6e52146419985ec1f0092dc | 1 + ...2b8637b47ce96c838ba2aa0de66d14f45cedc11423 | 30 +++++++++++ ...b77c933269586ad0226c83405776be08547e4d2a18 | 16 ++++++ .../fixtures/single_image_layout/index.json | 13 +++++ .../fixtures/single_image_layout/oci-layout | 1 + image/oci/layout/oci_dest_test.go | 54 +++++++++++++++++-- 6 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 image/oci/layout/fixtures/single_image_layout/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc create mode 100644 image/oci/layout/fixtures/single_image_layout/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 create mode 100644 image/oci/layout/fixtures/single_image_layout/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 create mode 100644 image/oci/layout/fixtures/single_image_layout/index.json create mode 100644 image/oci/layout/fixtures/single_image_layout/oci-layout diff --git a/image/oci/layout/fixtures/single_image_layout/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc b/image/oci/layout/fixtures/single_image_layout/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc new file mode 100644 index 0000000000..e7e64ba41b --- /dev/null +++ b/image/oci/layout/fixtures/single_image_layout/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc @@ -0,0 +1 @@ +insert binary content here #9671 diff --git a/image/oci/layout/fixtures/single_image_layout/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 b/image/oci/layout/fixtures/single_image_layout/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 new file mode 100644 index 0000000000..f0f06201be --- /dev/null +++ b/image/oci/layout/fixtures/single_image_layout/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 @@ -0,0 +1,30 @@ +{ + "created": "2019-08-20T20:19:55.211423266Z", + "architecture": "amd64", + "os": "linux", + "config": { + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "/bin/sh" + ] + }, + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0" + ] + }, + "history": [ + { + "created": "2019-08-20T20:19:55.062606894Z", + "created_by": "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / " + }, + { + "created": "2019-08-20T20:19:55.211423266Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + "empty_layer": true + } + ] +} diff --git a/image/oci/layout/fixtures/single_image_layout/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 b/image/oci/layout/fixtures/single_image_layout/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 new file mode 100644 index 0000000000..1ff195d0f3 --- /dev/null +++ b/image/oci/layout/fixtures/single_image_layout/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423", + "size": 585 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc", + "size": 33 + } + ] +} diff --git a/image/oci/layout/fixtures/single_image_layout/index.json b/image/oci/layout/fixtures/single_image_layout/index.json new file mode 100644 index 0000000000..b0a0c98478 --- /dev/null +++ b/image/oci/layout/fixtures/single_image_layout/index.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18", + "size": 476, + "annotations": { + "org.opencontainers.image.ref.name": "latest" + } + } + ] +} diff --git a/image/oci/layout/fixtures/single_image_layout/oci-layout b/image/oci/layout/fixtures/single_image_layout/oci-layout new file mode 100644 index 0000000000..21b1439d1c --- /dev/null +++ b/image/oci/layout/fixtures/single_image_layout/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion": "1.0.0"} \ No newline at end of file diff --git a/image/oci/layout/oci_dest_test.go b/image/oci/layout/oci_dest_test.go index 623fca58b5..d2838ca1a1 100644 --- a/image/oci/layout/oci_dest_test.go +++ b/image/oci/layout/oci_dest_test.go @@ -220,11 +220,46 @@ func TestPutblobFromLocalFile(t *testing.T) { // TestPutSignaturesWithFormat tests that sigstore signatures are properly stored in OCI layout func TestPutSignaturesWithFormat(t *testing.T) { - ref, tmpDir := refToTempOCI(t, false) - ociRef, ok := ref.(ociReference) + tmpDir := loadFixture(t, "single_image_layout") + ref, err := NewReference(tmpDir, "latest") + require.NoError(t, err) + dest, err := ref.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dest.Close() + ociDest, ok := dest.(*ociImageDestination) require.True(t, ok) - putTestManifest(t, ociRef, tmpDir) + desc, _, err := ociDest.ref.getManifestDescriptor() + require.NoError(t, err) + require.NotNil(t, desc) + + sigstoreSign := signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature"}, + ) + + err = ociDest.PutSignaturesWithFormat(context.Background(), []signature.Signature{sigstoreSign}, &desc.Digest) + require.NoError(t, err) + + err = ociDest.Commit(context.Background(), nil) + require.NoError(t, err) + + src, err := ref.NewImageSource(context.Background(), nil) + require.NoError(t, err) + ociSrc, ok := src.(*ociImageSource) + require.True(t, ok) + sign, err := ociSrc.GetSignaturesWithFormat(context.Background(), &desc.Digest) + require.NoError(t, err) + require.Len(t, sign, 1) + require.Equal(t, sigstoreSign, sign[0]) +} + +// TestPutSignaturesWithFormatTwice tests PutSignaturesWithFormat twice and checks +func TestPutSignaturesWithFormatTwice(t *testing.T) { + tmpDir := loadFixture(t, "single_image_layout") + ref, err := NewReference(tmpDir, "latest") + require.NoError(t, err) dest, err := ref.NewImageDestination(context.Background(), nil) require.NoError(t, err) defer dest.Close() @@ -240,6 +275,11 @@ func TestPutSignaturesWithFormat(t *testing.T) { []byte("test-payload"), map[string]string{"dev.cosignproject.cosign/signature": "test-signature"}, ) + sigstoreSign2 := signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload2"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature"}, + ) err = ociDest.PutSignaturesWithFormat(context.Background(), []signature.Signature{sigstoreSign}, &desc.Digest) require.NoError(t, err) @@ -247,13 +287,19 @@ func TestPutSignaturesWithFormat(t *testing.T) { err = ociDest.Commit(context.Background(), nil) require.NoError(t, err) + err = ociDest.PutSignaturesWithFormat(context.Background(), []signature.Signature{sigstoreSign, sigstoreSign2}, &desc.Digest) + require.NoError(t, err) + + err = ociDest.Commit(context.Background(), nil) + require.NoError(t, err) + src, err := ref.NewImageSource(context.Background(), nil) require.NoError(t, err) ociSrc, ok := src.(*ociImageSource) require.True(t, ok) sign, err := ociSrc.GetSignaturesWithFormat(context.Background(), &desc.Digest) require.NoError(t, err) - require.Len(t, sign, 1) + require.Len(t, sign, 2) require.Equal(t, sigstoreSign, sign[0]) } From c365899d27d168a05fe0fccc56d3d26444c22abd Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 3 Sep 2025 16:02:55 +0000 Subject: [PATCH 09/22] Don't store old signature manifest Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_dest.go | 44 ++++++++++++++++++++++++++++--- image/oci/layout/oci_dest_test.go | 1 + 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/image/oci/layout/oci_dest.go b/image/oci/layout/oci_dest.go index d8ed0042e6..902ae9c052 100644 --- a/image/oci/layout/oci_dest.go +++ b/image/oci/layout/oci_dest.go @@ -313,6 +313,33 @@ func (d *ociImageDestination) addManifest(desc *imgspecv1.Descriptor) { d.index.Manifests = append(slices.Clone(d.index.Manifests), *desc) } +// addSignatureManifest is similar to addManifest, but replace the entry based on imgspecv1.AnnotationRefName +// and returns the old digest to delete it later. +func (d *ociImageDestination) addSignatureManifest(desc *imgspecv1.Descriptor) (*imgspecv1.Descriptor, error) { + if desc.Annotations == nil || desc.Annotations[imgspecv1.AnnotationRefName] == "" { + return nil, errors.New("cannot add signature manifest without ref.name") + } + for i, m := range d.index.Manifests { + if m.Annotations[imgspecv1.AnnotationRefName] == desc.Annotations[imgspecv1.AnnotationRefName] { + // Replace it completely. + oldDesc := d.index.Manifests[i] + d.index.Manifests[i] = *desc + return &oldDesc, nil + } + } + // It shouldn't happen, but if there's no entry with the same ref name, but the same digest, just replace it. + for i, m := range d.index.Manifests { + if m.Digest == desc.Digest && m.Annotations[imgspecv1.AnnotationRefName] == "" { + // Replace it completely. + d.index.Manifests[i] = *desc + return nil, nil + } + } + // It's a new entry to be added to the index. Use slices.Clone() to avoid a remote dependency on how d.index was created. + d.index.Manifests = append(slices.Clone(d.index.Manifests), *desc) + return nil, nil +} + // CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. // WARNING: This does not have any transactional semantics: // - Uploaded data MAY be visible to others before CommitWithOptions() is called @@ -324,7 +351,7 @@ func (d *ociImageDestination) CommitWithOptions(ctx context.Context, options pri if err != nil { return err } - // Delete unreferenced blobs (e.g. old signature manifest config) + // Delete unreferenced blobs (e.g. old signature manifest and its config) if !d.blobsToDelete.Empty() { count := make(map[digest.Digest]int) err = d.ref.countBlobsReferencedByIndex(count, &d.index, d.sharedBlobDir) @@ -471,7 +498,7 @@ func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Cont if err != nil { return err } - d.addManifest(&imgspecv1.Descriptor{ + oldDesc, err := d.addSignatureManifest(&imgspecv1.Descriptor{ MediaType: signManifest.MediaType, Digest: signDigest, Size: int64(len(signManifestBlob)), @@ -479,7 +506,18 @@ func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Cont imgspecv1.AnnotationRefName: signTag, }, }) - + if err != nil { + return err + } + // If it overwrote an existing signature manifest, delete blobs referenced by the old manifest. + if oldDesc != nil { + referencedBlobs := make(map[digest.Digest]int) + err = d.ref.countBlobsForDescriptor(referencedBlobs, oldDesc, d.sharedBlobDir) + if err != nil { + return fmt.Errorf("error counting blobs for digest %s: %w", oldDesc.Digest.String(), err) + } + d.blobsToDelete.AddSeq(maps.Keys(referencedBlobs)) + } return nil } diff --git a/image/oci/layout/oci_dest_test.go b/image/oci/layout/oci_dest_test.go index d2838ca1a1..c9c5631c44 100644 --- a/image/oci/layout/oci_dest_test.go +++ b/image/oci/layout/oci_dest_test.go @@ -301,6 +301,7 @@ func TestPutSignaturesWithFormatTwice(t *testing.T) { require.NoError(t, err) require.Len(t, sign, 2) require.Equal(t, sigstoreSign, sign[0]) + require.Equal(t, sigstoreSign2, sign[1]) } // TestPutSignaturesWithFormatNilDigest tests error handling when instanceDigest is nil From 5e92317bae5f1c7a562b986f4be72c25aeb47719 Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 3 Sep 2025 16:26:18 +0000 Subject: [PATCH 10/22] Fix lint error Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_transport.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/image/oci/layout/oci_transport.go b/image/oci/layout/oci_transport.go index c999bc79df..009fad4516 100644 --- a/image/oci/layout/oci_transport.go +++ b/image/oci/layout/oci_transport.go @@ -335,6 +335,9 @@ func (ref ociReference) getSigstoreAttachmentManifest(d digest.Digest, idx *imgs } defer blobReader.Close() signBlob, err := iolimits.ReadAtMost(blobReader, iolimits.MaxManifestBodySize) + if err != nil { + return nil, fmt.Errorf("failed to read blob: %w", err) + } mimeType := manifest.GuessMIMEType(signBlob) if mimeType != imgspecv1.MediaTypeImageManifest { return nil, fmt.Errorf("unexpected MIME type for sigstore attachment manifest %s: %q", From ef902efd59999cc69bcf2aa0446d8f52996db2cc Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Fri, 5 Sep 2025 11:42:33 +0000 Subject: [PATCH 11/22] Ignore signature when getManifestDescriptor Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_delete.go | 11 --------- image/oci/layout/oci_transport.go | 41 ++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/image/oci/layout/oci_delete.go b/image/oci/layout/oci_delete.go index faf121acb3..1ebd452f6e 100644 --- a/image/oci/layout/oci_delete.go +++ b/image/oci/layout/oci_delete.go @@ -8,7 +8,6 @@ import ( "io/fs" "os" "slices" - "strings" digest "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -216,13 +215,3 @@ func (ref ociReference) deleteSignatures(ctx context.Context, sys *types.SystemC } return err } - -// isSigstoreTag returns true if the tag is sigstore signature tag. -func isSigstoreTag(tag string) bool { - if !strings.HasSuffix(tag, ".sig") { - return false - } - digestPart := strings.TrimSuffix(tag, ".sig") - digestPart = strings.Replace(digestPart, "-", ":", 1) - return digest.Digest(digestPart).Validate() == nil -} diff --git a/image/oci/layout/oci_transport.go b/image/oci/layout/oci_transport.go index 009fad4516..18a484093d 100644 --- a/image/oci/layout/oci_transport.go +++ b/image/oci/layout/oci_transport.go @@ -30,6 +30,9 @@ var ( // Transport is an ImageTransport for OCI directories. Transport = ociTransport{} + // ErrEmptyIndex is an error returned when the index includes no image. + ErrEmptyIndex = errors.New("no image in oci") + // ErrMoreThanOneImage is an error returned when the manifest includes // more than one image and the user should choose which one to use. ErrMoreThanOneImage = errors.New("more than one image in oci, choose an image") @@ -250,11 +253,33 @@ func (ref ociReference) getManifestDescriptor() (imgspecv1.Descriptor, int, erro default: // return manifest if only one image is in the oci directory - if len(index.Manifests) != 1 { - // ask user to choose image when more than one image in the oci directory + if len(index.Manifests) == 0 { + return imgspecv1.Descriptor{}, -1, ErrEmptyIndex + } + // if there's one image return it, even if it is a signature + if len(index.Manifests) == 1 { + return index.Manifests[0], 0, nil + } + // when there's more than one image, try to get a non-signature image + var desc imgspecv1.Descriptor + idx := -1 + for i, md := range index.Manifests { + if isSigstoreTag(md.Annotations[imgspecv1.AnnotationRefName]) { + continue + } + // More than one non-signature image was found + if idx != -1 { + // ask user to choose image when more than one image in the oci directory + return imgspecv1.Descriptor{}, -1, ErrMoreThanOneImage + } + desc = md + idx = i + } + // there's only multiple signature images + if idx == -1 { return imgspecv1.Descriptor{}, -1, ErrMoreThanOneImage } - return index.Manifests[0], 0, nil + return desc, idx, nil } } @@ -391,3 +416,13 @@ func (ref ociReference) getOCIDescriptorContents(desc imgspecv1.Descriptor, maxS } return payload, nil } + +// isSigstoreTag returns true if the tag is sigstore signature tag. +func isSigstoreTag(tag string) bool { + if !strings.HasSuffix(tag, ".sig") { + return false + } + digestPart := strings.TrimSuffix(tag, ".sig") + digestPart = strings.Replace(digestPart, "-", ":", 1) + return digest.Digest(digestPart).Validate() == nil +} From 75c616da34b1144d3b5f81fb3914a6102f3ea3a2 Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 24 Sep 2025 11:43:42 +0000 Subject: [PATCH 12/22] Improve sigstore tag validation with strings.CutSuffix Refactor `digestPart` validation by replacing `Validate` with `Parse` for clarity and correctness Use signDesc MediaType to validate signature MIMEType. Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_transport.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/image/oci/layout/oci_transport.go b/image/oci/layout/oci_transport.go index 18a484093d..c1be12e532 100644 --- a/image/oci/layout/oci_transport.go +++ b/image/oci/layout/oci_transport.go @@ -354,6 +354,10 @@ func (ref ociReference) getSigstoreAttachmentManifest(d digest.Digest, idx *imgs // No signature found return nil, nil } + if signDesc.MediaType != imgspecv1.MediaTypeImageManifest { + return nil, fmt.Errorf("unexpected MIME type for sigstore attachment manifest %s: %q", + signTag, signDesc.MediaType) + } blobReader, _, err := ref.getBlob(signDesc.Digest, sharedBlobDir) if err != nil { return nil, fmt.Errorf("failed to get Blob %s: %w", signTag, err) @@ -363,11 +367,6 @@ func (ref ociReference) getSigstoreAttachmentManifest(d digest.Digest, idx *imgs if err != nil { return nil, fmt.Errorf("failed to read blob: %w", err) } - mimeType := manifest.GuessMIMEType(signBlob) - if mimeType != imgspecv1.MediaTypeImageManifest { - return nil, fmt.Errorf("unexpected MIME type for sigstore attachment manifest %s: %q", - signTag, mimeType) - } res, err := manifest.OCI1FromManifest(signBlob) if err != nil { return nil, fmt.Errorf("parsing manifest %s: %w", signDesc.Digest, err) @@ -387,6 +386,7 @@ func (ref ociReference) getBlob(d digest.Digest, sharedBlobDir string) (io.ReadC } fi, err := r.Stat() if err != nil { + _ = r.Close() // Avoid leak r. return nil, 0, err } return r, fi.Size(), nil @@ -419,10 +419,11 @@ func (ref ociReference) getOCIDescriptorContents(desc imgspecv1.Descriptor, maxS // isSigstoreTag returns true if the tag is sigstore signature tag. func isSigstoreTag(tag string) bool { - if !strings.HasSuffix(tag, ".sig") { + digestPart, found := strings.CutSuffix(tag, ".sig") + if !found { return false } - digestPart := strings.TrimSuffix(tag, ".sig") digestPart = strings.Replace(digestPart, "-", ":", 1) - return digest.Digest(digestPart).Validate() == nil + _, err := digest.Parse(digestPart) + return err == nil } From 506fbd4528a51ed9fe63d3249cb597c39147bcef Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 24 Sep 2025 11:44:17 +0000 Subject: [PATCH 13/22] Refactor getOCIDescriptorContents to use digest.Digest parameter Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_dest.go | 2 +- image/oci/layout/oci_transport.go | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/image/oci/layout/oci_dest.go b/image/oci/layout/oci_dest.go index 902ae9c052..a09e9e5f29 100644 --- a/image/oci/layout/oci_dest.go +++ b/image/oci/layout/oci_dest.go @@ -422,7 +422,7 @@ func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Cont signConfig.RootFS.Type = "layers" } else { logrus.Debugf("Fetching sigstore attachment config %s", signManifest.Config.Digest.String()) - configBlob, err := d.ref.getOCIDescriptorContents(signManifest.Config, iolimits.MaxConfigBodySize, d.sharedBlobDir) + configBlob, err := d.ref.getOCIDescriptorContents(signManifest.Config.Digest, iolimits.MaxConfigBodySize, d.sharedBlobDir) if err != nil { return err } diff --git a/image/oci/layout/oci_transport.go b/image/oci/layout/oci_transport.go index c1be12e532..70dcbd2340 100644 --- a/image/oci/layout/oci_transport.go +++ b/image/oci/layout/oci_transport.go @@ -392,27 +392,27 @@ func (ref ociReference) getBlob(d digest.Digest, sharedBlobDir string) (io.ReadC return r, fi.Size(), nil } -func (ref ociReference) getOCIDescriptorContents(desc imgspecv1.Descriptor, maxSize int, sharedBlobDir string) ([]byte, error) { - if err := desc.Digest.Validate(); err != nil { // .Algorithm() might panic without this check - return nil, fmt.Errorf("invalid digest %q: %w", desc.Digest.String(), err) +func (ref ociReference) getOCIDescriptorContents(dgst digest.Digest, maxSize int, sharedBlobDir string) ([]byte, error) { + if err := dgst.Validate(); err != nil { // .Algorithm() might panic without this check + return nil, fmt.Errorf("invalid digest %q: %w", dgst.String(), err) } - digestAlgorithm := desc.Digest.Algorithm() + digestAlgorithm := dgst.Algorithm() if !digestAlgorithm.Available() { - return nil, fmt.Errorf("invalid digest %q: unsupported digest algorithm %q", desc.Digest.String(), digestAlgorithm.String()) + return nil, fmt.Errorf("invalid digest %q: unsupported digest algorithm %q", dgst.String(), digestAlgorithm.String()) } - reader, _, err := ref.getBlob(desc.Digest, sharedBlobDir) + reader, _, err := ref.getBlob(dgst, sharedBlobDir) if err != nil { return nil, err } defer reader.Close() payload, err := iolimits.ReadAtMost(reader, maxSize) if err != nil { - return nil, fmt.Errorf("reading blob %s in %s: %w", desc.Digest.String(), ref.image, err) + return nil, fmt.Errorf("reading blob %s in %s: %w", dgst.String(), ref.image, err) } actualDigest := digestAlgorithm.FromBytes(payload) - if actualDigest != desc.Digest { - return nil, fmt.Errorf("digest mismatch, expected %q, got %q", desc.Digest.String(), actualDigest.String()) + if actualDigest != dgst { + return nil, fmt.Errorf("digest mismatch, expected %q, got %q", dgst.String(), actualDigest.String()) } return payload, nil } From 6f6cd3875e482a244052bd0d127624d73ebf11e7 Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 24 Sep 2025 11:58:24 +0000 Subject: [PATCH 14/22] Simplify signature retrieval by refactoring to use getOCIDescriptorContents Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_src.go | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/image/oci/layout/oci_src.go b/image/oci/layout/oci_src.go index c8c01c1db2..16f9126ff3 100644 --- a/image/oci/layout/oci_src.go +++ b/image/oci/layout/oci_src.go @@ -20,7 +20,6 @@ import ( "go.podman.io/image/v5/internal/private" "go.podman.io/image/v5/internal/signature" "go.podman.io/image/v5/manifest" - "go.podman.io/image/v5/pkg/blobinfocache/none" "go.podman.io/image/v5/pkg/tlsclientconfig" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/fileutils" @@ -256,26 +255,10 @@ func (s *ociImageSource) GetSignaturesWithFormat(ctx context.Context, instanceDi signatures := make([]signature.Signature, 0, len(ociManifest.Layers)) for _, layer := range ociManifest.Layers { - layerBlob, _, err := s.GetBlob(ctx, types.BlobInfo{Digest: layer.Digest}, none.NoCache) + payload, err := s.ref.getOCIDescriptorContents(layer.Digest, iolimits.MaxSignatureBodySize, s.sharedBlobDir) if err != nil { return nil, err } - defer layerBlob.Close() - payload, err := iolimits.ReadAtMost(layerBlob, iolimits.MaxSignatureBodySize) - if err != nil { - return nil, fmt.Errorf("reading blob %s in %s: %w", layer.Digest.String(), instanceDigest, err) - } - if err := layer.Digest.Validate(); err != nil { - return nil, fmt.Errorf("invalid digest %q: %w", layer.Digest, err) - } - digestAlgorithm := layer.Digest.Algorithm() - if !digestAlgorithm.Available() { - return nil, fmt.Errorf("invalid digest %q: unsupported digest algorithm %q", layer.Digest.String(), digestAlgorithm.String()) - } - actualDigest := digestAlgorithm.FromBytes(payload) - if actualDigest != layer.Digest { - return nil, fmt.Errorf("digest mismatch, expected %q, got %q", layer.Digest.String(), actualDigest.String()) - } signatures = append(signatures, signature.SigstoreFromComponents(layer.MediaType, payload, layer.Annotations)) } return signatures, nil From 04bb65fac633f8be83a85284897618bc65e6fdd8 Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 24 Sep 2025 12:06:42 +0000 Subject: [PATCH 15/22] Refactor blob deletion logic by renaming `blobsToDelete` to `blobDeleteCandidates` and improving unused blob handling Remove redundant implementation in `ociImageDestination` Simplify error handling in putSignaturesToSigstoreAttachment logic Normalize error message for sigstore signature support in OCI layout Add comment clarifying manifest digest requirement in PutSignaturesWithFormat Clarify comment for `manifestDigest` field in `ociImageDestination` Signed-off-by: Ayato Tokubi --- .../imagedestination/stubs/signatures.go | 2 +- image/oci/layout/oci_dest.go | 42 +++++++++---------- image/oci/layout/oci_dest_test.go | 2 +- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/image/internal/imagedestination/stubs/signatures.go b/image/internal/imagedestination/stubs/signatures.go index c046449b18..b2d20ddf17 100644 --- a/image/internal/imagedestination/stubs/signatures.go +++ b/image/internal/imagedestination/stubs/signatures.go @@ -39,7 +39,7 @@ func (stub NoSignaturesInitialize) PutSignaturesWithFormat(ctx context.Context, return nil } -// SupportsSignatures implements SupportsSignatures() that returns nil. +// AlwaysSupportsSignatures implements SupportsSignatures() that returns nil. // Note that it might be even more useful to return a value dynamically detected based on type AlwaysSupportsSignatures struct{} diff --git a/image/oci/layout/oci_dest.go b/image/oci/layout/oci_dest.go index a09e9e5f29..4e4a89ef67 100644 --- a/image/oci/layout/oci_dest.go +++ b/image/oci/layout/oci_dest.go @@ -34,16 +34,17 @@ import ( type ociImageDestination struct { impl.Compat impl.PropertyMethodsInitialize + stubs.AlwaysSupportsSignatures stubs.IgnoresOriginalOCIConfig stubs.NoPutBlobPartialInitialize ref ociReference index imgspecv1.Index sharedBlobDir string - sys *types.SystemContext - manifestDigest digest.Digest - // blobsToDelete is a set of digests which may be deleted - blobsToDelete *set.Set[digest.Digest] + manifestDigest digest.Digest // or "" if not yet known. + // blobDeleteCandidates is a set of digests which may be deleted _if_ we find no other references to them; + // it’s safe to optimistically include entries which may have other references + blobDeleteCandidates *set.Set[digest.Digest] } // newImageDestination returns an ImageDestination for writing to an existing directory. @@ -86,10 +87,9 @@ func newImageDestination(sys *types.SystemContext, ref ociReference) (private.Im }), NoPutBlobPartialInitialize: stubs.NoPutBlobPartial(ref), - ref: ref, - index: *index, - sys: sys, - blobsToDelete: set.New[digest.Digest](), + ref: ref, + index: *index, + blobDeleteCandidates: set.New[digest.Digest](), } d.Compat = impl.AddCompat(d) if sys != nil { @@ -352,16 +352,16 @@ func (d *ociImageDestination) CommitWithOptions(ctx context.Context, options pri return err } // Delete unreferenced blobs (e.g. old signature manifest and its config) - if !d.blobsToDelete.Empty() { - count := make(map[digest.Digest]int) - err = d.ref.countBlobsReferencedByIndex(count, &d.index, d.sharedBlobDir) + if !d.blobDeleteCandidates.Empty() { + blobsUsedInRootIndex := make(map[digest.Digest]int) + err = d.ref.countBlobsReferencedByIndex(blobsUsedInRootIndex, &d.index, d.sharedBlobDir) if err != nil { return fmt.Errorf("error counting blobs to delete: %w", err) } // Don't delete blobs which are referenced actualBlobsToDelete := set.New[digest.Digest]() - for dgst := range d.blobsToDelete.All() { - if count[dgst] == 0 { + for dgst := range d.blobDeleteCandidates.All() { + if blobsUsedInRootIndex[dgst] == 0 { actualBlobsToDelete.Add(dgst) } } @@ -369,7 +369,7 @@ func (d *ociImageDestination) CommitWithOptions(ctx context.Context, options pri if err != nil { return fmt.Errorf("error deleting blobs: %w", err) } - d.blobsToDelete = set.New[digest.Digest]() + d.blobDeleteCandidates = set.New[digest.Digest]() } if err := os.WriteFile(d.ref.ociLayoutPath(), layoutBytes, 0644); err != nil { return err @@ -384,6 +384,7 @@ func (d *ociImageDestination) CommitWithOptions(ctx context.Context, options pri func (d *ociImageDestination) PutSignaturesWithFormat(ctx context.Context, signatures []signature.Signature, instanceDigest *digest.Digest) error { if instanceDigest == nil { if d.manifestDigest == "" { + // This shouldn’t happen, ImageDestination users are required to call PutManifest before PutSignatures return errors.New("unknown manifest digest, can't add signatures") } instanceDigest = &d.manifestDigest @@ -394,12 +395,11 @@ func (d *ociImageDestination) PutSignaturesWithFormat(ctx context.Context, signa if sigstoreSig, ok := sig.(signature.Sigstore); ok { sigstoreSignatures = append(sigstoreSignatures, sigstoreSig) } else { - return errors.New("OCI Layout only supports sigstoreSignatures") + return errors.New("oci: layout only supports sigstore signatures") } } - err := d.putSignaturesToSigstoreAttachment(ctx, sigstoreSignatures, *instanceDigest) - if err != nil { + if err := d.putSignaturesToSigstoreAttachment(ctx, sigstoreSignatures, *instanceDigest); err != nil { return err } @@ -431,7 +431,7 @@ func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Cont d.ref.StringWithinTransport(), err) } // The config of the signature manifest will be updated and unreferenced when a new config is created. - d.blobsToDelete.Add(signManifest.Config.Digest) + d.blobDeleteCandidates.Add(signManifest.Config.Digest) } desc, err := d.getDescriptor(&manifestDigest) @@ -516,7 +516,7 @@ func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Cont if err != nil { return fmt.Errorf("error counting blobs for digest %s: %w", oldDesc.Digest.String(), err) } - d.blobsToDelete.AddSeq(maps.Keys(referencedBlobs)) + d.blobDeleteCandidates.AddSeq(maps.Keys(referencedBlobs)) } return nil } @@ -553,10 +553,6 @@ func (d *ociImageDestination) putBlobBytesAsOCI(ctx context.Context, contents [] }, nil } -func (d *ociImageDestination) SupportsSignatures(ctx context.Context) error { - return nil -} - // PutBlobFromLocalFileOption is unused but may receive functionality in the future. type PutBlobFromLocalFileOption struct{} diff --git a/image/oci/layout/oci_dest_test.go b/image/oci/layout/oci_dest_test.go index c9c5631c44..82b7cbba72 100644 --- a/image/oci/layout/oci_dest_test.go +++ b/image/oci/layout/oci_dest_test.go @@ -348,5 +348,5 @@ func TestPutSignaturesWithFormatNonSigstore(t *testing.T) { // Test that PutSignaturesWithFormat fails for non-sigstore signatures err = ociDest.PutSignaturesWithFormat(context.Background(), []signature.Signature{simpleSig}, &testDigest) require.Error(t, err) - require.Contains(t, err.Error(), "OCI Layout only supports sigstoreSignatures") + require.Contains(t, err.Error(), "oci: layout only supports sigstore signatures") } From 41a602824a2e002c7eec42d51e30060d8c1b7f1b Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 24 Sep 2025 12:27:57 +0000 Subject: [PATCH 16/22] Add the same comment as appendSignaturesFromSigstoreAttachments in GetSignaturesWithFormat Remove unused `impl.NoSignatures` from `ociImageSource` structure Simplify `GetSignaturesWithFormat` by removing redundant nil check for `instanceDigest` Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_src.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/image/oci/layout/oci_src.go b/image/oci/layout/oci_src.go index 16f9126ff3..cee0f2aae9 100644 --- a/image/oci/layout/oci_src.go +++ b/image/oci/layout/oci_src.go @@ -39,7 +39,6 @@ func (e ImageNotFoundError) Error() string { type ociImageSource struct { impl.Compat impl.PropertyMethodsInitialize - impl.NoSignatures impl.DoesNotAffectLayerInfosForCopy stubs.NoGetBlobAtInitialize @@ -238,9 +237,6 @@ func GetLocalBlobPath(ctx context.Context, src types.ImageSource, digest digest. func (s *ociImageSource) GetSignaturesWithFormat(ctx context.Context, instanceDigest *digest.Digest) ([]signature.Signature, error) { if instanceDigest == nil { - if s.descriptor.Digest == "" { - return nil, errors.New("unknown manifest digest, can't get signatures") - } instanceDigest = &s.descriptor.Digest } @@ -255,6 +251,8 @@ func (s *ociImageSource) GetSignaturesWithFormat(ctx context.Context, instanceDi signatures := make([]signature.Signature, 0, len(ociManifest.Layers)) for _, layer := range ociManifest.Layers { + // Note that this copies all kinds of attachments: attestations, and whatever else is there, + // not just signatures. We leave the signature consumers to decide based on the MIME type. payload, err := s.ref.getOCIDescriptorContents(layer.Digest, iolimits.MaxSignatureBodySize, s.sharedBlobDir) if err != nil { return nil, err From 391ab6c9eef2dea19eb6884135d9db203e83a085 Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 24 Sep 2025 13:59:40 +0000 Subject: [PATCH 17/22] Add getManifestDescriptor tests for the new code path. Signed-off-by: Ayato Tokubi --- .../signature_multiple_images/index.json | 37 +++++++++++++++++++ .../index.json | 21 +++++++++++ .../index.json | 13 +++++++ .../signature_single_image/index.json | 21 +++++++++++ image/oci/layout/oci_transport_test.go | 34 +++++++++++++++++ 5 files changed, 126 insertions(+) create mode 100644 image/oci/layout/fixtures/signature_multiple_images/index.json create mode 100644 image/oci/layout/fixtures/signature_only_multiple_signatures/index.json create mode 100644 image/oci/layout/fixtures/signature_only_single_signature/index.json create mode 100644 image/oci/layout/fixtures/signature_single_image/index.json diff --git a/image/oci/layout/fixtures/signature_multiple_images/index.json b/image/oci/layout/fixtures/signature_multiple_images/index.json new file mode 100644 index 0000000000..0960c44255 --- /dev/null +++ b/image/oci/layout/fixtures/signature_multiple_images/index.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18", + "size": 476, + "annotations": { + "org.opencontainers.image.ref.name": "latest" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 704, + "annotations": { + "org.opencontainers.image.ref.name": "sha256-eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18.sig" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 476, + "annotations": { + "org.opencontainers.image.ref.name": "v1" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 704, + "annotations": { + "org.opencontainers.image.ref.name": "sha256-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.sig" + } + } + ] +} diff --git a/image/oci/layout/fixtures/signature_only_multiple_signatures/index.json b/image/oci/layout/fixtures/signature_only_multiple_signatures/index.json new file mode 100644 index 0000000000..ba2a241800 --- /dev/null +++ b/image/oci/layout/fixtures/signature_only_multiple_signatures/index.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 704, + "annotations": { + "org.opencontainers.image.ref.name": "sha256-eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18.sig" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 704, + "annotations": { + "org.opencontainers.image.ref.name": "sha256-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.sig" + } + } + ] +} diff --git a/image/oci/layout/fixtures/signature_only_single_signature/index.json b/image/oci/layout/fixtures/signature_only_single_signature/index.json new file mode 100644 index 0000000000..f4281c1bd1 --- /dev/null +++ b/image/oci/layout/fixtures/signature_only_single_signature/index.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 704, + "annotations": { + "org.opencontainers.image.ref.name": "sha256-eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18.sig" + } + } + ] +} diff --git a/image/oci/layout/fixtures/signature_single_image/index.json b/image/oci/layout/fixtures/signature_single_image/index.json new file mode 100644 index 0000000000..94c28500b2 --- /dev/null +++ b/image/oci/layout/fixtures/signature_single_image/index.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18", + "size": 476, + "annotations": { + "org.opencontainers.image.ref.name": "latest" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 704, + "annotations": { + "org.opencontainers.image.ref.name": "sha256-eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18.sig" + } + } + ] +} diff --git a/image/oci/layout/oci_transport_test.go b/image/oci/layout/oci_transport_test.go index 5a1616fd46..1482b5081c 100644 --- a/image/oci/layout/oci_transport_test.go +++ b/image/oci/layout/oci_transport_test.go @@ -104,6 +104,40 @@ func TestGetManifestDescriptor(t *testing.T) { image: "invalid-mime", expectedDescriptor: nil, }, + { // Directory with an image with a signature should return only an image, not a signature + dir: "fixtures/signature_single_image", + image: "", + expectedDescriptor: &imgspecv1.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: "sha256:eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18", + Size: 476, + Annotations: map[string]string{ + "org.opencontainers.image.ref.name": "latest", + }, + }, + }, + { // Directory with only a signature should return a signature + dir: "fixtures/signature_only_single_signature", + image: "", + expectedDescriptor: &imgspecv1.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + Size: 704, + Annotations: map[string]string{ + "org.opencontainers.image.ref.name": "sha256-eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18.sig", + }, + }, + }, + { // Directory with multiple images should return an error + dir: "fixtures/signature_multiple_images", + image: "", + errorIs: ErrMoreThanOneImage, + }, + { // Directory with only multiple signatures should return an error + dir: "fixtures/signature_only_multiple_signatures", + image: "", + errorIs: ErrMoreThanOneImage, + }, } { ref, err := NewReference(c.dir, c.image) require.NoError(t, err) From a30076b281ec8fb4237fb7de42e844def619ef4a Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 24 Sep 2025 14:27:24 +0000 Subject: [PATCH 18/22] Move `manifestDigest` assignment in `ociImageDestination` Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_dest.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/image/oci/layout/oci_dest.go b/image/oci/layout/oci_dest.go index 4e4a89ef67..151fe2d91a 100644 --- a/image/oci/layout/oci_dest.go +++ b/image/oci/layout/oci_dest.go @@ -262,11 +262,13 @@ func (d *ociImageDestination) PutManifest(ctx context.Context, m []byte, instanc if err := os.WriteFile(blobPath, m, 0644); err != nil { return err } - d.manifestDigest = digest if instanceDigest != nil { return nil } + // d.manifestDigest is used for a single image (not a manifest list). + // This should be placed after checking instanceDigest is nil. + d.manifestDigest = digest // If we had platform information, we'd build an imgspecv1.Platform structure here. From 6f9b1c0027f6d17304e3d17e13965269aeb75258 Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 24 Sep 2025 14:40:52 +0000 Subject: [PATCH 19/22] Remove pointer usage for digest in getDescriptor. Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_dest.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/image/oci/layout/oci_dest.go b/image/oci/layout/oci_dest.go index 151fe2d91a..47e94632e0 100644 --- a/image/oci/layout/oci_dest.go +++ b/image/oci/layout/oci_dest.go @@ -436,7 +436,7 @@ func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Cont d.blobDeleteCandidates.Add(signManifest.Config.Digest) } - desc, err := d.getDescriptor(&manifestDigest) + desc, err := d.getDescriptor(manifestDigest) if err != nil { return err } @@ -523,12 +523,9 @@ func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Cont return nil } -func (d *ociImageDestination) getDescriptor(digest *digest.Digest) (*imgspecv1.Descriptor, error) { - if digest == nil { - return nil, errors.New("digest is nil") - } +func (d *ociImageDestination) getDescriptor(digest digest.Digest) (*imgspecv1.Descriptor, error) { for _, desc := range d.index.Manifests { - if desc.Digest == *digest { + if desc.Digest == digest { return &desc, nil } } From e555fefc75fae4a1a8d79ab3274004fbd0c5d8eb Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Wed, 24 Sep 2025 16:28:13 +0000 Subject: [PATCH 20/22] Refactor `TestPutSignaturesWithFormat` to use table-driven tests for improved readability and maintainability. Signed-off-by: Ayato Tokubi --- ...e402ae2e97119c6007b6e52146419985ec1f0092dc | 1 - ...2b8637b47ce96c838ba2aa0de66d14f45cedc11423 | 30 -- ...b77c933269586ad0226c83405776be08547e4d2a18 | 16 - .../fixtures/single_image_layout/index.json | 13 - .../fixtures/single_image_layout/oci-layout | 1 - image/oci/layout/oci_dest_test.go | 277 ++++++++++-------- 6 files changed, 151 insertions(+), 187 deletions(-) delete mode 100644 image/oci/layout/fixtures/single_image_layout/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc delete mode 100644 image/oci/layout/fixtures/single_image_layout/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 delete mode 100644 image/oci/layout/fixtures/single_image_layout/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 delete mode 100644 image/oci/layout/fixtures/single_image_layout/index.json delete mode 100644 image/oci/layout/fixtures/single_image_layout/oci-layout diff --git a/image/oci/layout/fixtures/single_image_layout/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc b/image/oci/layout/fixtures/single_image_layout/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc deleted file mode 100644 index e7e64ba41b..0000000000 --- a/image/oci/layout/fixtures/single_image_layout/blobs/sha256/0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc +++ /dev/null @@ -1 +0,0 @@ -insert binary content here #9671 diff --git a/image/oci/layout/fixtures/single_image_layout/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 b/image/oci/layout/fixtures/single_image_layout/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 deleted file mode 100644 index f0f06201be..0000000000 --- a/image/oci/layout/fixtures/single_image_layout/blobs/sha256/a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423 +++ /dev/null @@ -1,30 +0,0 @@ -{ - "created": "2019-08-20T20:19:55.211423266Z", - "architecture": "amd64", - "os": "linux", - "config": { - "Env": [ - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" - ], - "Cmd": [ - "/bin/sh" - ] - }, - "rootfs": { - "type": "layers", - "diff_ids": [ - "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0" - ] - }, - "history": [ - { - "created": "2019-08-20T20:19:55.062606894Z", - "created_by": "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / " - }, - { - "created": "2019-08-20T20:19:55.211423266Z", - "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", - "empty_layer": true - } - ] -} diff --git a/image/oci/layout/fixtures/single_image_layout/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 b/image/oci/layout/fixtures/single_image_layout/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 deleted file mode 100644 index 1ff195d0f3..0000000000 --- a/image/oci/layout/fixtures/single_image_layout/blobs/sha256/eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18 +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "config": { - "mediaType": "application/vnd.oci.image.config.v1+json", - "digest": "sha256:a527179158cd5cebc11c152b8637b47ce96c838ba2aa0de66d14f45cedc11423", - "size": 585 - }, - "layers": [ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "digest": "sha256:0c8b263642b51b5c1dc40fe402ae2e97119c6007b6e52146419985ec1f0092dc", - "size": 33 - } - ] -} diff --git a/image/oci/layout/fixtures/single_image_layout/index.json b/image/oci/layout/fixtures/single_image_layout/index.json deleted file mode 100644 index b0a0c98478..0000000000 --- a/image/oci/layout/fixtures/single_image_layout/index.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "schemaVersion": 2, - "manifests": [ - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "digest": "sha256:eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18", - "size": 476, - "annotations": { - "org.opencontainers.image.ref.name": "latest" - } - } - ] -} diff --git a/image/oci/layout/fixtures/single_image_layout/oci-layout b/image/oci/layout/fixtures/single_image_layout/oci-layout deleted file mode 100644 index 21b1439d1c..0000000000 --- a/image/oci/layout/fixtures/single_image_layout/oci-layout +++ /dev/null @@ -1 +0,0 @@ -{"imageLayoutVersion": "1.0.0"} \ No newline at end of file diff --git a/image/oci/layout/oci_dest_test.go b/image/oci/layout/oci_dest_test.go index 82b7cbba72..bd7116240f 100644 --- a/image/oci/layout/oci_dest_test.go +++ b/image/oci/layout/oci_dest_test.go @@ -13,6 +13,8 @@ import ( imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.podman.io/image/v5/internal/imagedestination" + "go.podman.io/image/v5/internal/imagesource" "go.podman.io/image/v5/internal/private" "go.podman.io/image/v5/internal/signature" "go.podman.io/image/v5/pkg/blobinfocache/memory" @@ -220,133 +222,156 @@ func TestPutblobFromLocalFile(t *testing.T) { // TestPutSignaturesWithFormat tests that sigstore signatures are properly stored in OCI layout func TestPutSignaturesWithFormat(t *testing.T) { - tmpDir := loadFixture(t, "single_image_layout") - ref, err := NewReference(tmpDir, "latest") - require.NoError(t, err) - dest, err := ref.NewImageDestination(context.Background(), nil) - require.NoError(t, err) - defer dest.Close() - ociDest, ok := dest.(*ociImageDestination) - require.True(t, ok) - - desc, _, err := ociDest.ref.getManifestDescriptor() - require.NoError(t, err) - require.NotNil(t, desc) - - sigstoreSign := signature.SigstoreFromComponents( - "application/vnd.dev.cosign.simplesigning.v1+json", - []byte("test-payload"), - map[string]string{"dev.cosignproject.cosign/signature": "test-signature"}, - ) - - err = ociDest.PutSignaturesWithFormat(context.Background(), []signature.Signature{sigstoreSign}, &desc.Digest) - require.NoError(t, err) - - err = ociDest.Commit(context.Background(), nil) - require.NoError(t, err) - - src, err := ref.NewImageSource(context.Background(), nil) - require.NoError(t, err) - ociSrc, ok := src.(*ociImageSource) - require.True(t, ok) - sign, err := ociSrc.GetSignaturesWithFormat(context.Background(), &desc.Digest) - require.NoError(t, err) - require.Len(t, sign, 1) - require.Equal(t, sigstoreSign, sign[0]) -} - -// TestPutSignaturesWithFormatTwice tests PutSignaturesWithFormat twice and checks -func TestPutSignaturesWithFormatTwice(t *testing.T) { - tmpDir := loadFixture(t, "single_image_layout") - ref, err := NewReference(tmpDir, "latest") - require.NoError(t, err) - dest, err := ref.NewImageDestination(context.Background(), nil) - require.NoError(t, err) - defer dest.Close() - ociDest, ok := dest.(*ociImageDestination) - require.True(t, ok) - - desc, _, err := ociDest.ref.getManifestDescriptor() - require.NoError(t, err) - require.NotNil(t, desc) - - sigstoreSign := signature.SigstoreFromComponents( - "application/vnd.dev.cosign.simplesigning.v1+json", - []byte("test-payload"), - map[string]string{"dev.cosignproject.cosign/signature": "test-signature"}, - ) - sigstoreSign2 := signature.SigstoreFromComponents( - "application/vnd.dev.cosign.simplesigning.v1+json", - []byte("test-payload2"), - map[string]string{"dev.cosignproject.cosign/signature": "test-signature"}, - ) - - err = ociDest.PutSignaturesWithFormat(context.Background(), []signature.Signature{sigstoreSign}, &desc.Digest) - require.NoError(t, err) - - err = ociDest.Commit(context.Background(), nil) - require.NoError(t, err) - - err = ociDest.PutSignaturesWithFormat(context.Background(), []signature.Signature{sigstoreSign, sigstoreSign2}, &desc.Digest) - require.NoError(t, err) - - err = ociDest.Commit(context.Background(), nil) - require.NoError(t, err) - - src, err := ref.NewImageSource(context.Background(), nil) - require.NoError(t, err) - ociSrc, ok := src.(*ociImageSource) - require.True(t, ok) - sign, err := ociSrc.GetSignaturesWithFormat(context.Background(), &desc.Digest) - require.NoError(t, err) - require.Len(t, sign, 2) - require.Equal(t, sigstoreSign, sign[0]) - require.Equal(t, sigstoreSign2, sign[1]) -} - -// TestPutSignaturesWithFormatNilDigest tests error handling when instanceDigest is nil -func TestPutSignaturesWithFormatNilDigest(t *testing.T) { - ref, _ := refToTempOCI(t, false) - - dest, err := ref.NewImageDestination(context.Background(), nil) - require.NoError(t, err) - defer dest.Close() + for _, test := range []struct { + name string + manifestDigest digest.Digest + signaturesList [][]signature.Signature + expectedSignatures []signature.Signature + expectedError string + }{ + { + name: "single signature, single PutSignaturesWithFormat", + signaturesList: [][]signature.Signature{ + { + signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature"}, + ), + }, + }, + expectedSignatures: []signature.Signature{ + signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature"}, + ), + }, + }, + { + name: "multiple signatures", + signaturesList: [][]signature.Signature{ + { + signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload1"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature1"}, + ), + signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload2"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature2"}, + ), + }, + }, + expectedSignatures: []signature.Signature{ + signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload1"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature1"}, + ), + signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload2"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature2"}, + ), + }, + }, + { + name: "multiple PutSignaturesWithFormat with the same image", + signaturesList: [][]signature.Signature{ + { + signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature"}, + ), + }, + { + signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature"}, + ), + }, + }, + expectedSignatures: []signature.Signature{ + signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature"}, + ), + }, + }, + { + name: "multiple PutSignaturesWithFormat with the different images", + signaturesList: [][]signature.Signature{ + { + signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload1"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature1"}, + ), + }, + { + signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload2"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature2"}, + ), + }, + }, + expectedSignatures: []signature.Signature{ + signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload1"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature1"}, + ), + signature.SigstoreFromComponents( + "application/vnd.dev.cosign.simplesigning.v1+json", + []byte("test-payload2"), + map[string]string{"dev.cosignproject.cosign/signature": "test-signature2"}, + ), + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + tmpDir := t.TempDir() + ref, err := NewReference(tmpDir, "latest") + require.NoError(t, err) + ociRef := ref.(ociReference) + putTestManifest(t, ociRef, tmpDir) + + dest, err := ref.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dest.Close() + ociDest := imagedestination.FromPublic(dest) + + // get digest of the manifest + desc, _, err := ociRef.getManifestDescriptor() + require.NoError(t, err) + + for _, sigs := range test.signaturesList { + err = ociDest.PutSignaturesWithFormat(context.Background(), sigs, &desc.Digest) + if test.expectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), test.expectedError) + continue + } + require.NoError(t, err) + err = ociDest.Commit(context.Background(), nil) + require.NoError(t, err) + } - // Cast to ociImageDestination to access PutSignaturesWithFormat - ociDest, ok := dest.(*ociImageDestination) - require.True(t, ok) + src, err := ref.NewImageSource(context.Background(), nil) + require.NoError(t, err) + ociSrc := imagesource.FromPublic(src) + sign, err := ociSrc.GetSignaturesWithFormat(context.Background(), &desc.Digest) + require.NoError(t, err) - // Create a test signature - testPayload := []byte(`{"test": "payload"}`) - testAnnotations := map[string]string{ - "dev.cosignproject.cosign/signature": "test-signature", + for i, sig := range test.expectedSignatures { + require.Equal(t, sig, sign[i]) + } + }) } - sig := signature.SigstoreFromComponents("application/vnd.dev.cosign.simplesigning.v1+json", testPayload, testAnnotations) - - // Test that PutSignaturesWithFormat fails when instanceDigest is nil - err = ociDest.PutSignaturesWithFormat(context.Background(), []signature.Signature{sig}, nil) - require.Error(t, err) - require.Contains(t, err.Error(), "unknown manifest digest, can't add signatures") -} - -// TestPutSignaturesWithFormatNonSigstore tests error handling for non-sigstore signatures -func TestPutSignaturesWithFormatNonSigstore(t *testing.T) { - ref, _ := refToTempOCI(t, false) - - dest, err := ref.NewImageDestination(context.Background(), nil) - require.NoError(t, err) - defer dest.Close() - - // Cast to ociImageDestination to access PutSignaturesWithFormat - ociDest, ok := dest.(*ociImageDestination) - require.True(t, ok) - - // Create a non-sigstore signature (simple signing) - simpleSig := signature.SimpleSigningFromBlob([]byte("simple signature data")) - testDigest := digest.FromString("test-manifest") - - // Test that PutSignaturesWithFormat fails for non-sigstore signatures - err = ociDest.PutSignaturesWithFormat(context.Background(), []signature.Signature{simpleSig}, &testDigest) - require.Error(t, err) - require.Contains(t, err.Error(), "oci: layout only supports sigstore signatures") } From 278c5db365eb1b5fa4df9c5a899f213544dbefdf Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Mon, 29 Sep 2025 14:16:36 +0000 Subject: [PATCH 21/22] Refactor signature handling in `putSignaturesToSigstoreAttachment` and `getSigstoreAttachmentManifest` Signed-off-by: Ayato Tokubi --- image/oci/layout/oci_dest.go | 46 +++---------------------------- image/oci/layout/oci_src.go | 2 +- image/oci/layout/oci_transport.go | 16 +++++------ 3 files changed, 13 insertions(+), 51 deletions(-) diff --git a/image/oci/layout/oci_dest.go b/image/oci/layout/oci_dest.go index 47e94632e0..efa5ed66a7 100644 --- a/image/oci/layout/oci_dest.go +++ b/image/oci/layout/oci_dest.go @@ -315,33 +315,6 @@ func (d *ociImageDestination) addManifest(desc *imgspecv1.Descriptor) { d.index.Manifests = append(slices.Clone(d.index.Manifests), *desc) } -// addSignatureManifest is similar to addManifest, but replace the entry based on imgspecv1.AnnotationRefName -// and returns the old digest to delete it later. -func (d *ociImageDestination) addSignatureManifest(desc *imgspecv1.Descriptor) (*imgspecv1.Descriptor, error) { - if desc.Annotations == nil || desc.Annotations[imgspecv1.AnnotationRefName] == "" { - return nil, errors.New("cannot add signature manifest without ref.name") - } - for i, m := range d.index.Manifests { - if m.Annotations[imgspecv1.AnnotationRefName] == desc.Annotations[imgspecv1.AnnotationRefName] { - // Replace it completely. - oldDesc := d.index.Manifests[i] - d.index.Manifests[i] = *desc - return &oldDesc, nil - } - } - // It shouldn't happen, but if there's no entry with the same ref name, but the same digest, just replace it. - for i, m := range d.index.Manifests { - if m.Digest == desc.Digest && m.Annotations[imgspecv1.AnnotationRefName] == "" { - // Replace it completely. - d.index.Manifests[i] = *desc - return nil, nil - } - } - // It's a new entry to be added to the index. Use slices.Clone() to avoid a remote dependency on how d.index was created. - d.index.Manifests = append(slices.Clone(d.index.Manifests), *desc) - return nil, nil -} - // CommitWithOptions marks the process of storing the image as successful and asks for the image to be persisted. // WARNING: This does not have any transactional semantics: // - Uploaded data MAY be visible to others before CommitWithOptions() is called @@ -411,7 +384,7 @@ func (d *ociImageDestination) PutSignaturesWithFormat(ctx context.Context, signa func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Context, signatures []signature.Sigstore, manifestDigest digest.Digest) error { var signConfig imgspecv1.Image // Most fields empty by default - signManifest, err := d.ref.getSigstoreAttachmentManifest(manifestDigest, &d.index, d.sharedBlobDir) + signManifest, signDesc, err := d.ref.getSigstoreAttachmentManifest(manifestDigest, &d.index, d.sharedBlobDir) if err != nil { return err } @@ -432,7 +405,8 @@ func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Cont return fmt.Errorf("parsing sigstore attachment config %s in %s: %w", signManifest.Config.Digest.String(), d.ref.StringWithinTransport(), err) } - // The config of the signature manifest will be updated and unreferenced when a new config is created. + // The signature manifest and its config may be updated and unreferenced when a new config is created. + d.blobDeleteCandidates.Add(signDesc.Digest) d.blobDeleteCandidates.Add(signManifest.Config.Digest) } @@ -500,7 +474,7 @@ func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Cont if err != nil { return err } - oldDesc, err := d.addSignatureManifest(&imgspecv1.Descriptor{ + d.addManifest(&imgspecv1.Descriptor{ MediaType: signManifest.MediaType, Digest: signDigest, Size: int64(len(signManifestBlob)), @@ -508,18 +482,6 @@ func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Cont imgspecv1.AnnotationRefName: signTag, }, }) - if err != nil { - return err - } - // If it overwrote an existing signature manifest, delete blobs referenced by the old manifest. - if oldDesc != nil { - referencedBlobs := make(map[digest.Digest]int) - err = d.ref.countBlobsForDescriptor(referencedBlobs, oldDesc, d.sharedBlobDir) - if err != nil { - return fmt.Errorf("error counting blobs for digest %s: %w", oldDesc.Digest.String(), err) - } - d.blobDeleteCandidates.AddSeq(maps.Keys(referencedBlobs)) - } return nil } diff --git a/image/oci/layout/oci_src.go b/image/oci/layout/oci_src.go index cee0f2aae9..2fd1da7db6 100644 --- a/image/oci/layout/oci_src.go +++ b/image/oci/layout/oci_src.go @@ -240,7 +240,7 @@ func (s *ociImageSource) GetSignaturesWithFormat(ctx context.Context, instanceDi instanceDigest = &s.descriptor.Digest } - ociManifest, err := s.ref.getSigstoreAttachmentManifest(*instanceDigest, s.index, s.sharedBlobDir) + ociManifest, _, err := s.ref.getSigstoreAttachmentManifest(*instanceDigest, s.index, s.sharedBlobDir) if err != nil { return nil, err } diff --git a/image/oci/layout/oci_transport.go b/image/oci/layout/oci_transport.go index 70dcbd2340..8f5134e9b2 100644 --- a/image/oci/layout/oci_transport.go +++ b/image/oci/layout/oci_transport.go @@ -338,10 +338,10 @@ func sigstoreAttachmentTag(d digest.Digest) (string, error) { return strings.Replace(d.String(), ":", "-", 1) + ".sig", nil } -func (ref ociReference) getSigstoreAttachmentManifest(d digest.Digest, idx *imgspecv1.Index, sharedBlobDir string) (*manifest.OCI1, error) { +func (ref ociReference) getSigstoreAttachmentManifest(d digest.Digest, idx *imgspecv1.Index, sharedBlobDir string) (*manifest.OCI1, *imgspecv1.Descriptor, error) { signTag, err := sigstoreAttachmentTag(d) if err != nil { - return nil, err + return nil, nil, err } var signDesc *imgspecv1.Descriptor for _, m := range idx.Manifests { @@ -352,26 +352,26 @@ func (ref ociReference) getSigstoreAttachmentManifest(d digest.Digest, idx *imgs } if signDesc == nil { // No signature found - return nil, nil + return nil, nil, nil } if signDesc.MediaType != imgspecv1.MediaTypeImageManifest { - return nil, fmt.Errorf("unexpected MIME type for sigstore attachment manifest %s: %q", + return nil, nil, fmt.Errorf("unexpected MIME type for sigstore attachment manifest %s: %q", signTag, signDesc.MediaType) } blobReader, _, err := ref.getBlob(signDesc.Digest, sharedBlobDir) if err != nil { - return nil, fmt.Errorf("failed to get Blob %s: %w", signTag, err) + return nil, nil, fmt.Errorf("failed to get Blob %s: %w", signTag, err) } defer blobReader.Close() signBlob, err := iolimits.ReadAtMost(blobReader, iolimits.MaxManifestBodySize) if err != nil { - return nil, fmt.Errorf("failed to read blob: %w", err) + return nil, nil, fmt.Errorf("failed to read blob: %w", err) } res, err := manifest.OCI1FromManifest(signBlob) if err != nil { - return nil, fmt.Errorf("parsing manifest %s: %w", signDesc.Digest, err) + return nil, nil, fmt.Errorf("parsing manifest %s: %w", signDesc.Digest, err) } - return res, nil + return res, signDesc, nil } func (ref ociReference) getBlob(d digest.Digest, sharedBlobDir string) (io.ReadCloser, int64, error) { From f74ad300c684ea48fd9ebe07d137c8188b5a986d Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Mon, 29 Sep 2025 17:01:07 +0000 Subject: [PATCH 22/22] Refactor blob deletion logic Signed-off-by: Ayato Tokubi --- ...148a0d76459d390fea17f70e5af3c5833321d1939c | 1 + ...ea7ad4f8a5f0e23bc16068d612227507e54599c18a | 1 + ...1268640edf4e88beab1b4e1e1bfc9b1891a1cab861 | 1 + ...0aa79e5a8331095c422999387f9d52351009fcd801 | 1 + ...b1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 | 1 + ...8ad913a318a390488c9f34c46e43424795cdabffe8 | 1 + ...75499ae711bc6f8222de38d9f1b5a4097583195ad5 | 16 ++++ ...adcb769953de235e62e3e32051d57a5e66246de4a1 | 24 +++++ ...a40ce8766ecd552249305141de88f0ca61f3d1368f | 1 + ...67aef503b48ad51b32248511326474eed59f2fa38c | 27 ++++++ ...60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a | 1 + ...c8996fb92427ae41e4649b934ca495991b7852b855 | 27 ++++++ ...e15294d935f55da58ce57c4f9218cad38d0be82ce3 | 16 ++++ .../index.json | 29 +++++++ .../oci-layout | 1 + ...ea7ad4f8a5f0e23bc16068d612227507e54599c18a | 1 + ...1268640edf4e88beab1b4e1e1bfc9b1891a1cab861 | 1 + ...b1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 | 1 + ...8ad913a318a390488c9f34c46e43424795cdabffe8 | 1 + ...75499ae711bc6f8222de38d9f1b5a4097583195ad5 | 16 ++++ ...adcb769953de235e62e3e32051d57a5e66246de4a1 | 24 +++++ ...a40ce8766ecd552249305141de88f0ca61f3d1368f | 1 + ...60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a | 1 + ...c8996fb92427ae41e4649b934ca495991b7852b855 | 27 ++++++ ...e15294d935f55da58ce57c4f9218cad38d0be82ce3 | 16 ++++ .../index.json | 29 +++++++ .../oci-layout | 1 + ...c8996fb92427ae41e4649b934ca495991b7852b855 | 2 +- image/oci/layout/oci_delete.go | 87 ++++++++++++++++--- image/oci/layout/oci_delete_test.go | 44 +++++++++- 30 files changed, 386 insertions(+), 14 deletions(-) create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/06047b0580a020bb2d90af148a0d76459d390fea17f70e5af3c5833321d1939c create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861 create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/6bc6d9a506e2c6452307700aa79e5a8331095c422999387f9d52351009fcd801 create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8 create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5 create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1 create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/d1fd915a475c7d56aa31bc67aef503b48ad51b32248511326474eed59f2fa38c create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3 create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/index.json create mode 100644 image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/oci-layout create mode 100644 image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a create mode 100644 image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861 create mode 100644 image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 create mode 100644 image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8 create mode 100644 image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5 create mode 100644 image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1 create mode 100644 image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f create mode 100644 image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a create mode 100644 image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 create mode 100644 image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3 create mode 100644 image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/index.json create mode 100644 image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/oci-layout diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/06047b0580a020bb2d90af148a0d76459d390fea17f70e5af3c5833321d1939c b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/06047b0580a020bb2d90af148a0d76459d390fea17f70e5af3c5833321d1939c new file mode 100644 index 0000000000..8fe564848e --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/06047b0580a020bb2d90af148a0d76459d390fea17f70e5af3c5833321d1939c @@ -0,0 +1 @@ +{"architecture":"","os":"","config":{},"rootfs":{"type":"layers","diff_ids":["sha256:6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273"]}} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a new file mode 100644 index 0000000000..bf54928cb8 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a @@ -0,0 +1 @@ +{"architecture":"","os":"","config":{},"rootfs":{"type":"","diff_ids":["sha256:6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273"]}} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861 b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861 new file mode 100644 index 0000000000..19c1c1276f --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861 @@ -0,0 +1 @@ +insert binary content here #9811 diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/6bc6d9a506e2c6452307700aa79e5a8331095c422999387f9d52351009fcd801 b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/6bc6d9a506e2c6452307700aa79e5a8331095c422999387f9d52351009fcd801 new file mode 100644 index 0000000000..1332accaa4 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/6bc6d9a506e2c6452307700aa79e5a8331095c422999387f9d52351009fcd801 @@ -0,0 +1 @@ +test-payload2 \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 new file mode 100644 index 0000000000..adb6ebe808 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 @@ -0,0 +1 @@ +test-payload \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8 b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8 new file mode 100644 index 0000000000..ebe323d4df --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8 @@ -0,0 +1 @@ +{"created":"2023-08-07T19:20:20.894140623Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"]},"rootfs":{"type":"layers","diff_ids":["sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230"]},"history":[{"created":"2023-08-07T19:20:20.71894984Z","created_by":"/bin/sh -c #(nop) ADD file:32ff5e7a78b890996ee4681cc0a26185d3e9acdb4eb1e2aaccb2411f922fed6b in / "},{"created":"2023-08-07T19:20:20.894140623Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/sh\"]","empty_layer":true}]} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5 b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5 new file mode 100644 index 0000000000..ccf025c98f --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5 @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8", + "size": 584 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861", + "size": 33 + } + ] +} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1 b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1 new file mode 100644 index 0000000000..aeecdfac4e --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1 @@ -0,0 +1,24 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5", + "size": 525, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3", + "size": 525, + "platform": { + "architecture": "386", + "os": "linux" + } + } + ] +} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f new file mode 100644 index 0000000000..e1d45d3569 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f @@ -0,0 +1 @@ +{"created":"2023-08-07T19:38:27.007952531Z","architecture":"386","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"]},"rootfs":{"type":"layers","diff_ids":["sha256:f05b0759429ba12d5fda46c196f253cc1cab8f56cd874e9e7be674fc1b8337de"]},"history":[{"created":"2023-08-07T19:38:26.69689892Z","created_by":"/bin/sh -c #(nop) ADD file:4b33c52e11b19fde30197c62ead0b77bde28d34edaa08346a5302cd892d3cebe in / "},{"created":"2023-08-07T19:38:27.007952531Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/sh\"]","empty_layer":true}]} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/d1fd915a475c7d56aa31bc67aef503b48ad51b32248511326474eed59f2fa38c b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/d1fd915a475c7d56aa31bc67aef503b48ad51b32248511326474eed59f2fa38c new file mode 100644 index 0000000000..da5edd1633 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/d1fd915a475c7d56aa31bc67aef503b48ad51b32248511326474eed59f2fa38c @@ -0,0 +1,27 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:06047b0580a020bb2d90af148a0d76459d390fea17f70e5af3c5833321d1939c", + "size": 153 + }, + "layers": [ + { + "mediaType": "application/vnd.dev.cosign.simplesigning.v1+json", + "digest": "sha256:6bc6d9a506e2c6452307700aa79e5a8331095c422999387f9d52351009fcd801", + "size": 13, + "annotations": { + "dev.cosignproject.cosign/signature": "test-signature" + } + } + ], + "subject": { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3", + "size": 1506, + "annotations": { + "org.opencontainers.image.ref.name": "latest" + } + } +} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a new file mode 100644 index 0000000000..832c1185d8 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a @@ -0,0 +1 @@ +insert binary content here #28017 diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 new file mode 100644 index 0000000000..ea06b2573d --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -0,0 +1,27 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a", + "size": 147 + }, + "layers": [ + { + "mediaType": "application/vnd.dev.cosign.simplesigning.v1+json", + "digest": "sha256:6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273", + "size": 12, + "annotations": { + "dev.cosignproject.cosign/signature": "test-signature" + } + } + ], + "subject": { + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1", + "size": 759, + "annotations": { + "org.opencontainers.image.ref.name": "latest" + } + } +} diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3 b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3 new file mode 100644 index 0000000000..fb85ad20ac --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/blobs/sha256/f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3 @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f", + "size": 582 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a", + "size": 34 + } + ] +} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/index.json b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/index.json new file mode 100644 index 0000000000..bfcae7da3e --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/index.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1", + "size": 759, + "annotations": { + "org.opencontainers.image.ref.name": "latest" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 704, + "annotations": { + "org.opencontainers.image.ref.name": "sha256-a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1.sig" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:d1fd915a475c7d56aa31bc67aef503b48ad51b32248511326474eed59f2fa38c", + "size": 700, + "annotations": { + "org.opencontainers.image.ref.name": "sha256-f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3.sig" + } + } + ] +} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/oci-layout b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/oci-layout new file mode 100644 index 0000000000..21b1439d1c --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_index_with_multiple_signatures/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion": "1.0.0"} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a new file mode 100644 index 0000000000..bf54928cb8 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a @@ -0,0 +1 @@ +{"architecture":"","os":"","config":{},"rootfs":{"type":"","diff_ids":["sha256:6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273"]}} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861 b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861 new file mode 100644 index 0000000000..19c1c1276f --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861 @@ -0,0 +1 @@ +insert binary content here #9811 diff --git a/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 new file mode 100644 index 0000000000..adb6ebe808 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273 @@ -0,0 +1 @@ +test-payload \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8 b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8 new file mode 100644 index 0000000000..ebe323d4df --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8 @@ -0,0 +1 @@ +{"created":"2023-08-07T19:20:20.894140623Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"]},"rootfs":{"type":"layers","diff_ids":["sha256:4693057ce2364720d39e57e85a5b8e0bd9ac3573716237736d6470ec5b7b7230"]},"history":[{"created":"2023-08-07T19:20:20.71894984Z","created_by":"/bin/sh -c #(nop) ADD file:32ff5e7a78b890996ee4681cc0a26185d3e9acdb4eb1e2aaccb2411f922fed6b in / "},{"created":"2023-08-07T19:20:20.894140623Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/sh\"]","empty_layer":true}]} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5 b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5 new file mode 100644 index 0000000000..ccf025c98f --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5 @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:913cf3a39d377faf89ed388ad913a318a390488c9f34c46e43424795cdabffe8", + "size": 584 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861", + "size": 33 + } + ] +} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1 b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1 new file mode 100644 index 0000000000..aeecdfac4e --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1 @@ -0,0 +1,24 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:93cbd11a4f41467a0409b975499ae711bc6f8222de38d9f1b5a4097583195ad5", + "size": 525, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3", + "size": 525, + "platform": { + "architecture": "386", + "os": "linux" + } + } + ] +} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f new file mode 100644 index 0000000000..e1d45d3569 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f @@ -0,0 +1 @@ +{"created":"2023-08-07T19:38:27.007952531Z","architecture":"386","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"]},"rootfs":{"type":"layers","diff_ids":["sha256:f05b0759429ba12d5fda46c196f253cc1cab8f56cd874e9e7be674fc1b8337de"]},"history":[{"created":"2023-08-07T19:38:26.69689892Z","created_by":"/bin/sh -c #(nop) ADD file:4b33c52e11b19fde30197c62ead0b77bde28d34edaa08346a5302cd892d3cebe in / "},{"created":"2023-08-07T19:38:27.007952531Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/sh\"]","empty_layer":true}]} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a new file mode 100644 index 0000000000..832c1185d8 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a @@ -0,0 +1 @@ +insert binary content here #28017 diff --git a/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 new file mode 100644 index 0000000000..ea06b2573d --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -0,0 +1,27 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:44353f0bf0dd9507c2e9daea7ad4f8a5f0e23bc16068d612227507e54599c18a", + "size": 147 + }, + "layers": [ + { + "mediaType": "application/vnd.dev.cosign.simplesigning.v1+json", + "digest": "sha256:6f06dd0e26608013eff30bb1e951cda7de3fdd9e78e907470e0dd5c0ed25e273", + "size": 12, + "annotations": { + "dev.cosignproject.cosign/signature": "test-signature" + } + } + ], + "subject": { + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1", + "size": 759, + "annotations": { + "org.opencontainers.image.ref.name": "latest" + } + } +} diff --git a/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3 b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3 new file mode 100644 index 0000000000..fb85ad20ac --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/blobs/sha256/f6d60fd529b234d3e28837e15294d935f55da58ce57c4f9218cad38d0be82ce3 @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:aab808b283c3f654d84358a40ce8766ecd552249305141de88f0ca61f3d1368f", + "size": 582 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:e19729d5a968c71b4b691d60f4a6f85f93c303bb88635dcfef36e23b76cb7b3a", + "size": 34 + } + ] +} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/index.json b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/index.json new file mode 100644 index 0000000000..8c2a9dd0ae --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/index.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1", + "size": 759, + "annotations": { + "org.opencontainers.image.ref.name": "latest" + } + }, + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1", + "size": 759, + "annotations": { + "org.opencontainers.image.ref.name": "3.18.3" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 704, + "annotations": { + "org.opencontainers.image.ref.name": "sha256-a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1.sig" + } + } + ] +} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/oci-layout b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/oci-layout new file mode 100644 index 0000000000..21b1439d1c --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_multiple_images_with_single_signature/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion": "1.0.0"} \ No newline at end of file diff --git a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 index aa7a15becc..ad52fa3a2d 100644 --- a/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +++ b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -21,7 +21,7 @@ "digest": "sha256:eaa95f3cfaac07c8a5153eb77c933269586ad0226c83405776be08547e4d2a18", "size": 1506, "annotations": { - "org.opencontainers.image.ref.name": "imageValue" + "org.opencontainers.image.ref.name": "latest" } } } diff --git a/image/oci/layout/oci_delete.go b/image/oci/layout/oci_delete.go index 1ebd452f6e..84460ec6e5 100644 --- a/image/oci/layout/oci_delete.go +++ b/image/oci/layout/oci_delete.go @@ -3,7 +3,6 @@ package layout import ( "context" "encoding/json" - "errors" "fmt" "io/fs" "os" @@ -38,6 +37,11 @@ func (ref ociReference) DeleteImage(ctx context.Context, sys *types.SystemContex return err } + signaturesToDelete, err := ref.getObsoleteSignatures(blobsToDelete) + if err != nil { + return err + } + err = ref.deleteBlobs(blobsToDelete) if err != nil { return err @@ -48,10 +52,7 @@ func (ref ociReference) DeleteImage(ctx context.Context, sys *types.SystemContex return err } - if isSigstoreTag(ref.image) { - return nil - } - return ref.deleteSignatures(ctx, sys, descriptor.Digest) + return ref.deleteSignatures(sys, signaturesToDelete) } // countBlobsForDescriptor updates dest with usage counts of blobs required for descriptor, INCLUDING descriptor itself. @@ -157,6 +158,7 @@ func deleteBlob(blobPath string) error { } } +// deleteReferencesFromIndex deletes manifest from the root index. func (ref ociReference) deleteReferenceFromIndex(referenceIndex int) error { index, err := ref.getIndex() if err != nil { @@ -168,6 +170,25 @@ func (ref ociReference) deleteReferenceFromIndex(referenceIndex int) error { return saveJSON(ref.indexPath(), index) } +// deleteReferencesFromIndex deletes referenceIndex first, and then remove signatures. +func (ref ociReference) deleteSignaturesFromIndex(signatures []imgspecv1.Descriptor) error { + index, err := ref.getIndex() + if err != nil { + return err + } + + signaturesSet := set.New[digest.Digest]() + for _, sign := range signatures { + signaturesSet.Add(sign.Digest) + } + + index.Manifests = slices.DeleteFunc(index.Manifests, func(d imgspecv1.Descriptor) bool { + return signaturesSet.Contains(d.Digest) + }) + + return saveJSON(ref.indexPath(), index) +} + func saveJSON(path string, content any) (retErr error) { // If the file already exists, get its mode to preserve it var mode fs.FileMode @@ -197,21 +218,63 @@ func saveJSON(path string, content any) (retErr error) { return json.NewEncoder(file).Encode(content) } +func (ref ociReference) getObsoleteSignatures(blobsToDelete *set.Set[digest.Digest]) (signaturesToDelete []imgspecv1.Descriptor, err error) { + // create a mapping from sigstore tag to its descriptor + signDigestMap := make(map[string]imgspecv1.Descriptor) + index, err := ref.getIndex() + if err != nil { + return nil, err + } + for _, m := range index.Manifests { + if isSigstoreTag(m.Annotations[imgspecv1.AnnotationRefName]) { + signDigestMap[m.Annotations[imgspecv1.AnnotationRefName]] = m + } + } + + for dgst := range blobsToDelete.All() { + sigstoreTag, err := sigstoreAttachmentTag(dgst) + if err != nil { + // This shouldn't happen because all digests in the root index should be valid. + continue + } + signDesc, ok := signDigestMap[sigstoreTag] + if !ok { + // No signature found for this digest + continue + } + signaturesToDelete = append(signaturesToDelete, signDesc) + } + return signaturesToDelete, nil +} + // deleteSignatures delete sigstore signatures of the given manifest digest. -func (ref ociReference) deleteSignatures(ctx context.Context, sys *types.SystemContext, d digest.Digest) error { - signTag, err := sigstoreAttachmentTag(d) +func (ref ociReference) deleteSignatures(sys *types.SystemContext, signaturesToDelete []imgspecv1.Descriptor) error { + sharedBlobsDir := "" + if sys != nil && sys.OCISharedBlobDirPath != "" { + sharedBlobsDir = sys.OCISharedBlobDirPath + } + + blobsUsedByImage := make(map[digest.Digest]int) + for _, descriptor := range signaturesToDelete { + if err := ref.countBlobsForDescriptor(blobsUsedByImage, &descriptor, sharedBlobsDir); err != nil { + return err + } + } + + blobsToDelete, err := ref.getBlobsToDelete(blobsUsedByImage, sharedBlobsDir) if err != nil { return err } - signRef, err := newReference(ref.dir, signTag, -1) + err = ref.deleteBlobs(blobsToDelete) if err != nil { return err } - err = signRef.DeleteImage(ctx, sys) - if err != nil && errors.As(err, &ImageNotFoundError{}) { - return nil + err = ref.deleteSignaturesFromIndex(signaturesToDelete) + if err != nil { + return err } - return err + + return nil } diff --git a/image/oci/layout/oci_delete_test.go b/image/oci/layout/oci_delete_test.go index 9f2f54f0c9..30ae792065 100644 --- a/image/oci/layout/oci_delete_test.go +++ b/image/oci/layout/oci_delete_test.go @@ -40,7 +40,7 @@ func TestReferenceDeleteImage_onlyOneImage(t *testing.T) { require.Equal(t, 0, len(index.Manifests)) } -func TestReferenceDeleteImage_onlyOneImageWithSignatures(t *testing.T) { +func TestReferenceDeleteImage_onlyOneImageWithSignature(t *testing.T) { tmpDir := loadFixture(t, "delete_image_with_signature") ref, err := NewReference(tmpDir, "latest") @@ -67,6 +67,48 @@ func TestReferenceDeleteImage_onlyOneImageWithSignatures(t *testing.T) { require.Equal(t, 0, len(index.Manifests)) } +func TestReferenceDeleteImage_multipleImageWithSignature(t *testing.T) { + tmpDir := loadFixture(t, "delete_image_multiple_images_with_single_signature") + + ref, err := NewReference(tmpDir, "latest") + require.NoError(t, err) + + ociRef, ok := ref.(ociReference) + require.True(t, ok) + index, err := ociRef.getIndex() + require.NoError(t, err) + require.Equal(t, 3, len(index.Manifests)) + + err = ref.DeleteImage(context.Background(), nil) + require.NoError(t, err) + + // Check that the index is empty as there is only one image in the fixture + index, err = ociRef.getIndex() + require.NoError(t, err) + require.Equal(t, 2, len(index.Manifests)) +} + +func TestReferenceDeleteImage_indexWithMultipleSignatures(t *testing.T) { + tmpDir := loadFixture(t, "delete_image_index_with_multiple_signatures") + + ref, err := NewReference(tmpDir, "latest") + require.NoError(t, err) + + ociRef, ok := ref.(ociReference) + require.True(t, ok) + index, err := ociRef.getIndex() + require.NoError(t, err) + require.Equal(t, 3, len(index.Manifests)) + + err = ref.DeleteImage(context.Background(), nil) + require.NoError(t, err) + + // Check that the index is empty as there is only one image in the fixture + index, err = ociRef.getIndex() + require.NoError(t, err) + require.Equal(t, 0, len(index.Manifests)) +} + func TestReferenceDeleteImage_onlyOneImage_emptyImageName(t *testing.T) { tmpDir := loadFixture(t, "delete_image_only_one_image")