@@ -23,6 +23,7 @@ import (
2323 "net/url"
2424 "os"
2525 "path/filepath"
26+ "sort"
2627 "strings"
2728 "time"
2829
@@ -33,6 +34,7 @@ import (
3334 "github.com/fluxcd/pkg/runtime/logger"
3435 "github.com/fluxcd/pkg/runtime/secrets"
3536 "github.com/go-git/go-git/v5/plumbing/transport"
37+ "github.com/opencontainers/go-digest"
3638 ssh "golang.org/x/crypto/ssh"
3739 corev1 "k8s.io/api/core/v1"
3840 "k8s.io/apimachinery/pkg/runtime"
@@ -592,7 +594,9 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
592594 // reconciliation can be skipped if other configurations have not changed.
593595 if ! git .IsConcreteCommit (* commit ) {
594596 // Check if the content config contributing to the artifact has changed.
595- if ! gitContentConfigChanged (obj , includes ) {
597+ // A change to the verification policy (e.g. a key rotation) also
598+ // requires a new verification of the unchanged revision.
599+ if ! gitContentConfigChanged (obj , includes ) && ! r .verificationPolicyChanged (ctx , obj ) {
596600 ge := serror .NewGeneric (
597601 fmt .Errorf ("no changes since last reconciliation: observed revision '%s'" ,
598602 commitReference (obj , commit )), sourcev1 .GitOperationSucceedReason ,
@@ -1112,6 +1116,64 @@ func (r *GitRepositoryReconciler) fetchIncludes(ctx context.Context, obj *source
11121116 return & artifacts , nil
11131117}
11141118
1119+ // verificationKeys returns the PGP key rings and SSH authorized keys contained
1120+ // in the given Secret. Data entries with an SSH public key suffix are treated as
1121+ // authorized keys, entries with a PGP public key suffix (or no known suffix) as
1122+ // PGP key rings.
1123+ func verificationKeys (secret * corev1.Secret ) (keyRings , authorizedKeys []string ) {
1124+ for k , v := range secret .Data {
1125+ switch {
1126+ case strings .HasSuffix (k , publicKeySSHSuffix ):
1127+ authorizedKeys = append (authorizedKeys , string (v ))
1128+ case strings .HasSuffix (k , publicKeyPGPSuffix ):
1129+ keyRings = append (keyRings , string (v ))
1130+ default :
1131+ // Provide fallback to support previous undocumented behavior
1132+ keyRings = append (keyRings , string (v ))
1133+ }
1134+ }
1135+ return keyRings , authorizedKeys
1136+ }
1137+
1138+ // verificationFingerprint returns a stable fingerprint of the public keys in
1139+ // the given Secret, independent of the Secret name or the data key names. It is
1140+ // used to detect a change in the verification policy, e.g. a key rotation, that
1141+ // requires the current revision to be verified again.
1142+ func verificationFingerprint (secret * corev1.Secret ) string {
1143+ keyRings , authorizedKeys := verificationKeys (secret )
1144+ sort .Strings (keyRings )
1145+ sort .Strings (authorizedKeys )
1146+
1147+ var b strings.Builder
1148+ for _ , k := range keyRings {
1149+ b .WriteString ("pgp:" )
1150+ b .WriteString (k )
1151+ b .WriteByte (0 )
1152+ }
1153+ for _ , k := range authorizedKeys {
1154+ b .WriteString ("ssh:" )
1155+ b .WriteString (k )
1156+ b .WriteByte (0 )
1157+ }
1158+ return digest .Canonical .FromString (b .String ()).String ()
1159+ }
1160+
1161+ // verificationPolicyChanged returns true if the public keys trusted for
1162+ // verification differ from the ones used for the last successful verification,
1163+ // or if the current policy can not be determined. A changed policy requires the
1164+ // current revision to be verified again, even if it did not change.
1165+ func (r * GitRepositoryReconciler ) verificationPolicyChanged (ctx context.Context , obj * sourcev1.GitRepository ) bool {
1166+ if obj .Spec .Verification == nil || obj .Spec .Verification .Mode == "" {
1167+ return false
1168+ }
1169+ secret , err := r .getSecret (ctx , obj .Spec .Verification .SecretRef .Name , obj .GetNamespace ())
1170+ if err != nil {
1171+ // Return true so the full reconciliation surfaces the error.
1172+ return true
1173+ }
1174+ return verificationFingerprint (secret ) != obj .Status .SourceVerificationFingerprint
1175+ }
1176+
11151177// verifySignature verifies the signature of the given Git commit and/or its referencing tag
11161178// depending on the verification mode specified on the object.
11171179// If the signature can not be verified or the verification fails, it records
@@ -1124,6 +1186,7 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
11241186 // observations if there is none
11251187 if obj .Spec .Verification == nil || obj .Spec .Verification .Mode == "" {
11261188 obj .Status .SourceVerificationMode = nil
1189+ obj .Status .SourceVerificationFingerprint = ""
11271190 conditions .Delete (obj , sourcev1 .SourceVerifiedCondition )
11281191 return sreconcile .ResultSuccess , nil
11291192 }
@@ -1143,18 +1206,7 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
11431206 return sreconcile .ResultEmpty , e
11441207 }
11451208
1146- var keyRings []string
1147- var authorizedKeys []string
1148- for k , v := range secret .Data {
1149- if strings .HasSuffix (k , publicKeySSHSuffix ) {
1150- authorizedKeys = append (authorizedKeys , string (v ))
1151- } else if strings .HasSuffix (k , publicKeyPGPSuffix ) {
1152- keyRings = append (keyRings , string (v ))
1153- } else {
1154- // Provide fallback to support previous undocumented behavior
1155- keyRings = append (keyRings , string (v ))
1156- }
1157- }
1209+ keyRings , authorizedKeys := verificationKeys (secret )
11581210
11591211 var message strings.Builder
11601212 if obj .Spec .Verification .VerifyTag () {
@@ -1217,6 +1269,7 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
12171269 reason := meta .SucceededReason
12181270 mode := obj .Spec .Verification .GetMode ()
12191271 obj .Status .SourceVerificationMode = & mode
1272+ obj .Status .SourceVerificationFingerprint = verificationFingerprint (secret )
12201273 conditions .MarkTrue (obj , sourcev1 .SourceVerifiedCondition , reason , "%s" , message .String ())
12211274 r .eventLogf (ctx , obj , eventv1 .EventTypeTrace , reason , "%s" , message .String ())
12221275 return sreconcile .ResultSuccess , nil
0 commit comments