cosign: allow identity matching for keyless verification

Add `.spec.verify.matchOIDCIdentity` to OCIRepository and HelmChart.
It allows specifying regular expressions to match against the subject and
issuer of the certificate related to the artifact signature. Its used
only if the artifact was signed using Cosign keyless signing.

Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
This commit is contained in:
Sanskar Jaiswal 2023-10-05 17:25:03 +05:30
parent 8c63fba06b
commit d855805b8f
No known key found for this signature in database
GPG Key ID: 5982D0279C227FFD
11 changed files with 568 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -3319,6 +3319,71 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
trusted public keys.</p>
</td>
</tr>
<tr>
<td>
<code>matchOIDCIdentity</code><br>
<em>
<a href="#source.toolkit.fluxcd.io/v1beta2.OIDCIdentityMatch">
[]OIDCIdentityMatch
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>MatchOIDCIdentity specifies the identity matching criteria to use
while verifying an OCI artifact which was signed using Cosign keyless
signing. The artifact&rsquo;s identity is deemed to be verified if any of the
specified matchers match against the identity.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="source.toolkit.fluxcd.io/v1beta2.OIDCIdentityMatch">OIDCIdentityMatch
</h3>
<p>
(<em>Appears on:</em>
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositoryVerification">OCIRepositoryVerification</a>)
</p>
<p>OIDCIdentityMatch specifies options for verifying the certificate identity,
i.e. the issuer and the subject of the certificate.</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>issuer</code><br>
<em>
string
</em>
</td>
<td>
<p>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.</p>
</td>
</tr>
<tr>
<td>
<code>subject</code><br>
<em>
string
</em>
</td>
<td>
<p>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.</p>
</td>
</tr>
</tbody>
</table>
</div>

View File

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

View File

@ -2533,6 +2533,181 @@ 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
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 <version>"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled '<name>' chart with version '<version>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled '<name>' chart with version '<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 = []helmv1.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 <version>"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled '<name>' chart with version '<version>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled '<name>' chart with version '<version>'"),
},
revision: "6.5.1@sha256:af589b918022cd8d85a4543312d28170c2e894ccab8484050ff4bdefdde30b4e",
},
{
name: "signed image with incorrect and correct identity matchers should pass verification",
version: "6.5.1",
want: sreconcile.ResultSuccess,
beforeFunc: func(obj *helmv1.HelmChart) {
obj.Spec.Verify.MatchOIDCIdentity = []helmv1.OIDCIdentityMatch{
{
Subject: "intruder",
Issuer: "^https://honeypot.com$",
},
{
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 <version>"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled '<name>' chart with version '<version>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled '<name>' chart with version '<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 = []helmv1.OIDCIdentityMatch{
{
Subject: "intruder",
Issuer: "^https://honeypot.com$",
},
}
},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.BuildFailedCondition, "ChartVerificationError", "chart verification error: failed to verify <url>: 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 <url>: 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 <url>: no matching signatures"),
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "chart verification error: failed to verify <url>: 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, "<name>", obj.Spec.Chart)
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<version>", obj.Spec.Version)
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<url>", chartUrl)
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<provider>", "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 {
g.Expect(err).To(HaveOccurred())
} 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)

View File

@ -35,6 +35,7 @@ import (
"github.com/google/go-containerregistry/pkg/name"
gcrv1 "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"
@ -663,6 +664,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

View File

@ -1435,6 +1435,181 @@ 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 '<revision>' for '<url>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision <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 '<revision>' for '<url>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision <revision>"),
},
revision: "6.5.1@sha256:049fff8f9c92abba8615c6c3dcf9d10d30082213f6fe86c9305257f806c31e31",
},
{
name: "signed image with both incorrect and correct identity matchers 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: "intruder",
Issuer: "^https://honeypot.com$",
},
{
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 '<revision>' for '<url>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision <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 '<revision>' for '<url>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify the signature using provider '<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 '<revision>' for '<url>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '<revision>' for '<url>'"),
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify the signature using provider '<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, "<revision>", tt.revision)
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<url>", url)
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<provider>", "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 {
g.Expect(err).To(HaveOccurred())
tt.wantErrMsg = strings.ReplaceAll(tt.wantErrMsg, "<url>", url)
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)

View File

@ -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,7 @@ func NewCosignVerifier(ctx context.Context, opts ...Options) (*CosignVerifier, e
return nil, err
}
checkOpts.Identities = o.Identities
if o.ROpt != nil {
co = append(co, ociremote.WithRemoteOptions(o.ROpt...))
}
@ -141,17 +151,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.

View File

@ -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,30 @@ 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$",
},
{
SubjectRegExp: "dev-user",
IssuerRegExp: "^https://accounts.google.com$",
},
})},
want: &options{
Identities: []cosign.Identity{
{
SubjectRegExp: "test-user",
IssuerRegExp: "^https://token.actions.githubusercontent.com$",
},
{
SubjectRegExp: "dev-user",
IssuerRegExp: "^https://accounts.google.com$",
},
},
},
},
}