diff --git a/README.md b/README.md index fe262f7..db378a6 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ library to create, verify, and evaluate policy for attestations on container ima # usage ## verifying attestations -See example [example_verify.go](./pkg/attest/example_verify.go) +See [example_verify_test.go](./pkg/attest/example_verify_test.go) ## signing attestations -See example [example_sign.go](./pkg/attest/example_sign.go) +See [example_sign_test.go](./pkg/attest/example_sign_test.go) ## mirroring TUF repositories to OCI -See example [example_mirror.go](./pkg/mirror/example_mirror.go) +See [example_mirror_test.go](./pkg/mirror/example_mirror_test.go) ### using `go-tuf` OCI registry client -See example [example_registry](./pkg/tuf/example_registry.go) +See [example_registry_test.go](./pkg/tuf/example_registry_test.go) diff --git a/internal/test/test.go b/internal/test/test.go index 10baf71..73e26cc 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -10,7 +10,6 @@ import ( "testing" "github.com/docker/attest/pkg/attestation" - "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" "github.com/docker/attest/pkg/signerverifier" "github.com/docker/attest/pkg/tlog" @@ -58,7 +57,7 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) { var policyEvaluator policy.PolicyEvaluator if USE_MOCK_POLICY { - policyEvaluator = GetMockPolicy() + policyEvaluator = policy.GetMockPolicy() } else { policyEvaluator = policy.NewRegoEvaluator(true) } @@ -86,25 +85,6 @@ 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) error -} - -func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, policy []*policy.PolicyFile, input *policy.PolicyInput) error { - if pe.EvaluateFunc != nil { - return pe.EvaluateFunc(ctx, resolver, policy, input) - } - return nil -} - -func GetMockPolicy() policy.PolicyEvaluator { - return &MockPolicyEvaluator{ - EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, policy []*policy.PolicyFile, input *policy.PolicyInput) error { - return nil - }, - } -} - type AnnotatedStatement struct { OCIDescriptor *v1.Descriptor InTotoStatement *intoto.Statement diff --git a/pkg/attest/example_sign.go b/pkg/attest/example_sign_test.go similarity index 92% rename from pkg/attest/example_sign.go rename to pkg/attest/example_sign_test.go index 6330f7e..41bbee6 100644 --- a/pkg/attest/example_sign.go +++ b/pkg/attest/example_sign_test.go @@ -1,8 +1,9 @@ -package attest +package attest_test 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" @@ -25,7 +26,7 @@ func ExampleSign_remote() { // signer, err := signerverifier.GetAWSSigner(cmd.Context(), aws_arn, aws_region) // configure signing options - opts := &SigningOptions{ + opts := &attest.SigningOptions{ Replace: true, // replace unsigned intoto statements with signed intoto attestations, otherwise leave in place } @@ -50,7 +51,7 @@ func ExampleSign_remote() { // att, err := oci.AttestationIndexFromLocal(path) // sign attestations - signedImageIndex, err := Sign(context.Background(), att.Index, signer, opts) + signedImageIndex, err := attest.Sign(context.Background(), att.Index, signer, opts) if err != nil { panic(err) } diff --git a/pkg/attest/example_verify.go b/pkg/attest/example_verify_test.go similarity index 91% rename from pkg/attest/example_verify.go rename to pkg/attest/example_verify_test.go index 570b124..9e55f71 100644 --- a/pkg/attest/example_verify.go +++ b/pkg/attest/example_verify_test.go @@ -1,4 +1,4 @@ -package attest +package attest_test import ( "context" @@ -6,6 +6,7 @@ import ( "path/filepath" "github.com/docker/attest/internal/embed" + "github.com/docker/attest/pkg/attest" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" "github.com/docker/attest/pkg/tuf" @@ -19,7 +20,7 @@ func createTufClient(outputPath string) (*tuf.TufClient, error) { // metadataURI := "https://docker.github.io/tuf-staging/metadata" // targetsURI := "https://docker.github.io/tuf-staging/targets" - return tuf.NewTufClient(embed.DefaultRoot, outputPath, metadataURI, targetsURI) + return tuf.NewTufClient(embed.StagingRoot, outputPath, metadataURI, targetsURI) } func ExampleVerify_remote() { @@ -57,7 +58,7 @@ func ExampleVerify_remote() { } // verify attestations - policy, err := Verify(context.Background(), opts, resolver) + policy, err := attest.Verify(context.Background(), opts, resolver) if err != nil { panic(err) // failed policy or attestation signature verification } 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 5751da0..538081a 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -8,10 +8,10 @@ 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" ) @@ -42,9 +42,9 @@ func TestVerifyAttestations(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - mockPE := test.MockPolicyEvaluator{ - EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pfs []*policy.PolicyFile, input *policy.PolicyInput) error { - return tc.policyEvaluationError + mockPE := policy.MockPolicyEvaluator{ + EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pfs []*policy.PolicyFile, input *policy.PolicyInput) (*rego.ResultSet, error) { + return policy.AllowedResult(), tc.policyEvaluationError }, } diff --git a/pkg/mirror/example_mirror.go b/pkg/mirror/example_mirror_test.go similarity index 81% rename from pkg/mirror/example_mirror.go rename to pkg/mirror/example_mirror_test.go index d89312c..ea2bd2a 100644 --- a/pkg/mirror/example_mirror.go +++ b/pkg/mirror/example_mirror_test.go @@ -1,4 +1,4 @@ -package mirror +package mirror_test import ( "fmt" @@ -7,17 +7,18 @@ import ( "strings" "github.com/docker/attest/internal/embed" + "github.com/docker/attest/pkg/mirror" v1 "github.com/google/go-containerregistry/pkg/v1" ) type TufMirrorOutput struct { metadata *v1.Image - delegatedMetadata []*MirrorImage - targets []*MirrorImage - delegatedTargets []*MirrorIndex + delegatedMetadata []*mirror.MirrorImage + targets []*mirror.MirrorImage + delegatedTargets []*mirror.MirrorIndex } -func ExampleMirror() { +func ExampleNewTufMirror() { home, err := os.UserHomeDir() if err != nil { panic(err) @@ -27,7 +28,7 @@ func ExampleMirror() { // configure TUF mirror metadataURI := "https://docker.github.io/tuf-staging/metadata" targetsURI := "https://docker.github.io/tuf-staging/targets" - m, err := NewTufMirror(embed.DefaultRoot, tufOutputPath, metadataURI, targetsURI) + m, err := mirror.NewTufMirror(embed.StagingRoot, tufOutputPath, metadataURI, targetsURI) if err != nil { panic(err) } @@ -78,7 +79,7 @@ func ExampleMirror() { func mirrorToRegistry(o *TufMirrorOutput) error { // push metadata to registry metadataRepo := "registry-1.docker.io/docker/tuf-metadata:latest" - err := PushToRegistry(o.metadata, metadataRepo) + err := mirror.PushToRegistry(o.metadata, metadataRepo) if err != nil { return err } @@ -89,7 +90,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error { return fmt.Errorf("failed to get repo without tag: %s", metadataRepo) } imageName := fmt.Sprintf("%s:%s", repo, metadata.Tag) - err = PushToRegistry(metadata.Image, imageName) + err = mirror.PushToRegistry(metadata.Image, imageName) if err != nil { return err } @@ -99,7 +100,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error { targetsRepo := "registry-1.docker.io/docker/tuf-targets" for _, target := range o.targets { imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag) - err = PushToRegistry(target.Image, imageName) + err = mirror.PushToRegistry(target.Image, imageName) if err != nil { return err } @@ -107,7 +108,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error { // push delegated targets to registry for _, target := range o.delegatedTargets { imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag) - err = PushToRegistry(target.Index, imageName) + err = mirror.PushToRegistry(target.Index, imageName) if err != nil { return err } @@ -117,14 +118,14 @@ func mirrorToRegistry(o *TufMirrorOutput) error { func mirrorToLocal(o *TufMirrorOutput, outputPath string) error { // output metadata to local directory - err := SaveAsOCILayout(o.metadata, outputPath) + err := mirror.SaveAsOCILayout(o.metadata, outputPath) if err != nil { return err } // output delegated metadata to local directory for _, metadata := range o.delegatedMetadata { path := filepath.Join(outputPath, metadata.Tag) - err = SaveAsOCILayout(metadata.Image, path) + err = mirror.SaveAsOCILayout(metadata.Image, path) if err != nil { return err } @@ -133,7 +134,7 @@ func mirrorToLocal(o *TufMirrorOutput, outputPath string) error { // output top-level targets to local directory for _, target := range o.targets { path := filepath.Join(outputPath, target.Tag) - err = SaveAsOCILayout(target.Image, path) + err = mirror.SaveAsOCILayout(target.Image, path) if err != nil { return err } @@ -141,7 +142,7 @@ func mirrorToLocal(o *TufMirrorOutput, outputPath string) error { // output delegated targets to local directory for _, target := range o.delegatedTargets { path := filepath.Join(outputPath, target.Tag) - err = SaveAsOCILayout(target.Index, path) + err = mirror.SaveAsOCILayout(target.Index, path) if err != nil { return err } diff --git a/pkg/policy/evaluator.go b/pkg/policy/evaluator.go index 35a34cb..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,5 +27,5 @@ func GetPolicyEvaluator(ctx context.Context) (PolicyEvaluator, error) { } type PolicyEvaluator interface { - Evaluate(ctx context.Context, resolver oci.AttestationResolver, policy []*PolicyFile, input *PolicyInput) error + Evaluate(ctx context.Context, resolver oci.AttestationResolver, policy []*PolicyFile, input *PolicyInput) (*rego.ResultSet, error) } diff --git a/pkg/policy/mock.go b/pkg/policy/mock.go new file mode 100644 index 0000000..0f4e891 --- /dev/null +++ b/pkg/policy/mock.go @@ -0,0 +1,40 @@ +package policy + +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) +} + +func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, policy []*PolicyFile, input *PolicyInput) (*rego.ResultSet, error) { + if pe.EvaluateFunc != nil { + return pe.EvaluateFunc(ctx, resolver, policy, input) + } + return AllowedResult(), nil +} + +func GetMockPolicy() PolicyEvaluator { + return &MockPolicyEvaluator{ + EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pfs []*PolicyFile, input *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/policy/policy_test.go b/pkg/policy/policy_test.go index 2665cf7..bf389fb 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -97,11 +97,13 @@ 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") + assert.True(t, rs.Allowed(), "Evaluate should have succeeded") } 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 78f3de3..c6851b0 100644 --- a/pkg/policy/rego.go +++ b/pkg/policy/rego.go @@ -29,10 +29,11 @@ type regoEvaluator struct { 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) 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 @@ -41,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 { @@ -49,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))) @@ -63,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 { @@ -75,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), @@ -86,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)) diff --git a/pkg/policy/testdata/mock-tuf-allow/doi/policy.rego b/pkg/policy/testdata/mock-tuf-allow/doi/policy.rego index 5e3b8ab..b1b188a 100644 --- a/pkg/policy/testdata/mock-tuf-allow/doi/policy.rego +++ b/pkg/policy/testdata/mock-tuf-allow/doi/policy.rego @@ -1,4 +1,4 @@ -package docker +package attest import rego.v1 diff --git a/pkg/policy/testdata/mock-tuf-deny/doi/policy.rego b/pkg/policy/testdata/mock-tuf-deny/doi/policy.rego index ad73140..5e4ac7e 100644 --- a/pkg/policy/testdata/mock-tuf-deny/doi/policy.rego +++ b/pkg/policy/testdata/mock-tuf-deny/doi/policy.rego @@ -1,4 +1,4 @@ -package docker +package attest import rego.v1 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 e2456af..8952fb1 100644 --- a/pkg/policy/testdata/mock-tuf-verify-sig/doi/policy.rego +++ b/pkg/policy/testdata/mock-tuf-verify-sig/doi/policy.rego @@ -1,4 +1,4 @@ -package docker +package attest import rego.v1 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 131129a..aaa78d7 100644 --- a/pkg/policy/testdata/mock-tuf-wrong-key/doi/policy.rego +++ b/pkg/policy/testdata/mock-tuf-wrong-key/doi/policy.rego @@ -1,4 +1,4 @@ -package docker +package attest import rego.v1 diff --git a/pkg/tuf/example_registry.go b/pkg/tuf/example_registry_test.go similarity index 82% rename from pkg/tuf/example_registry.go rename to pkg/tuf/example_registry_test.go index 77bca56..02acfeb 100644 --- a/pkg/tuf/example_registry.go +++ b/pkg/tuf/example_registry_test.go @@ -1,14 +1,15 @@ -package tuf +package tuf_test import ( "os" "path/filepath" "github.com/docker/attest/internal/embed" + "github.com/docker/attest/pkg/tuf" "github.com/theupdateframework/go-tuf/v2/metadata" ) -func ExampleTufRegistryClient() { +func ExampleNewTufClient_registry() { // create a tuf client home, err := os.UserHomeDir() if err != nil { @@ -19,7 +20,7 @@ func ExampleTufRegistryClient() { // using oci tuf metadata and targets metadataURI := "regsitry-1.docker.io/docker/tuf-metadata:latest" targetsURI := "regsitry-1.docker.io/docker/tuf-targets" - registryClient, err := NewTufClient(embed.DefaultRoot, tufOutputPath, metadataURI, targetsURI) + registryClient, err := tuf.NewTufClient(embed.StagingRoot, tufOutputPath, metadataURI, targetsURI) if err != nil { panic(err) } diff --git a/test/testdata/local-policy/doi/policy.rego b/test/testdata/local-policy/doi/policy.rego index ed35cbe..d4ec593 100644 --- a/test/testdata/local-policy/doi/policy.rego +++ b/test/testdata/local-policy/doi/policy.rego @@ -1,4 +1,4 @@ -package docker +package attest import rego.v1 diff --git a/test/testdata/local-policy/doi/policy_test.rego b/test/testdata/local-policy/doi/policy_test.rego index fa81aa9..e2bff5c 100644 --- a/test/testdata/local-policy/doi/policy_test.rego +++ b/test/testdata/local-policy/doi/policy_test.rego @@ -1,4 +1,4 @@ -package docker +package attest import rego.v1 config := {"keys": []}