From 86363190b910f77a47d6331328a1dc4c2e8de2b7 Mon Sep 17 00:00:00 2001 From: Luiz Carvalho Date: Mon, 11 Mar 2024 17:01:54 -0400 Subject: [PATCH] Add ec.sigstore.verify* rego functions This commit introduces two new rego functions. `ec.sigstore.verify_image` and `ec.sigstore.verify_attestation` can be used to verify the signatures and attestations on an image. This version of the functions does not return the matched signatures and attestations. This is important and will be done in a follow up commit. Both functions return an verification object which can be enhanced to return these values without breaking users. Acceptance tests will also be added in a follow up commit. Ref: EC-447 Signed-off-by: Luiz Carvalho --- cmd/validate/validate.go | 1 + internal/rego/sigstore.go | 273 +++++++++++++++++++++++++ internal/rego/sigstore_test.go | 354 +++++++++++++++++++++++++++++++++ 3 files changed, 628 insertions(+) create mode 100644 internal/rego/sigstore.go create mode 100644 internal/rego/sigstore_test.go diff --git a/cmd/validate/validate.go b/cmd/validate/validate.go index 643902d4e..9a2d406d7 100644 --- a/cmd/validate/validate.go +++ b/cmd/validate/validate.go @@ -22,6 +22,7 @@ import ( "github.com/enterprise-contract/ec-cli/internal/definition" "github.com/enterprise-contract/ec-cli/internal/image" "github.com/enterprise-contract/ec-cli/internal/input" + _ "github.com/enterprise-contract/ec-cli/internal/rego" ) var ValidateCmd *cobra.Command diff --git a/internal/rego/sigstore.go b/internal/rego/sigstore.go new file mode 100644 index 000000000..9bf3a2e68 --- /dev/null +++ b/internal/rego/sigstore.go @@ -0,0 +1,273 @@ +// Copyright The Enterprise Contract Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +// IMPORTANT: The rego functions in this file never return an error. Instead, they return no value +// when an error is encountered. If they did return an error, opa would exit abruptly and it would +// not produce a report of which policy rules succeeded/failed. + +package evaluator + +import ( + "context" + "fmt" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/rego" + "github.com/open-policy-agent/opa/topdown/builtins" + "github.com/open-policy-agent/opa/types" + "github.com/sigstore/cosign/v2/pkg/cosign" + + "github.com/enterprise-contract/ec-cli/internal/evaluation_target/application_snapshot_image" + "github.com/enterprise-contract/ec-cli/internal/policy" +) + +const ( + sigstoreVerifyImageName = "ec.sigstore.verify_image" + sigstoreVerifyAttestationName = "ec.sigstore.verify_attestation" +) + +const ( + certificateIdentityAttribute = "certificate_identity" + certificateIdentityRegExpAttribute = "certificate_identity_regexp" + certificateOIDCIssuerAttribute = "certificate_oidc_issuer" + certificateOIDCIssuerRegExpAttribute = "certificate_oidc_issuer_regexp" + ignoreRekorAttribute = "ignore_rekor" + publicKeyAttribute = "public_key" + rekorURLAttribute = "rekor_url" +) + +var ociImageReferenceParameter = types.Named("ref", types.S).Description("OCI image reference") + +var sigstoreOptsParameter = types.Named("opts", + types.NewObject( + []*types.StaticProperty{ + {Key: certificateIdentityAttribute, Value: types.S}, + {Key: certificateIdentityRegExpAttribute, Value: types.S}, + {Key: certificateOIDCIssuerAttribute, Value: types.S}, + {Key: certificateOIDCIssuerRegExpAttribute, Value: types.S}, + {Key: ignoreRekorAttribute, Value: types.B}, + {Key: publicKeyAttribute, Value: types.S}, + {Key: rekorURLAttribute, Value: types.S}, + }, + nil, + )).Description("Sigstore verification options") + +// TODO: We want to enhance this verification result to return signatures and attestations. This is +// important, specially for verify_attestation so callers can make sure the expected predicate is +// found. At that time, it may not make sense to share the same result type between the different +// verify_* functions. +var verificationResult = types.Named( + "result", + types.NewObject([]*types.StaticProperty{ + {Key: "success", Value: types.Named("success", types.B).Description("true when verification is successful")}, + {Key: "errors", Value: types.Named("errors", types.NewArray([]types.Type{types.S}, nil)).Description("verification errors")}, + }, nil), +).Description("the result of the verification request") + +func registerSigstoreVerifyImage() { + decl := rego.Function{ + Name: sigstoreVerifyImageName, + Description: "Use sigstore to verify the signature of an image.", + Decl: types.NewFunction( + types.Args(ociImageReferenceParameter, sigstoreOptsParameter), + verificationResult, + ), + Memoize: true, + Nondeterministic: true, + } + rego.RegisterBuiltin2(&decl, sigstoreVerifyImage) +} + +func sigstoreVerifyImage(bctx rego.BuiltinContext, refTerm *ast.Term, optsTerm *ast.Term) (*ast.Term, error) { + ctx := bctx.Context + + uri, err := builtins.StringOperand(refTerm.Value, 0) + if err != nil { + return makeVerificationResult(fmt.Sprintf("ref parameter: %s", err)), nil + } + + ref, err := name.NewDigest(string(uri)) + if err != nil { + return makeVerificationResult(fmt.Sprintf("new digest: %s", err)), nil + } + + checkOpts, err := parseCheckOpts(ctx, optsTerm) + if err != nil { + return makeVerificationResult(fmt.Sprintf("opts parameter: %s", err)), nil + } + checkOpts.ClaimVerifier = cosign.SimpleClaimVerifier + + _, _, err = application_snapshot_image.NewClient(ctx).VerifyImageSignatures(ctx, ref, checkOpts) + if err != nil { + return makeVerificationResult(fmt.Sprintf("verify image signature: %s", err)), nil + } + + return makeVerificationResult(), nil +} + +func registerSigstoreVerifyAttestation() { + decl := rego.Function{ + Name: sigstoreVerifyAttestationName, + Description: "Use sigstore to verify the attestation of an image.", + Decl: types.NewFunction( + types.Args(ociImageReferenceParameter, sigstoreOptsParameter), + verificationResult, + ), + // As per the documentation, enable memoization to ensure function evaluation is + // deterministic. But also mark it as non-deterministic because it does rely on external + // entities, i.e. OCI registry. https://www.openpolicyagent.org/docs/latest/extensions/ + Memoize: true, + Nondeterministic: true, + } + rego.RegisterBuiltin2(&decl, sigstoreVerifyAttestation) +} + +func sigstoreVerifyAttestation(bctx rego.BuiltinContext, refTerm *ast.Term, optsTerm *ast.Term) (*ast.Term, error) { + ctx := bctx.Context + + uri, err := builtins.StringOperand(refTerm.Value, 0) + if err != nil { + return makeVerificationResult(fmt.Sprintf("ref parameter: %s", err)), nil + } + + ref, err := name.NewDigest(string(uri)) + if err != nil { + return makeVerificationResult(fmt.Sprintf("new digest: %s", err)), nil + } + + checkOpts, err := parseCheckOpts(ctx, optsTerm) + if err != nil { + return makeVerificationResult(fmt.Sprintf("opts parameter: %s", err)), nil + } + checkOpts.ClaimVerifier = cosign.IntotoSubjectClaimVerifier + + _, _, err = application_snapshot_image.NewClient(ctx).VerifyImageAttestations(ctx, ref, checkOpts) + if err != nil { + return makeVerificationResult(fmt.Sprintf("verify image attestation signature: %s", err)), nil + } + + return makeVerificationResult(), nil +} + +func parseCheckOpts(ctx context.Context, optsTerm *ast.Term) (*cosign.CheckOpts, error) { + if _, err := builtins.ObjectOperand(optsTerm.Value, 1); err != nil { + return nil, fmt.Errorf("opts parameter: %s", err) + } + opts := optionsFromTerm(optsTerm) + + policyOpts := policy.Options{ + // TODO: EffectiveTime is not actually used in this context, but it is required to be set + // by policy.NewPolicy. + EffectiveTime: "now", + Identity: cosign.Identity{ + Subject: opts.certificateIdentity, + SubjectRegExp: opts.certificateIdentityRegExp, + Issuer: opts.certificateOIDCIssuer, + IssuerRegExp: opts.certificateOIDCIssuerRegExp, + }, + IgnoreRekor: opts.ignoreRekor, + PublicKey: opts.publicKey, + RekorURL: opts.rekorURL, + } + + policy, err := policy.NewPolicy(ctx, policyOpts) + if err != nil { + return nil, fmt.Errorf("new policy: %s", err) + } + + return policy.CheckOpts() +} + +type options struct { + certificateIdentity string + certificateIdentityRegExp string + certificateOIDCIssuer string + certificateOIDCIssuerRegExp string + ignoreRekor bool + publicKey string + rekorURL string +} + +func (o options) toTerm() *ast.Term { + return ast.ObjectTerm( + ast.Item(ast.StringTerm(certificateIdentityAttribute), ast.StringTerm(o.certificateIdentity)), + ast.Item(ast.StringTerm(certificateIdentityRegExpAttribute), ast.StringTerm(o.certificateIdentityRegExp)), + ast.Item(ast.StringTerm(certificateOIDCIssuerAttribute), ast.StringTerm(o.certificateOIDCIssuer)), + ast.Item(ast.StringTerm(certificateOIDCIssuerRegExpAttribute), ast.StringTerm(o.certificateOIDCIssuerRegExp)), + ast.Item(ast.StringTerm(ignoreRekorAttribute), ast.BooleanTerm(o.ignoreRekor)), + ast.Item(ast.StringTerm(publicKeyAttribute), ast.StringTerm(o.publicKey)), + ast.Item(ast.StringTerm(rekorURLAttribute), ast.StringTerm(o.rekorURL)), + ) +} + +func optionsFromTerm(term *ast.Term) options { + opts := options{} + + if v, ok := term.Get(ast.StringTerm(certificateIdentityAttribute)).Value.(ast.String); ok { + opts.certificateIdentity = string(v) + } + + if v, ok := term.Get(ast.StringTerm(certificateIdentityRegExpAttribute)).Value.(ast.String); ok { + opts.certificateIdentityRegExp = string(v) + } + + if v, ok := term.Get(ast.StringTerm(certificateOIDCIssuerAttribute)).Value.(ast.String); ok { + opts.certificateOIDCIssuer = string(v) + } + + if v, ok := term.Get(ast.StringTerm(certificateOIDCIssuerRegExpAttribute)).Value.(ast.String); ok { + opts.certificateOIDCIssuerRegExp = string(v) + } + + if v, ok := term.Get(ast.StringTerm(ignoreRekorAttribute)).Value.(ast.Boolean); ok { + opts.ignoreRekor = bool(v) + } + + if v, ok := term.Get(ast.StringTerm(publicKeyAttribute)).Value.(ast.String); ok { + opts.publicKey = string(v) + } + + if v, ok := term.Get(ast.StringTerm(rekorURLAttribute)).Value.(ast.String); ok { + opts.rekorURL = string(v) + } + + return opts +} + +func makeVerificationResult(errors ...string) *ast.Term { + + var terms []*ast.Term + for _, err := range errors { + terms = append(terms, ast.StringTerm(err)) + } + errorsTerm := ast.ArrayTerm(terms...) + + var success bool + if len(errors) == 0 { + success = true + } + + return ast.ObjectTerm( + ast.Item(ast.StringTerm("success"), ast.BooleanTerm(success)), + ast.Item(ast.StringTerm("errors"), errorsTerm), + ) +} + +func init() { + registerSigstoreVerifyImage() + registerSigstoreVerifyAttestation() +} diff --git a/internal/rego/sigstore_test.go b/internal/rego/sigstore_test.go new file mode 100644 index 000000000..ecbe41448 --- /dev/null +++ b/internal/rego/sigstore_test.go @@ -0,0 +1,354 @@ +// Copyright The Enterprise Contract Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build unit + +package evaluator + +import ( + "context" + "errors" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/rego" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/cosign/v2/pkg/oci" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/enterprise-contract/ec-cli/internal/evaluation_target/application_snapshot_image" + "github.com/enterprise-contract/ec-cli/internal/utils" +) + +func TestSigstoreVerifyImage(t *testing.T) { + goodImage := name.MustParseReference( + "registry.local/spam@sha256:4e388ab32b10dc8dbc7e28144f552830adc74787c1e2c0824032078a79f227fb", + ) + + cases := []struct { + name string + success *ast.Term + errors *ast.Term + uri *ast.Term + opts options + optsVerifier func(mock.Arguments) + sigError error + }{ + { + name: "long lived key", + success: ast.BooleanTerm(true), + errors: ast.ArrayTerm(), + uri: ast.StringTerm(goodImage.String()), + opts: options{ignoreRekor: true, publicKey: utils.TestPublicKey}, + optsVerifier: func(args mock.Arguments) { + checkOpts := args.Get(2).(*cosign.CheckOpts) + require.NotNil(t, checkOpts) + require.True(t, checkOpts.IgnoreTlog) + require.Nil(t, checkOpts.RekorClient) + require.Empty(t, checkOpts.Identities) + }, + }, + { + name: "long lived key with rekor", + success: ast.BooleanTerm(true), + errors: ast.ArrayTerm(), + uri: ast.StringTerm(goodImage.String()), + opts: options{publicKey: utils.TestPublicKey, rekorURL: "https://rekor.local"}, + optsVerifier: func(args mock.Arguments) { + checkOpts := args.Get(2).(*cosign.CheckOpts) + require.NotNil(t, checkOpts) + require.False(t, checkOpts.IgnoreTlog) + require.Empty(t, checkOpts.Identities) + require.NotNil(t, checkOpts.RekorClient) + }, + }, + { + name: "fulcio key", + success: ast.BooleanTerm(true), + errors: ast.ArrayTerm(), + uri: ast.StringTerm(goodImage.String()), + opts: options{ + certificateIdentity: "subject", + certificateOIDCIssuer: "issuer", + rekorURL: "https://rekor.local", + }, + optsVerifier: func(args mock.Arguments) { + checkOpts := args.Get(2).(*cosign.CheckOpts) + require.NotNil(t, checkOpts) + require.False(t, checkOpts.IgnoreTlog) + require.NotNil(t, checkOpts.RekorClient) + identities := []cosign.Identity{{Issuer: "issuer", Subject: "subject"}} + require.Equal(t, checkOpts.Identities, identities) + }, + }, + { + name: "fulcio key regex", + success: ast.BooleanTerm(true), + errors: ast.ArrayTerm(), + uri: ast.StringTerm(goodImage.String()), + opts: options{ + certificateIdentityRegExp: `subject.*`, + certificateOIDCIssuerRegExp: `issuer.*`, + rekorURL: "https://rekor.local", + }, + optsVerifier: func(args mock.Arguments) { + checkOpts := args.Get(2).(*cosign.CheckOpts) + require.NotNil(t, checkOpts) + require.False(t, checkOpts.IgnoreTlog) + require.NotNil(t, checkOpts.RekorClient) + identities := []cosign.Identity{{IssuerRegExp: `issuer.*`, SubjectRegExp: `subject.*`}} + require.Equal(t, checkOpts.Identities, identities) + }, + }, + { + name: "bad public key", + success: ast.BooleanTerm(false), + errors: ast.ArrayTerm( + ast.StringTerm("opts parameter: new policy: loading URL: unrecognized scheme: spam://"), + ), + uri: ast.StringTerm(goodImage.String()), + opts: options{publicKey: "spam://this-key-does-not-exist"}, + }, + { + name: "insufficient options", + success: ast.BooleanTerm(false), + errors: ast.ArrayTerm( + ast.StringTerm("opts parameter: new policy: 2 errors occurred:\n\t* certificate OIDC issuer must be provided for keyless workflow\n\t* certificate identity must be provided for keyless workflow\n\n"), + ), + uri: ast.StringTerm(goodImage.String()), + }, + { + name: "image ref without digest", + success: ast.BooleanTerm(false), + errors: ast.ArrayTerm( + ast.StringTerm("new digest: a digest must contain exactly one '@' separator (e.g. registry/repository@digest) saw: registry.local/spam:latest"), + ), + uri: ast.StringTerm("registry.local/spam:latest"), + }, + { + name: "verification failure", + success: ast.BooleanTerm(false), + errors: ast.ArrayTerm( + ast.StringTerm("verify image signature: kaboom!"), + ), + uri: ast.StringTerm(goodImage.String()), + opts: options{ignoreRekor: true, publicKey: utils.TestPublicKey}, + sigError: errors.New("kaboom!"), + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + utils.SetTestRekorPublicKey(t) + utils.SetTestFulcioRoots(t) + utils.SetTestCTLogPublicKey(t) + + c := MockClient{} + ctx := application_snapshot_image.WithClient(context.Background(), &c) + + verifyCall := c.On( + "VerifyImageSignatures", ctx, goodImage, mock.Anything, + ).Return([]oci.Signature{}, false, tt.sigError) + + if tt.optsVerifier != nil { + verifyCall.Run(tt.optsVerifier) + } + + bctx := rego.BuiltinContext{Context: ctx} + + result, err := sigstoreVerifyImage(bctx, tt.uri, tt.opts.toTerm()) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, tt.errors, result.Get(ast.StringTerm("errors"))) + require.Equal(t, tt.success, result.Get(ast.StringTerm("success"))) + }) + } +} + +func TestSigstoreVerifyAttestation(t *testing.T) { + goodImage := name.MustParseReference( + "registry.local/spam@sha256:4e388ab32b10dc8dbc7e28144f552830adc74787c1e2c0824032078a79f227fb", + ) + + cases := []struct { + name string + success *ast.Term + errors *ast.Term + uri *ast.Term + opts options + optsVerifier func(mock.Arguments) + sigError error + }{ + { + name: "long lived key", + success: ast.BooleanTerm(true), + errors: ast.ArrayTerm(), + uri: ast.StringTerm(goodImage.String()), + opts: options{ignoreRekor: true, publicKey: utils.TestPublicKey}, + optsVerifier: func(args mock.Arguments) { + checkOpts := args.Get(2).(*cosign.CheckOpts) + require.NotNil(t, checkOpts) + require.True(t, checkOpts.IgnoreTlog) + require.Nil(t, checkOpts.RekorClient) + require.Empty(t, checkOpts.Identities) + }, + }, + { + name: "long lived key with rekor", + success: ast.BooleanTerm(true), + errors: ast.ArrayTerm(), + uri: ast.StringTerm(goodImage.String()), + opts: options{publicKey: utils.TestPublicKey, rekorURL: "https://rekor.local"}, + optsVerifier: func(args mock.Arguments) { + checkOpts := args.Get(2).(*cosign.CheckOpts) + require.NotNil(t, checkOpts) + require.False(t, checkOpts.IgnoreTlog) + require.Empty(t, checkOpts.Identities) + require.NotNil(t, checkOpts.RekorClient) + }, + }, + { + name: "fulcio key", + success: ast.BooleanTerm(true), + errors: ast.ArrayTerm(), + uri: ast.StringTerm(goodImage.String()), + opts: options{ + certificateIdentity: "subject", + certificateOIDCIssuer: "issuer", + rekorURL: "https://rekor.local", + }, + optsVerifier: func(args mock.Arguments) { + checkOpts := args.Get(2).(*cosign.CheckOpts) + require.NotNil(t, checkOpts) + require.False(t, checkOpts.IgnoreTlog) + require.NotNil(t, checkOpts.RekorClient) + identities := []cosign.Identity{{Issuer: "issuer", Subject: "subject"}} + require.Equal(t, checkOpts.Identities, identities) + }, + }, + { + name: "fulcio key regex", + success: ast.BooleanTerm(true), + errors: ast.ArrayTerm(), + uri: ast.StringTerm(goodImage.String()), + opts: options{ + certificateIdentityRegExp: `subject.*`, + certificateOIDCIssuerRegExp: `issuer.*`, + rekorURL: "https://rekor.local", + }, + optsVerifier: func(args mock.Arguments) { + checkOpts := args.Get(2).(*cosign.CheckOpts) + require.NotNil(t, checkOpts) + require.False(t, checkOpts.IgnoreTlog) + require.NotNil(t, checkOpts.RekorClient) + identities := []cosign.Identity{{IssuerRegExp: `issuer.*`, SubjectRegExp: `subject.*`}} + require.Equal(t, checkOpts.Identities, identities) + }, + }, + { + name: "bad public key", + success: ast.BooleanTerm(false), + errors: ast.ArrayTerm( + ast.StringTerm("opts parameter: new policy: loading URL: unrecognized scheme: spam://"), + ), + uri: ast.StringTerm(goodImage.String()), + opts: options{publicKey: "spam://this-key-does-not-exist"}, + }, + { + name: "insufficient options", + success: ast.BooleanTerm(false), + errors: ast.ArrayTerm( + ast.StringTerm("opts parameter: new policy: 2 errors occurred:\n\t* certificate OIDC issuer must be provided for keyless workflow\n\t* certificate identity must be provided for keyless workflow\n\n"), + ), + uri: ast.StringTerm(goodImage.String()), + }, + { + name: "image ref without digest", + success: ast.BooleanTerm(false), + errors: ast.ArrayTerm( + ast.StringTerm("new digest: a digest must contain exactly one '@' separator (e.g. registry/repository@digest) saw: registry.local/spam:latest"), + ), + uri: ast.StringTerm("registry.local/spam:latest"), + }, + { + name: "verification failure", + success: ast.BooleanTerm(false), + errors: ast.ArrayTerm( + ast.StringTerm("verify image attestation signature: kaboom!"), + ), + uri: ast.StringTerm(goodImage.String()), + opts: options{ignoreRekor: true, publicKey: utils.TestPublicKey}, + sigError: errors.New("kaboom!"), + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + utils.SetTestRekorPublicKey(t) + utils.SetTestFulcioRoots(t) + utils.SetTestCTLogPublicKey(t) + + c := MockClient{} + ctx := application_snapshot_image.WithClient(context.Background(), &c) + + verifyCall := c.On( + "VerifyImageAttestations", ctx, goodImage, mock.Anything, + ).Return([]oci.Signature{}, false, tt.sigError) + + if tt.optsVerifier != nil { + verifyCall.Run(tt.optsVerifier) + } + + bctx := rego.BuiltinContext{Context: ctx} + + result, err := sigstoreVerifyAttestation(bctx, tt.uri, tt.opts.toTerm()) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, tt.errors, result.Get(ast.StringTerm("errors"))) + require.Equal(t, tt.success, result.Get(ast.StringTerm("success"))) + }) + } +} + +type MockClient struct { + mock.Mock +} + +func (c *MockClient) VerifyImageSignatures(ctx context.Context, name name.Reference, opts *cosign.CheckOpts) ([]oci.Signature, bool, error) { + args := c.Called(ctx, name, opts) + + return args.Get(0).([]oci.Signature), args.Get(1).(bool), args.Error(2) +} + +func (c *MockClient) VerifyImageAttestations(ctx context.Context, name name.Reference, opts *cosign.CheckOpts) ([]oci.Signature, bool, error) { + args := c.Called(ctx, name, opts) + + return args.Get(0).([]oci.Signature), args.Get(1).(bool), args.Error(2) +} + +func (c *MockClient) Head(name name.Reference, options ...remote.Option) (*v1.Descriptor, error) { + args := c.Called(name, options) + + return args.Get(0).(*v1.Descriptor), args.Error(1) +} + +func (c *MockClient) ResolveDigest(ref name.Reference, opts *cosign.CheckOpts) (string, error) { + return "", nil +}