Skip to content

Commit ae37514

Browse files
committed
gitrepo: add support for Git tag verification
Add two new Git verification modes: * `tag`: Verify tag object referred to by `.spec.ref.tag` * `tagAndHead`: Verify tag object referred to by `.spec.ref.tag` and the commit the tag points to. Change the `Reason` of the `SourceVerified` condition from `Succeeded` to `VerifiedTag`/`VerifiedCommit`/`VerifiedTagAndCommit` as it reflects the actual reason behind the condition's status being True. This helps us to detect when we need to verify a new Git object even if the object's `.spec.ref` did not change. Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
1 parent 38cff76 commit ae37514

File tree

6 files changed

+141
-27
lines changed

6 files changed

+141
-27
lines changed

api/v1/condition_types.go

+9
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,13 @@ const (
104104

105105
// CacheOperationFailedReason signals a failure in cache operation.
106106
CacheOperationFailedReason string = "CacheOperationFailed"
107+
108+
// VerifiedCommitReason signals a success in verifying the commit.
109+
VerifiedCommitReason string = "VerifiedCommit"
110+
111+
// VerifiedTagReason signals a success in verifying the tag.
112+
VerifiedTagReason string = "VerifiedTag"
113+
114+
// VerifiedTagAndCommitReason signals a success in verifying the tag and commit.
115+
VerifiedTagAndCommitReason string = "VerifiedTagAndCommit"
107116
)

api/v1/gitrepository_types.go

+30-3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ const (
3838
IncludeUnavailableCondition string = "IncludeUnavailable"
3939
)
4040

41+
// GitVerificationMode specifies the verification mode for a Git repository.
42+
type GitVerificationMode string
43+
44+
const (
45+
// ModeHead implies that the HEAD of the Git repository (after it has been
46+
// checked out to the required commit) should be verified.
47+
ModeHead GitVerificationMode = "head"
48+
// ModeTag implies that the tag object specified in the checkout configuration
49+
// should be verified.
50+
ModeTag GitVerificationMode = "tag"
51+
// ModeTagAndHead implies that both the tag object and the commit it points
52+
// to should be verified.
53+
ModeTagAndHead GitVerificationMode = "tagAndHead"
54+
)
55+
4156
// GitRepositorySpec specifies the required configuration to produce an
4257
// Artifact for a Git repository.
4358
type GitRepositorySpec struct {
@@ -170,9 +185,11 @@ type GitRepositoryRef struct {
170185
// GitRepositoryVerification specifies the Git commit signature verification
171186
// strategy.
172187
type GitRepositoryVerification struct {
173-
// Mode specifies what Git object should be verified, currently ('head').
174-
// +kubebuilder:validation:Enum=head
175-
Mode string `json:"mode"`
188+
// Mode specifies which Git object(s) should be verified.
189+
// +kubebuilder:validation:Enum=head;tag;tagAndHead
190+
// +optional
191+
// +kubebuilder:default:=head
192+
Mode string `json:"mode,omitempty"`
176193

177194
// SecretRef specifies the Secret containing the public keys of trusted Git
178195
// authors.
@@ -250,6 +267,16 @@ func (in *GitRepository) GetArtifact() *Artifact {
250267
return in.Status.Artifact
251268
}
252269

270+
func (v *GitRepositoryVerification) GetMode() GitVerificationMode {
271+
if v.Mode == "tagAndHead" {
272+
return ModeTagAndHead
273+
}
274+
if v.Mode == "tag" {
275+
return ModeTag
276+
}
277+
return ModeHead
278+
}
279+
253280
// +genclient
254281
// +genclient:Namespaced
255282
// +kubebuilder:storageversion

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,12 @@ spec:
166166
Git commit signature(s).
167167
properties:
168168
mode:
169-
description: Mode specifies what Git object should be verified,
170-
currently ('head').
169+
default: head
170+
description: Mode specifies which Git object(s) should be verified.
171171
enum:
172172
- head
173+
- tag
174+
- tagAndHead
173175
type: string
174176
secretRef:
175177
description: SecretRef specifies the Secret containing the public
@@ -182,7 +184,6 @@ spec:
182184
- name
183185
type: object
184186
required:
185-
- mode
186187
- secretRef
187188
type: object
188189
required:

go.mod

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ replace github.com/opencontainers/go-digest => github.com/opencontainers/go-dige
1414
// Check again when oras.land/oras-go is updated, which is a dependency of Helm.
1515
replace github.com/docker/docker => github.com/docker/docker v23.0.6+incompatible
1616

17+
replace github.com/fluxcd/pkg/git => github.com/fluxcd/pkg/git v0.12.4-0.20230731130338-0830185ed818
18+
19+
replace github.com/fluxcd/pkg/git/gogit => github.com/fluxcd/pkg/git/gogit v0.12.2-0.20230731130338-0830185ed818
20+
1721
require (
1822
cloud.google.com/go/storage v1.31.0
1923
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1

go.sum

+4-4
Original file line numberDiff line numberDiff line change
@@ -391,10 +391,10 @@ github.com/fluxcd/pkg/apis/event v0.5.1 h1:UrEmKwTK/lt42gMZunl8BQBMzjf8PSqGbWDs/
391391
github.com/fluxcd/pkg/apis/event v0.5.1/go.mod h1:GzBAzS8bq7751wvNkaSnr3kuwFVuWTPL20D77UgSNJQ=
392392
github.com/fluxcd/pkg/apis/meta v1.1.1 h1:sLAKLbEu7rRzJ+Mytffu3NcpfdbOBTa6hcpOQzFWm+M=
393393
github.com/fluxcd/pkg/apis/meta v1.1.1/go.mod h1:soCfzjFWbm1mqybDcOywWKTCEYlH3skpoNGTboVk234=
394-
github.com/fluxcd/pkg/git v0.12.3 h1:1KmRYTdcBKDUutg6NIT4x0BCCMT72PjjXs3AnHjybHY=
395-
github.com/fluxcd/pkg/git v0.12.3/go.mod h1:ID2sry5OqYbgJxvANc7s6V/YwafnQd7e1AGoDvwztAU=
396-
github.com/fluxcd/pkg/git/gogit v0.12.1 h1:06jzHOTntYN5xCSQvyFXtLXdqoP8crLh7VYgtXS9+wo=
397-
github.com/fluxcd/pkg/git/gogit v0.12.1/go.mod h1:Z4Ysp8VifKTvWpjJMKncJsgb2iBqHuIeK80VGjlU41Y=
394+
github.com/fluxcd/pkg/git v0.12.4-0.20230731130338-0830185ed818 h1:5RiT/G6Lmbg5eRHh4cjTM9t2v/txc5Yx5rs7vDe3dVk=
395+
github.com/fluxcd/pkg/git v0.12.4-0.20230731130338-0830185ed818/go.mod h1:VBPwN/pjvkqWmGUzou53fAQDT2tRJAUJ45SC7TyW5TI=
396+
github.com/fluxcd/pkg/git/gogit v0.12.2-0.20230731130338-0830185ed818 h1:G01A3JDlqrh6JzK/5ydFtdIQE7Nkfaw8+WEshMwMLZQ=
397+
github.com/fluxcd/pkg/git/gogit v0.12.2-0.20230731130338-0830185ed818/go.mod h1:Z4Ysp8VifKTvWpjJMKncJsgb2iBqHuIeK80VGjlU41Y=
398398
github.com/fluxcd/pkg/gittestserver v0.8.4 h1:rA/QUZnfH77ZZG+5xfMqjgEHJdzeeE6Nn1o8cops/bU=
399399
github.com/fluxcd/pkg/gittestserver v0.8.4/go.mod h1:i3Vng3Stl5zOuGhN4+RuP2NWf5snJCeGUKA7pzAvcHU=
400400
github.com/fluxcd/pkg/helmtestserver v0.13.1 h1:SjEk9QaMWMjwnqTXGtfMeorC5H+KDvV2YK87Sr2dFng=

internal/controller/gitrepository_controller.go

+90-17
Original file line numberDiff line numberDiff line change
@@ -552,8 +552,25 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
552552
// If it's a partial commit obtained from an existing artifact, check if the
553553
// reconciliation can be skipped if other configurations have not changed.
554554
if !git.IsConcreteCommit(*commit) {
555+
// If we need to verify a Git object that we previously haven't verified, then
556+
// we need to do a full reconciliation.
557+
var verificationRequired bool
558+
sourceVerifiedCondition := conditions.Get(obj, sourcev1.SourceVerifiedCondition)
559+
if obj.Spec.Verification != nil {
560+
if sourceVerifiedCondition == nil {
561+
verificationRequired = true
562+
}
563+
mode := obj.Spec.Verification.GetMode()
564+
if sourceVerifiedCondition.Reason == sourcev1.VerifiedTagReason && (mode == sourcev1.ModeHead || mode == sourcev1.ModeTagAndHead) {
565+
verificationRequired = true
566+
}
567+
if sourceVerifiedCondition.Reason == sourcev1.VerifiedCommitReason && (mode == sourcev1.ModeTag || mode == sourcev1.ModeTagAndHead) {
568+
verificationRequired = true
569+
}
570+
}
571+
555572
// Check if the content config contributing to the artifact has changed.
556-
if !gitContentConfigChanged(obj, includes) {
573+
if !gitContentConfigChanged(obj, includes) && !verificationRequired {
557574
ge := serror.NewGeneric(
558575
fmt.Errorf("no changes since last reconcilation: observed revision '%s'",
559576
commitReference(obj, commit)), sourcev1.GitOperationSucceedReason,
@@ -923,8 +940,8 @@ func (r *GitRepositoryReconciler) fetchIncludes(ctx context.Context, obj *source
923940
return &artifacts, nil
924941
}
925942

926-
// verifyCommitSignature verifies the signature of the given Git commit, if a
927-
// verification mode is specified on the object.
943+
// verifyCommitSignature verifies the signature of the given Git commit and/or its parent tag
944+
// depedning on the verification mode specified on the object.
928945
// If the signature can not be verified or the verification fails, it records
929946
// v1beta2.SourceVerifiedCondition=False and returns.
930947
// When successful, it records v1beta2.SourceVerifiedCondition=True.
@@ -957,22 +974,78 @@ func (r *GitRepositoryReconciler) verifyCommitSignature(ctx context.Context, obj
957974
for _, v := range secret.Data {
958975
keyRings = append(keyRings, string(v))
959976
}
960-
// Verify commit with GPG data from secret
961-
entity, err := commit.Verify(keyRings...)
962-
if err != nil {
963-
e := serror.NewGeneric(
964-
fmt.Errorf("signature verification of commit '%s' failed: %w", commit.Hash.String(), err),
965-
"InvalidCommitSignature",
966-
)
967-
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
968-
// Return error in the hope the secret changes
969-
return sreconcile.ResultEmpty, e
977+
978+
// these flags help us later figure out which objects we need to verify
979+
var verifyHead bool
980+
var verifyTag bool
981+
switch obj.Spec.Verification.GetMode() {
982+
case sourcev1.ModeHead:
983+
verifyHead = true
984+
case sourcev1.ModeTag:
985+
verifyTag = true
986+
case sourcev1.ModeTagAndHead:
987+
verifyHead = true
988+
verifyTag = true
970989
}
971990

972-
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason,
973-
"verified signature of commit '%s' with key '%s'", commit.Hash.String(), entity)
974-
r.eventLogf(ctx, obj, eventv1.EventTypeTrace, "VerifiedCommit",
975-
"verified signature of commit '%s' with key '%s'", commit.Hash.String(), entity)
991+
if verifyTag && obj.Spec.Reference.Tag == "" {
992+
err := errors.New("cannot verify tag object's signature if a tag reference is not specified")
993+
conditions.MarkStalled(obj, "InvalidVerificationMode", err.Error())
994+
return sreconcile.ResultEmpty, err
995+
}
996+
conditions.Delete(obj, meta.StalledCondition)
997+
998+
var err error
999+
var headEntity string
1000+
if verifyHead {
1001+
// Verify commit with GPG data from secret
1002+
headEntity, err = commit.Verify(keyRings...)
1003+
if err != nil {
1004+
e := serror.NewGeneric(
1005+
fmt.Errorf("signature verification of commit '%s' failed: %w", commit.Hash.String(), err),
1006+
"InvalidCommitSignature",
1007+
)
1008+
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
1009+
// Return error in the hope the secret changes
1010+
return sreconcile.ResultEmpty, e
1011+
}
1012+
}
1013+
1014+
var tag *git.AnnotatedTag
1015+
var tagEntity string
1016+
if verifyTag {
1017+
// Verify tag with GPG data from secret
1018+
if tag = commit.ReferencingTag; tag != nil {
1019+
tagEntity, err = tag.Verify(keyRings...)
1020+
if err != nil {
1021+
e := serror.NewGeneric(
1022+
fmt.Errorf("signature verification of tag '%s' failed: %w", tag.String(), err),
1023+
"InvalidTagSignature",
1024+
)
1025+
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
1026+
// Return error in the hope the secret changes
1027+
return sreconcile.ResultEmpty, e
1028+
}
1029+
}
1030+
}
1031+
1032+
var message string
1033+
var reason string
1034+
if verifyHead {
1035+
message = fmt.Sprintf("verified signature of commit '%s' with key '%s'", commit.Hash.String(), headEntity)
1036+
reason = sourcev1.VerifiedCommitReason
1037+
}
1038+
if verifyTag {
1039+
if message == "" {
1040+
message = fmt.Sprintf("verified signature of tag '%s' with key '%s'", tag.String(), tagEntity)
1041+
reason = sourcev1.VerifiedTagReason
1042+
} else {
1043+
message = message + fmt.Sprintf(" and signature of tag '%s' with key '%s'", tag.String(), tagEntity)
1044+
reason = sourcev1.VerifiedTagAndCommitReason
1045+
}
1046+
}
1047+
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, reason, message)
1048+
r.eventLogf(ctx, obj, eventv1.EventTypeTrace, reason, message)
9761049
return sreconcile.ResultSuccess, nil
9771050
}
9781051

0 commit comments

Comments
 (0)