From ad3b8b9e4914b232883fceb0c21ebc33f5b63075 Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Thu, 1 Aug 2024 13:41:49 -0500 Subject: [PATCH 01/11] fix: let OCI layouts use referrers attestations --- pkg/policy/policy.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index dde953c..2f3650e 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -226,12 +226,18 @@ func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *confi } else { if mapping.Attestations != nil && mapping.Attestations.Repo != "" { return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo)) - } else { - return oci.NewReferrersAttestationResolver(resolver) } + return oci.NewReferrersAttestationResolver(resolver) } case *oci.OCILayoutResolver: - return resolver, nil + if mapping.Attestations != nil && mapping.Attestations.Style == config.AttestationStyleAttached { + return resolver, nil + } else { + if mapping.Attestations != nil && mapping.Attestations.Repo != "" { + return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo)) + } + return oci.NewReferrersAttestationResolver(resolver) + } default: return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver) } From 78ec0b766601f4fa59f234bed2af58b475544a39 Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Thu, 1 Aug 2024 15:24:35 -0500 Subject: [PATCH 02/11] fix: use referrers repo img spec and resolver --- pkg/policy/policy.go | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 27bab5b..ad6fb85 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -230,14 +230,28 @@ func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *confi return oci.NewReferrersAttestationResolver(resolver) } case *oci.LayoutResolver: - if mapping.Attestations != nil && mapping.Attestations.Style == config.AttestationStyleAttached { - return resolver, nil - } else { - if mapping.Attestations != nil && mapping.Attestations.Repo != "" { - return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo)) + if mapping.Attestations != nil { + switch mapping.Attestations.Style { + case config.AttestationStyleAttached: + return resolver, nil + case config.AttestationStyleReferrers: + if mapping.Attestations.Repo != "" { + referrersSpec, err := oci.ParseImageSpec(mapping.Attestations.Repo) + if err != nil { + return nil, fmt.Errorf("failed to parse referrers image spec: %w", err) + } + referrersResolver, err := CreateImageDetailsResolver(referrersSpec) + if err != nil { + return nil, fmt.Errorf("failed to create referrers resolver: %w", err) + } + return oci.NewReferrersAttestationResolver(referrersResolver, oci.WithReferrersRepo(mapping.Attestations.Repo)) + } + return oci.NewReferrersAttestationResolver(resolver) + default: + return nil, fmt.Errorf("unsupported attestation style: %s", mapping.Attestations.Style) } - return oci.NewReferrersAttestationResolver(resolver) } + return resolver, nil default: return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver) } From 5d096e226f30fb6712d9172e8c735b4bf767e2fe Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 5 Aug 2024 10:19:50 -0500 Subject: [PATCH 03/11] refactor: fix import cycle for mock resolver --- internal/test/test.go | 9 +++++++++ pkg/attest/verify_test.go | 2 +- pkg/mirror/mirror_test.go | 4 ++-- internal/test/mocks.go => pkg/oci/mock.go | 17 ++--------------- pkg/policy/policy_test.go | 2 +- 5 files changed, 15 insertions(+), 19 deletions(-) rename internal/test/mocks.go => pkg/oci/mock.go (71%) diff --git a/internal/test/test.go b/internal/test/test.go index 4f63668..f769476 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "strings" "testing" @@ -45,6 +46,14 @@ func CreateTempDir(t *testing.T, dir, pattern string) string { return tempDir } +func GetMockSigner(_ context.Context) (dsse.SignerVerifier, error) { + priv, err := os.ReadFile(filepath.Join("..", "..", "test", "testdata", "test-signing-key.pem")) + if err != nil { + return nil, err + } + return signerverifier.LoadKeyPair(priv) +} + func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) { var tl tlog.TL if UseMockTL { diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index 04c4805..dd2c33e 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -34,7 +34,7 @@ func TestVerifyAttestations(t *testing.T) { env := new(attestation.Envelope) err = json.Unmarshal(ex, env) assert.NoError(t, err) - resolver := &test.MockResolver{ + resolver := &oci.MockResolver{ Envs: []*attestation.Envelope{env}, } diff --git a/pkg/mirror/mirror_test.go b/pkg/mirror/mirror_test.go index 75ca87d..d1ad092 100644 --- a/pkg/mirror/mirror_test.go +++ b/pkg/mirror/mirror_test.go @@ -96,9 +96,9 @@ func TestSavingReferrers(t *testing.T) { err = SaveReferrers(manifest, output) require.NoError(t, err) - reg := &test.MockRegistryResolver{ + reg := &oci.MockRegistryResolver{ Subject: subject, - MockResolver: &test.MockResolver{}, + MockResolver: &oci.MockResolver{}, ImageNameStr: indexName, } require.NoError(t, err) diff --git a/internal/test/mocks.go b/pkg/oci/mock.go similarity index 71% rename from internal/test/mocks.go rename to pkg/oci/mock.go index 096ddf2..537d9ae 100644 --- a/internal/test/mocks.go +++ b/pkg/oci/mock.go @@ -1,15 +1,10 @@ -package test +package oci import ( "context" - "os" - "path/filepath" "github.com/docker/attest/pkg/attestation" - "github.com/docker/attest/pkg/oci" - "github.com/docker/attest/pkg/signerverifier" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/secure-systems-lab/go-securesystemslib/dsse" ) type MockResolver struct { @@ -37,7 +32,7 @@ func (r MockResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error) } func (r MockResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) { - return oci.ParsePlatform("linux/amd64") + return ParsePlatform("linux/amd64") } type MockRegistryResolver struct { @@ -53,11 +48,3 @@ func (r *MockRegistryResolver) ImageDescriptor(_ context.Context) (*v1.Descripto func (r *MockRegistryResolver) ImageName(_ context.Context) (string, error) { return r.ImageNameStr, nil } - -func GetMockSigner(_ context.Context) (dsse.SignerVerifier, error) { - priv, err := os.ReadFile(filepath.Join("..", "..", "test", "testdata", "test-signing-key.pem")) - if err != nil { - return nil, err - } - return signerverifier.LoadKeyPair(priv) -} diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index c9917c1..a3d155f 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -38,7 +38,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { re := policy.NewRegoEvaluator(true) - defaultResolver := test.MockResolver{ + defaultResolver := oci.MockResolver{ Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)}, } From 1d1c258f9c8679a963b5fd4639c03fdad2bb238e Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 5 Aug 2024 10:20:40 -0500 Subject: [PATCH 04/11] fix: referrers resolver only works for registry resolvers --- pkg/oci/referrers.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/oci/referrers.go b/pkg/oci/referrers.go index f5401c3..0be2183 100644 --- a/pkg/oci/referrers.go +++ b/pkg/oci/referrers.go @@ -16,6 +16,14 @@ type ReferrersResolver struct { } func NewReferrersAttestationResolver(src ImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) { + // currently only supports RegistryImageDetailsResolver + switch src.(type) { + case *RegistryImageDetailsResolver: + case *MockRegistryResolver: + default: + return nil, fmt.Errorf("unsupported referrers image details resolver type: %T", src) + } + res := &ReferrersResolver{ ImageDetailsResolver: src, } From c690d1090ca68d83513f9fa29e64b5c2c3a9aeea Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 5 Aug 2024 11:22:49 -0500 Subject: [PATCH 05/11] chore: use prefix const --- pkg/attest/verify_test.go | 6 +++--- pkg/mirror/mirror_test.go | 4 ++-- pkg/oci/oci_test.go | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index dd2c33e..9c38687 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -100,7 +100,7 @@ func TestVSA(t *testing.T) { policyOpts := &policy.Options{ LocalPolicyDir: PassPolicyDir, } - src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64)) + src, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64)) require.NoError(t, err) results, err := Verify(ctx, src, policyOpts) require.NoError(t, err) @@ -158,7 +158,7 @@ func TestVerificationFailure(t *testing.T) { policyOpts := &policy.Options{ LocalPolicyDir: FailPolicyDir, } - src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64)) + src, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64)) require.NoError(t, err) results, err := Verify(ctx, src, policyOpts) require.NoError(t, err) @@ -239,7 +239,7 @@ func TestSignVerify(t *testing.T) { policyOpts := &policy.Options{ LocalPolicyDir: tc.policyDir, } - src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64)) + src, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64)) require.NoError(t, err) results, err := Verify(ctx, src, policyOpts) if tc.expectError { diff --git a/pkg/mirror/mirror_test.go b/pkg/mirror/mirror_test.go index d1ad092..7a42d80 100644 --- a/pkg/mirror/mirror_test.go +++ b/pkg/mirror/mirror_test.go @@ -36,7 +36,7 @@ func TestSavingIndex(t *testing.T) { err = SaveIndex(output, attIdx.Index, indexName) require.NoError(t, err) - ociOutput, err := oci.ParseImageSpecs("oci://" + outputLayout) + ociOutput, err := oci.ParseImageSpecs(oci.LocalPrefix + outputLayout) require.NoError(t, err) err = SaveIndex(ociOutput, attIdx.Index, indexName) require.NoError(t, err) @@ -59,7 +59,7 @@ func TestSavingImage(t *testing.T) { err = SaveImage(output, img, indexName) require.NoError(t, err) - ociOutput, err := oci.ParseImageSpec("oci://" + outputLayout) + ociOutput, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout) require.NoError(t, err) err = SaveImage(ociOutput, img, indexName) require.NoError(t, err) diff --git a/pkg/oci/oci_test.go b/pkg/oci/oci_test.go index 545ad36..3039aac 100644 --- a/pkg/oci/oci_test.go +++ b/pkg/oci/oci_test.go @@ -95,10 +95,10 @@ func TestWithoutTag(t *testing.T) { {name: "image:tag", expected: "index.docker.io/library/image"}, {name: "image", expected: "index.docker.io/library/image"}, {name: "image:sha256-digest.att", expected: "index.docker.io/library/image"}, - {name: "docker://image:tag", expected: "docker://index.docker.io/library/image"}, + {name: RegistryPrefix + "image:tag", expected: RegistryPrefix + "index.docker.io/library/image"}, {name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image"}, - {name: "docker://image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "docker://index.docker.io/library/image"}, - {name: "docker://127.0.0.1:36555/repo:latest", expected: "docker://127.0.0.1:36555/repo"}, + {name: RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: RegistryPrefix + "index.docker.io/library/image"}, + {name: RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: RegistryPrefix + "127.0.0.1:36555/repo"}, } for _, c := range tc { t.Run(c.name, func(t *testing.T) { @@ -116,11 +116,11 @@ func TestReplaceTag(t *testing.T) { {name: "image:tag", expected: "index.docker.io/library/image:sha256-digest.att"}, {name: "image", expected: "index.docker.io/library/image:sha256-digest.att"}, {name: "image:sha256-digest.att", expected: "index.docker.io/library/image:sha256-digest.att"}, - {name: "docker://image:tag", expected: "docker://index.docker.io/library/image:sha256-digest.att"}, + {name: RegistryPrefix + "image:tag", expected: RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"}, {name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image:sha256-digest.att"}, - {name: "oci://foobar", expected: "oci://foobar"}, - {name: "docker://image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "docker://index.docker.io/library/image:sha256-digest.att"}, - {name: "docker://127.0.0.1:36555/repo:latest", expected: "docker://127.0.0.1:36555/repo:sha256-digest.att"}, + {name: LocalPrefix + "foobar", expected: LocalPrefix + "foobar"}, + {name: RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"}, + {name: RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: RegistryPrefix + "127.0.0.1:36555/repo:sha256-digest.att"}, } digest := v1.Hash{ From bb7a9a257e9c63d30305619de90b6d7551d6921b Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 5 Aug 2024 11:23:32 -0500 Subject: [PATCH 06/11] chore: remove duplicate code --- pkg/attest/sign_test.go | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/pkg/attest/sign_test.go b/pkg/attest/sign_test.go index 06c4b49..de577fe 100644 --- a/pkg/attest/sign_test.go +++ b/pkg/attest/sign_test.go @@ -15,9 +15,6 @@ import ( "github.com/docker/attest/pkg/policy" "github.com/google/go-containerregistry/pkg/registry" 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" @@ -66,21 +63,11 @@ func TestSignVerifyOCILayout(t *testing.T) { signedIndex := attIdx.Index signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests, attestation.WithReplacedLayers(tc.replace)) require.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) + spec, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout) require.NoError(t, err) - src, err := oci.ParseImageSpec("oci://" + outputLayout) + err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name) require.NoError(t, err) - policy, err := Verify(ctx, src, policyOpts) + policy, err := Verify(ctx, spec, policyOpts) require.NoError(t, err) assert.Equalf(t, OutcomeSuccess, policy.Outcome, "Policy should have been found") From 3e7a85e9b8e1d7dd8f2271c52d4d499de25a051e Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 5 Aug 2024 11:24:05 -0500 Subject: [PATCH 07/11] fix: nil pointer dereference --- pkg/oci/layout.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/oci/layout.go b/pkg/oci/layout.go index 5c54ebd..132c061 100644 --- a/pkg/oci/layout.go +++ b/pkg/oci/layout.go @@ -95,7 +95,6 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte } idxDescriptor := idxm.Manifests[0] - name := idxDescriptor.Annotations["org.opencontainers.image.ref.name"] idxDigest := idxDescriptor.Digest mfs, err := idx.ImageIndex(idxDigest) @@ -109,9 +108,11 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte var subjectDescriptor *v1.Descriptor for i := range mfs2.Manifests { manifest := &mfs2.Manifests[i] - if manifest.Platform.Equals(*platform) { - subjectDescriptor = manifest - break + if manifest.Platform != nil { + if manifest.Platform.Equals(*platform) { + subjectDescriptor = manifest + break + } } } for i := range mfs2.Manifests { @@ -135,7 +136,7 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte attest := &attestation.Manifest{ OriginalLayers: layers, OriginalDescriptor: mf, - SubjectName: name, + SubjectName: idxDescriptor.Annotations["org.opencontainers.image.ref.name"], SubjectDescriptor: subjectDescriptor, } return attest, nil From 58021646e31ea1ac95289dd302f4fc248dd5fe48 Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 5 Aug 2024 11:24:28 -0500 Subject: [PATCH 08/11] feat: add oci layout test --- pkg/oci/layout_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 pkg/oci/layout_test.go diff --git a/pkg/oci/layout_test.go b/pkg/oci/layout_test.go new file mode 100644 index 0000000..fa40a2a --- /dev/null +++ b/pkg/oci/layout_test.go @@ -0,0 +1,41 @@ +package oci_test + +import ( + "strings" + "testing" + + "github.com/docker/attest/internal/test" + "github.com/docker/attest/pkg/attest" + "github.com/docker/attest/pkg/attestation" + "github.com/docker/attest/pkg/mirror" + "github.com/docker/attest/pkg/oci" + "github.com/docker/attest/pkg/policy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAttestationFromOCILayout(t *testing.T) { + ctx, signer := test.Setup(t) + outputLayout := test.CreateTempDir(t, "", "attest-oci-layout") + + opts := &attestation.SigningOptions{} + attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage) + require.NoError(t, err) + signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts) + require.NoError(t, err) + signedIndex := attIdx.Index + signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests) + require.NoError(t, err) + + spec, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout) + require.NoError(t, err) + err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, outputLayout) + require.NoError(t, err) + + resolver, err := policy.CreateImageDetailsResolver(spec) + require.NoError(t, err) + desc, err := resolver.ImageDescriptor(ctx) + require.NoError(t, err) + digest := desc.Digest.String() + assert.True(t, strings.Contains(digest, "sha256:")) +} From c7d17faf059deb01f5e804d96c3ac36e499b0b14 Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 5 Aug 2024 13:24:58 -0500 Subject: [PATCH 09/11] fix: layout attestation resolver --- pkg/attest/verify_test.go | 65 ++++++++++++--------------------------- pkg/attestation/types.go | 2 +- pkg/mirror/mirror.go | 9 +++--- pkg/oci/layout.go | 3 ++ pkg/oci/referrers.go | 11 ++----- pkg/policy/policy.go | 42 +++++++------------------ 6 files changed, 41 insertions(+), 91 deletions(-) diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index 9c38687..34c4ecc 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -10,12 +10,10 @@ import ( "github.com/docker/attest/internal/test" "github.com/docker/attest/pkg/attestation" + "github.com/docker/attest/pkg/config" + "github.com/docker/attest/pkg/mirror" "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" intoto "github.com/in-toto/in-toto-golang/in_toto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -84,25 +82,17 @@ func TestVSA(t *testing.T) { require.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) + spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64)) + require.NoError(t, err) + err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name) assert.NoError(t, err) // mocked vsa query should pass policyOpts := &policy.Options{ - LocalPolicyDir: PassPolicyDir, + LocalPolicyDir: PassPolicyDir, + AttestationStyle: config.AttestationStyleAttached, } - src, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64)) - require.NoError(t, err) - results, err := Verify(ctx, src, policyOpts) + results, err := Verify(ctx, spec, policyOpts) require.NoError(t, err) assert.Equal(t, OutcomeSuccess, results.Outcome) assert.Empty(t, results.Violations) @@ -142,25 +132,17 @@ func TestVerificationFailure(t *testing.T) { require.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) + spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64)) + require.NoError(t, err) + err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name) assert.NoError(t, err) // mocked vsa query should fail policyOpts := &policy.Options{ - LocalPolicyDir: FailPolicyDir, + LocalPolicyDir: FailPolicyDir, + AttestationStyle: config.AttestationStyleAttached, } - src, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64)) - require.NoError(t, err) - results, err := Verify(ctx, src, policyOpts) + results, err := Verify(ctx, spec, policyOpts) require.NoError(t, err) assert.Equal(t, OutcomeFailure, results.Outcome) assert.Len(t, results.Violations, 1) @@ -224,24 +206,15 @@ func TestSignVerify(t *testing.T) { imageName = attIdx.Name } // 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: imageName, - }, - }, - }) - _, err = layout.Write(outputLayout, idx) - assert.NoError(t, err) + spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64)) + require.NoError(t, err) + err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, imageName) + require.NoError(t, err) policyOpts := &policy.Options{ LocalPolicyDir: tc.policyDir, } - src, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64)) - require.NoError(t, err) - results, err := Verify(ctx, src, policyOpts) + results, err := Verify(ctx, spec, policyOpts) if tc.expectError { require.Error(t, err) return diff --git a/pkg/attestation/types.go b/pkg/attestation/types.go index fd28c83..3305b24 100644 --- a/pkg/attestation/types.go +++ b/pkg/attestation/types.go @@ -36,7 +36,7 @@ type Manifest struct { // accumulated during signing SignedLayers []*Layer - // details of subect image + // details of subject image SubjectName string SubjectDescriptor *v1.Descriptor } diff --git a/pkg/mirror/mirror.go b/pkg/mirror/mirror.go index eb927ac..e9e35de 100644 --- a/pkg/mirror/mirror.go +++ b/pkg/mirror/mirror.go @@ -129,6 +129,7 @@ func SaveImage(output *oci.ImageSpec, image v1.Image, imageName string) error { func SaveReferrers(manifest *attestation.Manifest, outputs []*oci.ImageSpec) error { for _, output := range outputs { + // OCI layout output for referrers not supported if output.Type == oci.OCI { continue } @@ -147,10 +148,10 @@ func SaveReferrers(manifest *attestation.Manifest, outputs []*oci.ImageSpec) err return fmt.Errorf("failed to build image: %w", err) } for _, image := range images { - err = SaveImage(attOut, image, "") - } - if err != nil { - return fmt.Errorf("failed to push image: %w", err) + err := PushImageToRegistry(image, attOut.Identifier) + if err != nil { + return fmt.Errorf("failed to push image: %w", err) + } } } return nil diff --git a/pkg/oci/layout.go b/pkg/oci/layout.go index 132c061..634be9e 100644 --- a/pkg/oci/layout.go +++ b/pkg/oci/layout.go @@ -115,6 +115,9 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte } } } + if subjectDescriptor == nil { + return nil, fmt.Errorf("platform not found in index") + } for i := range mfs2.Manifests { mf := &mfs2.Manifests[i] if mf.Annotations[att.DockerReferenceType] != attestation.AttestationManifestType { diff --git a/pkg/oci/referrers.go b/pkg/oci/referrers.go index 0be2183..b74b4a8 100644 --- a/pkg/oci/referrers.go +++ b/pkg/oci/referrers.go @@ -3,6 +3,7 @@ package oci import ( "context" "fmt" + "strings" "github.com/docker/attest/pkg/attestation" att "github.com/docker/attest/pkg/attestation" @@ -16,14 +17,6 @@ type ReferrersResolver struct { } func NewReferrersAttestationResolver(src ImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) { - // currently only supports RegistryImageDetailsResolver - switch src.(type) { - case *RegistryImageDetailsResolver: - case *MockRegistryResolver: - default: - return nil, fmt.Errorf("unsupported referrers image details resolver type: %T", src) - } - res := &ReferrersResolver{ ImageDetailsResolver: src, } @@ -66,7 +59,7 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy } var referrersSubjectRef name.Digest if r.referrersRepo != "" { - referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", r.referrersRepo, subjectDigest)) + referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", strings.TrimPrefix(r.referrersRepo, RegistryPrefix), subjectDigest)) if err != nil { return nil, fmt.Errorf("failed to create referrers reference: %w", err) } diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index ad6fb85..0582192 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -219,40 +219,20 @@ func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsRes } func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (oci.AttestationResolver, error) { - switch resolver := resolver.(type) { - case *oci.RegistryImageDetailsResolver: - if mapping.Attestations != nil && mapping.Attestations.Style == config.AttestationStyleAttached { - return oci.NewRegistryAttestationResolver(resolver) - } else { - if mapping.Attestations != nil && mapping.Attestations.Repo != "" { - return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo)) - } - return oci.NewReferrersAttestationResolver(resolver) - } - case *oci.LayoutResolver: - if mapping.Attestations != nil { - switch mapping.Attestations.Style { - case config.AttestationStyleAttached: + if mapping.Attestations != nil { + if mapping.Attestations.Style == config.AttestationStyleAttached { + switch resolver := resolver.(type) { + case *oci.RegistryImageDetailsResolver: + return oci.NewRegistryAttestationResolver(resolver) + case *oci.LayoutResolver: return resolver, nil - case config.AttestationStyleReferrers: - if mapping.Attestations.Repo != "" { - referrersSpec, err := oci.ParseImageSpec(mapping.Attestations.Repo) - if err != nil { - return nil, fmt.Errorf("failed to parse referrers image spec: %w", err) - } - referrersResolver, err := CreateImageDetailsResolver(referrersSpec) - if err != nil { - return nil, fmt.Errorf("failed to create referrers resolver: %w", err) - } - return oci.NewReferrersAttestationResolver(referrersResolver, oci.WithReferrersRepo(mapping.Attestations.Repo)) - } - return oci.NewReferrersAttestationResolver(resolver) default: - return nil, fmt.Errorf("unsupported attestation style: %s", mapping.Attestations.Style) + return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver) } } - return resolver, nil - default: - return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver) + if mapping.Attestations.Repo != "" { + return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo)) + } } + return oci.NewReferrersAttestationResolver(resolver) } From 501b9b442d5d086dad104dc65b42f0cb1197dc1d Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 5 Aug 2024 16:31:28 -0500 Subject: [PATCH 10/11] feat: add CreateAttestationResolver tests --- pkg/policy/policy_test.go | 61 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index a3d155f..65f8a60 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -115,3 +115,64 @@ func TestLoadingMappings(t *testing.T) { } } } + +func TestCreateAttestationResolver(t *testing.T) { + mockResolver := oci.MockResolver{ + Envs: []*attestation.Envelope{}, + } + layoutResolver := &oci.LayoutResolver{} + registryResolver := &oci.RegistryImageDetailsResolver{} + + nilRepoReferrers := &config.PolicyMapping{ + Attestations: &config.AttestationConfig{ + Style: config.AttestationStyleReferrers, + }, + } + referrers := &config.PolicyMapping{ + Attestations: &config.AttestationConfig{ + Repo: "localhost:5000/repo", + Style: config.AttestationStyleReferrers, + }, + } + attached := &config.PolicyMapping{ + Attestations: &config.AttestationConfig{ + Style: config.AttestationStyleAttached, + }, + } + + testCases := []struct { + name string + resolver oci.ImageDetailsResolver + mapping *config.PolicyMapping + errorStr string + }{ + {name: "referrers", resolver: layoutResolver, mapping: referrers}, + {name: "referrers (no mapped repo)", resolver: layoutResolver, mapping: nilRepoReferrers}, + {name: "referrers (no mapping)", resolver: layoutResolver, mapping: &config.PolicyMapping{Attestations: nil}}, + {name: "attached (registry)", resolver: registryResolver, mapping: attached}, + {name: "attached (layout)", resolver: layoutResolver, mapping: attached}, + {name: "attached (unsupported)", resolver: mockResolver, mapping: attached, errorStr: "unsupported image details resolver type"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver, err := policy.CreateAttestationResolver(tc.resolver, tc.mapping) + if tc.errorStr == "" { + require.NoError(t, err) + } else { + assert.Contains(t, err.Error(), tc.errorStr) + } + if tc.mapping.Attestations == nil { + return + } + switch resolver.(type) { + case *oci.ReferrersResolver: + assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleReferrers) + case *oci.RegistryResolver: + assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached) + case *oci.LayoutResolver: + assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached) + } + }) + } +} From 0133423f0dbe81103fe566bd96c4c2c03433a96a Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Mon, 5 Aug 2024 16:50:40 -0500 Subject: [PATCH 11/11] feat: add nil pointer test --- pkg/oci/layout_test.go | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/pkg/oci/layout_test.go b/pkg/oci/layout_test.go index fa40a2a..12feb93 100644 --- a/pkg/oci/layout_test.go +++ b/pkg/oci/layout_test.go @@ -10,6 +10,7 @@ import ( "github.com/docker/attest/pkg/mirror" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -18,6 +19,11 @@ func TestAttestationFromOCILayout(t *testing.T) { ctx, signer := test.Setup(t) outputLayout := test.CreateTempDir(t, "", "attest-oci-layout") + invalidPlatform := &v1.Platform{ + Architecture: "invalid", + OS: "invalid", + } + opts := &attestation.SigningOptions{} attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage) require.NoError(t, err) @@ -26,16 +32,38 @@ func TestAttestationFromOCILayout(t *testing.T) { signedIndex := attIdx.Index signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests) require.NoError(t, err) - spec, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout) require.NoError(t, err) err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, outputLayout) require.NoError(t, err) - resolver, err := policy.CreateImageDetailsResolver(spec) - require.NoError(t, err) - desc, err := resolver.ImageDescriptor(ctx) - require.NoError(t, err) - digest := desc.Digest.String() - assert.True(t, strings.Contains(digest, "sha256:")) + testCases := []struct { + name string + platform *v1.Platform + errorStr string + }{ + {name: "nominal", platform: spec.Platform}, + {name: "invalid platform", platform: invalidPlatform, errorStr: "platform not found in index"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + spec := &oci.ImageSpec{ + Type: oci.OCI, + Identifier: outputLayout, + Platform: tc.platform, + } + resolver, err := policy.CreateImageDetailsResolver(spec) + if tc.errorStr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.errorStr) + return + } + require.NoError(t, err) + desc, err := resolver.ImageDescriptor(ctx) + require.NoError(t, err) + digest := desc.Digest.String() + assert.True(t, strings.Contains(digest, "sha256:")) + }) + } }