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:
parent
8c63fba06b
commit
d855805b8f
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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’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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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$",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue