From 1bb92548e469bc0d4dca01adc416e9026e12597c Mon Sep 17 00:00:00 2001
From: Max Jonas Werner <mail@makk.es>
Date: Tue, 16 Apr 2024 19:32:55 +0200
Subject: [PATCH] Add flags for issuer/subject OCI signature verification

This change introduces two new flags to `create source oci` for
providing the values to the
`OCIRepository.spec.verify.matchOIDCIdentity.(issuer,subject)` fields.

Signed-off-by: Max Jonas Werner <mail@makk.es>
---
 cmd/flux/create_source_oci.go                 | 49 ++++++++++++++-----
 cmd/flux/create_source_oci_test.go            | 27 +++++++++-
 .../export_with_complete_verification.golden  | 16 ++++++
 .../testdata/oci/export_with_issuer.golden    | 16 ++++++
 .../testdata/oci/export_with_subject.golden   | 16 ++++++
 5 files changed, 110 insertions(+), 14 deletions(-)
 create mode 100644 cmd/flux/testdata/oci/export_with_complete_verification.golden
 create mode 100644 cmd/flux/testdata/oci/export_with_issuer.golden
 create mode 100644 cmd/flux/testdata/oci/export_with_subject.golden

diff --git a/cmd/flux/create_source_oci.go b/cmd/flux/create_source_oci.go
index 6393380c42..cf58dcf823 100644
--- a/cmd/flux/create_source_oci.go
+++ b/cmd/flux/create_source_oci.go
@@ -43,25 +43,36 @@ var createSourceOCIRepositoryCmd = &cobra.Command{
 	Example: `  # Create an OCIRepository for a public container image
   flux create source oci podinfo \
     --url=oci://ghcr.io/stefanprodan/manifests/podinfo \
-    --tag=6.1.6 \
+    --tag=6.6.2 \
     --interval=10m
+
+  # Create an OCIRepository with OIDC signature verification
+  flux create source oci podinfo \
+    --url=oci://ghcr.io/stefanprodan/manifests/podinfo \
+    --tag=6.6.2 \
+    --interval=10m \
+    --verify-provider=cosign \
+    --verify-subject="^https://github.com/stefanprodan/podinfo/.github/workflows/release.yml@refs/tags/6.6.2$" \
+    --verify-issuer="^https://token.actions.githubusercontent.com$"
 `,
 	RunE: createSourceOCIRepositoryCmdRun,
 }
 
 type sourceOCIRepositoryFlags struct {
-	url             string
-	tag             string
-	semver          string
-	digest          string
-	secretRef       string
-	serviceAccount  string
-	certSecretRef   string
-	verifyProvider  flags.SourceOCIVerifyProvider
-	verifySecretRef string
-	ignorePaths     []string
-	provider        flags.SourceOCIProvider
-	insecure        bool
+	url              string
+	tag              string
+	semver           string
+	digest           string
+	secretRef        string
+	serviceAccount   string
+	certSecretRef    string
+	verifyProvider   flags.SourceOCIVerifyProvider
+	verifySecretRef  string
+	verifyOIDCIssuer string
+	verifySubject    string
+	ignorePaths      []string
+	provider         flags.SourceOCIProvider
+	insecure         bool
 }
 
 var sourceOCIRepositoryArgs = newSourceOCIFlags()
@@ -83,6 +94,8 @@ func init() {
 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.certSecretRef, "cert-ref", "", "the name of a secret to use for TLS certificates")
 	createSourceOCIRepositoryCmd.Flags().Var(&sourceOCIRepositoryArgs.verifyProvider, "verify-provider", sourceOCIRepositoryArgs.verifyProvider.Description())
 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifySecretRef, "verify-secret-ref", "", "the name of a secret to use for signature verification")
+	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifySubject, "verify-subject", "", "regular expression to use for the OIDC subject during signature verification")
+	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification")
 	createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)")
 	createSourceOCIRepositoryCmd.Flags().BoolVar(&sourceOCIRepositoryArgs.insecure, "insecure", false, "for when connecting to a non-TLS registries over plain HTTP")
 
@@ -168,8 +181,18 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
 				Name: secretName,
 			}
 		}
+		verifyIssuer := sourceOCIRepositoryArgs.verifyOIDCIssuer
+		verifySubject := sourceOCIRepositoryArgs.verifySubject
+		if verifyIssuer != "" || verifySubject != "" {
+			repository.Spec.Verify.MatchOIDCIdentity = []sourcev1.OIDCIdentityMatch{{
+				Issuer:  verifyIssuer,
+				Subject: verifySubject,
+			}}
+		}
 	} else if sourceOCIRepositoryArgs.verifySecretRef != "" {
 		return fmt.Errorf("a verification provider must be specified when a secret is specified")
+	} else if sourceOCIRepositoryArgs.verifyOIDCIssuer != "" || sourceOCIRepositoryArgs.verifySubject != "" {
+		return fmt.Errorf("a verification provider must be specified when OIDC issuer/subject is specified")
 	}
 
 	if createArgs.export {
diff --git a/cmd/flux/create_source_oci_test.go b/cmd/flux/create_source_oci_test.go
index 743632a6e2..da08d9f640 100644
--- a/cmd/flux/create_source_oci_test.go
+++ b/cmd/flux/create_source_oci_test.go
@@ -37,10 +37,35 @@ func TestCreateSourceOCI(t *testing.T) {
 			assertFunc: assertError("url is required"),
 		},
 		{
-			name:       "verify provider not specified",
+			name:       "verify secret specified but provider missing",
 			args:       "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-secret-ref=cosign-pub",
 			assertFunc: assertError("a verification provider must be specified when a secret is specified"),
 		},
+		{
+			name:       "verify issuer specified but provider missing",
+			args:       "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github.com",
+			assertFunc: assertError("a verification provider must be specified when OIDC issuer/subject is specified"),
+		},
+		{
+			name:       "verify identity specified but provider missing",
+			args:       "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-subject=developer",
+			assertFunc: assertError("a verification provider must be specified when OIDC issuer/subject is specified"),
+		},
+		{
+			name:       "verify issuer specified but subject missing",
+			args:       "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github --verify-provider=cosign --export",
+			assertFunc: assertGoldenFile("./testdata/oci/export_with_issuer.golden"),
+		},
+		{
+			name:       "all verify fields set",
+			args:       "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github verify-subject=stefanprodan --verify-provider=cosign --export",
+			assertFunc: assertGoldenFile("./testdata/oci/export_with_issuer.golden"),
+		},
+		{
+			name:       "verify subject specified but issuer missing",
+			args:       "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-subject=stefanprodan --verify-provider=cosign --export",
+			assertFunc: assertGoldenFile("./testdata/oci/export_with_subject.golden"),
+		},
 		{
 			name:       "export manifest",
 			args:       "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --export",
diff --git a/cmd/flux/testdata/oci/export_with_complete_verification.golden b/cmd/flux/testdata/oci/export_with_complete_verification.golden
new file mode 100644
index 0000000000..8b862a83dd
--- /dev/null
+++ b/cmd/flux/testdata/oci/export_with_complete_verification.golden
@@ -0,0 +1,16 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: OCIRepository
+metadata:
+  name: podinfo
+  namespace: flux-system
+spec:
+  interval: 0s
+  ref:
+    tag: 6.3.5
+  url: oci://ghcr.io/stefanprodan/manifests/podinfo
+  verify:
+    matchOIDCIdentity:
+    - issuer: github
+      subject: stefanprodan
+    provider: cosign
diff --git a/cmd/flux/testdata/oci/export_with_issuer.golden b/cmd/flux/testdata/oci/export_with_issuer.golden
new file mode 100644
index 0000000000..3abaf36a1b
--- /dev/null
+++ b/cmd/flux/testdata/oci/export_with_issuer.golden
@@ -0,0 +1,16 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: OCIRepository
+metadata:
+  name: podinfo
+  namespace: flux-system
+spec:
+  interval: 0s
+  ref:
+    tag: 6.3.5
+  url: oci://ghcr.io/stefanprodan/manifests/podinfo
+  verify:
+    matchOIDCIdentity:
+    - issuer: github
+      subject: ""
+    provider: cosign
diff --git a/cmd/flux/testdata/oci/export_with_subject.golden b/cmd/flux/testdata/oci/export_with_subject.golden
new file mode 100644
index 0000000000..93eca53b88
--- /dev/null
+++ b/cmd/flux/testdata/oci/export_with_subject.golden
@@ -0,0 +1,16 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: OCIRepository
+metadata:
+  name: podinfo
+  namespace: flux-system
+spec:
+  interval: 0s
+  ref:
+    tag: 6.3.5
+  url: oci://ghcr.io/stefanprodan/manifests/podinfo
+  verify:
+    matchOIDCIdentity:
+    - issuer: ""
+      subject: stefanprodan
+    provider: cosign