From f565efe66b3aa04d2bab88c7433cdbdbb4af7a35 Mon Sep 17 00:00:00 2001 From: Ricardo Bartels Date: Tue, 9 Jun 2026 06:54:03 +0200 Subject: [PATCH] gitrepository: adds additional SSH signature test condition Signed-off-by: Ricardo Bartels --- api/v1/gitrepository_types.go | 3 +- ...rce.toolkit.fluxcd.io_gitrepositories.yaml | 3 +- docs/api/v1/source.md | 3 +- docs/spec/v1/gitrepositories.md | 43 +- .../controller/gitrepository_controller.go | 65 ++- .../gitrepository_controller_test.go | 406 +++++++++++++++++- 6 files changed, 503 insertions(+), 20 deletions(-) diff --git a/api/v1/gitrepository_types.go b/api/v1/gitrepository_types.go index d7468fca6..f9fdaa906 100644 --- a/api/v1/gitrepository_types.go +++ b/api/v1/gitrepository_types.go @@ -242,7 +242,8 @@ type GitRepositoryVerification struct { Mode GitVerificationMode `json:"mode,omitempty"` // SecretRef specifies the Secret containing the public keys of trusted Git - // authors. + // authors. PGP public keys must be stored under keys with the .asc suffix, + // and SSH public keys must be stored under keys with the .sshpub suffix. // +required SecretRef meta.LocalObjectReference `json:"secretRef"` } diff --git a/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml index 1ad69e5ed..aeae0699d 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml @@ -226,7 +226,8 @@ spec: secretRef: description: |- SecretRef specifies the Secret containing the public keys of trusted Git - authors. + authors. PGP public keys must be stored under keys with the .asc suffix, + and SSH public keys must be stored under keys with the .sshpub suffix. properties: name: description: Name of the referent. diff --git a/docs/api/v1/source.md b/docs/api/v1/source.md index 647fa7269..78cbabcea 100644 --- a/docs/api/v1/source.md +++ b/docs/api/v1/source.md @@ -2491,7 +2491,8 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference

SecretRef specifies the Secret containing the public keys of trusted Git -authors.

+authors. PGP public keys must be stored under keys with the .asc suffix, +and SSH public keys must be stored under keys with the .sshpub suffix.

diff --git a/docs/spec/v1/gitrepositories.md b/docs/spec/v1/gitrepositories.md index 96e1590dc..97fecc4e7 100644 --- a/docs/spec/v1/gitrepositories.md +++ b/docs/spec/v1/gitrepositories.md @@ -639,7 +639,10 @@ signatures. The field offers two subfields: the commit object pointed to by the tag. - `.secretRef.name`, to specify a reference to a Secret in the same namespace as - the GitRepository. Containing the (PGP) public keys of trusted Git authors. + the GitRepository. Containing the public keys of trusted Git authors. PGP + public keys must be stored under keys with the `.asc` suffix, and SSH public + keys must be stored under keys with the `.sshpub` suffix. Keys without a + recognized suffix are treated as PGP key rings for backward compatibility. ```yaml --- @@ -695,6 +698,44 @@ kubectl create secret generic pgp-public-keys \ -o yaml ``` +#### SSH verification + +SSH-signed commits and tags can also be verified. Store SSH public keys in +`authorized_keys` format under keys with the `.sshpub` suffix in the same +Secret: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: verification-keys + namespace: default +type: Opaque +data: + author1.asc: + author2.sshpub: +``` + +Generating an SSH key pair and creating the Secret: + +```sh +# Generate an SSH key pair for signing +ssh-keygen -t ed25519 -N '' -f /tmp/signing_key +# Generate secret with the public key +kubectl create secret generic verification-keys \ + --from-file=author2.sshpub=/tmp/signing_key.pub \ + -o yaml +``` + +A single Secret can contain both PGP (`.asc`) and SSH (`.sshpub`) keys. The +controller detects the signature type of each Git object (PGP or SSH) and +dispatches verification accordingly. + +PGP verification reports the PGP key ID in the success message (e.g. +`5982D0279C227FFD`), while SSH verification reports the SHA256 fingerprint +(e.g. `SHA256:uNiVztksCsDhcc0u9e8BgrJXVGDaf6s7kOsTmI9N7sM`). + ### Ignore `.spec.ignore` is an optional field to specify rules in [the `.gitignore` diff --git a/internal/controller/gitrepository_controller.go b/internal/controller/gitrepository_controller.go index d5b39e7a9..a3c27c9b6 100644 --- a/internal/controller/gitrepository_controller.go +++ b/internal/controller/gitrepository_controller.go @@ -71,6 +71,41 @@ import ( "github.com/fluxcd/source-controller/internal/util" ) +const ( + // publicKeyPGPSuffix is the Secret data key suffix for PGP public keys. + publicKeyPGPSuffix = ".asc" + // publicKeySSHSuffix is the Secret data key suffix for SSH public keys. + publicKeySSHSuffix = ".sshpub" +) + +// gitSigner abstracts the verification methods shared by git.Commit and git.Tag. +type gitSigner interface { + SignatureType() string + VerifyPGP(keyRings ...string) (string, error) + VerifySSH(authorizedKeys ...string) (string, error) +} + +// verifyGitObject dispatches signature verification based on the signature type +// of the given git object. It returns the key identity (PGP key ID or SSH +// fingerprint) on success, or an error if verification fails or the required +// key type is missing from the Secret. +func verifyGitObject(obj gitSigner, keyRings []string, authorizedKeys []string) (string, error) { + switch obj.SignatureType() { + case "openpgp": + if len(keyRings) == 0 { + return "", fmt.Errorf("PGP signature detected but no PGP public keys found in secret (keys with %s suffix)", publicKeyPGPSuffix) + } + return obj.VerifyPGP(keyRings...) + case "ssh": + if len(authorizedKeys) == 0 { + return "", fmt.Errorf("SSH signature detected but no SSH public keys found in secret (keys with %s suffix)", publicKeySSHSuffix) + } + return obj.VerifySSH(authorizedKeys...) + default: + return "", fmt.Errorf("unsupported signature type: %s", obj.SignatureType()) + } +} + // gitRepositoryReadyCondition contains the information required to summarize a // v1.GitRepository Ready Condition. var gitRepositoryReadyCondition = summarize.Conditions{ @@ -1093,7 +1128,7 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour return sreconcile.ResultSuccess, nil } - // Get secret with GPG data + // Get secret with public key data publicKeySecret := types.NamespacedName{ Namespace: obj.Namespace, Name: obj.Spec.Verification.SecretRef.Name, @@ -1101,7 +1136,7 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour secret := &corev1.Secret{} if err := r.Client.Get(ctx, publicKeySecret, secret); err != nil { e := serror.NewGeneric( - fmt.Errorf("PGP public keys secret error: %w", err), + fmt.Errorf("public keys secret error: %w", err), "VerificationError", ) conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, "%s", e) @@ -1109,8 +1144,16 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour } var keyRings []string - for _, v := range secret.Data { - keyRings = append(keyRings, string(v)) + var authorizedKeys []string + for k, v := range secret.Data { + if strings.HasSuffix(k, publicKeySSHSuffix) { + authorizedKeys = append(authorizedKeys, string(v)) + } else if strings.HasSuffix(k, publicKeyPGPSuffix) { + keyRings = append(keyRings, string(v)) + } else { + // Provide fallback to support previous undocumented behavior + keyRings = append(keyRings, string(v)) + } } var message strings.Builder @@ -1140,38 +1183,34 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour return sreconcile.ResultEmpty, err } - // Verify tag with GPG data from secret - tagEntity, err := tag.Verify(keyRings...) + entity, err := verifyGitObject(tag, keyRings, authorizedKeys) if err != nil { e := serror.NewGeneric( fmt.Errorf("signature verification of tag '%s' failed: %w", tag.String(), err), "InvalidTagSignature", ) conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, "%s", e) - // Return error in the hope the secret changes return sreconcile.ResultEmpty, e } - message.WriteString(fmt.Sprintf("verified signature of\n\t- tag '%s' with key '%s'", tag.String(), tagEntity)) + message.WriteString(fmt.Sprintf("verified signature of\n\t- tag '%s' with key '%s'", tag.String(), entity)) } if obj.Spec.Verification.VerifyHEAD() { - // Verify commit with GPG data from secret - headEntity, err := commit.Verify(keyRings...) + entity, err := verifyGitObject(&commit, keyRings, authorizedKeys) if err != nil { e := serror.NewGeneric( fmt.Errorf("signature verification of commit '%s' failed: %w", commit.Hash.String(), err), "InvalidCommitSignature", ) conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, "%s", e) - // Return error in the hope the secret changes return sreconcile.ResultEmpty, e } // If we also verified the tag previously, then append to the message. if message.Len() > 0 { - message.WriteString(fmt.Sprintf("\n\t- commit '%s' with key '%s'", commit.Hash.String(), headEntity)) + message.WriteString(fmt.Sprintf("\n\t- commit '%s' with key '%s'", commit.Hash.String(), entity)) } else { - message.WriteString(fmt.Sprintf("verified signature of\n\t- commit '%s' with key '%s'", commit.Hash.String(), headEntity)) + message.WriteString(fmt.Sprintf("verified signature of\n\t- commit '%s' with key '%s'", commit.Hash.String(), entity)) } } diff --git a/internal/controller/gitrepository_controller_test.go b/internal/controller/gitrepository_controller_test.go index 4b874e070..84b2074a8 100644 --- a/internal/controller/gitrepository_controller_test.go +++ b/internal/controller/gitrepository_controller_test.go @@ -168,6 +168,40 @@ zh1+m2pFDEhWZkaFtQbSEpXMIJ9DsCoyQL4Knl+89VxHsrIyAJsmGb3V8xvtv5w9 QuWtsDnYbvDHtTpu1NZChVrnr/l1k3C2fcLhV1s583AvhGMkbgSXkQ== =Tdjz -----END PGP PUBLIC KEY BLOCK----- +` + + encodedSSHCommitFixture = `tree 7c5bd8f246ab8e8c6a5749c3d2f44018aa029fb8 +author Test User 1767225600 +0000 +committer Test User 1767225600 +0000 + +Test commit signed with ed25519 +` + + signatureSSHCommitFixture = `-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgrYSEhPKV/65kzG2JLYU+586anT +AORbbZ0UW9qzon28EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 +AAAAQD72vwusTGiRbH8ZtPm53vl065Ocv6Sp6VbHq4mkONAM0mzDLrD7BmAgWkjtmL2JpK +msqgFJcKs6Z3E1zH86fQ0= +-----END SSH SIGNATURE----- +` + + encodedSSHTagFixture = `object b01ca16de562c561f62427eb0a30cb775d1c1dab +type commit +tag test-tag-ed25519 +tagger Test User 1767225600 +0000 + +Test tag signed with ed25519 +` + + signatureSSHTagFixture = `-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgrYSEhPKV/65kzG2JLYU+586anT +AORbbZ0UW9qzon28EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 +AAAAQOZe8xDqF1oktx3h9p3EQKSx13zFVHQ2YcpF/HfeuQusKA1tvJY+T+ykPH4+zbva93 +KXi2P5xm89dQni0kTeAAY= +-----END SSH SIGNATURE----- +` + + sshAuthorizedKeysFixture = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK2EhITylf+uZMxtiS2FPufOmp0wDkW22dFFvas6J9vB test-ed25519@example.com ` ) @@ -2223,7 +2257,7 @@ func TestGitRepositoryReconciler_verifySignature(t *testing.T) { }, }, { - name: "Invalid commit makes SourceVerifiedCondition=False and returns error", + name: "No PGP keys in secret makes SourceVerifiedCondition=False and returns error", secret: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "existing", @@ -2245,7 +2279,7 @@ func TestGitRepositoryReconciler_verifySignature(t *testing.T) { }, wantErr: true, assertConditions: []metav1.Condition{ - *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidCommitSignature", "signature verification of commit 'shasum' failed: unable to verify Git commit: unable to verify payload with any of the given key rings"), + *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidCommitSignature", "signature verification of commit 'shasum' failed: PGP signature detected but no PGP public keys found in secret (keys with .asc suffix)"), }, }, { @@ -2325,7 +2359,7 @@ func TestGitRepositoryReconciler_verifySignature(t *testing.T) { }, wantErr: true, assertConditions: []metav1.Condition{ - *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "VerificationError", "PGP public keys secret error: secrets \"none-existing\" not found"), + *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "VerificationError", "public keys secret error: secrets \"none-existing\" not found"), }, }, { @@ -2347,6 +2381,372 @@ func TestGitRepositoryReconciler_verifySignature(t *testing.T) { want: sreconcile.ResultSuccess, assertConditions: []metav1.Condition{}, }, + { + name: "Valid SSH-signed commit with mode=HEAD makes SourceVerifiedCondition=True", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ssh-keys", + }, + Data: map[string][]byte{ + "author1.sshpub": []byte(sshAuthorizedKeysFixture), + }, + }, + commit: git.Commit{ + Hash: []byte("shasum"), + Encoded: []byte(encodedSSHCommitFixture), + Signature: signatureSSHCommitFixture, + }, + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Interval = metav1.Duration{Duration: interval} + obj.Spec.Verification = &sourcev1.GitRepositoryVerification{ + Mode: sourcev1.ModeGitHEAD, + SecretRef: meta.LocalObjectReference{ + Name: "ssh-keys", + }, + } + }, + want: sreconcile.ResultSuccess, + wantSourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitHEAD), + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of\n\t- commit 'shasum' with key 'SHA256:SDB4adE/BP2VLwX9Pdf7aFUwW9JNdzoPSsHjd/wZIw4'"), + }, + }, + { + name: "Valid SSH-signed tag with mode=Tag makes SourceVerifiedCondition=True", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ssh-keys", + }, + Data: map[string][]byte{ + "author1.sshpub": []byte(sshAuthorizedKeysFixture), + }, + }, + commit: git.Commit{ + ReferencingTag: &git.Tag{ + Name: "v0.1.0", + Hash: []byte("shasum"), + Encoded: []byte(encodedSSHTagFixture), + Signature: signatureSSHTagFixture, + }, + }, + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Reference = &sourcev1.GitRepositoryRef{ + Tag: "v0.1.0", + } + obj.Spec.Interval = metav1.Duration{Duration: interval} + obj.Spec.Verification = &sourcev1.GitRepositoryVerification{ + Mode: sourcev1.ModeGitTag, + SecretRef: meta.LocalObjectReference{ + Name: "ssh-keys", + }, + } + }, + want: sreconcile.ResultSuccess, + wantSourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitTag), + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of\n\t- tag 'v0.1.0@shasum' with key 'SHA256:SDB4adE/BP2VLwX9Pdf7aFUwW9JNdzoPSsHjd/wZIw4'"), + }, + }, + { + name: "Valid SSH-signed tag and commit with mode=TagAndHEAD makes SourceVerifiedCondition=True", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ssh-keys", + }, + Data: map[string][]byte{ + "author1.sshpub": []byte(sshAuthorizedKeysFixture), + }, + }, + commit: git.Commit{ + Hash: []byte("shasum"), + Encoded: []byte(encodedSSHCommitFixture), + Signature: signatureSSHCommitFixture, + ReferencingTag: &git.Tag{ + Name: "v0.1.0", + Hash: []byte("shasum"), + Encoded: []byte(encodedSSHTagFixture), + Signature: signatureSSHTagFixture, + }, + }, + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Reference = &sourcev1.GitRepositoryRef{ + Tag: "v0.1.0", + } + obj.Spec.Interval = metav1.Duration{Duration: interval} + obj.Spec.Verification = &sourcev1.GitRepositoryVerification{ + Mode: sourcev1.ModeGitTagAndHEAD, + SecretRef: meta.LocalObjectReference{ + Name: "ssh-keys", + }, + } + }, + want: sreconcile.ResultSuccess, + wantSourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitTagAndHEAD), + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of\n\t- tag 'v0.1.0@shasum' with key 'SHA256:SDB4adE/BP2VLwX9Pdf7aFUwW9JNdzoPSsHjd/wZIw4'\n\t- commit 'shasum' with key 'SHA256:SDB4adE/BP2VLwX9Pdf7aFUwW9JNdzoPSsHjd/wZIw4'"), + }, + }, + { + name: "SSH-signed commit but Secret only has .asc keys makes SourceVerifiedCondition=False", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pgp-keys-only", + }, + Data: map[string][]byte{ + "author1.asc": []byte(armoredKeyRingFixture), + }, + }, + commit: git.Commit{ + Hash: []byte("shasum"), + Encoded: []byte(encodedSSHCommitFixture), + Signature: signatureSSHCommitFixture, + }, + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Interval = metav1.Duration{Duration: interval} + obj.Spec.Verification = &sourcev1.GitRepositoryVerification{ + Mode: sourcev1.ModeGitHEAD, + SecretRef: meta.LocalObjectReference{ + Name: "pgp-keys-only", + }, + } + }, + wantErr: true, + assertConditions: []metav1.Condition{ + *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidCommitSignature", "SSH signature detected but no SSH public keys found in secret (keys with .sshpub suffix)"), + }, + }, + { + name: "PGP-signed commit but Secret only has .sshpub keys makes SourceVerifiedCondition=False", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ssh-keys-only", + }, + Data: map[string][]byte{ + "author1.sshpub": []byte(sshAuthorizedKeysFixture), + }, + }, + commit: git.Commit{ + Hash: []byte("shasum"), + Encoded: []byte(encodedCommitFixture), + Signature: signatureCommitFixture, + }, + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Interval = metav1.Duration{Duration: interval} + obj.Spec.Verification = &sourcev1.GitRepositoryVerification{ + Mode: sourcev1.ModeGitHEAD, + SecretRef: meta.LocalObjectReference{ + Name: "ssh-keys-only", + }, + } + }, + wantErr: true, + assertConditions: []metav1.Condition{ + *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidCommitSignature", "PGP signature detected but no PGP public keys found in secret (keys with .asc suffix)"), + }, + }, + { + name: "SSH-signed tag but Secret only has .asc keys makes SourceVerifiedCondition=False", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pgp-keys-only", + }, + Data: map[string][]byte{ + "author1.asc": []byte(armoredKeyRingFixture), + }, + }, + commit: git.Commit{ + ReferencingTag: &git.Tag{ + Name: "v0.1.0", + Hash: []byte("shasum"), + Encoded: []byte(encodedSSHTagFixture), + Signature: signatureSSHTagFixture, + }, + }, + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Reference = &sourcev1.GitRepositoryRef{ + Tag: "v0.1.0", + } + obj.Spec.Interval = metav1.Duration{Duration: interval} + obj.Spec.Verification = &sourcev1.GitRepositoryVerification{ + Mode: sourcev1.ModeGitTag, + SecretRef: meta.LocalObjectReference{ + Name: "pgp-keys-only", + }, + } + }, + wantErr: true, + assertConditions: []metav1.Condition{ + *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidTagSignature", "signature verification of tag 'v0.1.0@shasum' failed: SSH signature detected but no SSH public keys found in secret (keys with .sshpub suffix)"), + }, + }, + { + name: "Invalid SSH key in .sshpub entry makes SourceVerifiedCondition=False", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-ssh-key", + }, + Data: map[string][]byte{ + "author1.sshpub": []byte("ssh-ed25519 INVALIDKEY test@example.com"), + }, + }, + commit: git.Commit{ + Hash: []byte("shasum"), + Encoded: []byte(encodedSSHCommitFixture), + Signature: signatureSSHCommitFixture, + }, + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Interval = metav1.Duration{Duration: interval} + obj.Spec.Verification = &sourcev1.GitRepositoryVerification{ + Mode: sourcev1.ModeGitHEAD, + SecretRef: meta.LocalObjectReference{ + Name: "invalid-ssh-key", + }, + } + }, + wantErr: true, + assertConditions: []metav1.Condition{ + *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidCommitSignature", "signature verification of commit 'shasum' failed: unable to verify Git commit SSH signature"), + }, + }, + { + name: "Mixed: PGP-signed tag and SSH-signed commit with both key types in Secret and mode=TagAndHEAD makes SourceVerifiedCondition=True", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mixed-keys", + }, + Data: map[string][]byte{ + "author1.asc": []byte(armoredKeyRingFixture), + "author1.sshpub": []byte(sshAuthorizedKeysFixture), + }, + }, + commit: git.Commit{ + Hash: []byte("shasum"), + Encoded: []byte(encodedSSHCommitFixture), + Signature: signatureSSHCommitFixture, + ReferencingTag: &git.Tag{ + Name: "v0.1.0", + Hash: []byte("shasum"), + Encoded: []byte(encodedTagFixture), + Signature: signatureTagFixture, + }, + }, + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Reference = &sourcev1.GitRepositoryRef{ + Tag: "v0.1.0", + } + obj.Spec.Interval = metav1.Duration{Duration: interval} + obj.Spec.Verification = &sourcev1.GitRepositoryVerification{ + Mode: sourcev1.ModeGitTagAndHEAD, + SecretRef: meta.LocalObjectReference{ + Name: "mixed-keys", + }, + } + }, + want: sreconcile.ResultSuccess, + wantSourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitTagAndHEAD), + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of\n\t- tag 'v0.1.0@shasum' with key '5982D0279C227FFD'\n\t- commit 'shasum' with key 'SHA256:SDB4adE/BP2VLwX9Pdf7aFUwW9JNdzoPSsHjd/wZIw4'"), + }, + }, + { + name: "Mixed: SSH-signed tag and PGP-signed commit with both key types in Secret and mode=TagAndHEAD makes SourceVerifiedCondition=True", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mixed-keys-reverse", + }, + Data: map[string][]byte{ + "author1.asc": []byte(armoredKeyRingFixture), + "author1.sshpub": []byte(sshAuthorizedKeysFixture), + }, + }, + commit: git.Commit{ + Hash: []byte("shasum"), + Encoded: []byte(encodedCommitFixture), + Signature: signatureCommitFixture, + ReferencingTag: &git.Tag{ + Name: "v0.1.0", + Hash: []byte("shasum"), + Encoded: []byte(encodedSSHTagFixture), + Signature: signatureSSHTagFixture, + }, + }, + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Reference = &sourcev1.GitRepositoryRef{ + Tag: "v0.1.0", + } + obj.Spec.Interval = metav1.Duration{Duration: interval} + obj.Spec.Verification = &sourcev1.GitRepositoryVerification{ + Mode: sourcev1.ModeGitTagAndHEAD, + SecretRef: meta.LocalObjectReference{ + Name: "mixed-keys-reverse", + }, + } + }, + want: sreconcile.ResultSuccess, + wantSourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitTagAndHEAD), + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of\n\t- tag 'v0.1.0@shasum' with key 'SHA256:SDB4adE/BP2VLwX9Pdf7aFUwW9JNdzoPSsHjd/wZIw4'\n\t- commit 'shasum' with key '5982D0279C227FFD'"), + }, + }, + { + name: "Unsupported signature type makes SourceVerifiedCondition=False", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing", + }, + Data: map[string][]byte{ + "foo": []byte(armoredKeyRingFixture), + }, + }, + commit: git.Commit{ + Hash: []byte("shasum"), + Encoded: []byte(encodedCommitFixture), + Signature: "-----BEGIN SIGNED MESSAGE-----\nsome x509 signature\n-----END SIGNED MESSAGE-----", + }, + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Interval = metav1.Duration{Duration: interval} + obj.Spec.Verification = &sourcev1.GitRepositoryVerification{ + Mode: sourcev1.ModeGitHEAD, + SecretRef: meta.LocalObjectReference{ + Name: "existing", + }, + } + }, + wantErr: true, + assertConditions: []metav1.Condition{ + *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidCommitSignature", "unsupported signature type: x509"), + }, + }, + { + name: "Valid commit with explicit .asc key suffix makes SourceVerifiedCondition=True", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "explicit-asc", + }, + Data: map[string][]byte{ + "foo.asc": []byte(armoredKeyRingFixture), + }, + }, + commit: git.Commit{ + Hash: []byte("shasum"), + Encoded: []byte(encodedCommitFixture), + Signature: signatureCommitFixture, + }, + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Interval = metav1.Duration{Duration: interval} + obj.Spec.Verification = &sourcev1.GitRepositoryVerification{ + Mode: sourcev1.ModeGitHEAD, + SecretRef: meta.LocalObjectReference{ + Name: "explicit-asc", + }, + } + }, + want: sreconcile.ResultSuccess, + wantSourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitHEAD), + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of\n\t- commit 'shasum' with key '5982D0279C227FFD'"), + }, + }, } for _, tt := range tests {