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/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/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/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/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/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 b/image/oci/layout/fixtures/delete_image_with_signature/blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 new file mode 100644 index 0000000000..ad52fa3a2d --- /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": "latest" + } + } +} 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 new file mode 100644 index 0000000000..94c28500b2 --- /dev/null +++ b/image/oci/layout/fixtures/delete_image_with_signature/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/fixtures/delete_image_with_signature/oci-layout b/image/oci/layout/fixtures/delete_image_with_signature/oci-layout new file mode 100644 index 0000000000..21b1439d1c --- /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/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_delete.go b/image/oci/layout/oci_delete.go index 7eaf6f0889..84460ec6e5 100644 --- a/image/oci/layout/oci_delete.go +++ b/image/oci/layout/oci_delete.go @@ -37,12 +37,22 @@ 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 } - return ref.deleteReferenceFromIndex(descriptorIndex) + err = ref.deleteReferenceFromIndex(descriptorIndex) + if err != nil { + return err + } + + return ref.deleteSignatures(sys, signaturesToDelete) } // countBlobsForDescriptor updates dest with usage counts of blobs required for descriptor, INCLUDING descriptor itself. @@ -148,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 { @@ -159,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 @@ -187,3 +217,64 @@ 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(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 + } + + err = ref.deleteBlobs(blobsToDelete) + if err != nil { + return err + } + + err = ref.deleteSignaturesFromIndex(signaturesToDelete) + if err != nil { + return err + } + + return nil +} diff --git a/image/oci/layout/oci_delete_test.go b/image/oci/layout/oci_delete_test.go index a80bf04177..30ae792065 100644 --- a/image/oci/layout/oci_delete_test.go +++ b/image/oci/layout/oci_delete_test.go @@ -40,6 +40,75 @@ func TestReferenceDeleteImage_onlyOneImage(t *testing.T) { require.Equal(t, 0, len(index.Manifests)) } +func TestReferenceDeleteImage_onlyOneImageWithSignature(t *testing.T) { + tmpDir := loadFixture(t, "delete_image_with_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, 2, len(index.Manifests)) + + 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 + index, err = ociRef.getIndex() + require.NoError(t, err) + 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") diff --git a/image/oci/layout/oci_dest.go b/image/oci/layout/oci_dest.go index 48fe812df5..efa5ed66a7 100644 --- a/image/oci/layout/oci_dest.go +++ b/image/oci/layout/oci_dest.go @@ -1,25 +1,32 @@ package layout import ( + "bytes" "context" "encoding/json" "errors" "fmt" "io" "io/fs" + "maps" "os" "path/filepath" "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/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" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/fileutils" ) @@ -27,13 +34,17 @@ import ( type ociImageDestination struct { impl.Compat impl.PropertyMethodsInitialize + stubs.AlwaysSupportsSignatures stubs.IgnoresOriginalOCIConfig stubs.NoPutBlobPartialInitialize - stubs.NoSignaturesInitialize - ref ociReference - index imgspecv1.Index - sharedBlobDir string + ref ociReference + index imgspecv1.Index + sharedBlobDir string + 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. @@ -75,10 +86,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, + ref: ref, + index: *index, + blobDeleteCandidates: set.New[digest.Digest](), } d.Compat = impl.AddCompat(d) if sys != nil { @@ -255,6 +266,9 @@ func (d *ociImageDestination) PutManifest(ctx context.Context, m []byte, instanc 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. @@ -312,6 +326,26 @@ func (d *ociImageDestination) CommitWithOptions(ctx context.Context, options pri if err != nil { return err } + // Delete unreferenced blobs (e.g. old signature manifest and its config) + 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.blobDeleteCandidates.All() { + if blobsUsedInRootIndex[dgst] == 0 { + actualBlobsToDelete.Add(dgst) + } + } + err := d.ref.deleteBlobs(actualBlobsToDelete) + if err != nil { + return fmt.Errorf("error deleting blobs: %w", err) + } + d.blobDeleteCandidates = set.New[digest.Digest]() + } if err := os.WriteFile(d.ref.ociLayoutPath(), layoutBytes, 0644); err != nil { return err } @@ -322,6 +356,164 @@ 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 == "" { + // 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 + } + + var 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 sigstore signatures") + } + } + + if err := d.putSignaturesToSigstoreAttachment(ctx, sigstoreSignatures, *instanceDigest); 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, signDesc, 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.Digest, 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) + } + // 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) + } + + 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() + + // 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, + LayerIndex: nil, + }) + if err != nil { + return err + } + 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) + 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) { + 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 +} + // PutBlobFromLocalFileOption is unused but may receive functionality in the future. type PutBlobFromLocalFileOption struct{} @@ -412,3 +604,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_dest_test.go b/image/oci/layout/oci_dest_test.go index 464fc32d3e..bd7116240f 100644 --- a/image/oci/layout/oci_dest_test.go +++ b/image/oci/layout/oci_dest_test.go @@ -13,7 +13,10 @@ 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" "go.podman.io/image/v5/types" ) @@ -216,3 +219,159 @@ 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) { + 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) + } + + 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) + + for i, sig := range test.expectedSignatures { + require.Equal(t, sig, sign[i]) + } + }) + } +} diff --git a/image/oci/layout/oci_src.go b/image/oci/layout/oci_src.go index f265a21d70..2fd1da7db6 100644 --- a/image/oci/layout/oci_src.go +++ b/image/oci/layout/oci_src.go @@ -16,8 +16,10 @@ 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/tlsclientconfig" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/fileutils" @@ -37,7 +39,6 @@ func (e ImageNotFoundError) Error() string { type ociImageSource struct { impl.Compat impl.PropertyMethodsInitialize - impl.NoSignatures impl.DoesNotAffectLayerInfosForCopy stubs.NoGetBlobAtInitialize @@ -158,20 +159,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. @@ -246,3 +234,30 @@ 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 { + instanceDigest = &s.descriptor.Digest + } + + ociManifest, _, err := s.ref.getSigstoreAttachmentManifest(*instanceDigest, s.index, s.sharedBlobDir) + if err != nil { + return nil, err + } + if ociManifest == nil { + // No signature found + return nil, nil + } + + 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 + } + 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..8f5134e9b2 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" @@ -28,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") @@ -248,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 } } @@ -302,3 +329,101 @@ 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 +} + +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, 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, nil + } + if signDesc.MediaType != imgspecv1.MediaTypeImageManifest { + 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, 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, nil, fmt.Errorf("failed to read blob: %w", err) + } + res, err := manifest.OCI1FromManifest(signBlob) + if err != nil { + return nil, nil, fmt.Errorf("parsing manifest %s: %w", signDesc.Digest, err) + } + return res, signDesc, 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 { + _ = r.Close() // Avoid leak r. + return nil, 0, err + } + return r, fi.Size(), nil +} + +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 := dgst.Algorithm() + if !digestAlgorithm.Available() { + return nil, fmt.Errorf("invalid digest %q: unsupported digest algorithm %q", dgst.String(), digestAlgorithm.String()) + } + + 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", dgst.String(), ref.image, err) + } + actualDigest := digestAlgorithm.FromBytes(payload) + if actualDigest != dgst { + return nil, fmt.Errorf("digest mismatch, expected %q, got %q", dgst.String(), actualDigest.String()) + } + return payload, nil +} + +// isSigstoreTag returns true if the tag is sigstore signature tag. +func isSigstoreTag(tag string) bool { + digestPart, found := strings.CutSuffix(tag, ".sig") + if !found { + return false + } + digestPart = strings.Replace(digestPart, "-", ":", 1) + _, err := digest.Parse(digestPart) + return err == nil +} 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)