Skip to content

Commit 2db2715

Browse files
developer-guystefanprodan
authored andcommitted
feat: add condition tests for verification logic
Signed-off-by: Batuhan Apaydın <[email protected]>
1 parent 07b5326 commit 2db2715

File tree

6 files changed

+168
-139
lines changed

6 files changed

+168
-139
lines changed

.github/workflows/e2e.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ on:
99
push:
1010
branches:
1111
- main
12-
- feature/863
1312

1413
permissions:
1514
contents: read # for actions/checkout to fetch code

config/testdata/ocirepository/signed-with-key.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ spec:
1111
verify:
1212
provider: cosign
1313
secretRef:
14-
name: cosign-key
14+
name: cosign-key

config/testdata/ocirepository/signed-with-keyless.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ spec:
99
ref:
1010
semver: "6.2.x"
1111
verify:
12-
provider: cosign
12+
provider: cosign

controllers/ocirepository_controller.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ import (
2828
"strings"
2929
"time"
3030

31-
soci "github.com/fluxcd/source-controller/internal/oci"
32-
3331
"github.com/Masterminds/semver/v3"
32+
soci "github.com/fluxcd/source-controller/internal/oci"
3433
"github.com/google/go-containerregistry/pkg/authn"
3534
"github.com/google/go-containerregistry/pkg/authn/k8schain"
3635
"github.com/google/go-containerregistry/pkg/crane"
@@ -424,7 +423,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
424423
return sreconcile.ResultEmpty, e
425424
}
426425

427-
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "OCI image %s with digest %s verified.", url, imgDigest)
426+
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "OCI image %s with digest %s verified.", url, revision)
428427
}
429428
layers, err := img.Layers()
430429
if err != nil {
@@ -502,8 +501,8 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
502501
return sreconcile.ResultSuccess, nil
503502
}
504503

505-
// verifyOCISourceSignature verifies the authenticity of the given image reference url. First, it tries to keyful approach
506-
// by looking at whether the given secret exists. Then, if it does not exist, it pushes a keyless approach for verification.
504+
// verifyOCISourceSignature verifies the authenticity of the given image reference url. First, it tries using a key,
505+
// provided the secret exists and a public key exists in the secret . Then, if it does not exist, it pushes for a keyless approach for verification.
507506
func (r *OCIRepositoryReconciler) verifyOCISourceSignature(ctx context.Context, obj *sourcev1.OCIRepository, url string, keychain authn.Keychain) error {
508507
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
509508
defer cancel()
@@ -536,8 +535,6 @@ func (r *OCIRepositoryReconciler) verifyOCISourceSignature(ctx context.Context,
536535
}
537536

538537
signatureVerified := false
539-
// traverse all public keys and try to verify the signature
540-
// this is brute-force approach, but it is ok for now
541538
for k, data := range pubSecret.Data {
542539
// search for public keys in the secret
543540
if strings.HasSuffix(k, ".pub") {
@@ -546,7 +543,7 @@ func (r *OCIRepositoryReconciler) verifyOCISourceSignature(ctx context.Context,
546543
return err
547544
}
548545

549-
signatures, _, err := verifier.VerifyImageSignatures(ctx, ref)
546+
signatures, _, err := verifier.VerifyImageSignatures(ref)
550547
if err != nil {
551548
continue
552549
}
@@ -563,15 +560,14 @@ func (r *OCIRepositoryReconciler) verifyOCISourceSignature(ctx context.Context,
563560
}
564561

565562
return nil
566-
567563
} else {
568564
ctrl.LoggerFrom(ctx).Info("no secret reference is provided, trying to verify the image using keyless approach")
569565
verifier, err := soci.New(defaultCosignOciOpts...)
570566
if err != nil {
571567
return err
572568
}
573569

574-
signatures, _, err := verifier.VerifyImageSignatures(ctxTimeout, ref)
570+
signatures, _, err := verifier.VerifyImageSignatures(ref)
575571
if err != nil {
576572
return err
577573
}

controllers/ocirepository_controller_test.go

Lines changed: 154 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
66
You may obtain a copy of the License at
77
8-
http://www.apache.org/licenses/LICENSE-2.0
8+
http://www.apache.org/licenses/LICENSE-2.0
99
1010
Unless required by applicable law or agreed to in writing, software
1111
distributed under the License is distributed on an "AS IS" BASIS,
@@ -1009,6 +1009,159 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) {
10091009
}
10101010
}
10111011

1012+
func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) {
1013+
g := NewWithT(t)
1014+
1015+
tmpDir := t.TempDir()
1016+
server, err := setupRegistryServer(ctx, tmpDir, registryOptions{})
1017+
g.Expect(err).ToNot(HaveOccurred())
1018+
1019+
podinfoVersions, err := pushMultiplePodinfoImages(server.registryHost, "6.1.4", "6.1.5")
1020+
g.Expect(err).ToNot(HaveOccurred())
1021+
img4 := podinfoVersions["6.1.4"]
1022+
img5 := podinfoVersions["6.1.5"]
1023+
1024+
tests := []struct {
1025+
name string
1026+
reference *sourcev1.OCIRepositoryRef
1027+
digest string
1028+
want sreconcile.Result
1029+
wantErr bool
1030+
wantErrMsg string
1031+
shouldSign bool
1032+
assertConditions []metav1.Condition
1033+
}{
1034+
{
1035+
name: "signed image should pass verification",
1036+
reference: &sourcev1.OCIRepositoryRef{
1037+
Tag: "6.1.4",
1038+
},
1039+
digest: img4.digest.Hex,
1040+
shouldSign: true,
1041+
want: sreconcile.ResultSuccess,
1042+
assertConditions: []metav1.Condition{
1043+
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
1044+
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
1045+
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "OCI image <url> with digest <digest> verified."),
1046+
},
1047+
},
1048+
{
1049+
name: "not signed image should not pass verification",
1050+
reference: &sourcev1.OCIRepositoryRef{
1051+
Tag: "6.1.5",
1052+
},
1053+
digest: img5.digest.Hex,
1054+
wantErr: true,
1055+
wantErrMsg: "failed to verify OCI image signature '<url>' using provider 'cosign': no matching signatures were found for '<url>",
1056+
want: sreconcile.ResultEmpty,
1057+
assertConditions: []metav1.Condition{
1058+
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
1059+
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
1060+
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify OCI image signature '<url>' using provider '<provider>': no matching signatures were found for '<url>'"),
1061+
},
1062+
},
1063+
}
1064+
1065+
builder := fakeclient.NewClientBuilder().WithScheme(testEnv.GetScheme())
1066+
1067+
r := &OCIRepositoryReconciler{
1068+
Client: builder.Build(),
1069+
EventRecorder: record.NewFakeRecorder(32),
1070+
Storage: testStorage,
1071+
}
1072+
1073+
pf := func(b bool) ([]byte, error) {
1074+
return []byte("cosign-password"), nil
1075+
}
1076+
1077+
keys, err := cosign.GenerateKeyPair(pf)
1078+
g.Expect(err).ToNot(HaveOccurred())
1079+
1080+
err = os.WriteFile(path.Join(tmpDir, "cosign.key"), keys.PrivateBytes, 0600)
1081+
g.Expect(err).ToNot(HaveOccurred())
1082+
1083+
secret := &corev1.Secret{
1084+
ObjectMeta: metav1.ObjectMeta{
1085+
Name: "cosign-key",
1086+
},
1087+
Data: map[string][]byte{
1088+
"cosign.pub": keys.PublicBytes,
1089+
}}
1090+
1091+
err = r.Create(ctx, secret)
1092+
if err != nil {
1093+
g.Expect(err).NotTo(HaveOccurred())
1094+
}
1095+
1096+
for _, tt := range tests {
1097+
t.Run(tt.name, func(t *testing.T) {
1098+
obj := &sourcev1.OCIRepository{
1099+
ObjectMeta: metav1.ObjectMeta{
1100+
GenerateName: "verify-oci-source-signature-",
1101+
},
1102+
Spec: sourcev1.OCIRepositorySpec{
1103+
URL: fmt.Sprintf("oci://%s/podinfo", server.registryHost),
1104+
Verify: &sourcev1.OCIRepositoryVerification{
1105+
Provider: "cosign",
1106+
SecretRef: &meta.LocalObjectReference{Name: "cosign-key"}},
1107+
Interval: metav1.Duration{Duration: interval},
1108+
Timeout: &metav1.Duration{Duration: timeout},
1109+
},
1110+
}
1111+
1112+
if tt.reference != nil {
1113+
obj.Spec.Reference = tt.reference
1114+
}
1115+
1116+
keychain, err := r.keychain(ctx, obj)
1117+
if err != nil {
1118+
g.Expect(err).ToNot(HaveOccurred())
1119+
}
1120+
1121+
opts := r.craneOptions(ctx, true)
1122+
opts = append(opts, crane.WithAuthFromKeychain(keychain))
1123+
artifactURL, err := r.getArtifactURL(obj, opts)
1124+
g.Expect(err).ToNot(HaveOccurred())
1125+
1126+
if tt.shouldSign {
1127+
ko := coptions.KeyOpts{
1128+
KeyRef: path.Join(tmpDir, "cosign.key"),
1129+
PassFunc: pf,
1130+
}
1131+
1132+
ro := &coptions.RootOptions{
1133+
Timeout: timeout,
1134+
}
1135+
err = sign.SignCmd(ro, ko, coptions.RegistryOptions{Keychain: keychain},
1136+
nil, []string{artifactURL}, "",
1137+
"", true, "",
1138+
"", "", false,
1139+
false, "", false)
1140+
g.Expect(err).ToNot(HaveOccurred())
1141+
}
1142+
1143+
assertConditions := tt.assertConditions
1144+
for k := range assertConditions {
1145+
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<digest>", tt.digest)
1146+
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<url>", artifactURL)
1147+
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<provider>", "cosign")
1148+
}
1149+
1150+
artifact := &sourcev1.Artifact{}
1151+
got, err := r.reconcileSource(ctx, obj, artifact, tmpDir)
1152+
if tt.wantErr {
1153+
tt.wantErrMsg = strings.ReplaceAll(tt.wantErrMsg, "<url>", artifactURL)
1154+
g.Expect(err).ToNot(BeNil())
1155+
g.Expect(err.Error()).To(ContainSubstring(tt.wantErrMsg))
1156+
} else {
1157+
g.Expect(err).ToNot(HaveOccurred())
1158+
}
1159+
g.Expect(got).To(Equal(tt.want))
1160+
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
1161+
})
1162+
}
1163+
}
1164+
10121165
func TestOCIRepository_reconcileArtifact(t *testing.T) {
10131166
g := NewWithT(t)
10141167

@@ -1217,127 +1370,6 @@ func TestOCIRepository_getArtifactURL(t *testing.T) {
12171370
}
12181371
}
12191372

1220-
func TestOCIRepository_verifyOCISourceSignature(t *testing.T) {
1221-
g := NewWithT(t)
1222-
1223-
tmpDir := t.TempDir()
1224-
regServer, err := setupRegistryServer(ctx, tmpDir, registryOptions{})
1225-
g.Expect(err).ToNot(HaveOccurred())
1226-
1227-
_, err = pushMultiplePodinfoImages(regServer.registryHost, "6.1.4", "6.1.5", "6.1.6")
1228-
g.Expect(err).ToNot(HaveOccurred())
1229-
1230-
tests := []struct {
1231-
name string
1232-
url string
1233-
reference *sourcev1.OCIRepositoryRef
1234-
shouldSign bool
1235-
wantErrMsg string
1236-
}{
1237-
{
1238-
name: "signed image should pass verification",
1239-
reference: &sourcev1.OCIRepositoryRef{
1240-
Tag: "6.1.4",
1241-
},
1242-
shouldSign: true,
1243-
},
1244-
{
1245-
name: "unsigned image should not pass verification",
1246-
reference: &sourcev1.OCIRepositoryRef{
1247-
Tag: "6.1.5",
1248-
},
1249-
shouldSign: false,
1250-
wantErrMsg: "no matching signatures were found",
1251-
},
1252-
}
1253-
1254-
builder := fakeclient.NewClientBuilder().WithScheme(testEnv.GetScheme())
1255-
r := &OCIRepositoryReconciler{
1256-
Client: builder.Build(),
1257-
EventRecorder: record.NewFakeRecorder(32),
1258-
Storage: testStorage,
1259-
}
1260-
1261-
pf := func(b bool) ([]byte, error) {
1262-
return []byte("cosign-password"), nil
1263-
}
1264-
1265-
keys, err := cosign.GenerateKeyPair(pf)
1266-
g.Expect(err).ToNot(HaveOccurred())
1267-
1268-
err = os.WriteFile(path.Join(tmpDir, "cosign.key"), keys.PrivateBytes, 0600)
1269-
g.Expect(err).ToNot(HaveOccurred())
1270-
1271-
secret := &corev1.Secret{
1272-
ObjectMeta: metav1.ObjectMeta{
1273-
Name: "cosign-key",
1274-
},
1275-
Data: map[string][]byte{
1276-
"cosign.pub": keys.PublicBytes,
1277-
}}
1278-
1279-
err = r.Create(ctx, secret)
1280-
if err != nil {
1281-
g.Expect(err).NotTo(HaveOccurred())
1282-
}
1283-
1284-
for _, tt := range tests {
1285-
t.Run(tt.name, func(t *testing.T) {
1286-
obj := &sourcev1.OCIRepository{
1287-
ObjectMeta: metav1.ObjectMeta{
1288-
GenerateName: "artifact-url-",
1289-
},
1290-
Spec: sourcev1.OCIRepositorySpec{
1291-
URL: fmt.Sprintf("oci://%s/podinfo", regServer.registryHost),
1292-
Reference: tt.reference,
1293-
Verify: &sourcev1.OCIRepositoryVerification{
1294-
Provider: "cosign",
1295-
SecretRef: &meta.LocalObjectReference{Name: "cosign-key"}},
1296-
Interval: metav1.Duration{Duration: interval},
1297-
Timeout: &metav1.Duration{Duration: timeout},
1298-
},
1299-
}
1300-
1301-
keychain, err := r.keychain(ctx, obj)
1302-
if err != nil {
1303-
g.Expect(err).ToNot(HaveOccurred())
1304-
}
1305-
1306-
options := r.craneOptions(ctx, obj.Spec.Insecure)
1307-
options = append(options, crane.WithAuthFromKeychain(keychain))
1308-
artifactURL, err := r.getArtifactURL(obj, options)
1309-
if err != nil {
1310-
g.Expect(err).ToNot(HaveOccurred())
1311-
}
1312-
1313-
if tt.shouldSign {
1314-
ko := coptions.KeyOpts{
1315-
KeyRef: path.Join(tmpDir, "cosign.key"),
1316-
PassFunc: pf,
1317-
}
1318-
1319-
ro := &coptions.RootOptions{
1320-
Timeout: timeout,
1321-
}
1322-
err = sign.SignCmd(ro, ko, coptions.RegistryOptions{Keychain: keychain},
1323-
nil, []string{artifactURL}, "",
1324-
"", true, "",
1325-
"", "", false,
1326-
false, "", false)
1327-
g.Expect(err).ToNot(HaveOccurred())
1328-
}
1329-
1330-
err = r.verifyOCISourceSignature(ctx, obj, artifactURL, keychain)
1331-
if tt.wantErrMsg != "" {
1332-
g.Expect(err).ToNot(BeNil())
1333-
g.Expect(err.Error()).To(ContainSubstring(tt.wantErrMsg))
1334-
} else {
1335-
g.Expect(err).ToNot(HaveOccurred())
1336-
}
1337-
})
1338-
}
1339-
}
1340-
13411373
func TestOCIRepository_stalled(t *testing.T) {
13421374
g := NewWithT(t)
13431375

0 commit comments

Comments
 (0)