From 90393ea6fd6e1d01618415812f1e49283edb9084 Mon Sep 17 00:00:00 2001 From: James Carnegie Date: Tue, 30 Apr 2024 15:32:52 +0100 Subject: [PATCH] Return results from rego evaluation (#14) --- internal/test/test.go | 31 ++++++++++-- pkg/attest/sign.go | 100 ++++--------------------------------- pkg/attest/verify.go | 5 +- pkg/attest/verify_test.go | 8 +-- pkg/attest/vsa.go | 102 ++++++++++++++++++++++++++++++++++++++ pkg/attestation/types.go | 6 +-- pkg/policy/evaluator.go | 14 +----- pkg/policy/policy_test.go | 4 +- pkg/policy/rego.go | 24 ++++----- 9 files changed, 164 insertions(+), 130 deletions(-) create mode 100644 pkg/attest/vsa.go diff --git a/internal/test/test.go b/internal/test/test.go index 695dd1c..220d18b 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -18,6 +18,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/partial" intoto "github.com/in-toto/in-toto-golang/in_toto" + "github.com/open-policy-agent/opa/rego" "github.com/secure-systems-lab/go-securesystemslib/dsse" ) @@ -86,10 +87,34 @@ func GetMockSigner(ctx context.Context) (dsse.SignerVerifier, error) { return signerverifier.GenKeyPair() } +type MockPolicyEvaluator struct { + EvaluateFunc func(ctx context.Context, resolver oci.AttestationResolver, policy []*policy.PolicyFile, input *policy.PolicyInput) (*rego.ResultSet, error) +} + +func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, policy []*policy.PolicyFile, input *policy.PolicyInput) (*rego.ResultSet, error) { + if pe.EvaluateFunc != nil { + return pe.EvaluateFunc(ctx, resolver, policy, input) + } + return AllowedResult(), nil +} + func GetMockPolicy() policy.PolicyEvaluator { - return &policy.MockPolicyEvaluator{ - EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, policy []*policy.PolicyFile, input *policy.PolicyInput) error { - return nil + return &MockPolicyEvaluator{ + EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pfs []*policy.PolicyFile, input *policy.PolicyInput) (*rego.ResultSet, error) { + return AllowedResult(), nil + }, + } +} + +func AllowedResult() *rego.ResultSet { + return ®o.ResultSet{ + { + Bindings: rego.Vars{}, + Expressions: []*rego.ExpressionValue{ + { + Value: true, + }, + }, }, } } diff --git a/pkg/attest/sign.go b/pkg/attest/sign.go index 7641227..ec1c9ca 100644 --- a/pkg/attest/sign.go +++ b/pkg/attest/sign.go @@ -4,8 +4,6 @@ import ( "context" "encoding/json" "fmt" - "strings" - "time" "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/oci" @@ -142,9 +140,16 @@ func SignIndexAttestations(ctx context.Context, idx v1.ImageIndex, signer dsse.S } if opts.VSAOptions != nil { - newImg, err = addVSA(ctx, newImg, statements, manifest.MediaType, signer, opts) + newLayer, err := generateVSA(ctx, newImg, statements, signer, opts) if err != nil { - return nil, fmt.Errorf("failed to add VSA: %w", err) + return nil, fmt.Errorf("failed to generate VSA: %w", err) + } + vsaReplace := &SigningOptions{ + Replace: false, + } + newImg, err = addSignedLayers([]mutate.Addendum{*newLayer}, layers, manifest.MediaType, newImg, vsaReplace) + if err != nil { + return nil, fmt.Errorf("failed to add VSA layer: %w", err) } } newDesc, err := partial.Descriptor(newImg) @@ -171,93 +176,6 @@ func SignIndexAttestations(ctx context.Context, idx v1.ImageIndex, signer dsse.S return newIndex, nil } -func addVSA(ctx context.Context, image v1.Image, stmt []*intoto.Statement, outerMediaType types.MediaType, signer dsse.SignerVerifier, opts *SigningOptions) (v1.Image, error) { - if len(stmt) == 0 { - return nil, fmt.Errorf("no attestations found to generate VSA from") - } - sub := stmt[0].Subject[0] - stype := stmt[0].Type - - uri, err := attestation.ToVSAResourceURI(sub) - if err != nil { - return nil, fmt.Errorf("failed to generate VSA resource URI: %w", err) - } - - inputs := make([]attestation.VSAInputAttestation, 0, len(stmt)) - layers, err := image.Layers() - if err != nil { - return nil, fmt.Errorf("failed to get layers: %w", err) - } - for _, layer := range layers { - mt, err := layer.MediaType() - if err != nil { - return nil, fmt.Errorf("failed to get layer media type: %w", err) - } - mediaType := string(mt) - if !strings.HasPrefix(mediaType, "application/vnd.in-toto.") || - !strings.HasSuffix(mediaType, "+dsse") { - continue - } - - dgst, err := layer.Digest() - if err != nil { - return nil, fmt.Errorf("failed to get layer digest: %w", err) - } - inputs = append(inputs, attestation.VSAInputAttestation{ - Digest: map[string]string{"sha256": dgst.Hex}, - MediaType: string(mt), - }) - } - vsaStatement := &intoto.Statement{ - StatementHeader: intoto.StatementHeader{ - PredicateType: attestation.VSAPredicateType, - Type: stype, - Subject: stmt[0].Subject, - }, - Predicate: attestation.VSAPredicate{ - Verifier: attestation.VSAVerifier{ - ID: opts.VSAOptions.VerifierID, - }, - TimeVerified: time.Now().UTC().Format(time.RFC3339), - ResourceUri: uri, - Policy: attestation.VSAPolicy{URI: opts.VSAOptions.PolicyURI}, - VerificationResult: "PASSED", - VerifiedLevels: []string{opts.VSAOptions.BuildLevel}, - InputAttestations: inputs, - }, - } - payload, err := json.Marshal(vsaStatement) - if err != nil { - return nil, fmt.Errorf("failed to marshal statement: %w", err) - } - env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer) - if err != nil { - return nil, fmt.Errorf("failed to sign statement: %w", err) - } - mediaType, err := attestation.DSSEMediaType(vsaStatement.PredicateType) - if err != nil { - return nil, fmt.Errorf("failed to get DSSE media type: %w", err) - } - - data, err := json.Marshal(env) - if err != nil { - return nil, fmt.Errorf("failed to marshal envelope: %w", err) - } - mt := types.MediaType(mediaType) - newLayer := static.NewLayer(data, mt) - ann := make(map[string]string) - ann[InTotoReferenceLifecycleStage] = LifecycleStageExperimental - ann[oci.InTotoPredicateType] = attestation.VSAPredicateType - withAnnotations := mutate.Addendum{ - Layer: newLayer, - Annotations: ann, - } - opts = &SigningOptions{ - Replace: false, - } - return addSignedLayers([]mutate.Addendum{withAnnotations}, layers, outerMediaType, image, opts) -} - func addSignedLayers(signedLayers []mutate.Addendum, originalLayers []v1.Layer, mediaType types.MediaType, attestationImage v1.Image, opts *SigningOptions) (v1.Image, error) { var err error if opts.Replace { diff --git a/pkg/attest/verify.go b/pkg/attest/verify.go index 52f05c6..0a963d1 100644 --- a/pkg/attest/verify.go +++ b/pkg/attest/verify.go @@ -31,10 +31,13 @@ func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, f if err != nil { return err } - err = evaluator.Evaluate(ctx, resolver, files, input) + rs, err := evaluator.Evaluate(ctx, resolver, files, input) if err != nil { return fmt.Errorf("policy evaluation failed: %w", err) } + if !rs.Allowed() { + return fmt.Errorf("policy evaluation failed: %s", fmt.Sprint(rs)) + } return nil } diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index 42ea7b7..19e55ee 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -8,9 +8,11 @@ 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" "github.com/stretchr/testify/assert" ) @@ -41,9 +43,9 @@ func TestVerifyAttestations(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - mockPE := policy.MockPolicyEvaluator{ - EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, policy []*policy.PolicyFile, input *policy.PolicyInput) error { - return tc.policyEvaluationError + mockPE := test.MockPolicyEvaluator{ + EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pfs []*policy.PolicyFile, input *policy.PolicyInput) (*rego.ResultSet, error) { + return test.AllowedResult(), tc.policyEvaluationError }, } diff --git a/pkg/attest/vsa.go b/pkg/attest/vsa.go new file mode 100644 index 0000000..8343002 --- /dev/null +++ b/pkg/attest/vsa.go @@ -0,0 +1,102 @@ +package attest + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/docker/attest/pkg/attestation" + "github.com/docker/attest/pkg/oci" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/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" +) + +func generateVSA(ctx context.Context, image v1.Image, stmt []*intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*mutate.Addendum, error) { + if len(stmt) == 0 { + return nil, fmt.Errorf("no attestations found to generate VSA from") + } + sub := stmt[0].Subject[0] + stype := stmt[0].Type + + uri, err := attestation.ToVSAResourceURI(sub) + if err != nil { + return nil, fmt.Errorf("failed to generate VSA resource URI: %w", err) + } + + inputs := make([]attestation.VSAInputAttestation, 0, len(stmt)) + layers, err := image.Layers() + if err != nil { + return nil, fmt.Errorf("failed to get layers: %w", err) + } + for _, layer := range layers { + mt, err := layer.MediaType() + if err != nil { + return nil, fmt.Errorf("failed to get layer media type: %w", err) + } + mediaType := string(mt) + if !strings.HasPrefix(mediaType, "application/vnd.in-toto.") || + !strings.HasSuffix(mediaType, "+dsse") { + continue + } + + dgst, err := layer.Digest() + if err != nil { + return nil, fmt.Errorf("failed to get layer digest: %w", err) + } + inputs = append(inputs, attestation.VSAInputAttestation{ + Digest: map[string]string{"sha256": dgst.Hex}, + MediaType: string(mt), + }) + } + vsaStatement := &intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + PredicateType: attestation.VSAPredicateType, + Type: stype, + Subject: stmt[0].Subject, + }, + Predicate: attestation.VSAPredicate{ + Verifier: attestation.VSAVerifier{ + ID: opts.VSAOptions.VerifierID, + }, + TimeVerified: time.Now().UTC().Format(time.RFC3339), + ResourceUri: uri, + Policy: attestation.VSAPolicy{URI: opts.VSAOptions.PolicyURI}, + VerificationResult: "PASSED", + VerifiedLevels: []string{opts.VSAOptions.BuildLevel}, + InputAttestations: inputs, + }, + } + payload, err := json.Marshal(vsaStatement) + if err != nil { + return nil, fmt.Errorf("failed to marshal statement: %w", err) + } + env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer) + if err != nil { + return nil, fmt.Errorf("failed to sign statement: %w", err) + } + mediaType, err := attestation.DSSEMediaType(vsaStatement.PredicateType) + if err != nil { + return nil, fmt.Errorf("failed to get DSSE media type: %w", err) + } + + data, err := json.Marshal(env) + if err != nil { + return nil, fmt.Errorf("failed to marshal envelope: %w", err) + } + mt := types.MediaType(mediaType) + newLayer := static.NewLayer(data, mt) + ann := make(map[string]string) + ann[InTotoReferenceLifecycleStage] = LifecycleStageExperimental + ann[oci.InTotoPredicateType] = attestation.VSAPredicateType + withAnnotations := mutate.Addendum{ + Layer: newLayer, + Annotations: ann, + } + return &withAnnotations, nil +} diff --git a/pkg/attestation/types.go b/pkg/attestation/types.go index 609ff7e..54068b5 100644 --- a/pkg/attestation/types.go +++ b/pkg/attestation/types.go @@ -10,9 +10,9 @@ import ( ) const ( - DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json" - RekorTlExtKind = "Rekor" - OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse" + DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json" + RekorTlExtKind = "Rekor" + OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse" ) var base64Encoding = base64.StdEncoding.Strict() diff --git a/pkg/policy/evaluator.go b/pkg/policy/evaluator.go index 42cce24..d512e56 100644 --- a/pkg/policy/evaluator.go +++ b/pkg/policy/evaluator.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/docker/attest/pkg/oci" + "github.com/open-policy-agent/opa/rego" ) type policyEvaluatorCtxKeyType struct{} @@ -26,16 +27,5 @@ func GetPolicyEvaluator(ctx context.Context) (PolicyEvaluator, error) { } type PolicyEvaluator interface { - Evaluate(ctx context.Context, resolver oci.AttestationResolver, policy []*PolicyFile, input *PolicyInput) error -} - -type MockPolicyEvaluator struct { - EvaluateFunc func(ctx context.Context, resolver oci.AttestationResolver, policy []*PolicyFile, input *PolicyInput) error -} - -func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, policy []*PolicyFile, input *PolicyInput) error { - if pe.EvaluateFunc != nil { - return pe.EvaluateFunc(ctx, resolver, policy, input) - } - return nil + Evaluate(ctx context.Context, resolver oci.AttestationResolver, policy []*PolicyFile, input *PolicyInput) (*rego.ResultSet, error) } diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index 9e404b8..3740d36 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -97,12 +97,12 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { policyFiles, err := policy.ResolvePolicy(ctx, tc.resolver, tc.policy) assert.NoErrorf(t, err, "failed to resolve policy") - err = re.Evaluate(ctx, tc.resolver, policyFiles, tc.input) + rs, err := re.Evaluate(ctx, tc.resolver, policyFiles, tc.input) if tc.expectSuccess { assert.NoErrorf(t, err, "Evaluate failed") } else { - assert.Errorf(t, err, "Evaluate should have failed") + assert.False(t, rs.Allowed(), "Evaluate should have failed") } }) } diff --git a/pkg/policy/rego.go b/pkg/policy/rego.go index fc9141a..48ca0f3 100644 --- a/pkg/policy/rego.go +++ b/pkg/policy/rego.go @@ -23,15 +23,17 @@ import ( type regoEvaluator struct { debug bool + query string } func NewRegoEvaluator(debug bool) PolicyEvaluator { return ®oEvaluator{ debug: debug, + query: "data.attestations.allow", } } -func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, files []*PolicyFile, input *PolicyInput) error { +func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, files []*PolicyFile, input *PolicyInput) (*rego.ResultSet, error) { var regoOpts []func(*rego.Rego) // Create a new in-memory store @@ -40,7 +42,7 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR params.Write = true txn, err := store.NewTransaction(ctx, params) if err != nil { - return err + return nil, err } for _, target := range files { @@ -48,11 +50,11 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR if filepath.Ext(target.Path) == ".yaml" { yamlData, err := loadYAML(target.Path, target.Content) if err != nil { - return err + return nil, err } err = store.Write(ctx, txn, storage.AddOp, storage.Path{}, yamlData) if err != nil { - return err + return nil, err } } else { regoOpts = append(regoOpts, rego.Module(target.Path, string(target.Content))) @@ -62,7 +64,7 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR err = store.Commit(ctx, txn) if err != nil { store.Abort(ctx, txn) - return err + return nil, err } if re.debug { @@ -74,7 +76,7 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR } regoOpts = append(regoOpts, - rego.Query("data.docker.allow"), + rego.Query(re.query), rego.StrictBuiltinErrors(true), rego.Input(input), rego.Store(store), @@ -85,15 +87,7 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR r := rego.New(regoOpts...) rs, err := r.Eval(ctx) - if err != nil { - return fmt.Errorf("error from Eval: %w", err) - } - - if !rs.Allowed() { - return fmt.Errorf("policy evaluation failed") - } - - return nil + return &rs, err } var dynamicObj = types.NewObject(nil, types.NewDynamicProperty(types.S, types.A))