Skip to content

Commit

Permalink
cosign: allow identity matching for keyless verification
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
aryan9600 committed Oct 9, 2023
1 parent ff39d21 commit 769f2a3
Show file tree
Hide file tree
Showing 11 changed files with 519 additions and 13 deletions.
22 changes: 22 additions & 0 deletions api/v1beta2/ocirepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
// +optional
Issuer string `json:"issuer,omitempty"`
// 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.
// +optional
Subject string `json:"subject,omitempty"`
}

// OCIRepositoryStatus defines the observed state of OCIRepository
Expand Down
20 changes: 20 additions & 0 deletions api/v1beta2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,29 @@ 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
type: object
type: array
provider:
default: cosign
description: Provider specifies the technology used to sign the
Expand Down
23 changes: 23 additions & 0 deletions config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,29 @@ 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
type: object
type: array
provider:
default: cosign
description: Provider specifies the technology used to sign the
Expand Down
67 changes: 67 additions & 0 deletions docs/api/v1beta2/source.md
Original file line number Diff line number Diff line change
Expand Up @@ -3295,6 +3295,73 @@ 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>
<em>(Optional)</em>
<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>
<em>(Optional)</em>
<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>
Expand Down
10 changes: 10 additions & 0 deletions internal/controller/helmchart_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
155 changes: 155 additions & 0 deletions internal/controller/helmchart_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 <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 = []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 <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 = []ociv1.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 {
tt.wantErrMsg = strings.ReplaceAll(tt.wantErrMsg, "<url>", 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)

Expand Down
Loading

0 comments on commit 769f2a3

Please sign in to comment.