Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion internal/rego/sigstore/sigstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ package sigstore

import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"sync"

"github.com/google/go-containerregistry/pkg/name"
"github.com/open-policy-agent/opa/v1/ast"
Expand All @@ -35,6 +38,7 @@ import (
"github.com/sigstore/cosign/v3/pkg/oci"
"github.com/sigstore/sigstore/pkg/tuf"
log "github.com/sirupsen/logrus"
"golang.org/x/sync/singleflight"

"github.com/conforma/cli/internal/attestation"
"github.com/conforma/cli/internal/policy"
Expand All @@ -58,6 +62,11 @@ const (
rekorPublicKeyAttribute = "rekor_public_key"
)

var (
verifyImageCache sync.Map
verifyImageFlight singleflight.Group
)

var ociImageReferenceParameter = types.Named("ref", types.S).Description("OCI image reference")

var sigstoreOptsParameter = types.Named("opts",
Expand Down Expand Up @@ -111,7 +120,6 @@ func registerSigstoreVerifyImage() {

func sigstoreVerifyImage(bctx rego.BuiltinContext, refTerm *ast.Term, optsTerm *ast.Term) (*ast.Term, error) {
logger := log.WithField("function", sigstoreVerifyImageName)
ctx := bctx.Context

uri, err := builtins.StringOperand(refTerm.Value, 0)
if err != nil {
Expand All @@ -120,6 +128,33 @@ func sigstoreVerifyImage(bctx rego.BuiltinContext, refTerm *ast.Term, optsTerm *
}
logger = logger.WithField("ref", string(uri))

// Build cache key from ref + opts hash
Comment thread
simonbaird marked this conversation as resolved.
cacheKey := buildCacheKey(string(uri), optsTerm)

// Check cache first
if cached, ok := verifyImageCache.Load(cacheKey); ok {
logger.Debug("using cached image signature verification result")
return cached.(*ast.Term), nil
}

Comment thread
simonbaird marked this conversation as resolved.
// Use singleflight to deduplicate concurrent requests. doVerifyImage never returns a Go
Comment thread
simonbaird marked this conversation as resolved.
// error — failures are embedded in the result term — so the error is intentionally discarded.
Comment thread
simonbaird marked this conversation as resolved.
resultIface, _, _ := verifyImageFlight.Do(cacheKey, func() (interface{}, error) {
return doVerifyImage(bctx, logger, uri, optsTerm)
})

Comment thread
simonbaird marked this conversation as resolved.
// We cache successes and failures here. It would be possible to avoid caching
// failures such as transient network problems that might suceeed if tried again,
// but I think it's likely not worth the effort, so we won't do that now.
result := resultIface.(*ast.Term)
verifyImageCache.Store(cacheKey, result)

return result, nil
Comment thread
simonbaird marked this conversation as resolved.
}
Comment thread
simonbaird marked this conversation as resolved.

func doVerifyImage(bctx rego.BuiltinContext, logger *log.Entry, uri ast.String, optsTerm *ast.Term) (*ast.Term, error) {
ctx := bctx.Context

ref, err := name.NewDigest(string(uri))
if err != nil {
logger.WithField("error", err).Debug("failed to create new digest")
Expand Down Expand Up @@ -159,6 +194,12 @@ func sigstoreVerifyImage(bctx rego.BuiltinContext, refTerm *ast.Term, optsTerm *
return signatureResult(signatures, nil)
}

func buildCacheKey(ref string, optsTerm *ast.Term) string {
h := sha256.Sum256([]byte(optsTerm.String()))
optsHash := hex.EncodeToString(h[:])
return ref + "|" + optsHash
}

func registerSigstoreVerifyAttestation() {
attestationType := types.Named("attestation", types.NewObject([]*types.StaticProperty{
{Key: "statement", Value: types.Named("statement", types.A).Description("statement from attestation")},
Expand Down Expand Up @@ -440,6 +481,11 @@ func attestationResult(attestations []oci.Signature, useBundles bool, err error)
), nil
}

// ClearCaches clears the verify_image cache. Used for testing.
func ClearCaches() {
verifyImageCache = sync.Map{}
}

func init() {
registerSigstoreVerifyImage()
registerSigstoreVerifyAttestation()
Expand Down
89 changes: 89 additions & 0 deletions internal/rego/sigstore/sigstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ func TestSigstoreVerifyImage(t *testing.T) {

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
ClearCaches()
utils.SetTestRekorPublicKey(t)
utils.SetTestFulcioRoots(t)
utils.SetTestCTLogPublicKey(t)
Expand Down Expand Up @@ -210,6 +211,92 @@ func TestSigstoreVerifyImage(t *testing.T) {
}
Comment thread
simonbaird marked this conversation as resolved.
}

func TestVerifyImageCache(t *testing.T) {
image1 := name.MustParseReference(
"registry.local/spam@sha256:4e388ab32b10dc8dbc7e28144f552830adc74787c1e2c0824032078a79f227fb",
)
image2 := name.MustParseReference(
"registry.local/eggs@sha256:5e388ab32b10dc8dbc7e28144f552830adc74787c1e2c0824032078a79f227fb",
)
opts1 := options{ignoreRekor: true, publicKey: utils.TestPublicKey}
opts2 := options{publicKey: utils.TestPublicKey, rekorURL: "https://rekor.local"}

newFakeContext := func(t *testing.T) (*fake.FakeClient, rego.BuiltinContext) {
t.Helper()
utils.SetTestRekorPublicKey(t)
utils.SetTestFulcioRoots(t)
utils.SetTestCTLogPublicKey(t)

sig, err := static.NewSignature(
[]byte(`image`),
"signature",
static.WithLayerMediaType(types.MediaType(cosignTypes.DssePayloadType)),
)
require.NoError(t, err)

c := &fake.FakeClient{}
c.On("HasBundles", mock.Anything, mock.Anything).Return(false, nil)
c.On("VerifyImageSignatures", mock.Anything, mock.Anything).Return([]oci.Signature{sig}, false, nil)
ctx := o.WithClient(context.Background(), c)
return c, rego.BuiltinContext{Context: ctx}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

t.Run("same ref and opts returns cached result", func(t *testing.T) {
ClearCaches()
c, bctx := newFakeContext(t)

r1, err := sigstoreVerifyImage(bctx, ast.StringTerm(image1.String()), opts1.toTerm())
require.NoError(t, err)

r2, err := sigstoreVerifyImage(bctx, ast.StringTerm(image1.String()), opts1.toTerm())
require.NoError(t, err)

require.Equal(t, r1, r2)
c.AssertNumberOfCalls(t, "VerifyImageSignatures", 1)
})

t.Run("different ref is a cache miss", func(t *testing.T) {
ClearCaches()
c, bctx := newFakeContext(t)

_, err := sigstoreVerifyImage(bctx, ast.StringTerm(image1.String()), opts1.toTerm())
require.NoError(t, err)

_, err = sigstoreVerifyImage(bctx, ast.StringTerm(image2.String()), opts1.toTerm())
require.NoError(t, err)

c.AssertNumberOfCalls(t, "VerifyImageSignatures", 2)
})

t.Run("different opts is a cache miss", func(t *testing.T) {
ClearCaches()
c, bctx := newFakeContext(t)

_, err := sigstoreVerifyImage(bctx, ast.StringTerm(image1.String()), opts1.toTerm())
require.NoError(t, err)

_, err = sigstoreVerifyImage(bctx, ast.StringTerm(image1.String()), opts2.toTerm())
require.NoError(t, err)

c.AssertNumberOfCalls(t, "VerifyImageSignatures", 2)
})

t.Run("ClearCaches forces re-verification", func(t *testing.T) {
ClearCaches()
c, bctx := newFakeContext(t)

_, err := sigstoreVerifyImage(bctx, ast.StringTerm(image1.String()), opts1.toTerm())
require.NoError(t, err)
c.AssertNumberOfCalls(t, "VerifyImageSignatures", 1)

ClearCaches()

_, err = sigstoreVerifyImage(bctx, ast.StringTerm(image1.String()), opts1.toTerm())
require.NoError(t, err)
c.AssertNumberOfCalls(t, "VerifyImageSignatures", 2)
})
}

func TestSigstoreVerifyAttestation(t *testing.T) {
goodImage := name.MustParseReference(
"registry.local/spam@sha256:4e388ab32b10dc8dbc7e28144f552830adc74787c1e2c0824032078a79f227fb",
Expand Down Expand Up @@ -600,6 +687,7 @@ func TestSigstoreVerifyImageWithBundles(t *testing.T) {

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
ClearCaches()
utils.SetTestRekorPublicKey(t)
utils.SetTestFulcioRoots(t)
utils.SetTestCTLogPublicKey(t)
Expand Down Expand Up @@ -661,6 +749,7 @@ func TestSigstoreVerifyImageBundleFallback(t *testing.T) {

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
ClearCaches()
utils.SetTestRekorPublicKey(t)
utils.SetTestFulcioRoots(t)
utils.SetTestCTLogPublicKey(t)
Expand Down
Loading