Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/v1/ocirepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ type OCIRepositoryStatus struct {
// +optional
ObservedLayerSelector *OCILayerSelector `json:"observedLayerSelector,omitempty"`

// SourceVerificationFingerprint is the fingerprint of the verification
// material used to verify the signature of the current Artifact. It is
// used to detect changes to the verification policy, such as a key
// rotation, that require the current revision to be verified again.
// +optional
SourceVerificationFingerprint string `json:"sourceVerificationFingerprint,omitempty"`

meta.ReconcileRequestStatus `json:",inline"`
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,13 @@ spec:
- copy
type: string
type: object
sourceVerificationFingerprint:
description: |-
SourceVerificationFingerprint is the fingerprint of the verification
material used to verify the signature of the current Artifact. It is
used to detect changes to the verification policy, such as a key
rotation, that require the current revision to be verified again.
type: string
url:
description: URL is the download link for the artifact output of the
last OCI Repository sync.
Expand Down
15 changes: 15 additions & 0 deletions docs/api/v1/source.md
Original file line number Diff line number Diff line change
Expand Up @@ -3672,6 +3672,21 @@ the source artifact.</p>
</tr>
<tr>
<td>
<code>sourceVerificationFingerprint</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>SourceVerificationFingerprint is the fingerprint of the verification
material used to verify the signature of the current Artifact. It is
used to detect changes to the verification policy, such as a key
rotation, that require the current revision to be verified again.</p>
</td>
</tr>
<tr>
<td>
<code>ReconcileRequestStatus</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#ReconcileRequestStatus">
Expand Down
17 changes: 17 additions & 0 deletions docs/spec/v1/ocirepositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,12 @@ spec:
By default, the controller verifies the signatures using the Fulcio root CA and
the Rekor instance hosted at [rekor.sigstore.dev](https://rekor.sigstore.dev/).

Note that rotations of the public Sigstore trust anchors (Fulcio, Rekor, CT log
and TSA keys) are not detected. When no `.spec.verify.trustedRootSecretRef` is
set, the `SourceVerified` condition is not re-evaluated on a trust root change,
so verification is retried only when the artifact revision or the spec changes.
To track a specific trust root, pin it with `.spec.verify.trustedRootSecretRef`.

##### Custom Sigstore infrastructure (self-hosted Rekor / Fulcio)

To verify artifacts signed with a self-hosted Sigstore deployment, provide a
Expand Down Expand Up @@ -1183,6 +1189,17 @@ status:
...
```

### Source Verification Fingerprint

The source-controller reports a fingerprint of the verification material it used
to verify the signature of the current Artifact in the OCIRepository's
`.status.sourceVerificationFingerprint`. The fingerprint is derived from the
referenced Secrets, such as the Cosign public keys or the Notation trust policy
and certificates, and does not depend on the Secret names. It is used by the
controller to detect a change in the verification policy, such as a key
rotation, that requires the current revision to be verified again even when its
revision did not change.

### Observed Generation

The source-controller reports an [observed generation][typical-status-properties]
Expand Down
111 changes: 110 additions & 1 deletion internal/controller/ocirepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/opencontainers/go-digest"
"github.com/sigstore/cosign/v3/pkg/cosign"
"helm.sh/helm/v4/pkg/registry"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -473,12 +474,15 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
// - the upstream digest differs from the one in storage (revision drift)
// - the OCIRepository spec has changed (generation drift)
// - the previous reconciliation resulted in a failed artifact verification (retry with exponential backoff)
// - the verification policy (e.g. a key rotation) has changed (policy drift)
if obj.Spec.Verify == nil {
// Remove old observations if verification was disabled
conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
obj.Status.SourceVerificationFingerprint = ""
} else if !obj.GetArtifact().HasRevision(revision) ||
conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation ||
conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) {
conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) ||
r.verificationPolicyChanged(ctx, obj) {

result, err := r.verifySignature(ctx, obj, digestRef, keychain, authenticator, transport, opts...)
if err != nil {
Expand All @@ -496,6 +500,9 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch

if result == soci.VerificationResultSuccess {
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision %s", revision)
if fingerprint, err := r.verificationFingerprint(ctx, obj); err == nil {
obj.Status.SourceVerificationFingerprint = fingerprint
}
}
}

Expand Down Expand Up @@ -661,6 +668,108 @@ func (r *OCIRepositoryReconciler) digestFromRevision(revision string) string {
return parts[len(parts)-1]
}

// verificationPolicyChanged returns true if the verification material referenced
// by the object differs from the one used for the last successful verification,
// or if the current policy can not be determined. A changed policy requires the
// current revision to be verified again, even if it did not change.
func (r *OCIRepositoryReconciler) verificationPolicyChanged(ctx context.Context, obj *sourcev1.OCIRepository) bool {
if obj.Spec.Verify == nil {
return false
}
fingerprint, err := r.verificationFingerprint(ctx, obj)
if err != nil {
// Return true so the full reconciliation surfaces the error.
return true
}
return fingerprint != obj.Status.SourceVerificationFingerprint
}

// verificationFingerprint returns a stable fingerprint of the verification
// material referenced by the object. It is used to detect a change in the
// verification policy, e.g. a key rotation, that requires the current revision
// to be verified again even if it did not change.
func (r *OCIRepositoryReconciler) verificationFingerprint(ctx context.Context, obj *sourcev1.OCIRepository) (string, error) {
verify := obj.Spec.Verify
if verify == nil {
return "", nil
}

var trustedRoot []byte
if ref := verify.TrustedRootSecretRef; ref != nil && verify.Provider == "cosign" {
data, err := readTrustedRootFromSecret(ctx, r.Client, obj.Namespace, ref)
if err != nil {
return "", err
}
trustedRoot = data
}

var secret *corev1.Secret
if ref := verify.SecretRef; ref != nil {
s, err := r.retrieveSecret(ctx, types.NamespacedName{Namespace: obj.Namespace, Name: ref.Name})
if err != nil {
return "", err
}
secret = &s
}

return verificationMaterialFingerprint(verify.Provider, secret, trustedRoot), nil
}

// verificationMaterialFingerprint returns a stable fingerprint of the given
// verification material. It is independent of the Secret name and of the data
// key names, as the policy is defined by the material itself, not its location.
func verificationMaterialFingerprint(provider string, secret *corev1.Secret, trustedRoot []byte) string {
var b strings.Builder
b.WriteString("provider:")
b.WriteString(provider)
b.WriteByte(0)

if len(trustedRoot) > 0 {
b.WriteString("trustedroot:")
b.Write(trustedRoot)
b.WriteByte(0)
}

if secret != nil {
switch provider {
case "notation":
if data, ok := secret.Data[notation.DefaultTrustPolicyKey]; ok {
b.WriteString("trustpolicy:")
b.Write(data)
b.WriteByte(0)
}
var certs []string
for k, v := range secret.Data {
if strings.HasSuffix(k, ".crt") || strings.HasSuffix(k, ".pem") {
certs = append(certs, string(v))
}
}
sort.Strings(certs)
for _, cert := range certs {
b.WriteString("cert:")
b.WriteString(cert)
b.WriteByte(0)
}
default:
// cosign: the public keys used for verification.
var keys []string
for k, v := range secret.Data {
if strings.HasSuffix(k, ".pub") {
keys = append(keys, string(v))
}
}
sort.Strings(keys)
for _, key := range keys {
b.WriteString("pub:")
b.WriteString(key)
b.WriteByte(0)
}
}
}

return digest.Canonical.FromString(b.String()).String()
}

// verifySignature verifies the authenticity of the given image reference URL.
// It supports two different verification providers: cosign and notation.
// First, it tries to use a key if a Secret with a valid public key is provided.
Expand Down
Loading
Loading