From 1a7897a05256153f475ed949484336ab21cd2bb0 Mon Sep 17 00:00:00 2001 From: Jonny Stoten Date: Wed, 22 May 2024 14:49:23 +0100 Subject: [PATCH] Return VSA and rich errors from verification (#38) * Start of richer results from verification * Pull out VSA code from signing * Expose attestation signing fns * Add VSA test * Notes for policy result * Require separate policy for VSA creation * Load test signing key from tests * Return rich object from policy * Add result object schema and fix tests * Ensure example test runs * Remove data.yaml files from mock policies * Don't run example - TUF policy isn't compatible * Add attestation to manifests for all subjects * Ensure adding attestation doesn't touch statements * Don't export sign function * Remove attestations from VerificationResult * Change bool to Outcome enum in result * Use outputLayout directly * Make clearer that Outcome strings are for VSA * Return multiple SLSA levels from policy * Fix unmarshalling of policy-id (#39) * Rename function * Rename policy.VerificationResult -> policy.Result * Re-add test for canonical input --------- Co-authored-by: James Carnegie Co-authored-by: James Carnegie --- internal/test/test.go | 9 +- pkg/attest/example_sign_test.go | 11 -- pkg/attest/example_verify_test.go | 16 ++- pkg/attest/sign.go | 136 +++++++++++++----- pkg/attest/sign_test.go | 113 +++++++++++---- pkg/attest/types.go | 35 ++++- pkg/attest/verify.go | 107 ++++++++++---- pkg/attest/verify_test.go | 132 ++++++++++++++++- pkg/attest/vsa.go | 96 ------------- pkg/attestation/vsa.go | 6 - pkg/oci/oci.go | 17 ++- pkg/oci/types.go | 1 - pkg/policy/evaluator.go | 3 +- pkg/policy/mock.go | 22 +-- pkg/policy/policy.go | 48 +++++-- pkg/policy/policy_test.go | 73 ++++------ pkg/policy/rego.go | 70 +++++++-- .../mock-tuf-allow-canonical/doi/policy.rego | 7 + .../mock-tuf-allow-canonical/mapping.yaml | 16 +++ .../testdata/mock-tuf-allow/doi/policy.rego | 4 +- .../testdata/mock-tuf-allow/mapping.yaml | 5 + .../testdata/mock-tuf-deny/doi/policy.rego | 4 +- .../mock-tuf-verify-sig/doi/policy.rego | 14 +- .../mock-tuf-wrong-key/doi/policy.rego | 22 +-- pkg/signerverifier/common.go | 28 ++++ .../local-policy-fail/doi/policy.rego | 48 +++++++ .../mapping.yaml | 7 - .../local-policy-pass/doi/policy.rego | 39 +++++ test/testdata/local-policy-pass/mapping.yaml | 11 ++ test/testdata/local-policy/doi/data.yaml | 58 -------- test/testdata/local-policy/doi/policy.rego | 49 ------- .../local-policy/doi/policy_test.rego | 25 ---- test/testdata/test-signing-key.pem | 5 + 33 files changed, 776 insertions(+), 461 deletions(-) delete mode 100644 pkg/attest/vsa.go create mode 100644 pkg/policy/testdata/mock-tuf-allow-canonical/doi/policy.rego create mode 100644 pkg/policy/testdata/mock-tuf-allow-canonical/mapping.yaml create mode 100644 test/testdata/local-policy-fail/doi/policy.rego rename test/testdata/{local-policy => local-policy-fail}/mapping.yaml (63%) create mode 100644 test/testdata/local-policy-pass/doi/policy.rego create mode 100644 test/testdata/local-policy-pass/mapping.yaml delete mode 100644 test/testdata/local-policy/doi/data.yaml delete mode 100644 test/testdata/local-policy/doi/policy.rego delete mode 100644 test/testdata/local-policy/doi/policy_test.rego create mode 100644 test/testdata/test-signing-key.pem diff --git a/internal/test/test.go b/internal/test/test.go index 73e26cc..2ff8ebf 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "strings" "testing" @@ -82,7 +83,11 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) { } func GetMockSigner(ctx context.Context) (dsse.SignerVerifier, error) { - return signerverifier.GenKeyPair() + priv, err := os.ReadFile(filepath.Join("..", "..", "test", "testdata", "test-signing-key.pem")) + if err != nil { + return nil, err + } + return signerverifier.LoadKeyPair(priv) } type AnnotatedStatement struct { @@ -115,7 +120,7 @@ func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStat var statements []*AnnotatedStatement for _, mf := range mfs2.Manifests { - if mf.Annotations["vnd.docker.reference.type"] != "attestation-manifest" { + if mf.Annotations[attestation.DockerReferenceType] != "attestation-manifest" { continue } diff --git a/pkg/attest/example_sign_test.go b/pkg/attest/example_sign_test.go index 41bbee6..5368696 100644 --- a/pkg/attest/example_sign_test.go +++ b/pkg/attest/example_sign_test.go @@ -4,7 +4,6 @@ import ( "context" "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/signerverifier" @@ -30,16 +29,6 @@ func ExampleSign_remote() { Replace: true, // replace unsigned intoto statements with signed intoto attestations, otherwise leave in place } - // configure VSA options (optional) - slsaBuildLevel := "3" - slsaPolicyUri := "https://docker.com/attest/policy" - slsaVerifierId := "https://docker.com" - opts.VSAOptions = &attestation.VSAOptions{ - BuildLevel: "SLSA_BUILD_LEVEL_" + slsaBuildLevel, - PolicyURI: slsaPolicyUri, - VerifierID: slsaVerifierId, - } - // load image index with unsigned attestation-manifests ref := "docker/image-signer-verifier:latest" att, err := oci.AttestationIndexFromRemote(ref) diff --git a/pkg/attest/example_verify_test.go b/pkg/attest/example_verify_test.go index 7e80bff..8e46e50 100644 --- a/pkg/attest/example_verify_test.go +++ b/pkg/attest/example_verify_test.go @@ -59,14 +59,16 @@ func ExampleVerify_remote() { } // verify attestations - policy, err := attest.Verify(context.Background(), opts, resolver) + result, err := attest.Verify(context.Background(), opts, resolver) if err != nil { - panic(err) // failed policy or attestation signature verification + panic(err) } - if policy { - fmt.Printf("policy passed: %v\n", policy) - return // passed policy + switch result.Outcome { + case attest.OutcomeSuccess: + fmt.Println("policy passed") + case attest.OutcomeNoPolicy: + fmt.Println("no policy for image") + case attest.OutcomeFailure: + fmt.Println("policy failed") } - // no policy found for image - fmt.Printf("no policy for image") } diff --git a/pkg/attest/sign.go b/pkg/attest/sign.go index dd63183..f164528 100644 --- a/pkg/attest/sign.go +++ b/pkg/attest/sign.go @@ -6,6 +6,7 @@ import ( "fmt" "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" @@ -26,45 +27,98 @@ func Sign(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, op // sign every attestation layer in each manifest for _, manifest := range attestationManifests { - attestationLayers, err := attestation.GetAttestationsFromImage(manifest.Attestation.Image) - if err != nil { - return nil, fmt.Errorf("failed to get attestations from image: %w", err) - } - signedLayers, err := signLayers(ctx, attestationLayers, signer) - if err != nil { - return nil, fmt.Errorf("failed to sign attestations: %w", err) - } - if opts.VSAOptions != nil { - newLayer, err := generateVSA(ctx, manifest, signer, opts) - if err != nil { - return nil, fmt.Errorf("failed to generate VSA: %w", err) - } - signedLayers = append(signedLayers, *newLayer) - } - newImg, err := addSignedLayers(signedLayers, manifest, opts) + idx, err = signLayersAndAddToIndex(ctx, idx, manifest.Attestation.Layers, manifest, signer, opts) if err != nil { return nil, fmt.Errorf("failed to add signed layers: %w", err) } - newDesc, err := partial.Descriptor(newImg) - if err != nil { - return nil, fmt.Errorf("failed to get descriptor: %w", err) - } - cf, err := manifest.Attestation.Image.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 - idx = mutate.RemoveManifests(idx, match.Digests(manifest.Digest)) - idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ - Add: newImg, - Descriptor: *newDesc, - }) } return idx, nil } +func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.Statement, signer dsse.SignerVerifier) (v1.ImageIndex, error) { + if len(statement.Subject) == 0 { + return nil, fmt.Errorf("statement has no subjects") + } + + subjectDigests := make(map[string]bool) + for _, subject := range statement.Subject { + subjectDigest := fmt.Sprintf("sha256:%s", subject.Digest["sha256"]) + subjectDigests[subjectDigest] = true + } + + attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx) + if err != nil { + return nil, fmt.Errorf("failed to get attestation manifests: %w", err) + } + updatedIndex := false + for _, manifest := range attestationManifests { + if subjectDigests[manifest.Annotations[oci.DockerReferenceDigest]] { + attestationLayers := []attestation.AttestationLayer{ + { + Statement: statement, + MediaType: types.MediaType(intoto.PayloadType), + Annotations: map[string]string{ + oci.InTotoPredicateType: statement.PredicateType, + }, + }, + } + // hard-coding replace to false here, because if it's true we will remove any unsigned statements, even unrelated ones + idx, err = signLayersAndAddToIndex(ctx, idx, attestationLayers, manifest, signer, &SigningOptions{Replace: false}) + if err != nil { + return nil, fmt.Errorf("failed to add signed layers: %w", err) + } + updatedIndex = true + } + } + if !updatedIndex { + return nil, fmt.Errorf("no attestation manifest found for statement") + } + return idx, nil + +} + +func signLayersAndAddToIndex( + ctx context.Context, + idx v1.ImageIndex, + attestationLayers []attestation.AttestationLayer, + manifest attestation.AttestationManifest, + signer dsse.SignerVerifier, + opts *SigningOptions) (v1.ImageIndex, error) { + + signedLayers, err := signLayers(ctx, attestationLayers, signer) + if err != nil { + return nil, fmt.Errorf("failed to sign attestations: %w", err) + } + + newImg, err := addSignedLayers(signedLayers, manifest, opts) + if err != nil { + return nil, fmt.Errorf("failed to add signed layers: %w", err) + } + newDesc, err := partial.Descriptor(newImg) + if err != nil { + return nil, fmt.Errorf("failed to get descriptor: %w", err) + } + cf, err := manifest.Attestation.Image.ConfigFile() + if err != nil { + return nil, fmt.Errorf("failed to get config file: %w", err) + } + newDesc.Platform = cf.Platform() + if newDesc.Platform == nil { + newDesc.Platform = &v1.Platform{ + Architecture: "unknown", + OS: "unknown", + } + } + newDesc.MediaType = manifest.MediaType + newDesc.Annotations = manifest.Annotations + idx = mutate.RemoveManifests(idx, match.Digests(manifest.Digest)) + idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ + Add: newImg, + Descriptor: *newDesc, + }) + return idx, nil +} + // signLayers signs each intoto attestation layer with the given signer func signLayers(ctx context.Context, layers []attestation.AttestationLayer, signer dsse.SignerVerifier) ([]mutate.Addendum, error) { var signedLayers []mutate.Addendum @@ -77,11 +131,7 @@ func signLayers(ctx context.Context, layers []attestation.AttestationLayer, sign layer.Annotations[InTotoReferenceLifecycleStage] = LifecycleStageExperimental // sign the statement - payload, err := json.Marshal(layer.Statement) - if err != nil { - return nil, fmt.Errorf("failed to marshal statement: %w", err) - } - env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer) + env, err := signInTotoStatement(ctx, layer.Statement, signer) if err != nil { return nil, fmt.Errorf("failed to sign statement: %w", err) } @@ -103,6 +153,18 @@ func signLayers(ctx context.Context, layers []attestation.AttestationLayer, sign return signedLayers, nil } +func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier) (*attestation.Envelope, error) { + payload, err := json.Marshal(statement) + 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) + } + return env, nil +} + // addSignedLayers adds signed layers to a new or existing attestation image func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.AttestationManifest, opts *SigningOptions) (v1.Image, error) { var err error diff --git a/pkg/attest/sign_test.go b/pkg/attest/sign_test.go index f2f6787..07cf9dc 100644 --- a/pkg/attest/sign_test.go +++ b/pkg/attest/sign_test.go @@ -2,6 +2,7 @@ package attest import ( "encoding/json" + "fmt" "path/filepath" "testing" @@ -18,12 +19,14 @@ import ( 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" + "github.com/stretchr/testify/require" ) var ( UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image") - LocalPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy") + PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass") + FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail") TestTempDir = "attest-sign-test" ) @@ -38,31 +41,25 @@ func TestSignVerifyOCILayout(t *testing.T) { replace bool }{ - {"signed replaced (does nothing)", UnsignedTestImage, 0, 6, true}, - {"without replace", UnsignedTestImage, 4, 6, false}, + {"signed replaced (does nothing)", UnsignedTestImage, 0, 4, true}, + {"without replace", UnsignedTestImage, 4, 4, false}, // image without provenance doesn't fail - {"no provenance (replace)", NoProvenanceImage, 0, 4, true}, - {"no provenance (no replace)", NoProvenanceImage, 2, 4, false}, + {"no provenance (replace)", NoProvenanceImage, 0, 2, true}, + {"no provenance (no replace)", NoProvenanceImage, 2, 2, false}, } policyResolver := &policy.PolicyOptions{ - LocalPolicyDir: LocalPolicyDir, + LocalPolicyDir: PassPolicyDir, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - tempDir := test.CreateTempDir(t, "", TestTempDir) - outputLayout := tempDir + outputLayout := test.CreateTempDir(t, "", TestTempDir) opts := &SigningOptions{ Replace: tc.replace, - 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) + require.NoError(t, err) signedIndex, err := Sign(ctx, attIdx.Index, signer, opts) - assert.NoError(t, err) + require.NoError(t, err) // output signed attestations idx := v1.ImageIndex(empty.Index) @@ -75,25 +72,21 @@ func TestSignVerifyOCILayout(t *testing.T) { }, }) _, err = layout.Write(outputLayout, idx) - assert.NoError(t, err) + require.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") + require.NoError(t, err) + assert.Equalf(t, OutcomeSuccess, policy.Outcome, "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)) 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) + statements, err := test.ExtractAnnotatedStatements(outputLayout, mt) + require.NoError(t, err) allEnvelopes = append(allEnvelopes, statements...) for _, stmt := range statements { @@ -102,13 +95,77 @@ func TestSignVerifyOCILayout(t *testing.T) { } } 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) + statements, err := test.ExtractAnnotatedStatements(outputLayout, intoto.PayloadType) + require.NoError(t, err) assert.Equalf(t, tc.expectedStatements, len(statements), "expected %d statement, got %d", tc.expectedStatements, len(statements)) }) } } +func TestAddAttestation(t *testing.T) { + ctx, signer := test.Setup(t) + + expectedAttestations := 2 + expectedStatements := 4 + + outputLayout := test.CreateTempDir(t, "", TestTempDir) + attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage) + require.NoError(t, err) + + statementToAdd := &intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + PredicateType: attestation.VSAPredicateType, + Type: intoto.StatementInTotoV01, + Subject: []intoto.Subject{ + { + Name: attIdx.Name, + Digest: map[string]string{ + "sha256": "da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", + }, + }, + { + Name: attIdx.Name, + Digest: map[string]string{ + "sha256": "7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", + }, + }, + }, + }, + } + + signedIndex, err := AddAttestation(ctx, attIdx.Index, statementToAdd, signer) + 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) + require.NoError(t, err) + + var allEnvelopes []*test.AnnotatedStatement + mt, _ := attestation.DSSEMediaType(attestation.VSAPredicateType) + statements, err := test.ExtractAnnotatedStatements(outputLayout, mt) + require.NoError(t, err) + allEnvelopes = append(allEnvelopes, statements...) + + for _, stmt := range statements { + assert.Equalf(t, attestation.VSAPredicateType, stmt.Annotations[oci.InTotoPredicateType], "expected predicate-type annotation to be set to %s, got %s", attestation.VSAPredicateType, 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, expectedAttestations, len(allEnvelopes), "expected %d attestations, got %d", expectedAttestations, len(allEnvelopes)) + statements, err = test.ExtractAnnotatedStatements(outputLayout, intoto.PayloadType) + fmt.Printf("statements: %+v\n", statements) + require.NoError(t, err) + assert.Equalf(t, expectedStatements, len(statements), "expected %d statement, got %d", expectedStatements, len(statements)) +} + func TestAddSignedLayerAnnotations(t *testing.T) { testCases := []struct { name string @@ -147,7 +204,7 @@ func TestAddSignedLayerAnnotations(t *testing.T) { }, } newImg, err := addSignedLayers(signedLayers, manifest, opts) - assert.NoError(t, err) + require.NoError(t, err) mf, _ := newImg.RawManifest() type Annotations struct { Annotations map[string]string `json:"annotations"` @@ -157,7 +214,7 @@ func TestAddSignedLayerAnnotations(t *testing.T) { } l := &Layers{} err = json.Unmarshal(mf, l) - assert.NoError(t, err) + require.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 index a837e8d..1343034 100644 --- a/pkg/attest/types.go +++ b/pkg/attest/types.go @@ -1,7 +1,10 @@ package attest import ( - "github.com/docker/attest/pkg/attestation" + "fmt" + + "github.com/docker/attest/pkg/policy" + intoto "github.com/in-toto/in-toto-golang/in_toto" ) const ( @@ -10,6 +13,32 @@ const ( ) type SigningOptions struct { - Replace bool - VSAOptions *attestation.VSAOptions + Replace bool +} + +type Outcome string + +const ( + OutcomeSuccess Outcome = "success" + OutcomeFailure Outcome = "failure" + OutcomeNoPolicy Outcome = "no_policy" +) + +func (o Outcome) StringForVSA() (string, error) { + switch o { + case OutcomeSuccess: + return "PASSED", nil + case OutcomeFailure: + return "FAILED", nil + default: + return "", fmt.Errorf("unknown outcome: %s", o) + } +} + +type VerificationResult struct { + Outcome Outcome + Policy *policy.Policy + Input *policy.PolicyInput + VSA *intoto.Statement + Violations []policy.Violation } diff --git a/pkg/attest/verify.go b/pkg/attest/verify.go index 0a963d1..10c5f5d 100644 --- a/pkg/attest/verify.go +++ b/pkg/attest/verify.go @@ -3,23 +3,95 @@ package attest import ( "context" "fmt" + "time" + "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" + intoto "github.com/in-toto/in-toto-golang/in_toto" ) -func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, files []*policy.PolicyFile) error { +func Verify(ctx context.Context, opts *policy.PolicyOptions, resolver oci.AttestationResolver) (result *VerificationResult, err error) { + pctx, err := policy.ResolvePolicy(ctx, resolver, opts) + if err != nil { + return nil, fmt.Errorf("failed to resolve policy: %w", err) + } + + if pctx == nil { + return &VerificationResult{ + Outcome: OutcomeNoPolicy, + }, nil + } + + result, err = VerifyAttestations(ctx, resolver, pctx) + if err != nil { + return nil, fmt.Errorf("failed to evaluate policy: %w", err) + } + return result, nil +} + +func ToPolicyResult(p *policy.Policy, input *policy.PolicyInput, result *policy.Result) (*VerificationResult, error) { + dgst, err := oci.SplitDigest(input.Digest) + if err != nil { + return nil, fmt.Errorf("failed to split digest: %w", err) + } + subject := intoto.Subject{ + Name: input.Purl, + Digest: *dgst, + } + resourceUri, err := attestation.ToVSAResourceURI(subject) + if err != nil { + return nil, fmt.Errorf("failed to create resource uri: %w", err) + } + + var outcome Outcome + if result.Success { + outcome = OutcomeSuccess + } else { + outcome = OutcomeFailure + } + + outcomeStr, err := outcome.StringForVSA() + if err != nil { + return nil, err + } + + return &VerificationResult{ + Policy: p, + Outcome: outcome, + Violations: result.Violations, + VSA: &intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + PredicateType: attestation.VSAPredicateType, + Type: intoto.StatementInTotoV01, + Subject: result.Summary.Subjects, + }, + Predicate: attestation.VSAPredicate{ + Verifier: attestation.VSAVerifier{ + ID: result.Summary.Verifier, + }, + TimeVerified: time.Now().UTC().Format(time.RFC3339), + ResourceUri: resourceUri, + Policy: attestation.VSAPolicy{URI: result.Summary.PolicyURI}, + VerificationResult: outcomeStr, + VerifiedLevels: result.Summary.SLSALevels, + }, + }, + }, nil +} + +func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, pctx *policy.Policy) (*VerificationResult, error) { digest, err := resolver.ImageDigest(ctx) if err != nil { - return fmt.Errorf("failed to get image digest: %w", err) + return nil, fmt.Errorf("failed to get image digest: %w", err) } name, err := resolver.ImageName(ctx) if err != nil { - return fmt.Errorf("failed to get image name: %w", err) + return nil, fmt.Errorf("failed to get image name: %w", err) } purl, canonical, err := oci.RefToPURL(name, resolver.ImagePlatformStr()) if err != nil { - return fmt.Errorf("failed to convert ref to purl: %w", err) + return nil, fmt.Errorf("failed to convert ref to purl: %w", err) } input := &policy.PolicyInput{ Digest: digest, @@ -29,30 +101,11 @@ func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, f evaluator, err := policy.GetPolicyEvaluator(ctx) if err != nil { - return err + return nil, err } - rs, err := evaluator.Evaluate(ctx, resolver, files, input) + result, err := evaluator.Evaluate(ctx, resolver, pctx, input) if err != nil { - return fmt.Errorf("policy evaluation failed: %w", err) + return nil, fmt.Errorf("policy evaluation failed: %w", err) } - if !rs.Allowed() { - return fmt.Errorf("policy evaluation failed: %s", fmt.Sprint(rs)) - } - - return nil -} - -func Verify(ctx context.Context, opts *policy.PolicyOptions, resolver oci.AttestationResolver) (policyFound bool, err error) { - policyFiles, err := policy.ResolvePolicy(ctx, resolver, opts) - if err != nil { - return false, fmt.Errorf("failed to resolve policy: %w", err) - } - - // no policy for image -> success - if policyFiles == nil { - return false, nil - } - - // policy found -> verify - return true, VerifyAttestations(ctx, resolver, policyFiles) + return ToPolicyResult(pctx, input, result) } diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index 538081a..889841a 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -8,11 +8,17 @@ import ( "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" - "github.com/open-policy-agent/opa/rego" + 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" ) var ( @@ -43,19 +49,135 @@ func TestVerifyAttestations(t *testing.T) { t.Run(tc.name, func(t *testing.T) { mockPE := policy.MockPolicyEvaluator{ - EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pfs []*policy.PolicyFile, input *policy.PolicyInput) (*rego.ResultSet, error) { + EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pctx *policy.Policy, input *policy.PolicyInput) (*policy.Result, error) { return policy.AllowedResult(), tc.policyEvaluationError }, } ctx := policy.WithPolicyEvaluator(context.Background(), &mockPE) - err = VerifyAttestations(ctx, resolver, nil) + _, err := VerifyAttestations(ctx, resolver, nil) if tc.expectedError != nil { - assert.Error(t, err) - assert.Equal(t, tc.expectedError.Error(), err.Error()) + if assert.Error(t, err) { + assert.Equal(t, tc.expectedError.Error(), err.Error()) + } } else { assert.NoError(t, err) } }) } } + +func TestVSA(t *testing.T) { + ctx, signer := test.Setup(t) + ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true)) + // setup an image with signed attestations + outputLayout := test.CreateTempDir(t, "", TestTempDir) + + opts := &SigningOptions{ + Replace: true, + } + attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage) + assert.NoError(t, err) + signedIndex, err := Sign(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) + + //verify (without vsa should fail) + resolver := &oci.OCILayoutResolver{ + Path: outputLayout, + Platform: "linux/amd64", + } + + // mocked vsa query should pass + policyOpts := &policy.PolicyOptions{ + LocalPolicyDir: PassPolicyDir, + } + results, err := Verify(ctx, policyOpts, resolver) + require.NoError(t, err) + assert.Equal(t, OutcomeSuccess, results.Outcome) + assert.Empty(t, results.Violations) + + assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type) + assert.Equal(t, attestation.VSAPredicateType, results.VSA.PredicateType) + assert.Len(t, results.VSA.Subject, 1) + + require.IsType(t, attestation.VSAPredicate{}, results.VSA.Predicate) + attestationPredicate := results.VSA.Predicate.(attestation.VSAPredicate) + + assert.Equal(t, "PASSED", attestationPredicate.VerificationResult) + assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID) + assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels) + assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI) +} + +func TestVerificationFailure(t *testing.T) { + ctx, signer := test.Setup(t) + ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true)) + // setup an image with signed attestations + outputLayout := test.CreateTempDir(t, "", TestTempDir) + + opts := &SigningOptions{ + Replace: true, + } + attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage) + assert.NoError(t, err) + signedIndex, err := Sign(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) + + //verify (without vsa should fail) + resolver := &oci.OCILayoutResolver{ + Path: outputLayout, + Platform: "linux/amd64", + } + + // mocked vsa query should pass + policyOpts := &policy.PolicyOptions{ + LocalPolicyDir: FailPolicyDir, + } + results, err := Verify(ctx, policyOpts, resolver) + require.NoError(t, err) + assert.Equal(t, OutcomeFailure, results.Outcome) + assert.Len(t, results.Violations, 1) + + violation := results.Violations[0] + assert.Equal(t, "missing_attestation", violation.Type) + assert.Equal(t, "Attestation missing for subject", violation.Description) + assert.Nil(t, violation.Attestation) + + assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type) + assert.Equal(t, attestation.VSAPredicateType, results.VSA.PredicateType) + assert.Len(t, results.VSA.Subject, 1) + + require.IsType(t, attestation.VSAPredicate{}, results.VSA.Predicate) + attestationPredicate := results.VSA.Predicate.(attestation.VSAPredicate) + + assert.Equal(t, "FAILED", attestationPredicate.VerificationResult) + assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID) + assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels) + assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI) +} diff --git a/pkg/attest/vsa.go b/pkg/attest/vsa.go deleted file mode 100644 index 015ea0a..0000000 --- a/pkg/attest/vsa.go +++ /dev/null @@ -1,96 +0,0 @@ -package attest - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/docker/attest/pkg/attestation" - "github.com/docker/attest/pkg/oci" - "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" - "github.com/secure-systems-lab/go-securesystemslib/dsse" -) - -// generateVSA generates a VSA from the attestation manifest -// TODO: remove signing logic and move generateVSA to attestation/vsa.go -func generateVSA(ctx context.Context, manifest attestation.AttestationManifest, signer dsse.SignerVerifier, opts *SigningOptions) (*mutate.Addendum, error) { - if len(manifest.Attestation.Layers) == 0 { - return nil, fmt.Errorf("no attestations found to generate VSA from") - } - sub := manifest.Attestation.Layers[0].Statement.Subject[0] - stype := manifest.Attestation.Layers[0].Statement.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(manifest.Attestation.Layers)) - for _, att := range manifest.Attestation.Layers { - mt, err := att.Layer.MediaType() - if err != nil { - return nil, fmt.Errorf("failed to get layer media type: %w", err) - } - if !strings.HasSuffix(string(mt), "+dsse") { - continue - } - dgst, err := att.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: manifest.Attestation.Layers[0].Statement.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, - } - return &withAnnotations, nil -} diff --git a/pkg/attestation/vsa.go b/pkg/attestation/vsa.go index 4e0c59d..7b90539 100644 --- a/pkg/attestation/vsa.go +++ b/pkg/attestation/vsa.go @@ -34,12 +34,6 @@ type VSAInputAttestation struct { 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) diff --git a/pkg/oci/oci.go b/pkg/oci/oci.go index 8183c2b..0d49308 100644 --- a/pkg/oci/oci.go +++ b/pkg/oci/oci.go @@ -14,6 +14,7 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/package-url/packageurl-go" "github.com/pkg/errors" @@ -71,7 +72,7 @@ func attestationManifestFromOCILayout(path string, platformStr string) (*Attesta } } for _, mf := range mfs2.Manifests { - if mf.Annotations[DockerReferenceType] != AttestationManifestType { + if mf.Annotations[att.DockerReferenceType] != AttestationManifestType { continue } @@ -338,8 +339,8 @@ func imageDigestForPlatform(ix *v1.IndexManifest, platform *v1.Platform) (string func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) { for _, m := range ix.Manifests { - if v, ok := m.Annotations["vnd.docker.reference.type"]; ok && v == attestType { - if d, ok := m.Annotations["vnd.docker.reference.digest"]; ok && d == imageDigest { + if v, ok := m.Annotations[att.DockerReferenceType]; ok && v == attestType { + if d, ok := m.Annotations[DockerReferenceDigest]; ok && d == imageDigest { return m.Digest.String(), nil } } @@ -393,3 +394,13 @@ func RefToPURL(ref string, platform string) (string, bool, error) { p := packageurl.NewPackageURL("docker", ns, name, version, qualifiers, "") return p.ToString(), isCanonical, nil } + +func SplitDigest(digest string) (*common.DigestSet, error) { + parts := strings.SplitN(digest, ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid digest %q", digest) + } + return &common.DigestSet{ + parts[0]: parts[1], + }, nil +} diff --git a/pkg/oci/types.go b/pkg/oci/types.go index 268ba1a..73d6e4b 100644 --- a/pkg/oci/types.go +++ b/pkg/oci/types.go @@ -12,7 +12,6 @@ import ( ) const ( - DockerReferenceType = "vnd.docker.reference.type" DockerReferenceDigest = "vnd.docker.reference.digest" AttestationManifestType = "attestation-manifest" InTotoPredicateType = "in-toto.io/predicate-type" diff --git a/pkg/policy/evaluator.go b/pkg/policy/evaluator.go index d512e56..2df6390 100644 --- a/pkg/policy/evaluator.go +++ b/pkg/policy/evaluator.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/docker/attest/pkg/oci" - "github.com/open-policy-agent/opa/rego" ) type policyEvaluatorCtxKeyType struct{} @@ -27,5 +26,5 @@ func GetPolicyEvaluator(ctx context.Context) (PolicyEvaluator, error) { } type PolicyEvaluator interface { - Evaluate(ctx context.Context, resolver oci.AttestationResolver, policy []*PolicyFile, input *PolicyInput) (*rego.ResultSet, error) + Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error) } diff --git a/pkg/policy/mock.go b/pkg/policy/mock.go index 0f4e891..9904815 100644 --- a/pkg/policy/mock.go +++ b/pkg/policy/mock.go @@ -4,37 +4,29 @@ import ( "context" "github.com/docker/attest/pkg/oci" - "github.com/open-policy-agent/opa/rego" ) type MockPolicyEvaluator struct { - EvaluateFunc func(ctx context.Context, resolver oci.AttestationResolver, policy []*PolicyFile, input *PolicyInput) (*rego.ResultSet, error) + EvaluateFunc func(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error) } -func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, policy []*PolicyFile, input *PolicyInput) (*rego.ResultSet, error) { +func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error) { if pe.EvaluateFunc != nil { - return pe.EvaluateFunc(ctx, resolver, policy, input) + return pe.EvaluateFunc(ctx, resolver, pctx, input) } return AllowedResult(), nil } func GetMockPolicy() PolicyEvaluator { return &MockPolicyEvaluator{ - EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pfs []*PolicyFile, input *PolicyInput) (*rego.ResultSet, error) { + EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error) { return AllowedResult(), nil }, } } -func AllowedResult() *rego.ResultSet { - return ®o.ResultSet{ - { - Bindings: rego.Vars{}, - Expressions: []*rego.ExpressionValue{ - { - Value: true, - }, - }, - }, +func AllowedResult() *Result { + return &Result{ + Success: true, } } diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 3086515..4fd5f3f 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -12,6 +12,7 @@ import ( "github.com/distribution/reference" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/tuf" + intoto "github.com/in-toto/in-toto-golang/in_toto" goyaml "gopkg.in/yaml.v3" ) @@ -20,6 +21,26 @@ const ( PolicyMappingFileName = "mapping.yaml" ) +type Summary struct { + Subjects []intoto.Subject `json:"subjects"` + SLSALevels []string `json:"slsa_levels"` + Verifier string `json:"verifier"` + PolicyURI string `json:"policy_uri"` +} + +type Violation struct { + Type string `json:"type"` + Description string `json:"description"` + Attestation *intoto.Statement `json:"attestation"` + Details map[string]any `json:"details"` +} + +type Result struct { + Success bool `json:"success"` + Violations []Violation `json:"violations"` + Summary Summary `json:"summary"` +} + type PolicyMappings struct { Version string `json:"version"` Kind string `json:"kind"` @@ -39,7 +60,7 @@ type PolicyMappingFile struct { } type PolicyMirror struct { - PolicyId string `json:"policy-id"` + PolicyId string `yaml:"policy-id"` Mirror MirrorSpec `json:"mirror"` } @@ -60,6 +81,11 @@ type PolicyOptions struct { LocalPolicyDir string } +type Policy struct { + InputFiles []*PolicyFile + Query string +} + type PolicyInput struct { Digest string `json:"digest"` Purl string `json:"purl"` @@ -71,7 +97,7 @@ type PolicyFile struct { Content []byte } -func resolveLocalPolicy(opts *PolicyOptions, mapping *PolicyMapping) ([]*PolicyFile, error) { +func resolveLocalPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, error) { if opts.LocalPolicyDir == "" { return nil, fmt.Errorf("local policy dir not set") } @@ -88,10 +114,13 @@ func resolveLocalPolicy(opts *PolicyOptions, mapping *PolicyMapping) ([]*PolicyF Content: fileContents, }) } - return files, nil + policy := &Policy{ + InputFiles: files, + } + return policy, nil } -func loadLocalMappings(opts *PolicyOptions) (*PolicyMappings, error) { +func LoadLocalMappings(opts *PolicyOptions) (*PolicyMappings, error) { if opts.LocalPolicyDir == "" { return nil, nil } @@ -108,7 +137,7 @@ func loadLocalMappings(opts *PolicyOptions) (*PolicyMappings, error) { return mappings, nil } -func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) ([]*PolicyFile, error) { +func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, error) { files := make([]*PolicyFile, 0, len(mapping.Files)) for _, f := range mapping.Files { filename := f.Path @@ -121,7 +150,10 @@ func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) ([]*PolicyFil Content: fileContents, }) } - return files, nil + policy := &Policy{ + InputFiles: files, + } + return policy, nil } func loadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) { @@ -163,7 +195,7 @@ func findPolicyMatch(named reference.Named, mappings *PolicyMappings) (*PolicyMa return nil, nil } -func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *PolicyOptions) ([]*PolicyFile, error) { +func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *PolicyOptions) (*Policy, error) { imageName, err := resolver.ImageName(ctx) if err != nil { return nil, fmt.Errorf("failed to get image name: %w", err) @@ -172,7 +204,7 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts * if err != nil { return nil, fmt.Errorf("failed to parse image name: %w", err) } - localMappings, err := loadLocalMappings(opts) + localMappings, err := LoadLocalMappings(opts) if err != nil { return nil, fmt.Errorf("failed to load local policy mappings: %w", err) } diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index aec853e..552b990 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -12,6 +12,7 @@ import ( "github.com/docker/attest/pkg/policy" "github.com/docker/attest/pkg/tuf" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func loadAttestation(t *testing.T, path string) *attestation.Envelope { @@ -32,61 +33,37 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { ctx, _ := test.Setup(t) TestDataPath := filepath.Join("..", "..", "test", "testdata") - MockTufRepo := filepath.Join(TestDataPath, "local-policy") ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json") - VSA := filepath.Join(TestDataPath, "vsa.json") re := policy.NewRegoEvaluator(true) - defaultInput := &policy.PolicyInput{ - Digest: "sha256:test-digest", - Purl: "test-purl", - IsCanonical: true, - } - defaultResolver := oci.MockResolver{ Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)}, } - vsaResolver := oci.MockResolver{ - Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation), loadAttestation(t, VSA)}, - } - testCases := []struct { repo string expectSuccess bool - input *policy.PolicyInput + isCanonical bool resolver oci.AttestationResolver policy *policy.PolicyOptions }{ - {repo: "testdata/mock-tuf-allow", expectSuccess: true, input: defaultInput, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-deny", expectSuccess: false, input: defaultInput, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, input: defaultInput, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-wrong-key", expectSuccess: false, input: defaultInput, resolver: defaultResolver}, - {repo: MockTufRepo, expectSuccess: true, input: &policy.PolicyInput{ - Digest: "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", - Purl: "pkg:docker/test-image?digest=sha256%da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620&platform=linux%2Famd64", - IsCanonical: true, - }, resolver: vsaResolver}, - {repo: MockTufRepo, expectSuccess: true, input: &policy.PolicyInput{ - Digest: "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", - Purl: "pkg:docker/test-image@test?platform=linux%2Famd64", - IsCanonical: false, - }, resolver: vsaResolver}, - // not a doi - {repo: MockTufRepo, expectSuccess: false, input: defaultInput, resolver: vsaResolver, policy: &policy.PolicyOptions{ - LocalPolicyDir: "testdata/mock-tuf-deny", - }}, - // digest mismatch - {repo: MockTufRepo, expectSuccess: false, input: &policy.PolicyInput{ - Digest: "sha256:test-digest-wrong", - Purl: "test-purl", - IsCanonical: false, - }, resolver: vsaResolver}, + {repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver}, + {repo: "testdata/mock-tuf-deny", expectSuccess: false, isCanonical: false, resolver: defaultResolver}, + {repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, isCanonical: false, resolver: defaultResolver}, + {repo: "testdata/mock-tuf-wrong-key", expectSuccess: false, isCanonical: false, resolver: defaultResolver}, + {repo: "testdata/mock-tuf-allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver}, + {repo: "testdata/mock-tuf-allow-canonical", expectSuccess: false, isCanonical: false, resolver: defaultResolver}, } for _, tc := range testCases { t.Run(tc.repo, func(t *testing.T) { + input := &policy.PolicyInput{ + Digest: "sha256:test-digest", + Purl: "test-purl", + IsCanonical: tc.isCanonical, + } + tufClient := tuf.NewMockTufClient(tc.repo, test.CreateTempDir(t, "", "tuf-dest")) if tc.policy == nil { tc.policy = &policy.PolicyOptions{ @@ -95,17 +72,29 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { } } - policyFiles, err := policy.ResolvePolicy(ctx, tc.resolver, tc.policy) + policy, err := policy.ResolvePolicy(ctx, tc.resolver, tc.policy) assert.NoErrorf(t, err, "failed to resolve policy") - rs, err := re.Evaluate(ctx, tc.resolver, policyFiles, tc.input) + result, err := re.Evaluate(ctx, tc.resolver, policy, input) + require.NoErrorf(t, err, "Evaluate failed") if tc.expectSuccess { - assert.NoErrorf(t, err, "Evaluate failed") - assert.True(t, rs.Allowed(), "Evaluate should have succeeded") + assert.True(t, result.Success, "Evaluate should have succeeded") } else { - assert.False(t, rs.Allowed(), "Evaluate should have failed") + assert.False(t, result.Success, "Evaluate should have failed") } }) } } + +func TestLoadingMappings(t *testing.T) { + opts := &policy.PolicyOptions{ + LocalPolicyDir: filepath.Join("testdata", "mock-tuf-allow"), + } + policyMappings, err := policy.LoadLocalMappings(opts) + require.NoError(t, err) + assert.Equal(t, len(policyMappings.Mirrors), 1) + for _, mirror := range policyMappings.Mirrors { + assert.Equal(t, "docker-official-images", mirror.PolicyId) + } +} diff --git a/pkg/policy/rego.go b/pkg/policy/rego.go index c6851b0..c9ff39c 100644 --- a/pkg/policy/rego.go +++ b/pkg/policy/rego.go @@ -23,17 +23,20 @@ import ( type regoEvaluator struct { debug bool - query string } +const ( + DefaultQuery = "result := data.attest.result" + resultBinding = "result" +) + func NewRegoEvaluator(debug bool) PolicyEvaluator { return ®oEvaluator{ debug: debug, - query: "data.attest.allow", } } -func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, files []*PolicyFile, input *PolicyInput) (*rego.ResultSet, error) { +func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error) { var regoOpts []func(*rego.Rego) // Create a new in-memory store @@ -45,7 +48,7 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR return nil, err } - for _, target := range files { + for _, target := range pctx.InputFiles { // load yaml as data (no rego opt for this!?) if filepath.Ext(target.Path) == ".yaml" { yamlData, err := loadYAML(target.Path, target.Content) @@ -74,12 +77,15 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR rego.Dump(os.Stderr), ) } - + query := DefaultQuery + if pctx.Query != "" { + query = pctx.Query + } regoOpts = append(regoOpts, - rego.Query(re.query), - rego.StrictBuiltinErrors(true), + rego.Query(query), rego.Input(input), rego.Store(store), + rego.GenerateJSON(jsonGenerator[Result]()), ) for _, custom := range RegoFunctions(resolver) { regoOpts = append(regoOpts, custom.Func) @@ -87,11 +93,50 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR r := rego.New(regoOpts...) rs, err := r.Eval(ctx) - return &rs, err + if err != nil { + return nil, err + } + + if len(rs) == 0 { + return nil, fmt.Errorf("no policy evaluation result") + } + binding, ok := rs[0].Bindings[resultBinding] + if !ok { + return nil, fmt.Errorf("failed to extract verification result") + } + result, ok := binding.(Result) + if !ok { + return nil, fmt.Errorf("failed to extract verification result") + } + + return &result, nil +} + +func jsonGenerator[T any]() func(t *ast.Term, ec *rego.EvalContext) (any, error) { + return func(t *ast.Term, ec *rego.EvalContext) (any, error) { + // TODO: this is horrible - we're converting the AST to JSON and then back to AST, then using ast.As to convert it to a struct + // We can't use ast.As directly because it fails if the AST contains a set + json, err := ast.JSON(t.Value) + if err != nil { + return nil, err + } + v, err := ast.InterfaceToValue(json) + if err != nil { + return nil, err + } + var result T + err = ast.As(v, &result) + if err != nil { + return nil, err + } + return result, nil + } } var dynamicObj = types.NewObject(nil, types.NewDynamicProperty(types.S, types.A)) var arrayObj = types.NewArray(nil, dynamicObj) +var setObj = types.NewSet(dynamicObj) + var verifyDecl = &ast.Builtin{ Name: "attestations.verify_envelope", Decl: types.NewFunction(types.Args(dynamicObj, arrayObj), dynamicObj), @@ -99,7 +144,7 @@ var verifyDecl = &ast.Builtin{ } var attestDecl = &ast.Builtin{ Name: "attestations.attestation", - Decl: types.NewFunction(types.Args(types.S), dynamicObj), + Decl: types.NewFunction(types.Args(types.S), setObj), Nondeterministic: true, } @@ -153,12 +198,13 @@ func fetchIntotoAttestations(resolver oci.AttestationResolver) func(rego.Builtin values[i] = ast.NewTerm(value) } - // Wrap the values in an ast.Array and convert it to an ast.Term. - array := ast.NewTerm(ast.NewArray(values...)) + // Wrap the values in an ast.Set and convert it to an ast.Term. + set := ast.NewTerm(ast.NewSet(values...)) - return array, nil + return set, nil } } + func verifyIntotoEnvelope(rCtx rego.BuiltinContext, envTerm, keysTerm *ast.Term) (*ast.Term, error) { env := new(att.Envelope) var keys att.Keys diff --git a/pkg/policy/testdata/mock-tuf-allow-canonical/doi/policy.rego b/pkg/policy/testdata/mock-tuf-allow-canonical/doi/policy.rego new file mode 100644 index 0000000..aa197a2 --- /dev/null +++ b/pkg/policy/testdata/mock-tuf-allow-canonical/doi/policy.rego @@ -0,0 +1,7 @@ +package attest + +import rego.v1 + +result := { + "success": input.isCanonical, +} diff --git a/pkg/policy/testdata/mock-tuf-allow-canonical/mapping.yaml b/pkg/policy/testdata/mock-tuf-allow-canonical/mapping.yaml new file mode 100644 index 0000000..c324a86 --- /dev/null +++ b/pkg/policy/testdata/mock-tuf-allow-canonical/mapping.yaml @@ -0,0 +1,16 @@ +# map repos to policies +version: v1 +kind: policy-mapping +policies: + - origin: + domain: docker.io + prefix: library/ + id: docker-official-images + description: Docker Official Images + files: + - path: doi/policy.rego +mirrors: + - policy-id: docker-official-images + mirror: + domains: [localhost:5001, registry.local:5000] + prefix: "" diff --git a/pkg/policy/testdata/mock-tuf-allow/doi/policy.rego b/pkg/policy/testdata/mock-tuf-allow/doi/policy.rego index b1b188a..b1eda84 100644 --- a/pkg/policy/testdata/mock-tuf-allow/doi/policy.rego +++ b/pkg/policy/testdata/mock-tuf-allow/doi/policy.rego @@ -2,4 +2,6 @@ package attest import rego.v1 -allow := true +result := { + "success": true, +} diff --git a/pkg/policy/testdata/mock-tuf-allow/mapping.yaml b/pkg/policy/testdata/mock-tuf-allow/mapping.yaml index 1aa09d7..c324a86 100644 --- a/pkg/policy/testdata/mock-tuf-allow/mapping.yaml +++ b/pkg/policy/testdata/mock-tuf-allow/mapping.yaml @@ -9,3 +9,8 @@ policies: description: Docker Official Images files: - path: doi/policy.rego +mirrors: + - policy-id: docker-official-images + mirror: + domains: [localhost:5001, registry.local:5000] + prefix: "" diff --git a/pkg/policy/testdata/mock-tuf-deny/doi/policy.rego b/pkg/policy/testdata/mock-tuf-deny/doi/policy.rego index 5e4ac7e..45ba4fa 100644 --- a/pkg/policy/testdata/mock-tuf-deny/doi/policy.rego +++ b/pkg/policy/testdata/mock-tuf-deny/doi/policy.rego @@ -2,4 +2,6 @@ package attest import rego.v1 -allow := false +result := { + "success": false, +} diff --git a/pkg/policy/testdata/mock-tuf-verify-sig/doi/policy.rego b/pkg/policy/testdata/mock-tuf-verify-sig/doi/policy.rego index 8952fb1..6e5f504 100644 --- a/pkg/policy/testdata/mock-tuf-verify-sig/doi/policy.rego +++ b/pkg/policy/testdata/mock-tuf-verify-sig/doi/policy.rego @@ -3,13 +3,17 @@ package attest import rego.v1 keys := [{ - "id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4", - "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----", - "from": "2023-12-15T14:00:00Z", - "to": null + "id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4", + "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----", + "from": "2023-12-15T14:00:00Z", + "to": null }] -allow if { +success if { some env in attestations.attestation("foo") statement := attestations.verify_envelope(env, keys) } + +result := { + "success": success +} diff --git a/pkg/policy/testdata/mock-tuf-wrong-key/doi/policy.rego b/pkg/policy/testdata/mock-tuf-wrong-key/doi/policy.rego index aaa78d7..835ac7c 100644 --- a/pkg/policy/testdata/mock-tuf-wrong-key/doi/policy.rego +++ b/pkg/policy/testdata/mock-tuf-wrong-key/doi/policy.rego @@ -2,18 +2,20 @@ package attest import rego.v1 -keys := { - "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4": { - "id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4", - "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHyZpSgzvqFqNv7f3x7865OS38rAb\nQMcff55zM2UH/KR3Pr84a8QsGDNgaNGzJQJWjtMSgfV8WnNoffNK+svFNg==\n-----END PUBLIC KEY-----", - "from": "2023-12-15T14:00:00Z", - "to": null, - } -} +keys := [{ + "id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4", + "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHyZpSgzvqFqNv7f3x7865OS38rAb\nQMcff55zM2UH/KR3Pr84a8QsGDNgaNGzJQJWjtMSgfV8WnNoffNK+svFNg==\n-----END PUBLIC KEY-----", + "from": "2023-12-15T14:00:00Z", + "to": null, +}] -allow if { +default success := false + +success if { some env in attestations.attestation("foo") statement := attestations.verify_envelope(env, keys) } -allow := true +result := { + "success": success +} diff --git a/pkg/signerverifier/common.go b/pkg/signerverifier/common.go index 75e9212..f15034c 100644 --- a/pkg/signerverifier/common.go +++ b/pkg/signerverifier/common.go @@ -6,6 +6,8 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/x509" + "encoding/pem" "fmt" "github.com/docker/attest/internal/util" @@ -45,6 +47,32 @@ func (s *ECDSA256_SignerVerifier) Verify(ctx context.Context, data []byte, sig [ return nil } +func LoadKeyPair(priv []byte) (dsse.SignerVerifier, error) { + privateKey, err := parsePriv(priv) + if err != nil { + return nil, err + } + return &ECDSA256_SignerVerifier{ + Signer: privateKey, + }, nil +} + +func parsePriv(privkeyBytes []byte) (*ecdsa.PrivateKey, error) { + p, _ := pem.Decode(privkeyBytes) + if p == nil { + return nil, fmt.Errorf("privkey file does not contain any PEM data") + } + if p.Type != "EC PRIVATE KEY" { + return nil, fmt.Errorf("privkey file does not contain a priavte key") + } + privKey, err := x509.ParseECPrivateKey(p.Bytes) + if err != nil { + return nil, fmt.Errorf("error failed to parse public key: %w", err) + } + + return privKey, nil +} + func GenKeyPair() (dsse.SignerVerifier, error) { signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { diff --git a/test/testdata/local-policy-fail/doi/policy.rego b/test/testdata/local-policy-fail/doi/policy.rego new file mode 100644 index 0000000..828602b --- /dev/null +++ b/test/testdata/local-policy-fail/doi/policy.rego @@ -0,0 +1,48 @@ +package attest + +import rego.v1 + +keys := [{ + "id": "6b241993defaba26558c64f94a94303ce860e7ad9163d801495c91cf57197c75", + "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZmicqYSY38DprGr42jU0V3ND0ROj\nzSRH1+yjsxhh0bi52Hh/DuOhrSq2KJ5a09lW3ybnDjljowbkof0Y1i9Oow==\n-----END PUBLIC KEY-----", + "from": "2023-12-15T14:00:00Z", + "to": null, + # this key is still active + "status": "active", + "signing-format": "dssev1", +}] + +atts := union({ + attestations.attestation("https://slsa.dev/provenance/v0.2"), + attestations.attestation("https://spdx.dev/Document"), +}) + +statements contains s if { + some att in atts + s := attestations.verify_envelope(att, keys) +} + +subjects contains subject if { + some statement in statements + some subject in statement.subject +} + +violations contains v if { + v := { + "type": "missing_attestation", + "description": "Attestation missing for subject", + "attestation": null, + "details": {}, + } +} + +result := { + "success": false, + "violations": violations, + "summary": { + "subjects": subjects, + "slsa_levels": ["SLSA_BUILD_LEVEL_3"], + "verifier": "docker-official-images", + "policy_uri": "https://docker.com/official/policy/v0.1", + }, +} diff --git a/test/testdata/local-policy/mapping.yaml b/test/testdata/local-policy-fail/mapping.yaml similarity index 63% rename from test/testdata/local-policy/mapping.yaml rename to test/testdata/local-policy-fail/mapping.yaml index 1e2d18f..f729528 100644 --- a/test/testdata/local-policy/mapping.yaml +++ b/test/testdata/local-policy-fail/mapping.yaml @@ -8,11 +8,4 @@ policies: id: test-images description: Local test images files: - - path: doi/data.yaml - path: doi/policy.rego - -mirrors: - - policy-id: test-images - mirror: - domains: [localhost:5001] - prefix: "" diff --git a/test/testdata/local-policy-pass/doi/policy.rego b/test/testdata/local-policy-pass/doi/policy.rego new file mode 100644 index 0000000..032e3e4 --- /dev/null +++ b/test/testdata/local-policy-pass/doi/policy.rego @@ -0,0 +1,39 @@ +package attest + +import rego.v1 + +keys := [{ + "id": "6b241993defaba26558c64f94a94303ce860e7ad9163d801495c91cf57197c75", + "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZmicqYSY38DprGr42jU0V3ND0ROj\nzSRH1+yjsxhh0bi52Hh/DuOhrSq2KJ5a09lW3ybnDjljowbkof0Y1i9Oow==\n-----END PUBLIC KEY-----", + "from": "2023-12-15T14:00:00Z", + "to": null, + # this key is still active + "status": "active", + "signing-format": "dssev1", +}] + +atts := union({ + attestations.attestation("https://slsa.dev/provenance/v0.2"), + attestations.attestation("https://spdx.dev/Document"), +}) + +statements contains s if { + some att in atts + s := attestations.verify_envelope(att, keys) +} + +subjects contains subject if { + some statement in statements + some subject in statement.subject +} + +result := { + "success": true, + "violations": set(), + "summary": { + "subjects": subjects, + "slsa_levels": ["SLSA_BUILD_LEVEL_3"], + "verifier": "docker-official-images", + "policy_uri": "https://docker.com/official/policy/v0.1", + }, +} diff --git a/test/testdata/local-policy-pass/mapping.yaml b/test/testdata/local-policy-pass/mapping.yaml new file mode 100644 index 0000000..f729528 --- /dev/null +++ b/test/testdata/local-policy-pass/mapping.yaml @@ -0,0 +1,11 @@ +# map repos to policies +version: v1 +kind: policy-mapping +policies: + - origin: + domain: docker.io + prefix: library/ + id: test-images + description: Local test images + files: + - path: doi/policy.rego diff --git a/test/testdata/local-policy/doi/data.yaml b/test/testdata/local-policy/doi/data.yaml deleted file mode 100644 index f4c0bdd..0000000 --- a/test/testdata/local-policy/doi/data.yaml +++ /dev/null @@ -1,58 +0,0 @@ -config: - doi: - keys: - - id: "f6a29392b1c08891ff456100aa448b4f6bf9c315850e11cc0883fe9c3c4412db" - key: | - -----BEGIN PUBLIC KEY----- - MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+XOm2uWjLJhpsJtHCFdGic26suOy - mCl2pBgCof+AHGFZFca40JL833OT+nRSZJRMPKBGibWqsjFrLdRCkOB7bA== - -----END PUBLIC KEY----- - from: "2024-01-01T00:00:00Z" - to: "2024-01-15T12:00:00Z" - # this key was rotated at a planned time - status: "rotated" - signing-format: "dssev1" - - id: "e6f4c70fbba21cbcac44915fff53fd2fdf90dd8849445795fe58014c2b5f8c64" - key: | - -----BEGIN PUBLIC KEY----- - MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZSkTE3si/JkRbuLjaYraS3//YBnX - 8KtEcgdYKZQPl2DnSl4gPsu3KiVeEBWp5GK06IoZlcBAL3NF0OsUUP+yVg== - -----END PUBLIC KEY----- - from: "2024-01-15T12:00:00Z" - to: "2024-01-15T14:00:00Z" - # this key was leaked at a known time, so it revoked from that time - # this behaves the same way as "rotated" but might give another failure message - status: "revoked" - signing-format: "dssev1" - - id: "d45980c5cf39a5e1bab9febe3f16c1c0820b97a8fd061b0064e54b0826e856e4" - key: | - -----BEGIN PUBLIC KEY----- - MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEafssq2x1EDQcKDZhuSrCOxWWl5D4 - JBa9iDJYDnLZp9kPKvv4RnD4rz7Ucfmd0l/zzM45qT29fSBTlguKmnOA8A== - -----END PUBLIC KEY----- - # this key was leaked at an unknown time, so it's completely distrusted - distrust: true - status: "revoked" - signing-format: "dssev1" - - id: "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4" - key: | - -----BEGIN PUBLIC KEY----- - MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN - 9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw== - -----END PUBLIC KEY----- - from: "2023-12-15T14:00:00Z" - to: null - # this key is still active - status: "active" - signing-format: "dssev1" - - id: "b281835e00059de24fb06bd6db06eb0e4a33d7bd7210d7027c209f14b19e812a" - key: | - -----BEGIN PUBLIC KEY----- - MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgE4Jz6FrLc3lp/YRlbuwOjK4n6ac - jVkSDAmFhi3Ir2Jy+cKeEB7iRPcLvBy9qoMZ9E93m1NdWY6KtDo+Qi52Rg== - -----END PUBLIC KEY----- - from: "2024-01-15T14:00:00Z" - to: null - # this key is still active - status: "active" - signing-format: "dssev1" diff --git a/test/testdata/local-policy/doi/policy.rego b/test/testdata/local-policy/doi/policy.rego deleted file mode 100644 index 04a9603..0000000 --- a/test/testdata/local-policy/doi/policy.rego +++ /dev/null @@ -1,49 +0,0 @@ -package attest - -import rego.v1 - -import data.config - -splitDigest := split(input.digest, ":") - -digestType := splitDigest[0] - -digest := splitDigest[1] - -allow if { - some env in attestations.attestation("https://slsa.dev/verification_summary/v0.1") - some statement in verified_statements(config.doi.keys, env) -} - - -verified_statements(keys, env) := statements if { - statements := {statement | - statement := attestations.verify_envelope(env, keys) - some subject in statement.subject - valid_subject(subject) - } -} - - -valid_subject(sub) if { - print("valid_subject") - print("sub.digest[digestType]:", sub.digest[digestType]) - print("digest", digest) - sub.digest[digestType] == digest - print("digest matches") - valid_subject_name(sub.name) -} - -valid_subject_name(name) if { - input.isCanonical - print("is canonical, ignoring name") -} - -valid_subject_name(name) if { - not input.isCanonical - print("valid_subject_name...") - print("name:", name) - print("input.purl:", input.purl) - name == input.purl - print("name match") -} diff --git a/test/testdata/local-policy/doi/policy_test.rego b/test/testdata/local-policy/doi/policy_test.rego deleted file mode 100644 index e2bff5c..0000000 --- a/test/testdata/local-policy/doi/policy_test.rego +++ /dev/null @@ -1,25 +0,0 @@ -package attest -import rego.v1 - -config := {"keys": []} -envs := [{"env": "test"}] -purl := "pkg:docker/library/alpine:1.2.3" - -statement := {"subject": [{"name": purl, "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}}]} -input_digest := "sha256:dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0" - -test_with_mock_data if { - allow with attestations.attestation as envs - with attestations.verify_envelope as statement - with input.digest as input_digest - with input.purl as purl - with input.canonical as false -} - -layout_digest := "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620" -outout_purl := "pkg:docker/test-image@test?platform=linux%2Famd64" -test_with_signed_oci_layout if { - allow with input.digest as layout_digest - with input.purl as outout_purl - with input.canonical as false -} diff --git a/test/testdata/test-signing-key.pem b/test/testdata/test-signing-key.pem new file mode 100644 index 0000000..1d2c91e --- /dev/null +++ b/test/testdata/test-signing-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKZEqmmd++eAY3bmPoBdY6nC2wLy4da2yeVZNKCp6Oj2oAoGCCqGSM49 +AwEHoUQDQgAEZmicqYSY38DprGr42jU0V3ND0ROjzSRH1+yjsxhh0bi52Hh/DuOh +rSq2KJ5a09lW3ybnDjljowbkof0Y1i9Oow== +-----END EC PRIVATE KEY-----