Handle errors from Go in Rego. Support for skipping TL (#47)

* Make TL logging/verification optional

* Return errors from go-lang fns

* Update pkg/policy/rego.go

Co-authored-by: Jonny Stoten <jonny@jonnystoten.com>

* Update pkg/attestation/sign.go

Co-authored-by: Joel Kamp <joel.kamp@docker.com>

* Move public key marshelling until later

* Simplify logSignature and pass down opts

---------

Co-authored-by: Jonny Stoten <jonny@jonnystoten.com>
Co-authored-by: Joel Kamp <joel.kamp@docker.com>
This commit is contained in:
James Carnegie
2024-06-06 09:59:32 +01:00
committed by GitHub
parent 3b5c506739
commit 4be882aeb0
17 changed files with 337 additions and 138 deletions

View File

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

View File

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

View File

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

View File

@@ -12,10 +12,6 @@ const (
LifecycleStageExperimental = "experimental"
)
type SigningOptions struct {
Replace bool
}
type Outcome string
const (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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