From fb1a43acfd92173d99c02d0192b2db977847ebaf Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 29 Apr 2024 15:02:21 -0500 Subject: [PATCH] feat: add attest sign/verify --- internal/test/test.go | 109 +++++++ internal/test/test_test.go | 23 ++ pkg/attest/sign.go | 285 ++++++++++++++++++ pkg/attest/sign_test.go | 176 +++++++++++ pkg/attest/types.go | 33 ++ pkg/attestation/types.go | 31 +- pkg/attestation/vsa.go | 55 ++++ pkg/oci/types.go | 61 ++++ scripts/gen-testdata.sh | 45 +++ test/Dockerfile | 5 + ...27aa6c918c390c373294ec4fc48f2c6fe703fcc6fe | 1 + ...4553dbbe762e4457a099ab8b706e67f5f9fc934701 | 50 +++ ...ddbb054b2b4c01f717d408efba753da2bf6e8905da | 19 ++ ...b6e13bcbafb8e0dc0adc0443f1a25666f9518c5071 | 1 + ...976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e | 16 + ...dd621f59e0d43a3b346f34c34eb58da31f00a9b0ad | Bin 0 -> 116 bytes ...f919823446286a674ad669d0baa8ab2c358aeb3a82 | 19 ++ ...86ac5c2354a573ea041b8846409c4fc0f8c4a70850 | 1 + ...9fbabb4464fabd6dd32e82c67ea2b2a3c4e8bacdf5 | 1 + ...cd975b5752cf0acaedd668bb525fcd40c3587cc460 | 1 + ...f317fbeba28f5e06f1ce4d3895b3b8770140280a2e | 1 + ...273e2a9c96bfe291662f08e2860e868eef69c34620 | 16 + test/testdata/no-provenance-image/index.json | 1 + test/testdata/no-provenance-image/oci-layout | 1 + ...383582e5ef981b8a6bb7415d07d8d70c90d8cfd326 | 50 +++ ...27aa6c918c390c373294ec4fc48f2c6fe703fcc6fe | 1 + ...08db8fcbbd5d8ec68cf0047f954133e76d8e73d71c | 27 ++ ...8f9fb2003318350a8026ea082b63a249cfa60918a3 | 1 + ...16d96cbbdb6493e32d4f6abd8f7a191990e8efb289 | 1 + ...d70a787aac618a41d4c8ec8d6e12bd12d0cc601706 | 1 + ...d17ec2ce917fc9a500aab72f813d26fed8404e7162 | 27 ++ ...6b5853ea559248fdb4ab711bcea34b65c62f0e026b | 1 + ...435830fbf13cf3ab1ae27ec91246b280514e6a7b33 | 1 + ...95083e64d3764c798507596ceded776c4ab038c224 | 1 + ...0cd7813f814e2baad70141a3e315b7c3476b0f476f | 27 ++ ...eb1f2c06af73879adba0fcb4743339c9a54b377635 | 50 +++ ...976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e | 16 + ...d02b74310803129c3eb1e22d2e62279f8c72340b55 | 27 ++ ...20844157f4b0c8a6445d220af741e9fee8099bf532 | 1 + ...d73212578bb3a02e8c0da7fc175c79045e73519375 | 1 + ...c8e269e78c3c95df611b44580426c384d3f5057776 | 1 + ...461575c51e4a626a091dc2842b35cac18c787ff80f | 27 ++ ...dd621f59e0d43a3b346f34c34eb58da31f00a9b0ad | Bin 0 -> 116 bytes ...4c5538aec56315874a6db860fbf6874cd7a830e3c8 | 50 +++ ...c7db8e7affc05fd9bf98eb027038b7daf176861e85 | 1 + ...fd4ef5ce285f0aa928d2651f7ec3d5a78276249dec | 1 + ...86ac5c2354a573ea041b8846409c4fc0f8c4a70850 | 1 + ...99438518217417e01414d18189a3cf71c07f2a02c3 | 27 ++ ...162abec6f8bee4c463103161ab772c774e7ae9dd6d | 1 + ...6b61fbf88d9287a936b285a8b4dde8893a1f4ffedf | 1 + ...06b1340c501e59376a658b14b53c1828924c0ac668 | 1 + ...cd975b5752cf0acaedd668bb525fcd40c3587cc460 | 1 + ...f317fbeba28f5e06f1ce4d3895b3b8770140280a2e | 1 + ...273e2a9c96bfe291662f08e2860e868eef69c34620 | 16 + ...73269ecfac61e8dcdad3a4a643dcb577522492f898 | 50 +++ ...f217f22bb1e106efd5ee791640411764e1cf39ea2c | 27 ++ ...fac8f20e3645a45370e52abf9581dd4eedd152fce0 | 27 ++ ...1165631fda0cfe691a383e7b333269a53bf9a79c34 | 1 + ...c49c6440fcd6d565a8658141914a8a07c127e00d7e | 1 + test/testdata/unsigned-test-image/index.json | 1 + test/testdata/unsigned-test-image/oci-layout | 1 + 61 files changed, 1418 insertions(+), 3 deletions(-) create mode 100644 internal/test/test_test.go create mode 100644 pkg/attest/sign_test.go create mode 100644 pkg/attest/types.go create mode 100644 pkg/attestation/vsa.go create mode 100755 scripts/gen-testdata.sh create mode 100644 test/Dockerfile create mode 100644 test/testdata/no-provenance-image/blobs/sha256/1c70b3e7c3a57801501ec127aa6c918c390c373294ec4fc48f2c6fe703fcc6fe create mode 100644 test/testdata/no-provenance-image/blobs/sha256/1effe3a77c594e579388dc4553dbbe762e4457a099ab8b706e67f5f9fc934701 create mode 100644 test/testdata/no-provenance-image/blobs/sha256/2aaebbb079957470e7c0adddbb054b2b4c01f717d408efba753da2bf6e8905da create mode 100644 test/testdata/no-provenance-image/blobs/sha256/2e82727457f04f320b643cb6e13bcbafb8e0dc0adc0443f1a25666f9518c5071 create mode 100644 test/testdata/no-provenance-image/blobs/sha256/7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e create mode 100644 test/testdata/no-provenance-image/blobs/sha256/97a548f8d65d9ab617f608dd621f59e0d43a3b346f34c34eb58da31f00a9b0ad create mode 100644 test/testdata/no-provenance-image/blobs/sha256/9b009d6b84b1ed941070b3f919823446286a674ad669d0baa8ab2c358aeb3a82 create mode 100644 test/testdata/no-provenance-image/blobs/sha256/a9646604f9522bf59d203a86ac5c2354a573ea041b8846409c4fc0f8c4a70850 create mode 100644 test/testdata/no-provenance-image/blobs/sha256/b6ef78de3633e45d1c08019fbabb4464fabd6dd32e82c67ea2b2a3c4e8bacdf5 create mode 100644 test/testdata/no-provenance-image/blobs/sha256/d85d624a324422194b43cccd975b5752cf0acaedd668bb525fcd40c3587cc460 create mode 100644 test/testdata/no-provenance-image/blobs/sha256/da5651e8877b960aa30f32f317fbeba28f5e06f1ce4d3895b3b8770140280a2e create mode 100644 test/testdata/no-provenance-image/blobs/sha256/da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620 create mode 100644 test/testdata/no-provenance-image/index.json create mode 100644 test/testdata/no-provenance-image/oci-layout create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/0f2ee9a338149a5a1435a7383582e5ef981b8a6bb7415d07d8d70c90d8cfd326 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/1c70b3e7c3a57801501ec127aa6c918c390c373294ec4fc48f2c6fe703fcc6fe create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/26da286bbc886aa14d191808db8fcbbd5d8ec68cf0047f954133e76d8e73d71c create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/2953164d6cc6c8bb8271f78f9fb2003318350a8026ea082b63a249cfa60918a3 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/2a9b671f3fc9bc5ca967b616d96cbbdb6493e32d4f6abd8f7a191990e8efb289 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/371954672cfaa92735d6fbd70a787aac618a41d4c8ec8d6e12bd12d0cc601706 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/3883faf6acc3cae029364ed17ec2ce917fc9a500aab72f813d26fed8404e7162 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/3e64f9d2888ed9211fbf2c6b5853ea559248fdb4ab711bcea34b65c62f0e026b create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/4e5988d06eee647cb901d4435830fbf13cf3ab1ae27ec91246b280514e6a7b33 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/5171425b78a2aedb43eb4e95083e64d3764c798507596ceded776c4ab038c224 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/6658b8ba1e1221a6288bf50cd7813f814e2baad70141a3e315b7c3476b0f476f create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/6c3da8eeaba64ce5acfcbaeb1f2c06af73879adba0fcb4743339c9a54b377635 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/8049aa9ad3479085066b31d02b74310803129c3eb1e22d2e62279f8c72340b55 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/8f2f55fc493890c2482a1220844157f4b0c8a6445d220af741e9fee8099bf532 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/8f94b6e2a8be82e2e5b562d73212578bb3a02e8c0da7fc175c79045e73519375 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/92d3311aa91737ff81e2a4c8e269e78c3c95df611b44580426c384d3f5057776 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/9638ca53d2795806cf51b7461575c51e4a626a091dc2842b35cac18c787ff80f create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/97a548f8d65d9ab617f608dd621f59e0d43a3b346f34c34eb58da31f00a9b0ad create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/98e06f6b48edd74e21e8504c5538aec56315874a6db860fbf6874cd7a830e3c8 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/9fe102c03d71d47a24cd7fc7db8e7affc05fd9bf98eb027038b7daf176861e85 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/a4cf4b24f3fa8cd49a59e8fd4ef5ce285f0aa928d2651f7ec3d5a78276249dec create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/a9646604f9522bf59d203a86ac5c2354a573ea041b8846409c4fc0f8c4a70850 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/aeca14119e3242c51633a899438518217417e01414d18189a3cf71c07f2a02c3 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/c01e5307ec84299048d76f162abec6f8bee4c463103161ab772c774e7ae9dd6d create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/c6dd08ccc92ab60a87648a6b61fbf88d9287a936b285a8b4dde8893a1f4ffedf create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/c9f436179969b60ec0bbd406b1340c501e59376a658b14b53c1828924c0ac668 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/d85d624a324422194b43cccd975b5752cf0acaedd668bb525fcd40c3587cc460 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/da5651e8877b960aa30f32f317fbeba28f5e06f1ce4d3895b3b8770140280a2e create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/db8f2a6e112ea6396f57d073269ecfac61e8dcdad3a4a643dcb577522492f898 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/e0a9b9404ac2691b9b1c9ef217f22bb1e106efd5ee791640411764e1cf39ea2c create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/f2b95cecafef9c22a5d059fac8f20e3645a45370e52abf9581dd4eedd152fce0 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/f634c4c53b03bf8ff917b61165631fda0cfe691a383e7b333269a53bf9a79c34 create mode 100644 test/testdata/unsigned-test-image/blobs/sha256/fed2c8841731e2cf1ceb53c49c6440fcd6d565a8658141914a8a07c127e00d7e create mode 100644 test/testdata/unsigned-test-image/index.json create mode 100644 test/testdata/unsigned-test-image/oci-layout diff --git a/internal/test/test.go b/internal/test/test.go index 87533d7..695dd1c 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -2,13 +2,22 @@ package test import ( "context" + "encoding/base64" + "encoding/json" + "fmt" "os" + "strings" "testing" + "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" "github.com/docker/attest/pkg/signerverifier" "github.com/docker/attest/pkg/tlog" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/partial" + intoto "github.com/in-toto/in-toto-golang/in_toto" "github.com/secure-systems-lab/go-securesystemslib/dsse" ) @@ -84,3 +93,103 @@ func GetMockPolicy() policy.PolicyEvaluator { }, } } + +type AnnotatedStatement struct { + OCIDescriptor *v1.Descriptor + InTotoStatement *intoto.Statement + Annotations map[string]string +} + +func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStatement, error) { + idx, err := layout.ImageIndexFromPath(path) + if err != nil { + return nil, fmt.Errorf("failed to load image index: %w", err) + } + + idxm, err := idx.IndexManifest() + if err != nil { + return nil, fmt.Errorf("failed to get digest: %w", err) + } + idxDigest := idxm.Manifests[0].Digest + + mfs, err := idx.ImageIndex(idxDigest) + if err != nil { + return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err) + } + mfs2, err := mfs.IndexManifest() + if err != nil { + return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err) + } + + var statements []*AnnotatedStatement + + for _, mf := range mfs2.Manifests { + if mf.Annotations["vnd.docker.reference.type"] != "attestation-manifest" { + continue + } + + attestationImage, err := mfs.Image(mf.Digest) + if err != nil { + return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err) + } + layers, err := attestationImage.Layers() + if err != nil { + return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err) + } + + for _, layer := range layers { + // parse layer blob as json + mt, err := layer.MediaType() + if err != nil { + return nil, fmt.Errorf("failed to get layer media type: %w", err) + } + + if string(mt) != mediaType { + continue + } + r, err := layer.Uncompressed() + if err != nil { + return nil, fmt.Errorf("failed to get layer contents: %w", err) + } + defer r.Close() + var intotoStatement = new(intoto.Statement) + var desc *v1.Descriptor + if strings.HasSuffix(string(mt), "+dsse") { + var env = new(attestation.Envelope) + err = json.NewDecoder(r).Decode(env) + if err != nil { + return nil, fmt.Errorf("failed to decode env: %w", err) + } + payload, err := base64.StdEncoding.Strict().DecodeString(env.Payload) + if err != nil { + return nil, fmt.Errorf("failed to decode payload: %w", err) + } + err = json.Unmarshal([]byte(payload), intotoStatement) + if err != nil { + return nil, fmt.Errorf("failed to decode %s statement: %w", mediaType, err) + } + } else { + desc := new(v1.Descriptor) + err = json.NewDecoder(r).Decode(desc) + if err != nil { + return nil, fmt.Errorf("failed to decode statement: %w", err) + } + } + + layerDesc, err := partial.Descriptor(layer) + if err != nil { + return nil, fmt.Errorf("failed to get descriptor for layer: %w", err) + } + annotations := make(map[string]string) + for k, v := range layerDesc.Annotations { + annotations[k] = v + } + statements = append(statements, &AnnotatedStatement{ + OCIDescriptor: desc, + InTotoStatement: intotoStatement, + Annotations: annotations, + }) + } + } + return statements, nil +} diff --git a/internal/test/test_test.go b/internal/test/test_test.go new file mode 100644 index 0000000..812c019 --- /dev/null +++ b/internal/test/test_test.go @@ -0,0 +1,23 @@ +package test + +import ( + "path/filepath" + "testing" + + intoto "github.com/in-toto/in-toto-golang/in_toto" + "github.com/stretchr/testify/assert" +) + +var ( + UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") +) + +const ( + ExpectedStatements = 4 +) + +func TestExtractAnnotatedStatements(t *testing.T) { + statements, err := ExtractAnnotatedStatements(UnsignedTestImage, intoto.PayloadType) + assert.NoError(t, err) + assert.Equalf(t, len(statements), ExpectedStatements, "expected %d statement, got %d", ExpectedStatements, len(statements)) +} diff --git a/pkg/attest/sign.go b/pkg/attest/sign.go index 1c255bd..7641227 100644 --- a/pkg/attest/sign.go +++ b/pkg/attest/sign.go @@ -1,2 +1,287 @@ package attest +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/docker/attest/pkg/attestation" + "github.com/docker/attest/pkg/oci" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/match" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/types" + intoto "github.com/in-toto/in-toto-golang/in_toto" + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/secure-systems-lab/go-securesystemslib/dsse" +) + +func SignIndexAttestations(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *SigningOptions) (v1.ImageIndex, error) { + indexManifest, err := idx.IndexManifest() + if err != nil { + return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err) + } + + var originalManifestDigests []v1.Hash + var muts []mutate.IndexAddendum + for _, manifest := range indexManifest.Manifests { + if manifest.Annotations[oci.DockerReferenceType] != oci.AttestationManifestType { + continue + } + + originalManifestDigests = append(originalManifestDigests, manifest.Digest) + + attestationImage, err := idx.Image(manifest.Digest) + if err != nil { + return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", manifest.Digest.String(), err) + } + layers, err := attestationImage.Layers() + if err != nil { + return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err) + } + + var signedLayers []mutate.Addendum + var originalLayers []v1.Layer + var statements []*intoto.Statement + + for _, layer := range layers { + // parse layer blob as json + r, err := layer.Uncompressed() + if err != nil { + return nil, fmt.Errorf("failed to get layer contents: %w", err) + } + defer r.Close() + mt, err := layer.MediaType() + if err != nil { + return nil, fmt.Errorf("failed to get layer media type: %w", err) + } + + if mt != types.MediaType(intoto.PayloadType) { + originalLayers = append(originalLayers, layer) + continue + } + var stmt = new(intoto.Statement) + err = json.NewDecoder(r).Decode(&stmt) + if err != nil { + return nil, fmt.Errorf("failed to decode statement layer contents: %w", err) + } + + statements = append(statements, stmt) + layerDesc, err := partial.Descriptor(layer) + if err != nil { + return nil, fmt.Errorf("failed to get descriptor for layer: %w", err) + } + // copy original annotations and add new ones + ann := make(map[string]string) + for k, v := range layerDesc.Annotations { + ann[k] = v + } + ann[InTotoReferenceLifecycleStage] = LifecycleStageExperimental + + var env *attestation.Envelope + var mediaType string + switch opts.EnvelopeStyle { + case OCIContentDescriptor: + // Ensure we sign just the digest, size, and media type + payloadDesc := v1.Descriptor{ + Digest: layerDesc.Digest, + Size: layerDesc.Size, + MediaType: layerDesc.MediaType, + } + payload, err := json.Marshal(payloadDesc) + if err != nil { + return nil, fmt.Errorf("failed to marshal descriptor: %w", err) + } + env, err = attestation.SignDSSE(ctx, payload, ociv1.MediaTypeDescriptor, signer) + if err != nil { + return nil, fmt.Errorf("failed to sign statement: %w", err) + } + ann[oci.DockerReferenceDigest] = layerDesc.Digest.String() + // this is a reference type + opts.Replace = false + mediaType = attestation.OCIDescriptorDSSEMediaType + case EmbeddedDSSE: + payload, err := json.Marshal(stmt) + if err != nil { + return nil, fmt.Errorf("failed to marshal statement: %w", err) + } + env, err = attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer) + if err != nil { + return nil, fmt.Errorf("failed to sign statement: %w", err) + } + mediaType, err = attestation.DSSEMediaType(stmt.PredicateType) + if err != nil { + return nil, fmt.Errorf("failed to get DSSE media type: %w", err) + } + + default: + return nil, fmt.Errorf("unknown envelope style %q", opts.EnvelopeStyle) + } + + data, err := json.Marshal(env) + if err != nil { + return nil, fmt.Errorf("failed to marshal envelope: %w", err) + } + newLayer := static.NewLayer(data, types.MediaType(mediaType)) + + withAnnotations := mutate.Addendum{ + Layer: newLayer, + Annotations: ann, + } + signedLayers = append(signedLayers, withAnnotations) + } + + newImg, err := addSignedLayers(signedLayers, originalLayers, manifest.MediaType, attestationImage, opts) + if err != nil { + return nil, fmt.Errorf("failed to add signed layers: %w", err) + } + + if opts.VSAOptions != nil { + newImg, err = addVSA(ctx, newImg, statements, manifest.MediaType, signer, opts) + if err != nil { + return nil, fmt.Errorf("failed to add VSA: %w", err) + } + } + newDesc, err := partial.Descriptor(newImg) + if err != nil { + return nil, fmt.Errorf("failed to get descriptor: %w", err) + } + cf, err := attestationImage.ConfigFile() + if err != nil { + return nil, fmt.Errorf("failed to get config file: %w", err) + } + newDesc.Platform = cf.Platform() + newDesc.MediaType = manifest.MediaType + newDesc.Annotations = manifest.Annotations + + muts = append(muts, mutate.IndexAddendum{ + Add: newImg, + Descriptor: *newDesc, + }) + } + // create new index with signed images + newIndex := mutate.RemoveManifests(idx, match.Digests(originalManifestDigests...)) + newIndex = mutate.AppendManifests(newIndex, muts...) + + return newIndex, nil +} + +func addVSA(ctx context.Context, image v1.Image, stmt []*intoto.Statement, outerMediaType types.MediaType, signer dsse.SignerVerifier, opts *SigningOptions) (v1.Image, error) { + if len(stmt) == 0 { + return nil, fmt.Errorf("no attestations found to generate VSA from") + } + sub := stmt[0].Subject[0] + stype := stmt[0].Type + + uri, err := attestation.ToVSAResourceURI(sub) + if err != nil { + return nil, fmt.Errorf("failed to generate VSA resource URI: %w", err) + } + + inputs := make([]attestation.VSAInputAttestation, 0, len(stmt)) + layers, err := image.Layers() + if err != nil { + return nil, fmt.Errorf("failed to get layers: %w", err) + } + for _, layer := range layers { + mt, err := layer.MediaType() + if err != nil { + return nil, fmt.Errorf("failed to get layer media type: %w", err) + } + mediaType := string(mt) + if !strings.HasPrefix(mediaType, "application/vnd.in-toto.") || + !strings.HasSuffix(mediaType, "+dsse") { + continue + } + + dgst, err := layer.Digest() + if err != nil { + return nil, fmt.Errorf("failed to get layer digest: %w", err) + } + inputs = append(inputs, attestation.VSAInputAttestation{ + Digest: map[string]string{"sha256": dgst.Hex}, + MediaType: string(mt), + }) + } + vsaStatement := &intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + PredicateType: attestation.VSAPredicateType, + Type: stype, + Subject: stmt[0].Subject, + }, + Predicate: attestation.VSAPredicate{ + Verifier: attestation.VSAVerifier{ + ID: opts.VSAOptions.VerifierID, + }, + TimeVerified: time.Now().UTC().Format(time.RFC3339), + ResourceUri: uri, + Policy: attestation.VSAPolicy{URI: opts.VSAOptions.PolicyURI}, + VerificationResult: "PASSED", + VerifiedLevels: []string{opts.VSAOptions.BuildLevel}, + InputAttestations: inputs, + }, + } + payload, err := json.Marshal(vsaStatement) + if err != nil { + return nil, fmt.Errorf("failed to marshal statement: %w", err) + } + env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer) + if err != nil { + return nil, fmt.Errorf("failed to sign statement: %w", err) + } + mediaType, err := attestation.DSSEMediaType(vsaStatement.PredicateType) + if err != nil { + return nil, fmt.Errorf("failed to get DSSE media type: %w", err) + } + + data, err := json.Marshal(env) + if err != nil { + return nil, fmt.Errorf("failed to marshal envelope: %w", err) + } + mt := types.MediaType(mediaType) + newLayer := static.NewLayer(data, mt) + ann := make(map[string]string) + ann[InTotoReferenceLifecycleStage] = LifecycleStageExperimental + ann[oci.InTotoPredicateType] = attestation.VSAPredicateType + withAnnotations := mutate.Addendum{ + Layer: newLayer, + Annotations: ann, + } + opts = &SigningOptions{ + Replace: false, + } + return addSignedLayers([]mutate.Addendum{withAnnotations}, layers, outerMediaType, image, opts) +} + +func addSignedLayers(signedLayers []mutate.Addendum, originalLayers []v1.Layer, mediaType types.MediaType, attestationImage v1.Image, opts *SigningOptions) (v1.Image, error) { + var err error + if opts.Replace { + newImg := empty.Image + newImg = mutate.MediaType(newImg, mediaType) + newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.image.config.v1+json") + for _, layer := range signedLayers { + newImg, err = mutate.Append(newImg, layer) + if err != nil { + return nil, fmt.Errorf("failed to append layer: %w", err) + } + } + newImg, err = mutate.AppendLayers(newImg, originalLayers...) + if err != nil { + return nil, fmt.Errorf("failed to append original layers: %w", err) + } + return newImg, nil + + } + for _, layer := range signedLayers { + attestationImage, err = mutate.Append(attestationImage, layer) + if err != nil { + return nil, fmt.Errorf("failed to append layer: %w", err) + } + } + return attestationImage, nil +} diff --git a/pkg/attest/sign_test.go b/pkg/attest/sign_test.go new file mode 100644 index 0000000..3b7e39a --- /dev/null +++ b/pkg/attest/sign_test.go @@ -0,0 +1,176 @@ +package attest + +import ( + "encoding/json" + "path/filepath" + "testing" + + "github.com/docker/attest/internal/test" + "github.com/docker/attest/pkg/attestation" + "github.com/docker/attest/pkg/oci" + "github.com/docker/attest/pkg/policy" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/types" + intoto "github.com/in-toto/in-toto-golang/in_toto" + v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" + "github.com/stretchr/testify/assert" +) + +var ( + UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") + NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image") + LocalPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy") + TestTempDir = "attest-sign-test" +) + +func TestSignVerifyOCILayout(t *testing.T) { + ctx, signer := test.Setup(t) + + testCases := []struct { + name string + TestImage string + expectedStatements int + expectedAttestations int + envelopeStyle envelopeStyle + replace bool + }{ + + {"signed replaced (does nothing)", UnsignedTestImage, 4, 4, OCIContentDescriptor, true}, + {"without replace", UnsignedTestImage, 4, 4, OCIContentDescriptor, false}, + {"embedded", UnsignedTestImage, 0, 6, EmbeddedDSSE, true}, + {"embedded", UnsignedTestImage, 4, 6, EmbeddedDSSE, false}, + + // image without provenance doesn't fail + {"no provenance (replace)", NoProvenanceImage, 0, 4, EmbeddedDSSE, true}, + {"no provenance (no replace)", NoProvenanceImage, 2, 4, EmbeddedDSSE, false}, + } + policyResolver := &policy.PolicyOptions{ + LocalPolicyDir: LocalPolicyDir, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tempDir := test.CreateTempDir(t, "", TestTempDir) + outputLayout := tempDir + opts := &SigningOptions{ + Replace: tc.replace, + EnvelopeStyle: tc.envelopeStyle, + VSAOptions: &attestation.VSAOptions{ + BuildLevel: "SLSA_BUILD_LEVEL_3", + PolicyURI: "https://docker.com/attest/policy", + VerifierID: "https://docker.com", + }, + } + attIdx, err := oci.AttestationIndexFromPath(tc.TestImage) + assert.NoError(t, err) + signedIndex, err := SignIndexAttestations(ctx, attIdx.Index, signer, opts) + assert.NoError(t, err) + + // output signed attestations + idx := v1.ImageIndex(empty.Index) + idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ + Add: signedIndex, + Descriptor: v1.Descriptor{ + Annotations: map[string]string{ + oci.OciReferenceTarget: attIdx.Name, + }, + }, + }) + _, err = layout.Write(outputLayout, idx) + assert.NoError(t, err) + + resolver := &oci.OCILayoutResolver{ + Path: outputLayout, + Platform: "", + } + policy, err := Verify(ctx, policyResolver, resolver) + assert.NoError(t, err) + assert.Truef(t, policy, "Policy should have been found") + + mt, _ := attestation.DSSEMediaType(attestation.VSAPredicateType) + vsas, err := test.ExtractAnnotatedStatements(tempDir, mt) + assert.NoError(t, err) + assert.Equalf(t, len(vsas), 2, "expected %d vsa statement, got %d", 2, len(vsas)) + + switch tc.envelopeStyle { + case OCIContentDescriptor: + { + statements, err := test.ExtractAnnotatedStatements(tempDir, intoto.PayloadType) + assert.NoError(t, err) + assert.Equalf(t, tc.expectedStatements, len(statements), "expected %d statement, got %d", tc.expectedStatements, len(statements)) + + statements, err = test.ExtractAnnotatedStatements(tempDir, attestation.OCIDescriptorDSSEMediaType) + assert.NoError(t, err) + + assert.Equalf(t, tc.expectedAttestations, len(statements), "expected %d attestations, got %d", tc.expectedAttestations, len(statements)) + } + case EmbeddedDSSE: + { + var allEnvelopes []*test.AnnotatedStatement + for _, predicate := range []string{intoto.PredicateSPDX, v02.PredicateSLSAProvenance, attestation.VSAPredicateType} { + mt, _ := attestation.DSSEMediaType(predicate) + statements, err := test.ExtractAnnotatedStatements(tempDir, mt) + assert.NoError(t, err) + allEnvelopes = append(allEnvelopes, statements...) + + for _, stmt := range statements { + assert.Equalf(t, predicate, stmt.Annotations[oci.InTotoPredicateType], "expected predicate-type annotation to be set to %s, got %s", predicate, stmt.Annotations[oci.InTotoPredicateType]) + assert.Equalf(t, LifecycleStageExperimental, stmt.Annotations[InTotoReferenceLifecycleStage], "expected reference lifecycle stage annotation to be set to %s, got %s", LifecycleStageExperimental, stmt.Annotations[InTotoReferenceLifecycleStage]) + } + } + assert.Equalf(t, tc.expectedAttestations, len(allEnvelopes), "expected %d attestations, got %d", tc.expectedAttestations, len(allEnvelopes)) + statements, err := test.ExtractAnnotatedStatements(tempDir, intoto.PayloadType) + assert.NoError(t, err) + assert.Equalf(t, tc.expectedStatements, len(statements), "expected %d statement, got %d", tc.expectedStatements, len(statements)) + } + } + }) + } +} + +func TestAddSignedLayerAnnotations(t *testing.T) { + testCases := []struct { + name string + replace bool + }{ + {"replaced", true}, + {"not replaced", false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + data := []byte("signed") + signedLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType)) + signedLayers := []mutate.Addendum{ + { + Layer: signedLayer, + Annotations: map[string]string{"test": "test"}, + }, + } + data = []byte("test") + testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType)) + mediaType := types.OCIManifestSchema1 + img := empty.Image + opts := &SigningOptions{ + Replace: tc.replace, + } + newImg, err := addSignedLayers(signedLayers, []v1.Layer{testLayer}, mediaType, img, opts) + assert.NoError(t, err) + mf, _ := newImg.RawManifest() + type Annotations struct { + Annotations map[string]string `json:"annotations"` + } + type Layers struct { + Layers []Annotations `json:"layers"` + } + l := &Layers{} + err = json.Unmarshal(mf, l) + assert.NoError(t, err) + _, ok := l.Layers[0].Annotations["test"] + assert.Truef(t, ok, "missing annotations") + }) + } +} diff --git a/pkg/attest/types.go b/pkg/attest/types.go new file mode 100644 index 0000000..e7e1963 --- /dev/null +++ b/pkg/attest/types.go @@ -0,0 +1,33 @@ +package attest + +import ( + "fmt" + + "github.com/docker/attest/pkg/attestation" +) + +type envelopeStyle string + +const ( + OCIContentDescriptor envelopeStyle = "oci-content-descriptor" + EmbeddedDSSE envelopeStyle = "embedded-dsse" + InTotoReferenceLifecycleStage = "vnd.docker.lifecycle-stage" + LifecycleStageExperimental = "experimental" +) + +type SigningOptions struct { + Replace bool + EnvelopeStyle envelopeStyle + VSAOptions *attestation.VSAOptions +} + +func EnvelopeStyle(style string) (envelopeStyle, error) { + switch style { + case string(OCIContentDescriptor): + return OCIContentDescriptor, nil + case string(EmbeddedDSSE): + return EmbeddedDSSE, nil + default: + return "", fmt.Errorf("unknown envelope style %q", style) + } +} diff --git a/pkg/attestation/types.go b/pkg/attestation/types.go index 7292e8d..609ff7e 100644 --- a/pkg/attestation/types.go +++ b/pkg/attestation/types.go @@ -1,10 +1,18 @@ package attestation -import "encoding/base64" +import ( + "encoding/base64" + "fmt" + + intoto "github.com/in-toto/in-toto-golang/in_toto" + v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" +) const ( - DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json" - RekorTlExtKind = "Rekor" + DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json" + RekorTlExtKind = "Rekor" + OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse" ) var base64Encoding = base64.StdEncoding.Strict() @@ -33,3 +41,20 @@ type DockerTlExtension struct { Kind string `json:"kind"` Data any `json:"data"` } + +func DSSEMediaType(predicateType string) (string, error) { + var predicateName string + switch predicateType { + case v02.PredicateSLSAProvenance: + predicateName = "provenance" + case intoto.PredicateSPDX: + predicateName = "spdx" + case VSAPredicateType: + predicateName = "verification_summary" + + default: + return "", fmt.Errorf("unknown predicate type %q", predicateType) + } + + return fmt.Sprintf("application/vnd.in-toto.%s+dsse", predicateName), nil +} diff --git a/pkg/attestation/vsa.go b/pkg/attestation/vsa.go new file mode 100644 index 0000000..4e0c59d --- /dev/null +++ b/pkg/attestation/vsa.go @@ -0,0 +1,55 @@ +package attestation + +import ( + "fmt" + + intoto "github.com/in-toto/in-toto-golang/in_toto" + "github.com/package-url/packageurl-go" +) + +const ( + VSAPredicateType = "https://slsa.dev/verification_summary/v1" +) + +type VSAPredicate struct { + Verifier VSAVerifier `json:"verifier"` + TimeVerified string `json:"timeVerified"` + ResourceUri string `json:"resourceUri"` + Policy VSAPolicy `json:"policy"` + InputAttestations []VSAInputAttestation `json:"inputAttestations"` + VerificationResult string `json:"verificationResult"` + VerifiedLevels []string `json:"verifiedLevels"` +} + +type VSAVerifier struct { + ID string `json:"id"` +} + +type VSAPolicy struct { + URI string `json:"uri"` +} + +type VSAInputAttestation struct { + Digest map[string]string `json:"digest"` + MediaType string `json:"mediaType"` +} + +type VSAOptions struct { + BuildLevel string + PolicyURI string + VerifierID string +} + +func ToVSAResourceURI(sub intoto.Subject) (string, error) { + //parse purl + purl, err := packageurl.FromString(sub.Name) + if err != nil { + return "", fmt.Errorf("failed to parse package url: %w", err) + } + quals := purl.Qualifiers.Map() + if quals["digest"] == "" { + quals["digest"] = "sha256:" + sub.Digest["sha256"] + } + purl.Qualifiers = packageurl.QualifiersFromMap(quals) + return purl.String(), nil +} diff --git a/pkg/oci/types.go b/pkg/oci/types.go index 5af15eb..268ba1a 100644 --- a/pkg/oci/types.go +++ b/pkg/oci/types.go @@ -1,8 +1,69 @@ package oci +import ( + "fmt" + "log" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + const ( DockerReferenceType = "vnd.docker.reference.type" DockerReferenceDigest = "vnd.docker.reference.digest" AttestationManifestType = "attestation-manifest" InTotoPredicateType = "in-toto.io/predicate-type" + OciReferenceTarget = "org.opencontainers.image.ref.name" ) + +type AttestationIndex struct { + Index v1.ImageIndex + Name string +} + +func AttestationIndexFromPath(path string) (*AttestationIndex, error) { + wrapperIdx, err := layout.ImageIndexFromPath(path) + if err != nil { + return nil, fmt.Errorf("failed to load image index: %w", err) + } + + idxm, err := wrapperIdx.IndexManifest() + if err != nil { + return nil, fmt.Errorf("failed to get digest: %w", err) + } + imageName := idxm.Manifests[0].Annotations[OciReferenceTarget] + idxDigest := idxm.Manifests[0].Digest + + idx, err := wrapperIdx.ImageIndex(idxDigest) + if err != nil { + return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err) + } + return &AttestationIndex{ + Index: idx, + Name: imageName, + }, nil +} + +func AttestationIndexFromRemote(image string) (*AttestationIndex, error) { + ref, err := name.ParseReference(image) + if err != nil { + log.Fatalf("Failed to parse image name: %v", err) + } + // Get the authenticator from the default Docker keychain + auth, err := authn.DefaultKeychain.Resolve(ref.Context()) + if err != nil { + log.Fatalf("Failed to get authenticator: %v", err) + } + // Pull the image from the registry + idx, err := remote.Index(ref, remote.WithAuth(auth)) + if err != nil { + return nil, fmt.Errorf("failed to pull image %s: %w", image, err) + } + return &AttestationIndex{ + Index: idx, + Name: image, + }, nil +} diff --git a/scripts/gen-testdata.sh b/scripts/gen-testdata.sh new file mode 100755 index 0000000..815f1c1 --- /dev/null +++ b/scripts/gen-testdata.sh @@ -0,0 +1,45 @@ +#!/bin/bash +set -eo pipefail + +echo "Starting the process to generate testdata..." + +# Define functions +function check_command () { + command -v "$1" >/dev/null 2>&1 || { echo >&2 "This script requires $1 but it's not installed. Aborting."; exit 1; } +} + +function cleanup_testdata () { + echo "Cleaning up existing testdata..." + rm -rf "${TESTDATA_PATH:?}/${UNSIGNED_IMAGE_DIR:?}" + rm -rf "${TESTDATA_PATH:?}/${NO_PROVENANCE_IMAGE_DIR:?}" +} + +function build_unsigned_image () { + echo "Building $UNSIGNED_IMAGE_DIR..." + docker buildx build "$TEST_IMAGE_DOCKERFILE_PATH" --sbom true --provenance true --platform linux/amd64,linux/arm64 \ + --output type=oci,tar=false,name="$TEST_IMAGE_REPO:$TEST_IMAGE_TAG",dest="$TESTDATA_PATH/$UNSIGNED_IMAGE_DIR" +} + +function build_no_provenance_image () { + echo "Building unsigned $NO_PROVENANCE_IMAGE_DIR..." + docker buildx build "$TEST_IMAGE_DOCKERFILE_PATH" --sbom true --provenance false --platform linux/amd64,linux/arm64 \ + --output type=oci,tar=false,name="$TEST_IMAGE_REPO:$TEST_IMAGE_TAG",dest="$TESTDATA_PATH/$NO_PROVENANCE_IMAGE_DIR" +} + +# Check required commands +check_command docker + +TESTDATA_PATH="../test/testdata" +TEST_IMAGE_DOCKERFILE_PATH="../test" +TEST_IMAGE_REPO="test-image" +TEST_IMAGE_TAG="test" +UNSIGNED_IMAGE_DIR="unsigned-test-image" +NO_PROVENANCE_IMAGE_DIR="no-provenance-image" +ATTESTATION_PAYLOADTYPE="application/vnd.in-toto+json" + +# Run steps +cleanup_testdata +build_unsigned_image +build_no_provenance_image + +echo "Process completed successfully." diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 0000000..274a7c6 --- /dev/null +++ b/test/Dockerfile @@ -0,0 +1,5 @@ +FROM alpine AS build +RUN echo "hello world" > /tmp/hello.txt + +FROM scratch +COPY --from=build /tmp/hello.txt / diff --git a/test/testdata/no-provenance-image/blobs/sha256/1c70b3e7c3a57801501ec127aa6c918c390c373294ec4fc48f2c6fe703fcc6fe b/test/testdata/no-provenance-image/blobs/sha256/1c70b3e7c3a57801501ec127aa6c918c390c373294ec4fc48f2c6fe703fcc6fe new file mode 100644 index 0000000..d2651b3 --- /dev/null +++ b/test/testdata/no-provenance-image/blobs/sha256/1c70b3e7c3a57801501ec127aa6c918c390c373294ec4fc48f2c6fe703fcc6fe @@ -0,0 +1 @@ +{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"WorkingDir":"/","OnBuild":null},"created":"2024-03-08T16:42:30.065465358Z","history":[{"created":"2024-03-08T16:42:30.065465358Z","created_by":"COPY /tmp/hello.txt / # buildkit","comment":"buildkit.dockerfile.v0"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:b842af8c2f1451ffc802ae4139819eaea8441223357642548d8a25ab5c52cff7"]}} \ No newline at end of file diff --git a/test/testdata/no-provenance-image/blobs/sha256/1effe3a77c594e579388dc4553dbbe762e4457a099ab8b706e67f5f9fc934701 b/test/testdata/no-provenance-image/blobs/sha256/1effe3a77c594e579388dc4553dbbe762e4457a099ab8b706e67f5f9fc934701 new file mode 100644 index 0000000..19c50eb --- /dev/null +++ b/test/testdata/no-provenance-image/blobs/sha256/1effe3a77c594e579388dc4553dbbe762e4457a099ab8b706e67f5f9fc934701 @@ -0,0 +1,50 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", + "size": 476, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", + "size": 476, + "platform": { + "architecture": "arm64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:2aaebbb079957470e7c0adddbb054b2b4c01f717d408efba753da2bf6e8905da", + "size": 558, + "annotations": { + "vnd.docker.reference.digest": "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", + "vnd.docker.reference.type": "attestation-manifest" + }, + "platform": { + "architecture": "unknown", + "os": "unknown" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:9b009d6b84b1ed941070b3f919823446286a674ad669d0baa8ab2c358aeb3a82", + "size": 558, + "annotations": { + "vnd.docker.reference.digest": "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", + "vnd.docker.reference.type": "attestation-manifest" + }, + "platform": { + "architecture": "unknown", + "os": "unknown" + } + } + ] +} \ No newline at end of file diff --git a/test/testdata/no-provenance-image/blobs/sha256/2aaebbb079957470e7c0adddbb054b2b4c01f717d408efba753da2bf6e8905da b/test/testdata/no-provenance-image/blobs/sha256/2aaebbb079957470e7c0adddbb054b2b4c01f717d408efba753da2bf6e8905da new file mode 100644 index 0000000..da3a9d6 --- /dev/null +++ b/test/testdata/no-provenance-image/blobs/sha256/2aaebbb079957470e7c0adddbb054b2b4c01f717d408efba753da2bf6e8905da @@ -0,0 +1,19 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:b6ef78de3633e45d1c08019fbabb4464fabd6dd32e82c67ea2b2a3c4e8bacdf5", + "size": 167 + }, + "layers": [ + { + "mediaType": "application/vnd.in-toto+json", + "digest": "sha256:a9646604f9522bf59d203a86ac5c2354a573ea041b8846409c4fc0f8c4a70850", + "size": 946, + "annotations": { + "in-toto.io/predicate-type": "https://spdx.dev/Document" + } + } + ] +} \ No newline at end of file diff --git a/test/testdata/no-provenance-image/blobs/sha256/2e82727457f04f320b643cb6e13bcbafb8e0dc0adc0443f1a25666f9518c5071 b/test/testdata/no-provenance-image/blobs/sha256/2e82727457f04f320b643cb6e13bcbafb8e0dc0adc0443f1a25666f9518c5071 new file mode 100644 index 0000000..49c9cbe --- /dev/null +++ b/test/testdata/no-provenance-image/blobs/sha256/2e82727457f04f320b643cb6e13bcbafb8e0dc0adc0443f1a25666f9518c5071 @@ -0,0 +1 @@ +{"architecture":"unknown","os":"unknown","config":{},"rootfs":{"type":"layers","diff_ids":["sha256:da5651e8877b960aa30f32f317fbeba28f5e06f1ce4d3895b3b8770140280a2e"]}} \ No newline at end of file diff --git a/test/testdata/no-provenance-image/blobs/sha256/7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e b/test/testdata/no-provenance-image/blobs/sha256/7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e new file mode 100644 index 0000000..1e9ebfd --- /dev/null +++ b/test/testdata/no-provenance-image/blobs/sha256/7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e @@ -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:d85d624a324422194b43cccd975b5752cf0acaedd668bb525fcd40c3587cc460", + "size": 453 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:97a548f8d65d9ab617f608dd621f59e0d43a3b346f34c34eb58da31f00a9b0ad", + "size": 116 + } + ] +} \ No newline at end of file diff --git a/test/testdata/no-provenance-image/blobs/sha256/97a548f8d65d9ab617f608dd621f59e0d43a3b346f34c34eb58da31f00a9b0ad b/test/testdata/no-provenance-image/blobs/sha256/97a548f8d65d9ab617f608dd621f59e0d43a3b346f34c34eb58da31f00a9b0ad new file mode 100644 index 0000000000000000000000000000000000000000..48e62f9199b1211a68948e2ffa86e777f568b0af GIT binary patch literal 116 zcmb2|=3oGW|EE08o;-P7d!g?;H!)lOF)Hj&${?{+FO9r(CDU|#%`$%jlYg?;H!)lOF)Hj&${?{+FO9r(CDU|#%`$%jlY