diff --git a/api/v1beta2/ocirepository_types.go b/api/v1beta2/ocirepository_types.go index 299f20a52..861003a53 100644 --- a/api/v1beta2/ocirepository_types.go +++ b/api/v1beta2/ocirepository_types.go @@ -190,6 +190,28 @@ type OCIRepositoryVerification struct { // trusted public keys. // +optional SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"` + + // MatchOIDCIdentity specifies the identity matching criteria to use + // while verifying an OCI artifact which was signed using Cosign keyless + // signing. The artifact's identity is deemed to be verified if any of the + // specified matchers match against the identity. + // +optional + MatchOIDCIdentity []OIDCIdentityMatch `json:"matchOIDCIdentity,omitempty"` +} + +// OIDCIdentityMatch specifies options for verifying the certificate identity, +// i.e. the issuer and the subject of the certificate. +type OIDCIdentityMatch struct { + // Issuer specifies the regex pattern to match against to verify + // the OIDC issuer in the Fulcio certificate. The pattern must be a + // valid Go regular expression. + // +required + Issuer string `json:"issuer"` + // Subject specifies the regex pattern to match against to verify + // the identity subject in the Fulcio certificate. The pattern must + // be a valid Go regular expression. + // +required + Subject string `json:"subject"` } // OCIRepositoryStatus defines the observed state of OCIRepository diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 5c2169a33..e522081f2 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -834,6 +834,11 @@ func (in *OCIRepositoryVerification) DeepCopyInto(out *OCIRepositoryVerification *out = new(meta.LocalObjectReference) **out = **in } + if in.MatchOIDCIdentity != nil { + in, out := &in.MatchOIDCIdentity, &out.MatchOIDCIdentity + *out = make([]OIDCIdentityMatch, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIRepositoryVerification. @@ -845,3 +850,18 @@ func (in *OCIRepositoryVerification) DeepCopy() *OCIRepositoryVerification { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCIdentityMatch) DeepCopyInto(out *OIDCIdentityMatch) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCIdentityMatch. +func (in *OIDCIdentityMatch) DeepCopy() *OIDCIdentityMatch { + if in == nil { + return nil + } + out := new(OIDCIdentityMatch) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml b/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml index 9448f29f3..49bdcdd93 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml @@ -411,6 +411,32 @@ spec: Chart dependencies, which are not bundled in the umbrella chart artifact, are not verified. properties: + matchOIDCIdentity: + description: MatchOIDCIdentity specifies the identity matching + criteria to use while verifying an OCI artifact which was signed + using Cosign keyless signing. The artifact's identity is deemed + to be verified if any of the specified matchers match against + the identity. + items: + description: OIDCIdentityMatch specifies options for verifying + the certificate identity, i.e. the issuer and the subject + of the certificate. + properties: + issuer: + description: Issuer specifies the regex pattern to match + against to verify the OIDC issuer in the Fulcio certificate. + The pattern must be a valid Go regular expression. + type: string + subject: + description: Subject specifies the regex pattern to match + against to verify the identity subject in the Fulcio certificate. + The pattern must be a valid Go regular expression. + type: string + required: + - issuer + - subject + type: object + type: array provider: default: cosign description: Provider specifies the technology used to sign the diff --git a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml index df40334a4..b795c8fda 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml @@ -164,6 +164,32 @@ spec: public keys used to verify the signature and specifies which provider to use to check whether OCI image is authentic. properties: + matchOIDCIdentity: + description: MatchOIDCIdentity specifies the identity matching + criteria to use while verifying an OCI artifact which was signed + using Cosign keyless signing. The artifact's identity is deemed + to be verified if any of the specified matchers match against + the identity. + items: + description: OIDCIdentityMatch specifies options for verifying + the certificate identity, i.e. the issuer and the subject + of the certificate. + properties: + issuer: + description: Issuer specifies the regex pattern to match + against to verify the OIDC issuer in the Fulcio certificate. + The pattern must be a valid Go regular expression. + type: string + subject: + description: Subject specifies the regex pattern to match + against to verify the identity subject in the Fulcio certificate. + The pattern must be a valid Go regular expression. + type: string + required: + - issuer + - subject + type: object + type: array provider: default: cosign description: Provider specifies the technology used to sign the diff --git a/docs/api/v1beta2/source.md b/docs/api/v1beta2/source.md index 3d58db692..31fa0f06b 100644 --- a/docs/api/v1beta2/source.md +++ b/docs/api/v1beta2/source.md @@ -3295,6 +3295,71 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference trusted public keys.

+ + +matchOIDCIdentity
+ + +[]OIDCIdentityMatch + + + + +(Optional) +

MatchOIDCIdentity specifies the identity matching criteria to use +while verifying an OCI artifact which was signed using Cosign keyless +signing. The artifact’s identity is deemed to be verified if any of the +specified matchers match against the identity.

+ + + + + + +

OIDCIdentityMatch +

+

+(Appears on: +OCIRepositoryVerification) +

+

OIDCIdentityMatch specifies options for verifying the certificate identity, +i.e. the issuer and the subject of the certificate.

+
+
+ + + + + + + + + + + + + + + +
FieldDescription
+issuer
+ +string + +
+

Issuer specifies the regex pattern to match against to verify +the OIDC issuer in the Fulcio certificate. The pattern must be a +valid Go regular expression.

+
+subject
+ +string + +
+

Subject specifies the regex pattern to match against to verify +the identity subject in the Fulcio certificate. The pattern must +be a valid Go regular expression.

+
diff --git a/internal/controller/helmchart_controller.go b/internal/controller/helmchart_controller.go index 556253efe..d60fd1880 100644 --- a/internal/controller/helmchart_controller.go +++ b/internal/controller/helmchart_controller.go @@ -29,6 +29,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/opencontainers/go-digest" + "github.com/sigstore/cosign/v2/pkg/cosign" helmgetter "helm.sh/helm/v3/pkg/getter" helmreg "helm.sh/helm/v3/pkg/registry" helmrepo "helm.sh/helm/v3/pkg/repo" @@ -1338,6 +1339,15 @@ func (r *HelmChartReconciler) makeVerifiers(ctx context.Context, obj *helmv1.Hel } // if no secret is provided, add a keyless verifier + var identities []cosign.Identity + for _, match := range obj.Spec.Verify.MatchOIDCIdentity { + identities = append(identities, cosign.Identity{ + IssuerRegExp: match.Issuer, + SubjectRegExp: match.Subject, + }) + } + defaultCosignOciOpts = append(defaultCosignOciOpts, soci.WithIdentities(identities)) + verifier, err := soci.NewCosignVerifier(ctx, defaultCosignOciOpts...) if err != nil { return nil, err diff --git a/internal/controller/helmchart_controller_test.go b/internal/controller/helmchart_controller_test.go index 3d5fc5c7d..b7d5a275a 100644 --- a/internal/controller/helmchart_controller_test.go +++ b/internal/controller/helmchart_controller_test.go @@ -59,6 +59,7 @@ import ( sourcev1 "github.com/fluxcd/source-controller/api/v1" helmv1 "github.com/fluxcd/source-controller/api/v1beta2" + ociv1 "github.com/fluxcd/source-controller/api/v1beta2" serror "github.com/fluxcd/source-controller/internal/error" "github.com/fluxcd/source-controller/internal/helm/chart" "github.com/fluxcd/source-controller/internal/helm/chart/secureloader" @@ -2533,6 +2534,160 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) { } } +func TestHelmChartRepository_reconcileSource_verifyOCISourceSignature_keyless(t *testing.T) { + tests := []struct { + name string + version string + want sreconcile.Result + wantErr bool + wantErrMsg string + beforeFunc func(obj *helmv1.HelmChart) + assertConditions []metav1.Condition + revision string + }{ + { + name: "signed image with no identity matching specified should pass verification", + version: "6.5.1", + want: sreconcile.ResultSuccess, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of version "), + *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled '' chart with version ''"), + *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled '' chart with version ''"), + }, + revision: "6.5.1@sha256:af589b918022cd8d85a4543312d28170c2e894ccab8484050ff4bdefdde30b4e", + }, + { + name: "signed image with correct subject and issuer should pass verification", + version: "6.5.1", + want: sreconcile.ResultSuccess, + beforeFunc: func(obj *helmv1.HelmChart) { + obj.Spec.Verify.MatchOIDCIdentity = []ociv1.OIDCIdentityMatch{ + { + + Subject: "^https://github.com/stefanprodan/podinfo.*$", + Issuer: "^https://token.actions.githubusercontent.com$", + }, + } + }, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of version "), + *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled '' chart with version ''"), + *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled '' chart with version ''"), + }, + revision: "6.5.1@sha256:af589b918022cd8d85a4543312d28170c2e894ccab8484050ff4bdefdde30b4e", + }, + { + name: "signed image with incorrect subject and issuer should not pass verification", + version: "6.5.1", + wantErr: true, + want: sreconcile.ResultEmpty, + beforeFunc: func(obj *helmv1.HelmChart) { + obj.Spec.Verify.MatchOIDCIdentity = []ociv1.OIDCIdentityMatch{ + { + Subject: "intruder", + Issuer: "^https://honeypot.com$", + }, + } + }, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.BuildFailedCondition, "ChartVerificationError", "chart verification error: failed to verify : no matching signatures: none of the expected identities matched what was in the certificate"), + *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "chart verification error: failed to verify : no matching signatures"), + }, + revision: "6.5.1@sha256:af589b918022cd8d85a4543312d28170c2e894ccab8484050ff4bdefdde30b4e", + }, + { + name: "unsigned image should not pass verification", + version: "6.1.0", + wantErr: true, + want: sreconcile.ResultEmpty, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.BuildFailedCondition, "ChartVerificationError", "chart verification error: failed to verify : no matching signatures"), + *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "chart verification error: failed to verify : no matching signatures"), + }, + revision: "6.1.0@sha256:642383f56ccb529e3f658d40312d01b58d9bc6caeef653da43e58d1afe88982a", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + clientBuilder := fakeclient.NewClientBuilder() + + repository := &helmv1.HelmRepository{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "helmrepository-", + }, + Spec: helmv1.HelmRepositorySpec{ + URL: "oci://ghcr.io/stefanprodan/charts", + Timeout: &metav1.Duration{Duration: timeout}, + Provider: helmv1.GenericOCIProvider, + Type: helmv1.HelmRepositoryTypeOCI, + }, + } + clientBuilder.WithObjects(repository) + + r := &HelmChartReconciler{ + Client: clientBuilder.Build(), + EventRecorder: record.NewFakeRecorder(32), + Getters: testGetters, + Storage: testStorage, + RegistryClientGenerator: registry.ClientGenerator, + patchOptions: getPatchOptions(helmChartReadyCondition.Owned, "sc"), + } + + obj := &helmv1.HelmChart{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "helmchart-", + }, + Spec: helmv1.HelmChartSpec{ + SourceRef: helmv1.LocalHelmChartSourceReference{ + Kind: helmv1.HelmRepositoryKind, + Name: repository.Name, + }, + Version: tt.version, + Chart: "podinfo", + Verify: &helmv1.OCIRepositoryVerification{ + Provider: "cosign", + }, + }, + } + chartUrl := fmt.Sprintf("%s/%s:%s", repository.Spec.URL, obj.Spec.Chart, obj.Spec.Version) + + assertConditions := tt.assertConditions + for k := range assertConditions { + assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", obj.Spec.Chart) + assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", obj.Spec.Version) + assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", chartUrl) + assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", "cosign") + } + + if tt.beforeFunc != nil { + tt.beforeFunc(obj) + } + + g.Expect(r.Client.Create(ctx, obj)).ToNot(HaveOccurred()) + defer func() { + g.Expect(r.Client.Delete(ctx, obj)).ToNot(HaveOccurred()) + }() + + sp := patch.NewSerialPatcher(obj, r.Client) + + var b chart.Build + got, err := r.reconcileSource(ctx, sp, obj, &b) + if tt.wantErr { + tt.wantErrMsg = strings.ReplaceAll(tt.wantErrMsg, "", chartUrl) + g.Expect(err).ToNot(BeNil()) + g.Expect(err.Error()).To(ContainSubstring(tt.wantErrMsg)) + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + g.Expect(got).To(Equal(tt.want)) + g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions)) + }) + } +} + func TestHelmChartReconciler_reconcileSourceFromOCI_verifySignature(t *testing.T) { g := NewWithT(t) diff --git a/internal/controller/ocirepository_controller.go b/internal/controller/ocirepository_controller.go index 7257b9665..5141c6763 100644 --- a/internal/controller/ocirepository_controller.go +++ b/internal/controller/ocirepository_controller.go @@ -36,6 +36,7 @@ import ( gcrv1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/sigstore/cosign/v2/pkg/cosign" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -664,6 +665,16 @@ func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *ociv // if no secret is provided, try keyless verification ctrl.LoggerFrom(ctx).Info("no secret reference is provided, trying to verify the image using keyless method") + + var identities []cosign.Identity + for _, match := range obj.Spec.Verify.MatchOIDCIdentity { + identities = append(identities, cosign.Identity{ + IssuerRegExp: match.Issuer, + SubjectRegExp: match.Subject, + }) + } + defaultCosignOciOpts = append(defaultCosignOciOpts, soci.WithIdentities(identities)) + verifier, err := soci.NewCosignVerifier(ctxTimeout, defaultCosignOciOpts...) if err != nil { return err diff --git a/internal/controller/ocirepository_controller_test.go b/internal/controller/ocirepository_controller_test.go index 18ee68dc8..bb0bc0277 100644 --- a/internal/controller/ocirepository_controller_test.go +++ b/internal/controller/ocirepository_controller_test.go @@ -1436,6 +1436,155 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { } } +func TestOCIRepository_reconcileSource_verifyOCISourceSignature_keyless(t *testing.T) { + tests := []struct { + name string + reference *ociv1.OCIRepositoryRef + want sreconcile.Result + wantErr bool + wantErrMsg string + beforeFunc func(obj *ociv1.OCIRepository) + assertConditions []metav1.Condition + revision string + }{ + { + name: "signed image with no identity matching specified should pass verification", + reference: &ociv1.OCIRepositoryRef{ + Tag: "6.5.1", + }, + want: sreconcile.ResultSuccess, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '' for ''"), + *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '' for ''"), + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision "), + }, + revision: "6.5.1@sha256:049fff8f9c92abba8615c6c3dcf9d10d30082213f6fe86c9305257f806c31e31", + }, + { + name: "signed image with correct subject and issuer should pass verification", + reference: &ociv1.OCIRepositoryRef{ + Tag: "6.5.1", + }, + want: sreconcile.ResultSuccess, + beforeFunc: func(obj *ociv1.OCIRepository) { + obj.Spec.Verify.MatchOIDCIdentity = []ociv1.OIDCIdentityMatch{ + { + + Subject: "^https://github.com/stefanprodan/podinfo.*$", + Issuer: "^https://token.actions.githubusercontent.com$", + }, + } + }, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '' for ''"), + *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '' for ''"), + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision "), + }, + revision: "6.5.1@sha256:049fff8f9c92abba8615c6c3dcf9d10d30082213f6fe86c9305257f806c31e31", + }, + { + name: "signed image with incorrect subject and issuer should not pass verification", + reference: &ociv1.OCIRepositoryRef{ + Tag: "6.5.1", + }, + wantErr: true, + want: sreconcile.ResultEmpty, + beforeFunc: func(obj *ociv1.OCIRepository) { + obj.Spec.Verify.MatchOIDCIdentity = []ociv1.OIDCIdentityMatch{ + { + Subject: "intruder", + Issuer: "^https://honeypot.com$", + }, + } + }, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '' for ''"), + *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '' for ''"), + *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify the signature using provider ' keyless': no matching signatures: none of the expected identities matched what was in the certificate"), + }, + revision: "6.5.1@sha256:049fff8f9c92abba8615c6c3dcf9d10d30082213f6fe86c9305257f806c31e31", + }, + { + name: "unsigned image should not pass verification", + reference: &ociv1.OCIRepositoryRef{ + Tag: "6.1.0", + }, + wantErr: true, + want: sreconcile.ResultEmpty, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '' for ''"), + *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '' for ''"), + *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify the signature using provider ' keyless': no matching signatures"), + }, + revision: "6.1.0@sha256:3816fe9636a297f0c934b1fa0f46fe4c068920375536ac2803604adfb4c55894", + }, + } + + clientBuilder := fakeclient.NewClientBuilder(). + WithScheme(testEnv.GetScheme()). + WithStatusSubresource(&ociv1.OCIRepository{}) + + r := &OCIRepositoryReconciler{ + Client: clientBuilder.Build(), + EventRecorder: record.NewFakeRecorder(32), + Storage: testStorage, + patchOptions: getPatchOptions(ociRepositoryReadyCondition.Owned, "sc"), + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + obj := &ociv1.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "verify-oci-source-signature-", + Generation: 1, + }, + Spec: ociv1.OCIRepositorySpec{ + URL: "oci://ghcr.io/stefanprodan/manifests/podinfo", + Verify: &ociv1.OCIRepositoryVerification{ + Provider: "cosign", + }, + Interval: metav1.Duration{Duration: interval}, + Timeout: &metav1.Duration{Duration: timeout}, + Reference: tt.reference, + }, + } + url := strings.TrimPrefix(obj.Spec.URL, "oci://") + ":" + tt.reference.Tag + + assertConditions := tt.assertConditions + for k := range assertConditions { + assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", tt.revision) + assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", url) + assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", "cosign") + } + + if tt.beforeFunc != nil { + tt.beforeFunc(obj) + } + + g.Expect(r.Client.Create(ctx, obj)).ToNot(HaveOccurred()) + defer func() { + g.Expect(r.Client.Delete(ctx, obj)).ToNot(HaveOccurred()) + }() + + sp := patch.NewSerialPatcher(obj, r.Client) + + artifact := &sourcev1.Artifact{} + got, err := r.reconcileSource(ctx, sp, obj, artifact, t.TempDir()) + if tt.wantErr { + tt.wantErrMsg = strings.ReplaceAll(tt.wantErrMsg, "", url) + g.Expect(err).ToNot(BeNil()) + g.Expect(err.Error()).To(ContainSubstring(tt.wantErrMsg)) + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + g.Expect(got).To(Equal(tt.want)) + g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions)) + }) + } +} + func TestOCIRepository_reconcileSource_noop(t *testing.T) { g := NewWithT(t) diff --git a/internal/oci/verifier.go b/internal/oci/verifier.go index 77306c7d7..b066ce966 100644 --- a/internal/oci/verifier.go +++ b/internal/oci/verifier.go @@ -40,8 +40,9 @@ type Verifier interface { // options is a struct that holds options for verifier. type options struct { - PublicKey []byte - ROpt []remote.Option + PublicKey []byte + ROpt []remote.Option + Identities []cosign.Identity } // Options is a function that configures the options applied to a Verifier. @@ -62,6 +63,14 @@ func WithRemoteOptions(opts ...remote.Option) Options { } } +// WithIdentities specifies the identity matchers that have to be met +// for the signature to be deemed valid. +func WithIdentities(identities []cosign.Identity) Options { + return func(opts *options) { + opts.Identities = identities + } +} + // CosignVerifier is a struct which is responsible for executing verification logic. type CosignVerifier struct { opts *cosign.CheckOpts @@ -82,6 +91,16 @@ func NewCosignVerifier(ctx context.Context, opts ...Options) (*CosignVerifier, e return nil, err } + checkOpts.Identities = o.Identities + if len(checkOpts.Identities) == 0 { + checkOpts.Identities = []cosign.Identity{ + { + IssuerRegExp: ".*", + SubjectRegExp: ".*", + }, + } + } + if o.ROpt != nil { co = append(co, ociremote.WithRemoteOptions(o.ROpt...)) } @@ -141,17 +160,7 @@ func NewCosignVerifier(ctx context.Context, opts ...Options) (*CosignVerifier, e // VerifyImageSignatures verify the authenticity of the given ref OCI image. func (v *CosignVerifier) VerifyImageSignatures(ctx context.Context, ref name.Reference) ([]oci.Signature, bool, error) { - opts := v.opts - - // TODO: expose the match conditions in the CRD - opts.Identities = []cosign.Identity{ - { - IssuerRegExp: ".*", - SubjectRegExp: ".*", - }, - } - - return cosign.VerifyImageSignatures(ctx, ref, opts) + return cosign.VerifyImageSignatures(ctx, ref, v.opts) } // Verify verifies the authenticity of the given ref OCI image. diff --git a/internal/oci/verifier_test.go b/internal/oci/verifier_test.go index 8b3ae3865..a3220684a 100644 --- a/internal/oci/verifier_test.go +++ b/internal/oci/verifier_test.go @@ -23,6 +23,7 @@ import ( "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/sigstore/cosign/v2/pkg/cosign" ) func TestOptions(t *testing.T) { @@ -75,6 +76,22 @@ func TestOptions(t *testing.T) { remote.WithTransport(http.DefaultTransport), }, }, + }, { + name: "identities option", + opts: []Options{WithIdentities([]cosign.Identity{ + { + SubjectRegExp: "test-user", + IssuerRegExp: "^https://token.actions.githubusercontent.com$", + }, + })}, + want: &options{ + Identities: []cosign.Identity{ + { + SubjectRegExp: "test-user", + IssuerRegExp: "^https://token.actions.githubusercontent.com$", + }, + }, + }, }, }