Return results from rego evaluation (#14)

This commit is contained in:
James Carnegie
2024-04-30 15:32:52 +01:00
committed by GitHub
parent fb1a43acfd
commit 90393ea6fd
9 changed files with 164 additions and 130 deletions

View File

@@ -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 &rego.ResultSet{
{
Bindings: rego.Vars{},
Expressions: []*rego.ExpressionValue{
{
Value: true,
},
},
},
}
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
},
}

102
pkg/attest/vsa.go Normal file
View File

@@ -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
}

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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")
}
})
}

View File

@@ -23,15 +23,17 @@ import (
type regoEvaluator struct {
debug bool
query string
}
func NewRegoEvaluator(debug bool) PolicyEvaluator {
return &regoEvaluator{
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))