Skip to content

Commit 12dba7c

Browse files
authored
Merge pull request #2077 from bb-Ricardo/rb-feature/adds-git-commit-ssh-signature-verification
Add git commit/tag ssh signature verification
2 parents b008bd0 + f565efe commit 12dba7c

6 files changed

Lines changed: 503 additions & 20 deletions

File tree

‎api/v1/gitrepository_types.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,8 @@ type GitRepositoryVerification struct {
242242
Mode GitVerificationMode `json:"mode,omitempty"`
243243

244244
// SecretRef specifies the Secret containing the public keys of trusted Git
245-
// authors.
245+
// authors. PGP public keys must be stored under keys with the .asc suffix,
246+
// and SSH public keys must be stored under keys with the .sshpub suffix.
246247
// +required
247248
SecretRef meta.LocalObjectReference `json:"secretRef"`
248249
}

‎config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,8 @@ spec:
226226
secretRef:
227227
description: |-
228228
SecretRef specifies the Secret containing the public keys of trusted Git
229-
authors.
229+
authors. PGP public keys must be stored under keys with the .asc suffix,
230+
and SSH public keys must be stored under keys with the .sshpub suffix.
230231
properties:
231232
name:
232233
description: Name of the referent.

‎docs/api/v1/source.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2491,7 +2491,8 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
24912491
</td>
24922492
<td>
24932493
<p>SecretRef specifies the Secret containing the public keys of trusted Git
2494-
authors.</p>
2494+
authors. PGP public keys must be stored under keys with the .asc suffix,
2495+
and SSH public keys must be stored under keys with the .sshpub suffix.</p>
24952496
</td>
24962497
</tr>
24972498
</tbody>

‎docs/spec/v1/gitrepositories.md‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,10 @@ signatures. The field offers two subfields:
639639
the commit object pointed to by the tag.
640640

641641
- `.secretRef.name`, to specify a reference to a Secret in the same namespace as
642-
the GitRepository. Containing the (PGP) public keys of trusted Git authors.
642+
the GitRepository. Containing the public keys of trusted Git authors. PGP
643+
public keys must be stored under keys with the `.asc` suffix, and SSH public
644+
keys must be stored under keys with the `.sshpub` suffix. Keys without a
645+
recognized suffix are treated as PGP key rings for backward compatibility.
643646

644647
```yaml
645648
---
@@ -695,6 +698,44 @@ kubectl create secret generic pgp-public-keys \
695698
-o yaml
696699
```
697700

701+
#### SSH verification
702+
703+
SSH-signed commits and tags can also be verified. Store SSH public keys in
704+
`authorized_keys` format under keys with the `.sshpub` suffix in the same
705+
Secret:
706+
707+
```yaml
708+
---
709+
apiVersion: v1
710+
kind: Secret
711+
metadata:
712+
name: verification-keys
713+
namespace: default
714+
type: Opaque
715+
data:
716+
author1.asc: <BASE64 PGP public key>
717+
author2.sshpub: <BASE64 SSH public key>
718+
```
719+
720+
Generating an SSH key pair and creating the Secret:
721+
722+
```sh
723+
# Generate an SSH key pair for signing
724+
ssh-keygen -t ed25519 -N '' -f /tmp/signing_key
725+
# Generate secret with the public key
726+
kubectl create secret generic verification-keys \
727+
--from-file=author2.sshpub=/tmp/signing_key.pub \
728+
-o yaml
729+
```
730+
731+
A single Secret can contain both PGP (`.asc`) and SSH (`.sshpub`) keys. The
732+
controller detects the signature type of each Git object (PGP or SSH) and
733+
dispatches verification accordingly.
734+
735+
PGP verification reports the PGP key ID in the success message (e.g.
736+
`5982D0279C227FFD`), while SSH verification reports the SHA256 fingerprint
737+
(e.g. `SHA256:uNiVztksCsDhcc0u9e8BgrJXVGDaf6s7kOsTmI9N7sM`).
738+
698739
### Ignore
699740

700741
`.spec.ignore` is an optional field to specify rules in [the `.gitignore`

‎internal/controller/gitrepository_controller.go‎

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,41 @@ import (
7171
"github.com/fluxcd/source-controller/internal/util"
7272
)
7373

74+
const (
75+
// publicKeyPGPSuffix is the Secret data key suffix for PGP public keys.
76+
publicKeyPGPSuffix = ".asc"
77+
// publicKeySSHSuffix is the Secret data key suffix for SSH public keys.
78+
publicKeySSHSuffix = ".sshpub"
79+
)
80+
81+
// gitSigner abstracts the verification methods shared by git.Commit and git.Tag.
82+
type gitSigner interface {
83+
SignatureType() string
84+
VerifyPGP(keyRings ...string) (string, error)
85+
VerifySSH(authorizedKeys ...string) (string, error)
86+
}
87+
88+
// verifyGitObject dispatches signature verification based on the signature type
89+
// of the given git object. It returns the key identity (PGP key ID or SSH
90+
// fingerprint) on success, or an error if verification fails or the required
91+
// key type is missing from the Secret.
92+
func verifyGitObject(obj gitSigner, keyRings []string, authorizedKeys []string) (string, error) {
93+
switch obj.SignatureType() {
94+
case "openpgp":
95+
if len(keyRings) == 0 {
96+
return "", fmt.Errorf("PGP signature detected but no PGP public keys found in secret (keys with %s suffix)", publicKeyPGPSuffix)
97+
}
98+
return obj.VerifyPGP(keyRings...)
99+
case "ssh":
100+
if len(authorizedKeys) == 0 {
101+
return "", fmt.Errorf("SSH signature detected but no SSH public keys found in secret (keys with %s suffix)", publicKeySSHSuffix)
102+
}
103+
return obj.VerifySSH(authorizedKeys...)
104+
default:
105+
return "", fmt.Errorf("unsupported signature type: %s", obj.SignatureType())
106+
}
107+
}
108+
74109
// gitRepositoryReadyCondition contains the information required to summarize a
75110
// v1.GitRepository Ready Condition.
76111
var gitRepositoryReadyCondition = summarize.Conditions{
@@ -1093,24 +1128,32 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
10931128
return sreconcile.ResultSuccess, nil
10941129
}
10951130

1096-
// Get secret with GPG data
1131+
// Get secret with public key data
10971132
publicKeySecret := types.NamespacedName{
10981133
Namespace: obj.Namespace,
10991134
Name: obj.Spec.Verification.SecretRef.Name,
11001135
}
11011136
secret := &corev1.Secret{}
11021137
if err := r.Client.Get(ctx, publicKeySecret, secret); err != nil {
11031138
e := serror.NewGeneric(
1104-
fmt.Errorf("PGP public keys secret error: %w", err),
1139+
fmt.Errorf("public keys secret error: %w", err),
11051140
"VerificationError",
11061141
)
11071142
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, "%s", e)
11081143
return sreconcile.ResultEmpty, e
11091144
}
11101145

11111146
var keyRings []string
1112-
for _, v := range secret.Data {
1113-
keyRings = append(keyRings, string(v))
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+
}
11141157
}
11151158

11161159
var message strings.Builder
@@ -1140,38 +1183,34 @@ func (r *GitRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
11401183
return sreconcile.ResultEmpty, err
11411184
}
11421185

1143-
// Verify tag with GPG data from secret
1144-
tagEntity, err := tag.Verify(keyRings...)
1186+
entity, err := verifyGitObject(tag, keyRings, authorizedKeys)
11451187
if err != nil {
11461188
e := serror.NewGeneric(
11471189
fmt.Errorf("signature verification of tag '%s' failed: %w", tag.String(), err),
11481190
"InvalidTagSignature",
11491191
)
11501192
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, "%s", e)
1151-
// Return error in the hope the secret changes
11521193
return sreconcile.ResultEmpty, e
11531194
}
11541195

1155-
message.WriteString(fmt.Sprintf("verified signature of\n\t- tag '%s' with key '%s'", tag.String(), tagEntity))
1196+
message.WriteString(fmt.Sprintf("verified signature of\n\t- tag '%s' with key '%s'", tag.String(), entity))
11561197
}
11571198

11581199
if obj.Spec.Verification.VerifyHEAD() {
1159-
// Verify commit with GPG data from secret
1160-
headEntity, err := commit.Verify(keyRings...)
1200+
entity, err := verifyGitObject(&commit, keyRings, authorizedKeys)
11611201
if err != nil {
11621202
e := serror.NewGeneric(
11631203
fmt.Errorf("signature verification of commit '%s' failed: %w", commit.Hash.String(), err),
11641204
"InvalidCommitSignature",
11651205
)
11661206
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, "%s", e)
1167-
// Return error in the hope the secret changes
11681207
return sreconcile.ResultEmpty, e
11691208
}
11701209
// If we also verified the tag previously, then append to the message.
11711210
if message.Len() > 0 {
1172-
message.WriteString(fmt.Sprintf("\n\t- commit '%s' with key '%s'", commit.Hash.String(), headEntity))
1211+
message.WriteString(fmt.Sprintf("\n\t- commit '%s' with key '%s'", commit.Hash.String(), entity))
11731212
} else {
1174-
message.WriteString(fmt.Sprintf("verified signature of\n\t- commit '%s' with key '%s'", commit.Hash.String(), headEntity))
1213+
message.WriteString(fmt.Sprintf("verified signature of\n\t- commit '%s' with key '%s'", commit.Hash.String(), entity))
11751214
}
11761215
}
11771216

0 commit comments

Comments
 (0)