diff --git a/pkg/attest/example_sign_test.go b/pkg/attest/example_sign_test.go index 5368696..78c2ba4 100644 --- a/pkg/attest/example_sign_test.go +++ b/pkg/attest/example_sign_test.go @@ -4,6 +4,7 @@ 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" @@ -25,7 +26,7 @@ func ExampleSign_remote() { // signer, err := signerverifier.GetAWSSigner(cmd.Context(), aws_arn, aws_region) // configure signing options - opts := &attest.SigningOptions{ + opts := &attestation.SigningOptions{ Replace: true, // replace unsigned intoto statements with signed intoto attestations, otherwise leave in place } diff --git a/pkg/attest/sign.go b/pkg/attest/sign.go index f164528..d0d69f2 100644 --- a/pkg/attest/sign.go +++ b/pkg/attest/sign.go @@ -18,7 +18,7 @@ import ( "github.com/secure-systems-lab/go-securesystemslib/dsse" ) -func Sign(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *SigningOptions) (v1.ImageIndex, error) { +func Sign(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) (v1.ImageIndex, error) { // extract attestation manifests from index attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx) if err != nil { @@ -63,7 +63,7 @@ func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.St }, } // 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}) + idx, err = signLayersAndAddToIndex(ctx, idx, attestationLayers, manifest, signer, &attestation.SigningOptions{Replace: false}) if err != nil { return nil, fmt.Errorf("failed to add signed layers: %w", err) } @@ -83,9 +83,9 @@ func signLayersAndAddToIndex( attestationLayers []attestation.AttestationLayer, manifest attestation.AttestationManifest, signer dsse.SignerVerifier, - opts *SigningOptions) (v1.ImageIndex, error) { + opts *attestation.SigningOptions) (v1.ImageIndex, error) { - signedLayers, err := signLayers(ctx, attestationLayers, signer) + signedLayers, err := signLayers(ctx, attestationLayers, signer, opts) if err != nil { return nil, fmt.Errorf("failed to sign attestations: %w", err) } @@ -120,7 +120,7 @@ func signLayersAndAddToIndex( } // signLayers signs each intoto attestation layer with the given signer -func signLayers(ctx context.Context, layers []attestation.AttestationLayer, signer dsse.SignerVerifier) ([]mutate.Addendum, error) { +func signLayers(ctx context.Context, layers []attestation.AttestationLayer, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]mutate.Addendum, error) { var signedLayers []mutate.Addendum for _, layer := range layers { // only sign intoto layers @@ -131,10 +131,11 @@ func signLayers(ctx context.Context, layers []attestation.AttestationLayer, sign layer.Annotations[InTotoReferenceLifecycleStage] = LifecycleStageExperimental // sign the statement - env, err := signInTotoStatement(ctx, layer.Statement, signer) + env, err := signInTotoStatement(ctx, layer.Statement, signer, opts) if err != nil { return nil, fmt.Errorf("failed to sign statement: %w", err) } + mediaType, err := attestation.DSSEMediaType(layer.Statement.PredicateType) if err != nil { return nil, fmt.Errorf("failed to get DSSE media type: %w", err) @@ -153,12 +154,12 @@ 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) { +func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *attestation.SigningOptions) (*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) + env, err := attestation.SignDSSE(ctx, payload, signer, opts) if err != nil { return nil, fmt.Errorf("failed to sign statement: %w", err) } @@ -166,7 +167,7 @@ func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signe } // addSignedLayers adds signed layers to a new or existing attestation image -func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.AttestationManifest, opts *SigningOptions) (v1.Image, error) { +func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.AttestationManifest, opts *attestation.SigningOptions) (v1.Image, error) { var err error if opts.Replace { // create a new attestation image with only signed layers diff --git a/pkg/attest/sign_test.go b/pkg/attest/sign_test.go index 56d5e2b..15b0d8f 100644 --- a/pkg/attest/sign_test.go +++ b/pkg/attest/sign_test.go @@ -26,6 +26,7 @@ var ( UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image") PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass") + PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl") FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail") TestTempDir = "attest-sign-test" ) @@ -53,7 +54,7 @@ func TestSignVerifyOCILayout(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { outputLayout := test.CreateTempDir(t, "", TestTempDir) - opts := &SigningOptions{ + opts := &attestation.SigningOptions{ Replace: tc.replace, } attIdx, err := oci.AttestationIndexFromPath(tc.TestImage) @@ -185,7 +186,7 @@ func TestAddSignedLayerAnnotations(t *testing.T) { data = []byte("test") testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType)) mediaType := types.OCIManifestSchema1 - opts := &SigningOptions{ + opts := &attestation.SigningOptions{ Replace: tc.replace, } manifest := attestation.AttestationManifest{ diff --git a/pkg/attest/types.go b/pkg/attest/types.go index 1343034..5a37bab 100644 --- a/pkg/attest/types.go +++ b/pkg/attest/types.go @@ -12,10 +12,6 @@ const ( LifecycleStageExperimental = "experimental" ) -type SigningOptions struct { - Replace bool -} - type Outcome string const ( diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index c181cab..50e5bff 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -73,7 +73,7 @@ func TestVSA(t *testing.T) { // setup an image with signed attestations outputLayout := test.CreateTempDir(t, "", TestTempDir) - opts := &SigningOptions{ + opts := &attestation.SigningOptions{ Replace: true, } attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage) @@ -94,7 +94,6 @@ func TestVSA(t *testing.T) { _, err = layout.Write(outputLayout, idx) assert.NoError(t, err) - //verify (without vsa should fail) resolver, err := oci.NewOCILayoutAttestationResolver(outputLayout, "linux/amd64") require.NoError(t, err) @@ -126,7 +125,7 @@ func TestVerificationFailure(t *testing.T) { // setup an image with signed attestations outputLayout := test.CreateTempDir(t, "", TestTempDir) - opts := &SigningOptions{ + opts := &attestation.SigningOptions{ Replace: true, } attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage) @@ -147,11 +146,10 @@ func TestVerificationFailure(t *testing.T) { _, err = layout.Write(outputLayout, idx) assert.NoError(t, err) - //verify (without vsa should fail) resolver, err := oci.NewOCILayoutAttestationResolver(outputLayout, "linux/amd64") require.NoError(t, err) - // mocked vsa query should pass + // mocked vsa query should fail policyOpts := &policy.PolicyOptions{ LocalPolicyDir: FailPolicyDir, } @@ -177,3 +175,60 @@ func TestVerificationFailure(t *testing.T) { assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels) assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI) } + +// test signing without a TL entry +func TestSignVerifyNoTL(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) + + testCases := []struct { + name string + signTL bool + policyDir string + success bool + }{ + {name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir, success: true}, + {name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir, success: false}, + {name: "no tl", signTL: false, policyDir: PassPolicyDir, success: false}, + } + + attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage) + assert.NoError(t, err) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + opts := &attestation.SigningOptions{ + Replace: true, + SkipTL: tc.signTL, + } + + 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) + + resolver, err := oci.NewOCILayoutAttestationResolver(outputLayout, "linux/amd64") + require.NoError(t, err) + + policyOpts := &policy.PolicyOptions{ + LocalPolicyDir: tc.policyDir, + } + results, err := Verify(ctx, policyOpts, resolver) + require.NoError(t, err) + assert.Equal(t, OutcomeSuccess, results.Outcome) + }) + } +} diff --git a/pkg/attestation/sign.go b/pkg/attestation/sign.go index f5efc89..91cd640 100644 --- a/pkg/attestation/sign.go +++ b/pkg/attestation/sign.go @@ -6,13 +6,16 @@ import ( "github.com/docker/attest/internal/util" "github.com/docker/attest/pkg/tlog" + intoto "github.com/in-toto/in-toto-golang/in_toto" "github.com/secure-systems-lab/go-securesystemslib/dsse" ) // SignDSSE signs a payload with a given signer and uploads the signature to the transparency log -func SignDSSE(ctx context.Context, payload []byte, payloadType string, signer dsse.SignerVerifier) (*Envelope, error) { - t := tlog.GetTL(ctx) - +func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) { + payloadType := opts.PayloadType + if payloadType == "" { + payloadType = intoto.PayloadType + } env := new(Envelope) env.Payload = base64Encoding.EncodeToString(payload) env.PayloadType = payloadType @@ -33,8 +36,31 @@ func SignDSSE(ctx context.Context, payload []byte, payloadType string, signer ds return nil, fmt.Errorf("error getting public key ID: %w", err) } - // upload to TL - entry, err := t.UploadLogEntry(ctx, keyId, encPayload, sig, signer) + dsseSig := Signature{ + KeyID: keyId, + Sig: base64Encoding.EncodeToString(sig), + } + if !opts.SkipTL { + ext, err := logSignature(ctx, tlog.GetTL(ctx), &sig, &encPayload, signer) + if err != nil { + return nil, fmt.Errorf("failed to log to rekor: %w", err) + } + dsseSig.Extension = *ext + } + // add signature to dsse envelope + env.Signatures = []Signature{dsseSig} + + return env, nil +} + +// returns a new envelope with the transparency log entry added to the signature extension +func logSignature(ctx context.Context, t tlog.TL, sig *[]byte, encPayload *[]byte, signer dsse.SignerVerifier) (*Extension, error) { + // get Key ID from signer + keyId, err := signer.KeyID() + if err != nil { + return nil, fmt.Errorf("error getting public key ID: %w", err) + } + entry, err := t.UploadLogEntry(ctx, keyId, *encPayload, *sig, signer) if err != nil { return nil, fmt.Errorf("error uploading TL entry: %w", err) } @@ -42,21 +68,13 @@ func SignDSSE(ctx context.Context, payload []byte, payloadType string, signer ds if err != nil { return nil, fmt.Errorf("error unmarshaling tl entry: %w", err) } - - // add signature w/ tl extension to dsse envelope - env.Signatures = append(env.Signatures, Signature{ - KeyID: keyId, - Sig: base64Encoding.EncodeToString(sig), - Extension: Extension{ - Kind: DockerDsseExtKind, - Ext: DockerDsseExtension{ - Tl: DockerTlExtension{ - Kind: RekorTlExtKind, - Data: entryObj, // transparency log entry metadata - }, + return &Extension{ + Kind: DockerDsseExtKind, + Ext: DockerDsseExtension{ + Tl: DockerTlExtension{ + Kind: RekorTlExtKind, + Data: entryObj, // transparency log entry metadata }, }, - }) - - return env, nil + }, nil } diff --git a/pkg/attestation/sign_test.go b/pkg/attestation/sign_test.go index 3af09df..81fbb9c 100644 --- a/pkg/attestation/sign_test.go +++ b/pkg/attestation/sign_test.go @@ -14,6 +14,7 @@ import ( "github.com/docker/attest/pkg/signerverifier" intoto "github.com/in-toto/in-toto-golang/in_toto" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSignVerifyAttestation(t *testing.T) { @@ -27,17 +28,17 @@ func TestSignVerifyAttestation(t *testing.T) { } payload, err := json.Marshal(stmt) - assert.NoError(t, err) - - env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer) - assert.NoError(t, err) + require.NoError(t, err) + opts := &attestation.SigningOptions{} + env, err := attestation.SignDSSE(ctx, payload, signer, opts) + require.NoError(t, err) // marshal envelope to json to test for bugs when marshaling envelope data serializedEnv, err := json.Marshal(env) - assert.NoError(t, err) + require.NoError(t, err) deserializedEnv := new(attestation.Envelope) err = json.Unmarshal(serializedEnv, deserializedEnv) - assert.NoError(t, err) + require.NoError(t, err) // signer.Public() calls AWS API when using AWS signer, use attestation.GetPublicVerificationKey() to get key from TUF repo // signer.Public() used here for test purposes @@ -49,10 +50,10 @@ func TestSignVerifyAttestation(t *testing.T) { assert.NoError(t, err) badKeyPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - assert.NoError(t, err) + require.NoError(t, err) badKey := &badKeyPriv.PublicKey badPEM, err := signerverifier.ToPEM(badKey) - assert.NoError(t, err) + require.NoError(t, err) testCases := []struct { name string @@ -136,7 +137,10 @@ func TestSignVerifyAttestation(t *testing.T) { To: tc.to, Status: tc.status, } - _, err = attestation.VerifyDSSE(ctx, deserializedEnv, attestation.KeysMap{tc.keyId: keyMeta}) + opts := &attestation.VerifyOptions{ + Keys: attestation.Keys{keyMeta}, + } + _, err = attestation.VerifyDSSE(ctx, deserializedEnv, opts) if tc.expectedError != "" { assert.Contains(t, err.Error(), tc.expectedError) } else { diff --git a/pkg/attestation/types.go b/pkg/attestation/types.go index 5ee6717..ae3ecce 100644 --- a/pkg/attestation/types.go +++ b/pkg/attestation/types.go @@ -66,6 +66,17 @@ type DockerTlExtension struct { Data any `json:"data"` } +type VerifyOptions struct { + Keys []KeyMetadata `json:"keys"` + SkipTL bool `json:"skip_tl"` +} + +type SigningOptions struct { + Replace bool + SkipTL bool + PayloadType string +} + func DSSEMediaType(predicateType string) (string, error) { var predicateName string switch predicateType { diff --git a/pkg/attestation/verify.go b/pkg/attestation/verify.go index 42471db..3eb7361 100644 --- a/pkg/attestation/verify.go +++ b/pkg/attestation/verify.go @@ -30,7 +30,7 @@ type KeyMetadata struct { type Keys []KeyMetadata type KeysMap map[string]KeyMetadata -func VerifyDSSE(ctx context.Context, env *Envelope, keys KeysMap) ([]byte, error) { +func VerifyDSSE(ctx context.Context, env *Envelope, opts *VerifyOptions) ([]byte, error) { // enforce payload type if !ValidPayloadType(env.PayloadType) { return nil, fmt.Errorf("unsupported payload type %s", env.PayloadType) @@ -49,7 +49,7 @@ func VerifyDSSE(ctx context.Context, env *Envelope, keys KeysMap) ([]byte, error // verify signatures and transparency log entry for _, sig := range env.Signatures { - err := verifySignature(ctx, sig, encPayload, keys) + err := verifySignature(ctx, sig, encPayload, opts) if err != nil { return nil, err } @@ -58,31 +58,11 @@ func VerifyDSSE(ctx context.Context, env *Envelope, keys KeysMap) ([]byte, error return payload, nil } -func verifySignature(ctx context.Context, sig Signature, payload []byte, keys KeysMap) error { - t := tlog.GetTL(ctx) - - if sig.Extension.Kind == "" { - return fmt.Errorf("error missing signature extension kind") +func verifySignature(ctx context.Context, sig Signature, payload []byte, opts *VerifyOptions) error { + keys := make(map[string]KeyMetadata, len(opts.Keys)) + for _, key := range opts.Keys { + keys[key.ID] = key } - if sig.Extension.Kind != DockerDsseExtKind { - return fmt.Errorf("error unsupported signature extension kind: %s", sig.Extension.Kind) - } - - // verify TL entry - if sig.Extension.Ext.Tl.Kind != RekorTlExtKind { - return fmt.Errorf("error unsupported TL extension kind: %s", sig.Extension.Ext.Tl.Kind) - } - entry := sig.Extension.Ext.Tl.Data - entryBytes, err := json.Marshal(entry) - if err != nil { - return fmt.Errorf("failed to marshal TL entry: %w", err) - } - - integratedTime, err := t.VerifyLogEntry(ctx, entryBytes) - if err != nil { - return fmt.Errorf("TL entry failed verification: %w", err) - } - keyMeta, ok := keys[sig.KeyID] if !ok { return fmt.Errorf("error key not found: %s", sig.KeyID) @@ -91,30 +71,53 @@ func verifySignature(ctx context.Context, sig Signature, payload []byte, keys Ke if keyMeta.Distrust { return fmt.Errorf("key %s is distrusted", keyMeta.ID) } - - if integratedTime.Before(keyMeta.From) { - return fmt.Errorf("key %s was not yet valid at TL log time %s (key valid from %s)", keyMeta.ID, integratedTime, keyMeta.From) - } - - if keyMeta.To != nil && !integratedTime.Before(*keyMeta.To) { - return fmt.Errorf("key %s was already %s at TL log time %s (key %s at %s)", keyMeta.ID, keyMeta.Status, integratedTime, keyMeta.Status, *keyMeta.To) - } - // TODO: this is unmarshalling with MarshalPKIXPublicKey only for us to marshal it again publicKey, err := signerverifier.Parse([]byte(keyMeta.PEM)) if err != nil { return fmt.Errorf("failed to parse public key: %w", err) } - // verify TL entry payload - encodedPub, err := x509.MarshalPKIXPublicKey(publicKey) - if err != nil { - return fmt.Errorf("error failed to marshal public key: %w", err) - } - err = t.VerifyEntryPayload(entryBytes, payload, encodedPub) - if err != nil { - return fmt.Errorf("TL entry failed payload verification: %w", err) + if !opts.SkipTL { + t := tlog.GetTL(ctx) + + if sig.Extension.Kind == "" { + return fmt.Errorf("error missing signature extension kind") + } + if sig.Extension.Kind != DockerDsseExtKind { + return fmt.Errorf("error unsupported signature extension kind: %s", sig.Extension.Kind) + } + + // verify TL entry + if sig.Extension.Ext.Tl.Kind != RekorTlExtKind { + return fmt.Errorf("error unsupported TL extension kind: %s", sig.Extension.Ext.Tl.Kind) + } + entry := sig.Extension.Ext.Tl.Data + entryBytes, err := json.Marshal(entry) + if err != nil { + return fmt.Errorf("failed to marshal TL entry: %w", err) + } + + integratedTime, err := t.VerifyLogEntry(ctx, entryBytes) + if err != nil { + return fmt.Errorf("TL entry failed verification: %w", err) + } + if integratedTime.Before(keyMeta.From) { + return fmt.Errorf("key %s was not yet valid at TL log time %s (key valid from %s)", keyMeta.ID, integratedTime, keyMeta.From) + } + if keyMeta.To != nil && !integratedTime.Before(*keyMeta.To) { + return fmt.Errorf("key %s was already %s at TL log time %s (key %s at %s)", keyMeta.ID, keyMeta.Status, integratedTime, keyMeta.Status, *keyMeta.To) + } + // verify TL entry payload + encodedPub, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return fmt.Errorf("error failed to marshal public key: %w", err) + } + err = t.VerifyEntryPayload(entryBytes, payload, encodedPub) + if err != nil { + return fmt.Errorf("TL entry failed payload verification: %w", err) + } } + // decode signature signature, err := base64.StdEncoding.Strict().DecodeString(sig.Sig) if err != nil { diff --git a/pkg/attestation/verify_test.go b/pkg/attestation/verify_test.go index 7878523..7ba351a 100644 --- a/pkg/attestation/verify_test.go +++ b/pkg/attestation/verify_test.go @@ -39,8 +39,11 @@ func TestVerifyUnsignedAttestation(t *testing.T) { Payload: base64.StdEncoding.EncodeToString(payload), PayloadType: intoto.PayloadType, } + opts := &attestation.VerifyOptions{ + Keys: attestation.Keys{}, + } - _, err := attestation.VerifyDSSE(ctx, env, attestation.KeysMap{}) + _, err := attestation.VerifyDSSE(ctx, env, opts) assert.Error(t, err) assert.Contains(t, err.Error(), "no signatures") } diff --git a/pkg/policy/rego.go b/pkg/policy/rego.go index c9ff39c..72ebc8f 100644 --- a/pkg/policy/rego.go +++ b/pkg/policy/rego.go @@ -133,21 +133,42 @@ func jsonGenerator[T any]() func(t *ast.Term, ec *rego.EvalContext) (any, error) } } -var dynamicObj = types.NewObject(nil, types.NewDynamicProperty(types.S, types.A)) -var arrayObj = types.NewArray(nil, dynamicObj) -var setObj = types.NewSet(dynamicObj) +var dynamicObj = types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)) var verifyDecl = &ast.Builtin{ - Name: "attestations.verify_envelope", - Decl: types.NewFunction(types.Args(dynamicObj, arrayObj), dynamicObj), + Name: "attest.verify", + Decl: types.NewFunction(types.Args(dynamicObj, dynamicObj), dynamicObj), Nondeterministic: true, } var attestDecl = &ast.Builtin{ - Name: "attestations.attestation", - Decl: types.NewFunction(types.Args(types.S), setObj), + Name: "attest.fetch", + Decl: types.NewFunction(types.Args(types.S), dynamicObj), Nondeterministic: true, } +func wrapFunctionResult(value *ast.Term, err error) (*ast.Term, error) { + var terms [][2]*ast.Term + if err != nil { + terms = append(terms, [2]*ast.Term{ast.StringTerm("error"), ast.StringTerm(err.Error())}) + } + if value != nil { + terms = append(terms, [2]*ast.Term{ast.StringTerm("value"), value}) + } + return ast.ObjectTerm(terms...), nil +} + +func handleErrors1(f func(rCtx rego.BuiltinContext, a *ast.Term) (*ast.Term, error)) rego.Builtin1 { + return func(rCtx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) { + return wrapFunctionResult(f(rCtx, a)) + } +} + +func handleErrors2(f func(rCtx rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error)) rego.Builtin2 { + return func(rCtx rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error) { + return wrapFunctionResult(f(rCtx, a, b)) + } +} + func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin { return []*tester.Builtin{ { @@ -159,7 +180,7 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin { Memoize: true, Nondeterministic: verifyDecl.Nondeterministic, }, - verifyIntotoEnvelope), + handleErrors2(verifyIntotoEnvelope)), }, { Decl: attestDecl, @@ -170,12 +191,12 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin { Memoize: true, Nondeterministic: attestDecl.Nondeterministic, }, - fetchIntotoAttestations(resolver)), + handleErrors1(fetchIntotoAttestations(resolver))), }, } } -func fetchIntotoAttestations(resolver oci.AttestationResolver) func(rego.BuiltinContext, *ast.Term) (*ast.Term, error) { +func fetchIntotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 { return func(rCtx rego.BuiltinContext, predicateTypeTerm *ast.Term) (*ast.Term, error) { predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String) if !ok { @@ -205,22 +226,19 @@ func fetchIntotoAttestations(resolver oci.AttestationResolver) func(rego.Builtin } } -func verifyIntotoEnvelope(rCtx rego.BuiltinContext, envTerm, keysTerm *ast.Term) (*ast.Term, error) { +func verifyIntotoEnvelope(rCtx rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) { env := new(att.Envelope) - var keys att.Keys + opts := new(att.VerifyOptions) err := ast.As(envTerm.Value, env) if err != nil { return nil, fmt.Errorf("failed to cast envelope: %w", err) } - err = ast.As(keysTerm.Value, &keys) + err = ast.As(optsTerm.Value, &opts) if err != nil { - return nil, fmt.Errorf("failed to cast keys: %w", err) + return nil, fmt.Errorf("failed to cast verifier options: %w", err) } - keysmap := make(map[string]att.KeyMetadata, len(keys)) - for _, key := range keys { - keysmap[key.ID] = key - } - payload, err := att.VerifyDSSE(rCtx.Context, env, keysmap) + + payload, err := att.VerifyDSSE(rCtx.Context, env, opts) if err != nil { return nil, err } @@ -242,7 +260,6 @@ func verifyIntotoEnvelope(rCtx rego.BuiltinContext, envTerm, keysTerm *ast.Term) if err != nil { return nil, err } - return ast.NewTerm(value), nil } 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 6e5f504..d7985a0 100644 --- a/pkg/policy/testdata/mock-tuf-verify-sig/doi/policy.rego +++ b/pkg/policy/testdata/mock-tuf-verify-sig/doi/policy.rego @@ -3,17 +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, }] +opts := {"keys": keys} + success if { - some env in attestations.attestation("foo") - statement := attestations.verify_envelope(env, keys) + some env in attest.fetch("foo") + statement := attest.verify(env, opts) } -result := { - "success": success -} +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 835ac7c..86f342e 100644 --- a/pkg/policy/testdata/mock-tuf-wrong-key/doi/policy.rego +++ b/pkg/policy/testdata/mock-tuf-wrong-key/doi/policy.rego @@ -3,19 +3,28 @@ package attest import rego.v1 keys := [{ - "id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4", - "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHyZpSgzvqFqNv7f3x7865OS38rAb\nQMcff55zM2UH/KR3Pr84a8QsGDNgaNGzJQJWjtMSgfV8WnNoffNK+svFNg==\n-----END PUBLIC KEY-----", - "from": "2023-12-15T14:00:00Z", - "to": null, + "id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4", + "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHyZpSgzvqFqNv7f3x7865OS38rAb\nQMcff55zM2UH/KR3Pr84a8QsGDNgaNGzJQJWjtMSgfV8WnNoffNK+svFNg==\n-----END PUBLIC KEY-----", + "from": "2023-12-15T14:00:00Z", + "to": null, }] default success := false -success if { - some env in attestations.attestation("foo") - statement := attestations.verify_envelope(env, keys) +provs(pred) := p if { + res := attest.fetch(pred) + not res.error + p := res.value } -result := { - "success": success +atts := union({provs("foo")}) + +opts := {"keys": keys} + +success if { + some env in atts + res := attest.verify(env, opts) + not res.error } + +result := {"success": success} diff --git a/test/testdata/local-policy-fail/doi/policy.rego b/test/testdata/local-policy-fail/doi/policy.rego index 828602b..7f58cbb 100644 --- a/test/testdata/local-policy-fail/doi/policy.rego +++ b/test/testdata/local-policy-fail/doi/policy.rego @@ -12,14 +12,24 @@ keys := [{ "signing-format": "dssev1", }] +provs(pred) := p if { + res := attest.fetch(pred) + not res.error + p := res.value +} + atts := union({ - attestations.attestation("https://slsa.dev/provenance/v0.2"), - attestations.attestation("https://spdx.dev/Document"), + provs("https://slsa.dev/provenance/v0.2"), + provs("https://spdx.dev/Document"), }) +opts := {"keys": keys} + statements contains s if { some att in atts - s := attestations.verify_envelope(att, keys) + res := attest.verify(att, opts) + not res.error + s := res.value } subjects contains subject if { diff --git a/test/testdata/local-policy-no-tl/doi/policy.rego b/test/testdata/local-policy-no-tl/doi/policy.rego new file mode 100644 index 0000000..71a3b82 --- /dev/null +++ b/test/testdata/local-policy-no-tl/doi/policy.rego @@ -0,0 +1,49 @@ +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", +}] + +provs(pred) := p if { + res := attest.fetch(pred) + not res.error + p := res.value +} + +atts := union({ + provs("https://slsa.dev/provenance/v0.2"), + provs("https://spdx.dev/Document"), +}) + +opts := {"keys": keys, "skip_tl": true} + +statements contains s if { + some att in atts + res := attest.verify(att, opts) + not res.error + s := res.value +} + +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-no-tl/mapping.yaml b/test/testdata/local-policy-no-tl/mapping.yaml new file mode 100644 index 0000000..f729528 --- /dev/null +++ b/test/testdata/local-policy-no-tl/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-pass/doi/policy.rego b/test/testdata/local-policy-pass/doi/policy.rego index 032e3e4..ff9f04b 100644 --- a/test/testdata/local-policy-pass/doi/policy.rego +++ b/test/testdata/local-policy-pass/doi/policy.rego @@ -12,14 +12,24 @@ keys := [{ "signing-format": "dssev1", }] +provs(pred) := p if { + res := attest.fetch(pred) + not res.error + p := res.value +} + atts := union({ - attestations.attestation("https://slsa.dev/provenance/v0.2"), - attestations.attestation("https://spdx.dev/Document"), + provs("https://slsa.dev/provenance/v0.2"), + provs("https://spdx.dev/Document"), }) +opts := {"keys": keys} + statements contains s if { some att in atts - s := attestations.verify_envelope(att, keys) + res := attest.verify(att, opts) + not res.error + s := res.value } subjects contains subject if {