diff --git a/.gitignore b/.gitignore index 1e919fc0b..63ad2a1c7 100644 --- a/.gitignore +++ b/.gitignore @@ -182,4 +182,8 @@ hack/**/charts benchmark/*/data.tar.gz # JUnit file output from acceptance tests -junit-acceptance.xml \ No newline at end of file +junit-acceptance.xml + +# Generated TUF ConfigMaps (now created just-in-time) +hack/tuf/tuf-files-configmap.yaml +hack/tuf/tuf-root-configmap.yaml diff --git a/Makefile b/Makefile index a15d5d0fd..dac54bfde 100644 --- a/Makefile +++ b/Makefile @@ -118,6 +118,26 @@ test: ## Run all unit tests ACCEPTANCE_TIMEOUT:=20m .ONESHELL: .SHELLFLAGS=-e -c + +# Generate TUF ConfigMaps if they don't exist or source files are newer +hack/tuf/tuf-files-configmap.yaml: acceptance/wiremock/recordings/tuf/__files/* acceptance/wiremock/recordings/tuf/mappings/* acceptance/tuf/root.json hack/tuf/create-tuf-files.sh + @echo "Generating TUF ConfigMaps..." + @hack/tuf/create-tuf-files.sh + +# The root ConfigMap is generated at the same time as the files ConfigMap +hack/tuf/tuf-root-configmap.yaml: hack/tuf/tuf-files-configmap.yaml + +.PHONY: tuf-yaml-clean +tuf-yaml-clean: ## Remove generated TUF ConfigMap YAML files + @echo "Cleaning TUF ConfigMap YAML files..." + @rm -f hack/tuf/tuf-files-configmap.yaml hack/tuf/tuf-root-configmap.yaml + +.PHONY: tuf-yaml-refresh +tuf-yaml-refresh: tuf-yaml-clean tuf-yaml ## Force regeneration of TUF ConfigMap YAML files + +.PHONY: tuf-yaml +tuf-yaml: hack/tuf/tuf-files-configmap.yaml hack/tuf/tuf-root-configmap.yaml ## Generate TUF ConfigMap YAML files for acceptance tests + .PHONY: acceptance acceptance: ## Run all acceptance tests @@ -130,13 +150,14 @@ acceptance: ## Run all acceptance tests cp -R . "$$ACCEPTANCE_WORKDIR"; \ cd "$$ACCEPTANCE_WORKDIR" && \ $(MAKE) build && \ + $(MAKE) tuf-yaml && \ export GOCOVERDIR="$${ACCEPTANCE_WORKDIR}/coverage"; \ cd acceptance && go test -timeout $(ACCEPTANCE_TIMEOUT) ./... ; go tool covdata textfmt -i=$${GOCOVERDIR} -o="$(ROOT_DIR)/coverage-acceptance.out" # Add @focus above the feature you're hacking on to use this # (Mainly for use with the feature-% target below) .PHONY: focus-acceptance -focus-acceptance: build ## Run acceptance tests with @focus tag +focus-acceptance: build tuf-yaml ## Run acceptance tests with @focus tag @cd acceptance && go test . -args -tags=@focus # Uses sed hackery to insert a @focus tag and then remove it afterwards. @@ -157,7 +178,7 @@ feature_%: ## Run acceptance tests for a single feature file, e.g. make feature_ fi # (Replace spaces with underscores in the scenario name.) -scenario_%: build ## Run acceptance tests for a single scenario, e.g. make scenario_inline_policy +scenario_%: build tuf-yaml ## Run acceptance tests for a single scenario, e.g. make scenario_inline_policy @cd acceptance && go test -test.run 'TestFeatures/$*' benchmark/%/data.tar.gz: diff --git a/acceptance/image/image.go b/acceptance/image/image.go index 30774fc43..6328cf66a 100644 --- a/acceptance/image/image.go +++ b/acceptance/image/image.go @@ -761,6 +761,11 @@ func createAndPushKeylessImage(ctx context.Context, imageName string) (context.C continue } + // Skip empty signatures (DSSE envelopes have empty signature annotations) + if signature == "" { + continue + } + sig := Signature{ Signature: signature, } @@ -995,7 +1000,10 @@ func RawAttestationSignaturesFrom(ctx context.Context) map[string]string { ret := map[string]string{} for ref, signature := range state.AttestationSignatures { - ret[fmt.Sprintf("ATTESTATION_SIGNATURE_%s", ref)] = signature.Signature + // Only add the variable if the signature is not empty to avoid polluting snapshots + if signature.Signature != "" { + ret[fmt.Sprintf("ATTESTATION_SIGNATURE_%s", ref)] = signature.Signature + } } return ret diff --git a/acceptance/kubernetes/kind/kind.go b/acceptance/kubernetes/kind/kind.go index abf104542..146a1a9cd 100644 --- a/acceptance/kubernetes/kind/kind.go +++ b/acceptance/kubernetes/kind/kind.go @@ -166,7 +166,7 @@ func Start(givenCtx context.Context) (ctx context.Context, kCluster types.Cluste var port int if port, err = freeport.GetFreePort(); err != nil { - logger.Errorf("Unable to determine a free port: %v", err) + logger.Errorf("Unable to determine a free port for registry: %v", err) return } else { kCluster.registryPort = int32(port) //nolint:gosec // G115 - ports shouldn't be larger than int32 diff --git a/acceptance/kubernetes/kind/kubernetes.go b/acceptance/kubernetes/kind/kubernetes.go index 1752d60cf..dc8dcb5f4 100644 --- a/acceptance/kubernetes/kind/kubernetes.go +++ b/acceptance/kubernetes/kind/kubernetes.go @@ -32,6 +32,8 @@ import ( pipeline "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" tekton "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/typed/pipeline/v1" v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -39,7 +41,9 @@ import ( "github.com/conforma/cli/acceptance/crypto" "github.com/conforma/cli/acceptance/kubernetes/types" "github.com/conforma/cli/acceptance/kustomize" + "github.com/conforma/cli/acceptance/rekor" "github.com/conforma/cli/acceptance/testenv" + "github.com/conforma/cli/acceptance/wiremock" ) // createPolicyObject creates the EnterpriseContractPolicy object with the given @@ -189,6 +193,137 @@ func (k *kindCluster) CreateNamedSnapshot(ctx context.Context, name string, spec return k.createSnapshot(ctx, snapshot) } +// CreateConfigMap creates a ConfigMap with the given name and namespace with the provided content +// Also creates necessary RBAC permissions for cross-namespace access +func (k *kindCluster) CreateConfigMap(ctx context.Context, name, namespace, content string) error { + var data map[string]string + + // Parse JSON content and extract individual fields as ConfigMap data keys + if strings.HasPrefix(strings.TrimSpace(content), "{") { + // Parse JSON content + var jsonData map[string]interface{} + if err := json.Unmarshal([]byte(content), &jsonData); err != nil { + return fmt.Errorf("failed to parse JSON content: %w", err) + } + + // Convert to string map for ConfigMap data + data = make(map[string]string) + for key, value := range jsonData { + if value != nil { + data[key] = fmt.Sprintf("%v", value) + } + } + } else { + // For non-JSON content, store as-is under a single key + data = map[string]string{ + "content": content, + } + } + + configMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: data, + } + + // Create the ConfigMap (or update if it already exists) + if _, err := k.client.CoreV1().ConfigMaps(namespace).Create(ctx, configMap, metav1.CreateOptions{}); err != nil { + if apierrors.IsAlreadyExists(err) { + // ConfigMap exists, so get the existing one to retrieve its ResourceVersion + existing, err := k.client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get existing ConfigMap: %w", err) + } + // Set the ResourceVersion from the existing ConfigMap + configMap.ResourceVersion = existing.ResourceVersion + // Now update with the proper ResourceVersion + if _, err := k.client.CoreV1().ConfigMaps(namespace).Update(ctx, configMap, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("failed to update existing ConfigMap: %w", err) + } + } else { + return err + } + } + + // Create RBAC permissions for cross-namespace ConfigMap access + // This allows any service account to read ConfigMaps from any namespace + if err := k.ensureConfigMapRBAC(ctx); err != nil { + return fmt.Errorf("failed to create RBAC permissions: %w", err) + } + + return nil +} + +// ensureConfigMapRBAC creates necessary RBAC permissions for ConfigMap access across namespaces +func (k *kindCluster) ensureConfigMapRBAC(ctx context.Context) error { + // Create ClusterRole for ConfigMap reading (idempotent) + clusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "acceptance-configmap-reader", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "list"}, + }, + }, + } + + if _, err := k.client.RbacV1().ClusterRoles().Create(ctx, clusterRole, metav1.CreateOptions{}); err != nil { + // Ignore error if ClusterRole already exists + if !strings.Contains(err.Error(), "already exists") { + return fmt.Errorf("failed to create ClusterRole: %w", err) + } + } + + // Create ClusterRoleBinding for all service accounts (idempotent) + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "acceptance-configmap-reader-binding", + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "acceptance-configmap-reader", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "Group", + Name: "system:serviceaccounts", + APIGroup: "rbac.authorization.k8s.io", + }, + }, + } + + if _, err := k.client.RbacV1().ClusterRoleBindings().Create(ctx, clusterRoleBinding, metav1.CreateOptions{}); err != nil { + // Ignore error if ClusterRoleBinding already exists + if !strings.Contains(err.Error(), "already exists") { + return fmt.Errorf("failed to create ClusterRoleBinding: %w", err) + } + } + + return nil +} + +// CreateNamedNamespace creates a namespace with the specified name +func (k *kindCluster) CreateNamedNamespace(ctx context.Context, name string) error { + _, err := k.client.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }, metav1.CreateOptions{}) + + // Ignore error if namespace already exists + if err != nil && strings.Contains(err.Error(), "already exists") { + return nil + } + + return err +} + // CreateNamespace creates a randomly-named namespace for the test to execute in // and stores it in the test context func (k *kindCluster) CreateNamespace(ctx context.Context) (context.Context, error) { @@ -254,6 +389,19 @@ func stringParam(ctx context.Context, name, value string, t *testState) pipeline vars["BUILD_SNAPSHOT_DIGEST"] = t.snapshotDigest } + // Add TUF and certificate variables for keyless verification + // For Tekton tasks, always use the cluster-internal TUF service + vars["TUF"] = "http://tuf.tuf-service.svc.cluster.local:8080" + vars["CERT_IDENTITY"] = "https://kubernetes.io/namespaces/default/serviceaccounts/default" + vars["CERT_ISSUER"] = "https://kubernetes.default.svc.cluster.local" + + // Only set REKOR variable if stub rekord was started + if wiremock.IsRunning(ctx) { + if rekorURL, err := rekor.StubRekor(ctx); err == nil { + vars["REKOR"] = rekorURL + } + } + publicKeys := crypto.PublicKeysFrom(ctx) for name, key := range publicKeys { vars[fmt.Sprintf("%s_PUBLIC_KEY", name)] = key diff --git a/acceptance/kubernetes/kubernetes.go b/acceptance/kubernetes/kubernetes.go index eb5507a84..380f618df 100644 --- a/acceptance/kubernetes/kubernetes.go +++ b/acceptance/kubernetes/kubernetes.go @@ -36,6 +36,7 @@ import ( "github.com/conforma/cli/acceptance/kubernetes/stub" "github.com/conforma/cli/acceptance/kubernetes/types" "github.com/conforma/cli/acceptance/registry" + "github.com/conforma/cli/acceptance/rekor" "github.com/conforma/cli/acceptance/snaps" "github.com/conforma/cli/acceptance/testenv" ) @@ -121,7 +122,6 @@ func buildSnapshotArtifact(ctx context.Context, specification *godog.DocString) } return c.cluster.BuildSnapshotArtifact(ctx, specification.Content) - } func createNamedPolicy(ctx context.Context, name string, specification *godog.DocString) error { @@ -174,6 +174,26 @@ func createNamedSnapshot(ctx context.Context, name string, specification *godog. return c.cluster.CreateNamedSnapshot(ctx, name, specification.Content) } +func createConfigMap(ctx context.Context, name, namespace string, content *godog.DocString) error { + c := testenv.FetchState[ClusterState](ctx) + + if err := mustBeUp(ctx, *c); err != nil { + return err + } + + return c.cluster.CreateConfigMap(ctx, name, namespace, content.Content) +} + +func createNamedNamespace(ctx context.Context, name string) error { + c := testenv.FetchState[ClusterState](ctx) + + if err := mustBeUp(ctx, *c); err != nil { + return err + } + + return c.cluster.CreateNamedNamespace(ctx, name) +} + func createNamedSnapshotWithManyComponents(ctx context.Context, name string, amount int, key string) (context.Context, error) { c := testenv.FetchState[ClusterState](ctx) @@ -366,6 +386,11 @@ func taskLogsShouldMatchTheSnapshot(ctx context.Context, stepName string) error vars["EC_VERSION"] = v + // Add Rekor URL for snapshot normalization + if rekorURL, err := rekor.StubRekor(ctx); err == nil { + vars["REKOR"] = rekorURL + } + for _, step := range info.Steps { if step.Name == stepName { return snaps.MatchSnapshot(ctx, step.Name, step.Logs, vars) @@ -472,6 +497,56 @@ func stepEnvVarShouldBe(ctx context.Context, stepName, envName, want string) err return fmt.Errorf("step %q not found when looking for the %q env var", stepName, envName) } +// createBasicPolicyWithKnownKey creates a basic policy with ${known_PUBLIC_KEY} +// Avoid some repetition in feature files that use the same policy repeatedly +func createBasicPolicyWithKnownKey(ctx context.Context) error { + policyContent := `{"publicKey": ${known_PUBLIC_KEY}}` + return createPolicy(ctx, &godog.DocString{Content: policyContent}) +} + +// createSLSAProvenancePolicy creates a policy for SLSA provenance checking +// Avoid some repetition in feature files that use the same policy repeatedly +func createSLSAProvenancePolicy(ctx context.Context) error { + policyContent := `{ + "sources": [ + { + "policy": [ + "github.com/conforma/policy//policy/release?ref=0de5461c14413484575e63e96ddb514d8ab954b5", + "github.com/conforma/policy//policy/lib?ref=0de5461c14413484575e63e96ddb514d8ab954b5" + ], + "config": { + "include": [ + "slsa_provenance_available" + ] + } + } + ] +}` + return createPolicy(ctx, &godog.DocString{Content: policyContent}) +} + +// createGoldenContainerPolicy creates the hardcoded policy used in golden container scenarios +// Avoid some repetition in feature files that use the same policy repeatedly +func createGoldenContainerPolicy(ctx context.Context) error { + policyContent := `{ + "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERhr8Zj4dZW67zucg8fDr11M4lmRp\nzN6SIcIjkvH39siYg1DkCoa2h2xMUZ10ecbM3/ECqvBV55YwQ2rcIEa7XQ==\n-----END PUBLIC KEY-----", + "sources": [ + { + "policy": [ + "github.com/conforma/policy//policy/release?ref=0de5461c14413484575e63e96ddb514d8ab954b5", + "github.com/conforma/policy//policy/lib?ref=0de5461c14413484575e63e96ddb514d8ab954b5" + ], + "config": { + "include": [ + "slsa_provenance_available" + ] + } + } + ] +}` + return createPolicy(ctx, &godog.DocString{Content: policyContent}) +} + // AddStepsTo adds cluster-related steps to the context func AddStepsTo(sc *godog.ScenarioContext) { sc.Step(`^a stub cluster running$`, startAndSetupState(stub.Start)) @@ -480,6 +555,9 @@ func AddStepsTo(sc *godog.ScenarioContext) { sc.Step(`^a snapshot artifact with content:$`, buildSnapshotArtifact) sc.Step(`^policy configuration named "([^"]*)" with specification$`, createNamedPolicy) sc.Step(`^a cluster policy with content:$`, createPolicy) + sc.Step(`^a basic policy with known public key$`, createBasicPolicyWithKnownKey) + sc.Step(`^a policy for SLSA provenance checking$`, createSLSAProvenancePolicy) + sc.Step(`^a golden container policy$`, createGoldenContainerPolicy) sc.Step(`^version ([\d.]+) of the task named "([^"]*)" is run with parameters:$`, runTask) sc.Step(`^version ([\d.]+) of the task named "([^"]*)" with workspace "([^"]*)" is run with parameters:$`, runTaskWithWorkspace) sc.Step(`^the task should succeed$`, theTaskShouldSucceed) @@ -493,6 +571,8 @@ func AddStepsTo(sc *godog.ScenarioContext) { sc.Step(`^the task results should match the snapshot$`, taskResultsShouldMatchTheSnapshot) sc.Step(`^the task result "([^"]*)" should equal "([^"]*)"$`, taskResultShouldEqual) sc.Step(`^policy configuration named "([^"]*)" with (\d+) policy sources from "([^"]*)"(?:, patched with)$`, createNamedPolicyWithManySources) + sc.Step(`^a namespace named "([^"]*)" exists$`, createNamedNamespace) + sc.Step(`^a ConfigMap "([^"]*)" in namespace "([^"]*)" with content:$`, createConfigMap) // stop usage of the cluster once a test is done, godog will call this // function on failure and on the last step, so more than once if the // failure is not on the last step and once if there was no failure or the diff --git a/acceptance/kubernetes/stub/stub.go b/acceptance/kubernetes/stub/stub.go index e5c61e065..ca3100a2f 100644 --- a/acceptance/kubernetes/stub/stub.go +++ b/acceptance/kubernetes/stub/stub.go @@ -135,6 +135,14 @@ func (s stubCluster) CreateNamedSnapshot(ctx context.Context, name string, speci }))).WithHeaders(map[string]string{"Content-Type": "application/json"}).WithStatus(200))) } +func (s stubCluster) CreateConfigMap(_ context.Context, _, _, _ string) error { + return errors.New("ConfigMap creation is not supported when using the stub Kubernetes") +} + +func (s stubCluster) CreateNamedNamespace(_ context.Context, _ string) error { + return errors.New("Named namespace creation is not supported when using the stub Kubernetes") +} + func (s stubCluster) CreatePolicy(_ context.Context, _ string) error { return errors.New("use `Given policy configuration named \"\" with specification` when using the stub Kubernetes") } diff --git a/acceptance/kubernetes/types/types.go b/acceptance/kubernetes/types/types.go index c747a5144..4c4dd62d8 100644 --- a/acceptance/kubernetes/types/types.go +++ b/acceptance/kubernetes/types/types.go @@ -31,6 +31,8 @@ type Cluster interface { AwaitUntilTaskIsDone(context.Context) (bool, error) TaskInfo(context.Context) (*TaskInfo, error) CreateNamedSnapshot(context.Context, string, string) error + CreateNamedNamespace(context.Context, string) error + CreateConfigMap(context.Context, string, string, string) error Registry(context.Context) (string, error) BuildSnapshotArtifact(context.Context, string) (context.Context, error) } diff --git a/acceptance/tuf/tuf.go b/acceptance/tuf/tuf.go index c3cc5946d..fd978640c 100644 --- a/acceptance/tuf/tuf.go +++ b/acceptance/tuf/tuf.go @@ -100,6 +100,7 @@ func initializeRoot(ctx context.Context) (context.Context, error) { } // Stub returns the `http://host:port` of the stubbed TUF. +// This is used for CLI context (external WireMock) func Stub(ctx context.Context) (string, error) { endpoint, err := wiremock.Endpoint(ctx) if err != nil { diff --git a/acceptance/wiremock/wiremock.go b/acceptance/wiremock/wiremock.go index 08f7a94b2..2301072a8 100644 --- a/acceptance/wiremock/wiremock.go +++ b/acceptance/wiremock/wiremock.go @@ -187,6 +187,11 @@ func (c *client) UnmatchedRequests() ([]unmatchedRequest, error) { // StartWiremock starts the WireMock instance if one is not already running func StartWiremock(ctx context.Context) (context.Context, error) { + return StartWiremockOnPort(ctx, 0) // 0 means use any available port +} + +// StartWiremockOnPort starts the WireMock instance on a specific port if one is not already running +func StartWiremockOnPort(ctx context.Context, hostPort int) (context.Context, error) { var state *wiremockState ctx, err := testenv.SetupState(ctx, &state) if err != nil { @@ -204,9 +209,18 @@ func StartWiremock(ctx context.Context) (context.Context, error) { return ctx, err } + var exposedPorts []string + if hostPort > 0 { + // Use specific port + exposedPorts = []string{fmt.Sprintf("0.0.0.0:%d:8080/tcp", hostPort), "0.0.0.0::8443/tcp"} + } else { + // Use any available port + exposedPorts = []string{"0.0.0.0::8080/tcp", "0.0.0.0::8443/tcp"} + } + req := testenv.TestContainersRequest(ctx, testcontainers.ContainerRequest{ Image: wireMockImage, - ExposedPorts: []string{"0.0.0.0::8080/tcp", "0.0.0.0::8443/tcp"}, + ExposedPorts: exposedPorts, WaitingFor: wait.ForHTTP("/__admin/mappings").WithPort("8080/tcp"), Binds: []string{fmt.Sprintf("%s:/recordings:z", recordings)}, Cmd: []string{ @@ -225,12 +239,18 @@ func StartWiremock(ctx context.Context) (context.Context, error) { return ctx, fmt.Errorf("unable to run GenericContainer: %v", err) } - port, err := w.MappedPort(ctx, "8080/tcp") - if err != nil { - return ctx, err + var url string + if hostPort > 0 { + // Use the specific port that was requested + url = fmt.Sprintf("http://localhost:%d", hostPort) + } else { + // Get the dynamically assigned port + port, err := w.MappedPort(ctx, "8080/tcp") + if err != nil { + return ctx, err + } + url = fmt.Sprintf("http://localhost:%d", port.Int()) } - - url := fmt.Sprintf("http://localhost:%d", port.Int()) state.URL = url return ctx, nil diff --git a/docs/modules/ROOT/pages/collect-keyless-signing-params.adoc b/docs/modules/ROOT/pages/collect-keyless-signing-params.adoc new file mode 100644 index 000000000..bbdab3267 --- /dev/null +++ b/docs/modules/ROOT/pages/collect-keyless-signing-params.adoc @@ -0,0 +1,41 @@ += collect-keyless-signing-params + +Version: 0.1 + +== Synopsis + +Tekton task to collect Konflux configuration parameters related to +keyless signing using cosign. The task attempts to read the "cluster-config" +ConfigMap in the "konflux-info" namespace to extract signing parameters. + +In case the ConfigMap is not found, the task will output empty strings for all parameters, +allowing the pipeline to continue without signing parameters. + + +== Params +[horizontal] + +*configMapName* (`string`):: The name of the ConfigMap to read signing parameters from ++ +*Default*: `cluster-config` +*configMapNamespace* (`string`):: The namespace where the ConfigMap is located ++ +*Default*: `konflux-info` + +== Results + +[horizontal] +*defaultOIDCIssuer*:: A default OIDC issuer URL to be used for signing. + +*rekorExternalUrl*:: The external URL of the Rekor transparency log. + +*fulcioExternalUrl*:: The external URL of the Fulcio certificate authority. + +*tufExternalUrl*:: The external URL of the TUF repository. + +*buildIdentity*:: The build identity from the OIDC token claims, if applicable. + +*buildIdentityRegexp*:: A regular expression to extract build identity from the OIDC token claims, if applicable. + +*keylessSigningEnabled*:: A flag indicating whether keyless signing is enabled based on the presence of signing parameters. + diff --git a/docs/modules/ROOT/pages/verify-conforma-konflux-ta.adoc b/docs/modules/ROOT/pages/verify-conforma-konflux-ta.adoc index 844d2c63d..c4ef66d46 100644 --- a/docs/modules/ROOT/pages/verify-conforma-konflux-ta.adoc +++ b/docs/modules/ROOT/pages/verify-conforma-konflux-ta.adoc @@ -25,6 +25,8 @@ You can also specify a policy configuration using a git url, e.g. *REKOR_HOST* (`string`):: Rekor host for transparency log lookups *CERTIFICATE_IDENTITY* (`string`):: Expected identity in the signing certificate for keyless verification. This should be the email or URI that was used when signing. You should provide both CERTIFICATE_OIDC_ISSUER and CERTIFICATE_IDENTITY for keyless verification. The PUBLIC_KEY param will be ignored if this is provided. *CERTIFICATE_OIDC_ISSUER* (`string`):: Expected OIDC issuer in the signing certificate for keyless verification. This should match the issuer that provided the identity token used for signing. You should provide both CERTIFICATE_OIDC_ISSUER and CERTIFICATE_IDENTITY for keyless verification. The PUBLIC_KEY param will be ignored if this is provided. +*CERTIFICATE_IDENTITY_REGEXP* (`string`):: Similar to CERTIFICATE_IDENTITY but the value is a regexp that will be matched. Note that CERTIFICATE_IDENTITY takes precendence over this if both are present. +*CERTIFICATE_OIDC_ISSUER_REGEXP* (`string`):: Similar to CERTIFICATE_OIDC_ISSUER but a regexp that will be matched. Note that CERTIFICATE_OIDC_ISSUER takes precendence over this if both are present. *IGNORE_REKOR* (`string`):: Skip Rekor transparency log checks during validation. + *Default*: `false` diff --git a/docs/modules/ROOT/pages/verify-enterprise-contract.adoc b/docs/modules/ROOT/pages/verify-enterprise-contract.adoc index 2a0a9c25a..e96d85832 100644 --- a/docs/modules/ROOT/pages/verify-enterprise-contract.adoc +++ b/docs/modules/ROOT/pages/verify-enterprise-contract.adoc @@ -36,6 +36,8 @@ You can also specify a policy configuration using a git url, e.g. *REKOR_HOST* (`string`):: Rekor host for transparency log lookups *CERTIFICATE_IDENTITY* (`string`):: Expected identity in the signing certificate for keyless verification. This should be the email or URI that was used when signing. You should provide both CERTIFICATE_OIDC_ISSUER and CERTIFICATE_IDENTITY for keyless verification. The PUBLIC_KEY param will be ignored if this is provided. *CERTIFICATE_OIDC_ISSUER* (`string`):: Expected OIDC issuer in the signing certificate for keyless verification. This should match the issuer that provided the identity token used for signing. You should provide both CERTIFICATE_OIDC_ISSUER and CERTIFICATE_IDENTITY for keyless verification. The PUBLIC_KEY param will be ignored if this is provided. +*CERTIFICATE_IDENTITY_REGEXP* (`string`):: Similar to CERTIFICATE_IDENTITY but the value is a regexp that will be matched. Note that CERTIFICATE_IDENTITY takes precendence over this if both are present. +*CERTIFICATE_OIDC_ISSUER_REGEXP* (`string`):: Similar to CERTIFICATE_OIDC_ISSUER but a regexp that will be matched. Note that CERTIFICATE_OIDC_ISSUER takes precendence over this if both are present. *IGNORE_REKOR* (`string`):: Skip Rekor transparency log checks during validation. + *Default*: `false` diff --git a/docs/modules/ROOT/partials/tasks_nav.adoc b/docs/modules/ROOT/partials/tasks_nav.adoc index 6ba57f992..77e80365b 100644 --- a/docs/modules/ROOT/partials/tasks_nav.adoc +++ b/docs/modules/ROOT/partials/tasks_nav.adoc @@ -1,4 +1,5 @@ * xref:tasks.adoc[Tekton Tasks] +** xref:collect-keyless-signing-params.adoc[collect-keyless-signing-params] ** xref:verify-conforma-konflux-ta.adoc[verify-conforma-konflux-ta] ** xref:verify-conforma-vsa-release-ta.adoc[verify-conforma-vsa-release-ta] ** xref:verify-enterprise-contract.adoc[verify-enterprise-contract] diff --git a/features/__snapshots__/task_validate_image.snap b/features/__snapshots__/task_validate_image.snap index 91a7a9e69..192e7a358 100755 --- a/features/__snapshots__/task_validate_image.snap +++ b/features/__snapshots__/task_validate_image.snap @@ -63,7 +63,7 @@ Error: Get "http://tuf.invalid/root.json": dial tcp: lookup tuf.invalid on 10.96 --- [Initialize TUF succeeds:initialize-tuf - 1] -${TIMESTAMP} INFO Step was skipped due to when expressions were evaluated to false. +WARNING: Could not fetch signing_config.json from the TUF mirror (encountered error: getting info for target "signing_config.v0.2.json": target signing_config.v0.2.json not found). It is recommended to use a signing config file rather than provide service URLs when signing. --- @@ -453,3 +453,169 @@ true "TEST_OUTPUT": "{\"timestamp\":\"${TIMESTAMP}\",\"namespace\":\"\",\"successes\":5,\"failures\":0,\"warnings\":0,\"result\":\"SUCCESS\"}\n" } --- + +[Keyless signing verification with local test image:detailed-report - 1] +Success: true +Result: SUCCESS +Violations: 0, Warnings: 0, Successes: 5 +Component: +ImageRef: ${REGISTRY}/acceptance/ec-happy-day-keyless@sha256:${REGISTRY_acceptance/ec-happy-day-keyless:latest_DIGEST} + + +--- + +[Keyless signing verification with local test image:results - 1] +{ + "TEST_OUTPUT": "{\"timestamp\":\"${TIMESTAMP}\",\"namespace\":\"\",\"successes\":5,\"failures\":0,\"warnings\":0,\"result\":\"SUCCESS\"}\n" +} +--- + +[Keyless signing verification with local test image using regexp params:detailed-report - 1] +Success: true +Result: SUCCESS +Violations: 0, Warnings: 0, Successes: 5 +Component: +ImageRef: ${REGISTRY}/acceptance/ec-happy-day-keyless@sha256:${REGISTRY_acceptance/ec-happy-day-keyless:latest_DIGEST} + + +--- + +[Keyless signing verification with local test image using regexp params:results - 1] +{ + "TEST_OUTPUT": "{\"timestamp\":\"${TIMESTAMP}\",\"namespace\":\"\",\"successes\":5,\"failures\":0,\"warnings\":0,\"result\":\"SUCCESS\"}\n" +} +--- + +[Collect keyless signing parameters when there is a malformed ConfigMap:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config-3 +ConfigMap found, extracting keyless signing parameters +enableKeylessSigning is not set, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters when the namespace does not exist:collect-signing-params - 1] +Reading ConfigMap doesnt-exist-namespace/whatever +ConfigMap not found, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters when the ConfigMap does not exist:collect-signing-params - 1] +Reading ConfigMap konflux-info/doesnt-exist-config +ConfigMap not found, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters from ConfigMap with keyless signing disabled:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config-2 +ConfigMap found, extracting keyless signing parameters +enableKeylessSigning is not set, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters from ConfigMap:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config +ConfigMap found, extracting keyless signing parameters +results.keylessSigningEnabled: true +results.defaultOIDCIssuer: https://kubernetes.default.svc.cluster.local +results.rekorExternalUrl: https://rekor.example.com +results.fulcioExternalUrl: https://fulcio.example.com +results.tufExternalUrl: https://tuf.example.com +results.buildIdentity: https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller +results.buildIdentityRegexp: ^https://konflux-ci.dev/.*$ + +--- + +[Collect keyless signing parameters when there is a malformed ConfigMap:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config-3 +ConfigMap found, extracting keyless signing parameters +enableKeylessSigning is not set, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters when the namespace does not exist:collect-signing-params - 1] +Reading ConfigMap doesnt-exist-namespace/whatever +ConfigMap not found, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters when the ConfigMap does not exist:collect-signing-params - 1] +Reading ConfigMap konflux-info/doesnt-exist-config +ConfigMap not found, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters from ConfigMap with keyless signing disabled:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config-2 +ConfigMap found, extracting keyless signing parameters +enableKeylessSigning is not set, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.rekorExternalUrl: +results.fulcioExternalUrl: +results.tufExternalUrl: +results.buildIdentity: +results.buildIdentityRegexp: + +--- + +[Collect keyless signing parameters from ConfigMap:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config +ConfigMap found, extracting keyless signing parameters +results.keylessSigningEnabled: true +results.defaultOIDCIssuer: https://kubernetes.default.svc.cluster.local +results.rekorExternalUrl: https://rekor.example.com +results.fulcioExternalUrl: https://fulcio.example.com +results.tufExternalUrl: https://tuf.example.com +results.buildIdentity: https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller +results.buildIdentityRegexp: ^https://konflux-ci.dev/.*$ + +--- diff --git a/features/task_validate_image.feature b/features/task_validate_image.feature index de205d33b..acdce54d2 100644 --- a/features/task_validate_image.feature +++ b/features/task_validate_image.feature @@ -9,25 +9,7 @@ Feature: Verify Enterprise Contract Tekton Tasks Scenario: Golden container image Given a working namespace - Given a cluster policy with content: - ``` - { - "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERhr8Zj4dZW67zucg8fDr11M4lmRp\nzN6SIcIjkvH39siYg1DkCoa2h2xMUZ10ecbM3/ECqvBV55YwQ2rcIEa7XQ==\n-----END PUBLIC KEY-----", - "sources": [ - { - "policy": [ - "github.com/conforma/policy//policy/release?ref=0de5461c14413484575e63e96ddb514d8ab954b5", - "github.com/conforma/policy//policy/lib?ref=0de5461c14413484575e63e96ddb514d8ab954b5" - ], - "config": { - "include": [ - "slsa_provenance_available" - ] - } - } - ] - } - ``` + Given a golden container policy When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "quay.io/hacbs-contract-demo/golden-container@sha256:e76a4ae9dd8a52a0d191fd34ca133af5b4f2609536d32200a4a40a09fdc93a0d"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -40,25 +22,7 @@ Feature: Verify Enterprise Contract Tekton Tasks Scenario: Extra rule data provided to task Given a working namespace - Given a cluster policy with content: - ``` - { - "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERhr8Zj4dZW67zucg8fDr11M4lmRp\nzN6SIcIjkvH39siYg1DkCoa2h2xMUZ10ecbM3/ECqvBV55YwQ2rcIEa7XQ==\n-----END PUBLIC KEY-----", - "sources": [ - { - "policy": [ - "github.com/conforma/policy//policy/release", - "github.com/conforma/policy//policy/lib" - ], - "config": { - "include": [ - "slsa_provenance_available" - ] - } - } - ] - } - ``` + Given a golden container policy When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "quay.io/hacbs-contract-demo/golden-container@sha256:e76a4ae9dd8a52a0d191fd34ca133af5b4f2609536d32200a4a40a09fdc93a0d"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -71,25 +35,7 @@ Feature: Verify Enterprise Contract Tekton Tasks Scenario: Initialize TUF succeeds Given a working namespace - Given a cluster policy with content: - ``` - { - "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERhr8Zj4dZW67zucg8fDr11M4lmRp\nzN6SIcIjkvH39siYg1DkCoa2h2xMUZ10ecbM3/ECqvBV55YwQ2rcIEa7XQ==\n-----END PUBLIC KEY-----", - "sources": [ - { - "policy": [ - "github.com/conforma/policy//policy/release", - "github.com/conforma/policy//policy/lib" - ], - "config": { - "include": [ - "slsa_provenance_available" - ] - } - } - ] - } - ``` + Given a golden container policy When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "quay.io/hacbs-contract-demo/golden-container@sha256:e76a4ae9dd8a52a0d191fd34ca133af5b4f2609536d32200a4a40a09fdc93a0d"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -103,25 +49,7 @@ Feature: Verify Enterprise Contract Tekton Tasks Scenario: Initialize TUF fails Given a working namespace - Given a cluster policy with content: - ``` - { - "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERhr8Zj4dZW67zucg8fDr11M4lmRp\nzN6SIcIjkvH39siYg1DkCoa2h2xMUZ10ecbM3/ECqvBV55YwQ2rcIEa7XQ==\n-----END PUBLIC KEY-----", - "sources": [ - { - "policy": [ - "github.com/conforma/policy//policy/release", - "github.com/conforma/policy//policy/lib" - ], - "config": { - "include": [ - "slsa_provenance_available" - ] - } - } - ] - } - ``` + Given a golden container policy When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "quay.io/hacbs-contract-demo/golden-container@sha256:e76a4ae9dd8a52a0d191fd34ca133af5b4f2609536d32200a4a40a09fdc93a0d"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -206,12 +134,7 @@ Feature: Verify Enterprise Contract Tekton Tasks Scenario: Non strict with failures Given a working namespace And a key pair named "known" - And a cluster policy with content: - ``` - { - "publicKey": ${known_PUBLIC_KEY} - } - ``` + And a basic policy with known public key When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/does-not-exist"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -224,12 +147,7 @@ Feature: Verify Enterprise Contract Tekton Tasks Scenario: Strict with failures Given a working namespace And a key pair named "known" - And a cluster policy with content: - ``` - { - "publicKey": ${known_PUBLIC_KEY} - } - ``` + And a basic policy with known public key When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/does-not-exist"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -245,12 +163,7 @@ Feature: Verify Enterprise Contract Tekton Tasks And an image named "acceptance/okayish" And a valid image signature of "acceptance/okayish" image signed by the "known" key And a valid attestation of "acceptance/okayish" signed by the "known" key - And a cluster policy with content: - ``` - { - "publicKey": ${known_PUBLIC_KEY} - } - ``` + And a basic policy with known public key When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/okayish"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -269,10 +182,7 @@ Feature: Verify Enterprise Contract Tekton Tasks And an image named "acceptance/info" And a valid image signature of "acceptance/info" image signed by the "known" key And a valid attestation of "acceptance/info" signed by the "known" key - And a cluster policy with content: - ``` - {"publicKey": ${known_PUBLIC_KEY}} - ``` + And a basic policy with known public key When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/info"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -288,10 +198,7 @@ Feature: Verify Enterprise Contract Tekton Tasks And an image named "acceptance/effective-time" And a valid image signature of "acceptance/effective-time" image signed by the "known" key And a valid attestation of "acceptance/effective-time" signed by the "known" key - And a cluster policy with content: - ``` - {"publicKey": ${known_PUBLIC_KEY}} - ``` + And a basic policy with known public key When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/effective-time"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -306,10 +213,7 @@ Feature: Verify Enterprise Contract Tekton Tasks And an image named "acceptance/ssl-cert-dir" And a valid image signature of "acceptance/ssl-cert-dir" image signed by the "known" key And a valid attestation of "acceptance/ssl-cert-dir" signed by the "known" key - And a cluster policy with content: - ``` - {"publicKey": ${known_PUBLIC_KEY}} - ``` + And a basic policy with known public key When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/ssl-cert-dir"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -350,24 +254,7 @@ Feature: Verify Enterprise Contract Tekton Tasks # Confirm we can verify the signatures on a keylessly signed image signed with cosign v2 Scenario: Keyless signing verification cosign v2 style Given a working namespace - Given a cluster policy with content: - ``` - { - "sources": [ - { - "policy": [ - "github.com/conforma/policy//policy/release?ref=0de5461c14413484575e63e96ddb514d8ab954b5", - "github.com/conforma/policy//policy/lib?ref=0de5461c14413484575e63e96ddb514d8ab954b5" - ], - "config": { - "include": [ - "slsa_provenance_available" - ] - } - } - ] - } - ``` + Given a policy for SLSA provenance checking When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "quay.io/conforma/test:keyless_v2@sha256:03a10dff06ae364ef9727d562e7077b135b00c7a978e571c4354519e6d0f23b8"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -383,24 +270,7 @@ Feature: Verify Enterprise Contract Tekton Tasks # Confirm we can verify the signatures on a keylessly signed image signed with cosign v3 Scenario: Keyless signing verification cosign v3 style Given a working namespace - Given a cluster policy with content: - ``` - { - "sources": [ - { - "policy": [ - "github.com/conforma/policy//policy/release?ref=0de5461c14413484575e63e96ddb514d8ab954b5", - "github.com/conforma/policy//policy/lib?ref=0de5461c14413484575e63e96ddb514d8ab954b5" - ], - "config": { - "include": [ - "slsa_provenance_available" - ] - } - } - ] - } - ``` + Given a policy for SLSA provenance checking When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: | IMAGES | {"components": [{"containerImage": "quay.io/conforma/test:keyless_v3@sha256:712ca3a7fcd41fe6b3e6f434a31f738743b6c31f1d81ad458502d6b0239a8903"}]} | | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | @@ -412,3 +282,157 @@ Feature: Verify Enterprise Contract Tekton Tasks Then the task should succeed And the task logs for step "report-json" should match the snapshot And the task results should match the snapshot + + Scenario: Keyless signing verification with local test image + Given a working namespace + Given a signed and attested keyless image named "acceptance/ec-happy-day-keyless" + Given a initialized tuf root + Given a policy for SLSA provenance checking + When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: + | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/ec-happy-day-keyless"}]} | + | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | + | CERTIFICATE_IDENTITY | https://kubernetes.io/namespaces/default/serviceaccounts/default | + | CERTIFICATE_OIDC_ISSUER | https://kubernetes.default.svc.cluster.local | + | TUF_MIRROR | ${TUF} | + | REKOR_HOST | ${REKOR} | + | IGNORE_REKOR | false | + | STRICT | true | + Then the task should succeed + And the task logs for step "detailed-report" should match the snapshot + And the task results should match the snapshot + + Scenario: Keyless signing verification with local test image using regexp params + Given a working namespace + Given a signed and attested keyless image named "acceptance/ec-happy-day-keyless" + Given a initialized tuf root + Given a policy for SLSA provenance checking + When version 0.1 of the task named "verify-enterprise-contract" is run with parameters: + | IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/ec-happy-day-keyless"}]} | + | POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} | + | CERTIFICATE_IDENTITY_REGEXP | ^https://kubernetes\.io/namespaces/.*/serviceaccounts/.*$ | + | CERTIFICATE_OIDC_ISSUER_REGEXP | ^https://kubernetes\.default\.svc\.cluster\.local$ | + | TUF_MIRROR | ${TUF} | + | REKOR_HOST | ${REKOR} | + | IGNORE_REKOR | false | + | STRICT | true | + Then the task should succeed + And the task logs for step "detailed-report" should match the snapshot + And the task results should match the snapshot + + Scenario: Collect keyless signing parameters from ConfigMap + Given a working namespace + And a namespace named "konflux-info" exists + # Note: These scenarios might run in parallel so let's use a different config map + # for each scenario so we don't have to worry about them clashing with each other + And a ConfigMap "cluster-config" in namespace "konflux-info" with content: + """ + { + "defaultOIDCIssuer": "https://kubernetes.default.svc.cluster.local", + "rekorExternalUrl": "https://rekor.example.com", + "fulcioExternalUrl": "https://fulcio.example.com", + "tufExternalUrl": "https://tuf.example.com", + "buildIdentity": "https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller", + "buildIdentityRegexp": "^https://konflux-ci.dev/.*$", + "enableKeylessSigning": "true" + } + """ + When version 0.1 of the task named "collect-keyless-signing-params" is run with parameters: + | configMapName | cluster-config | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "defaultOIDCIssuer" should equal "https://kubernetes.default.svc.cluster.local" + And the task result "rekorExternalUrl" should equal "https://rekor.example.com" + And the task result "fulcioExternalUrl" should equal "https://fulcio.example.com" + And the task result "tufExternalUrl" should equal "https://tuf.example.com" + And the task result "buildIdentity" should equal "https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller" + And the task result "buildIdentityRegexp" should equal "^https://konflux-ci.dev/.*$" + And the task result "keylessSigningEnabled" should equal "true" + + Scenario: Collect keyless signing parameters from ConfigMap with keyless signing disabled + Given a working namespace + And a namespace named "konflux-info" exists + # Note: These scenarios might run in parallel so let's use a different config map + # for each scenario so we don't have to worry about them clashing with each other + And a ConfigMap "cluster-config-2" in namespace "konflux-info" with content: + """ + { + "defaultOIDCIssuer": "https://kubernetes.default.svc.cluster.local", + "rekorExternalUrl": "https://rekor.example.com", + "fulcioExternalUrl": "https://fulcio.example.com", + "tufExternalUrl": "https://tuf.example.com", + "buildIdentity": "https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller", + "buildIdentityRegexp": "^https://konflux-ci.dev/.*$", + "enableKeylessSigning": "false" + } + """ + When version 0.1 of the task named "collect-keyless-signing-params" is run with parameters: + | configMapName | cluster-config-2 | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "defaultOIDCIssuer" should equal "" + And the task result "rekorExternalUrl" should equal "" + And the task result "fulcioExternalUrl" should equal "" + And the task result "tufExternalUrl" should equal "" + And the task result "buildIdentity" should equal "" + And the task result "buildIdentityRegexp" should equal "" + And the task result "keylessSigningEnabled" should equal "false" + + Scenario: Collect keyless signing parameters when there is a malformed ConfigMap + Given a working namespace + And a namespace named "konflux-info" exists + # Note: These scenarios might run in parallel so let's use a different config map + # for each scenario so we don't have to worry about them clashing with each other + And a ConfigMap "cluster-config-3" in namespace "konflux-info" with content: + """ + {"foo": "bar"} + """ + When version 0.1 of the task named "collect-keyless-signing-params" is run with parameters: + | configMapName | cluster-config-3 | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "defaultOIDCIssuer" should equal "" + And the task result "rekorExternalUrl" should equal "" + And the task result "fulcioExternalUrl" should equal "" + And the task result "tufExternalUrl" should equal "" + And the task result "buildIdentity" should equal "" + And the task result "buildIdentityRegexp" should equal "" + And the task result "keylessSigningEnabled" should equal "false" + + Scenario: Collect keyless signing parameters when the ConfigMap does not exist + Given a working namespace + And a namespace named "konflux-info" exists + # Note: These scenarios might run in parallel so let's use a different config map + # for each scenario so we don't have to worry about them clashing with each other. + # Creating a config map deliberately so we are sure the rbac is created. (I might + # be wrong but I think it could matter if this secenario runs before any of the + # others.) + And a ConfigMap "cluster-config-4" in namespace "konflux-info" with content: + """ + {"foo": "bar"} + """ + When version 0.1 of the task named "collect-keyless-signing-params" is run with parameters: + | configMapNamespace | konflux-info | + | configMapName | doesnt-exist-config | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "defaultOIDCIssuer" should equal "" + And the task result "rekorExternalUrl" should equal "" + And the task result "fulcioExternalUrl" should equal "" + And the task result "tufExternalUrl" should equal "" + And the task result "buildIdentityRegexp" should equal "" + And the task result "keylessSigningEnabled" should equal "false" + + Scenario: Collect keyless signing parameters when the namespace does not exist + Given a working namespace + When version 0.1 of the task named "collect-keyless-signing-params" is run with parameters: + | configMapNamespace | doesnt-exist-namespace | + | configMapName | whatever | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "defaultOIDCIssuer" should equal "" + And the task result "rekorExternalUrl" should equal "" + And the task result "fulcioExternalUrl" should equal "" + And the task result "tufExternalUrl" should equal "" + And the task result "buildIdentity" should equal "" + And the task result "buildIdentityRegexp" should equal "" + And the task result "keylessSigningEnabled" should equal "false" diff --git a/hack/test/kustomization.yaml b/hack/test/kustomization.yaml index 9d61c5bc4..b6eabfbb8 100644 --- a/hack/test/kustomization.yaml +++ b/hack/test/kustomization.yaml @@ -22,4 +22,5 @@ resources: - ../tekton - ../ecc - ../registry + - ../tuf - ../work diff --git a/hack/tuf/create-tuf-files.sh b/hack/tuf/create-tuf-files.sh new file mode 100755 index 000000000..c98bac8bb --- /dev/null +++ b/hack/tuf/create-tuf-files.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# Copyright The Conforma Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + + +# Script to create TUF ConfigMaps with exact file content +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +WIREMOCK_TUF_DIR="$PROJECT_ROOT/acceptance/wiremock/recordings/tuf" + +# Extract file names from wiremock mapping files +extract_body_files() { + local files_dir="$WIREMOCK_TUF_DIR/__files" + local mappings_dir="$WIREMOCK_TUF_DIR/mappings" + + # Check if directories exist + if [[ ! -d "$files_dir" ]]; then + echo "Error: WireMock files directory not found: $files_dir" >&2 + exit 1 + fi + + if [[ ! -d "$mappings_dir" ]]; then + echo "Error: WireMock mappings directory not found: $mappings_dir" >&2 + exit 1 + fi + + local body_files=() + + # Extract bodyFileName from each mapping file using jq + while IFS= read -r -d '' mapping_file; do + if [[ -f "$mapping_file" ]]; then + local body_file + body_file=$(jq -r '.response.bodyFileName // empty' "$mapping_file") + if [[ -n "$body_file" && -f "$files_dir/$body_file" ]]; then + # Skip files that aren't useful for TUF (like root.json which we handle separately) + if [[ "$body_file" != "root.json" ]]; then + body_files+=("$body_file") + fi + fi + fi + done < <(find "$mappings_dir" -name "mapping-*.json" -print0) + + printf '%s\n' "${body_files[@]}" +} + +# Build kubectl command with dynamic file list +build_kubectl_command() { + local files_dir="$WIREMOCK_TUF_DIR/__files" + local cmd_args=("kubectl" "create" "configmap" "tuf-files" "--namespace=tuf-service") + + local body_files + readarray -t body_files < <(extract_body_files) + + if [[ ${#body_files[@]} -eq 0 ]]; then + echo "Error: No TUF body files found in wiremock recordings" >&2 + exit 1 + fi + + echo "Found ${#body_files[@]} TUF files to include in ConfigMap:" >&2 + + for body_file in "${body_files[@]}"; do + echo " - $body_file" >&2 + # Use the filename as the key in the ConfigMap + cmd_args+=("--from-file=$body_file=$files_dir/$body_file") + done + + cmd_args+=("--dry-run=client" "-o" "yaml" "--validate=false") + + printf '%s\n' "${cmd_args[@]}" +} + +# Create ConfigMap YAML files +echo "Creating TUF files ConfigMap YAML..." >&2 +readarray -t kubectl_cmd < <(build_kubectl_command) +"${kubectl_cmd[@]}" > "$SCRIPT_DIR/tuf-files-configmap.yaml" + +echo "Creating TUF root ConfigMap YAML..." >&2 +kubectl create configmap tuf-root-data \ + --namespace=tuf-service \ + --from-file=root.json="$PROJECT_ROOT/acceptance/tuf/root.json" \ + --dry-run=client -o yaml --validate=false > "$SCRIPT_DIR/tuf-root-configmap.yaml" + +echo "TUF ConfigMaps YAML files created successfully" diff --git a/hack/tuf/kustomization.yaml b/hack/tuf/kustomization.yaml new file mode 100644 index 000000000..1e1fecb24 --- /dev/null +++ b/hack/tuf/kustomization.yaml @@ -0,0 +1,27 @@ +# Copyright The Conforma Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - tuf.yaml + # These two files are generated using `make tuf-yaml` from wiremock data. + # See also the hack/tuf/create-tuf-files.sh script in this directory. + - tuf-files-configmap.yaml + - tuf-root-configmap.yaml diff --git a/hack/tuf/namespace.yaml b/hack/tuf/namespace.yaml new file mode 100644 index 000000000..bde5be034 --- /dev/null +++ b/hack/tuf/namespace.yaml @@ -0,0 +1,21 @@ +# Copyright The Conforma Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +--- +apiVersion: v1 +kind: Namespace +metadata: + name: tuf-service diff --git a/hack/tuf/tuf.yaml b/hack/tuf/tuf.yaml new file mode 100644 index 000000000..10d3d6de7 --- /dev/null +++ b/hack/tuf/tuf.yaml @@ -0,0 +1,177 @@ +# Copyright The Conforma Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: tuf-port-number + namespace: tuf-service +data: + PORT: "8080" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tuf-server + namespace: tuf-service + labels: + app.kubernetes.io/name: tuf-server +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: tuf-server + template: + metadata: + labels: + app.kubernetes.io/name: tuf-server + spec: + initContainers: + - name: setup-tuf-data + image: busybox:1.35 + command: + - sh + - -c + - | + cp /tuf-files/* /tuf-data/ + + # Create the versioned metadata files that TUF expects + cp /tuf-data/body-timestamp.json-*.json /tuf-data/timestamp.json + cp /tuf-data/body-1.snapshot.json-*.json /tuf-data/1.snapshot.json + cp /tuf-data/body-1.targets.json-*.json /tuf-data/1.targets.json + + # Create root.json from embedded content + cp /tuf-root/root.json /tuf-data/root.json + + # Create targets directory and copy target files with hash-based names dynamically + mkdir -p /tuf-data/targets + + # Process all target files using patterns instead of hardcoded names + for file in /tuf-data/body-targets-*; do + [ -f "$file" ] || continue + + # Extract the original filename by removing the body-targets- prefix and the -HASH.ext suffix + basename_file=$(basename "$file") + + # Pattern: body-targets-.-. + # We want to extract .. + # Handle different patterns by removing the suffix intelligently + without_prefix=$(echo "$basename_file" | sed 's/^body-targets-//') + + # Special case for tsa_intermedi -> tsa_intermediate_0.crt + if echo "$without_prefix" | grep -q "tsa_intermedi-"; then + target_name=$(echo "$without_prefix" | sed 's/tsa_intermedi-.*/tsa_intermediate_0.crt.pem/') + # For files ending with .-HASH.ext, remove the .-HASH part + elif echo "$without_prefix" | grep -q '\.-[A-Za-z0-9]*\.'; then + target_name=$(echo "$without_prefix" | sed 's/\.-[A-Za-z0-9]*\./\./') + # For files ending with -HASH.ext, remove the -HASH part but keep the extension + elif echo "$without_prefix" | grep -q '\-[A-Za-z0-9]*\.[a-z]*$'; then + # Extract everything before -HASH, then add the extension + ext=$(echo "$without_prefix" | sed 's/.*\.//') + base=$(echo "$without_prefix" | sed 's/\(.*\)-[A-Za-z0-9]*\.[a-z]*$/\1/') + target_name="$base.$ext" + else + # Fallback: remove -HASH.ext and add the extension + ext=$(echo "$basename_file" | sed 's/.*\.//') + base=$(echo "$without_prefix" | sed 's/-[A-Za-z0-9]*\.[a-z]*$//') + target_name="$base.$ext" + fi + + if [ -n "$target_name" ]; then + cp "$file" "/tuf-data/targets/$target_name" + echo "Copied $basename_file -> targets/$target_name" + else + echo "Warning: Could not parse filename pattern for $basename_file" + fi + done + + ls -la /tuf-data/ + ls -la /tuf-data/targets/ + volumeMounts: + - name: tuf-files + mountPath: /tuf-files + readOnly: true + - name: tuf-root + mountPath: /tuf-root + readOnly: true + - name: tuf-data + mountPath: /tuf-data + containers: + - name: tuf-server + image: docker.io/nginx:1.25-alpine + ports: + - name: http + containerPort: 8080 + env: + - name: NGINX_PORT + value: "8080" + volumeMounts: + - name: tuf-data + mountPath: /usr/share/nginx/html + readOnly: true + - name: nginx-config + mountPath: /etc/nginx/conf.d/default.conf + subPath: nginx.conf + volumes: + - name: tuf-files + configMap: + name: tuf-files + - name: tuf-root + configMap: + name: tuf-root-data + - name: tuf-data + emptyDir: {} + - name: nginx-config + configMap: + name: tuf-nginx-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: tuf-nginx-config + namespace: tuf-service +data: + nginx.conf: | + server { + listen 8080; + server_name localhost; + root /usr/share/nginx/html; + + location / { + try_files $uri $uri/ =404; + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods "GET, OPTIONS"; + add_header Access-Control-Allow-Headers "Content-Type"; + } + } +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: tuf-server + name: tuf + namespace: tuf-service +spec: + ports: + - name: http + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + app.kubernetes.io/name: tuf-server + type: ClusterIP diff --git a/tasks/collect-keyless-signing-params/0.1/collect-keyless-signing-params.yaml b/tasks/collect-keyless-signing-params/0.1/collect-keyless-signing-params.yaml new file mode 100644 index 000000000..5b9f353d9 --- /dev/null +++ b/tasks/collect-keyless-signing-params/0.1/collect-keyless-signing-params.yaml @@ -0,0 +1,167 @@ +--- +# Copyright The Conforma Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: collect-keyless-signing-params + annotations: + tekton.dev/displayName: Collect Keyless Signing Parameters + tekton.dev/pipelines.minVersion: "0.12.1" + tekton.dev/tags: ec, keyless, signing, configuration + labels: + app.kubernetes.io/version: "0.1" + +spec: + description: | + Tekton task to collect Konflux configuration parameters related to + keyless signing using cosign. The task attempts to read the "cluster-config" + ConfigMap in the "konflux-info" namespace to extract signing parameters. + + In case the ConfigMap is not found, the task will output empty strings for all parameters, + allowing the pipeline to continue without signing parameters. + + params: + - name: configMapName + type: string + description: The name of the ConfigMap to read signing parameters from + default: cluster-config + + - name: configMapNamespace + type: string + description: The namespace where the ConfigMap is located + default: konflux-info + + results: + - name: defaultOIDCIssuer + type: string + description: | + A default OIDC issuer URL to be used for signing. + + - name: rekorExternalUrl + type: string + description: | + The external URL of the Rekor transparency log. + + - name: fulcioExternalUrl + type: string + description: | + The external URL of the Fulcio certificate authority. + + - name: tufExternalUrl + type: string + description: | + The external URL of the TUF repository. + + - name: buildIdentity + type: string + description: | + The build identity from the OIDC token claims, if applicable. + + - name: buildIdentityRegexp + type: string + description: | + A regular expression to extract build identity from the OIDC token claims, if applicable. + + - name: keylessSigningEnabled + type: string + description: | + A flag indicating whether keyless signing is enabled based on the presence of signing parameters. + + stepTemplate: + securityContext: + runAsUser: 1001 + + steps: + - name: collect-signing-params + image: quay.io/conforma/cli:latest + computeResources: + limits: + memory: 128Mi + cpu: 50m + requests: + memory: 128Mi + cpu: 50m + script: | + #!/bin/bash + set -euo pipefail + + # Default value is "false" + keylessSigningEnabled="false" + + # Default values are empty strings + defaultOIDCIssuer="" + rekorExternalUrl="" + fulcioExternalUrl="" + tufExternalUrl="" + buildIdentity="" + buildIdentityRegexp="" + + # Read from the ConfigMap + echo "Reading ConfigMap ${configMapNamespace}/${configMapName}" + KFLX_CONFIG_PATH='/tmp/cluster-config.json' + if kubectl get configmap "${configMapName}" -n "${configMapNamespace}" -o json --ignore-not-found > "${KFLX_CONFIG_PATH}"; then + if [ -s "${KFLX_CONFIG_PATH}" ]; then + echo "ConfigMap found, extracting keyless signing parameters" + + # First we read "keylessSigningEnabled" + keylessSigningEnabled=$(jq -r '.data.enableKeylessSigning // "false"' "$KFLX_CONFIG_PATH") + + if [ "$keylessSigningEnabled" = "true" ]; then + # If that is set to "true" then read the other values + defaultOIDCIssuer=$(jq -r '.data.defaultOIDCIssuer // ""' "$KFLX_CONFIG_PATH") + rekorExternalUrl=$(jq -r '.data.rekorExternalUrl // ""' "$KFLX_CONFIG_PATH") + fulcioExternalUrl=$(jq -r '.data.fulcioExternalUrl // ""' "$KFLX_CONFIG_PATH") + tufExternalUrl=$(jq -r '.data.tufExternalUrl // ""' "$KFLX_CONFIG_PATH") + buildIdentity=$(jq -r '.data.buildIdentity // ""' "$KFLX_CONFIG_PATH") + buildIdentityRegexp=$(jq -r '.data.buildIdentityRegexp // ""' "$KFLX_CONFIG_PATH") + else + # Otherwise we ignore the rest of the ConfigMap + echo "enableKeylessSigning is not set, using default empty values" + fi + else + # Because we used --ignore-not-found this doesn't produce an error + echo "ConfigMap not found, using default empty values" + fi + else + # Some error other than "not found" + # (Stderr from kubectl should be visible in the task log.) + echo "Problem reading ConfigMap, using default empty values" + fi + + # Write to task results + echo -n "$keylessSigningEnabled" > "$(results.keylessSigningEnabled.path)" + echo -n "$defaultOIDCIssuer" > "$(results.defaultOIDCIssuer.path)" + echo -n "$rekorExternalUrl" > "$(results.rekorExternalUrl.path)" + echo -n "$fulcioExternalUrl" > "$(results.fulcioExternalUrl.path)" + echo -n "$tufExternalUrl" > "$(results.tufExternalUrl.path)" + echo -n "$buildIdentity" > "$(results.buildIdentity.path)" + echo -n "$buildIdentityRegexp" > "$(results.buildIdentityRegexp.path)" + + # Output for troubleshooting/debugging + echo "results.keylessSigningEnabled: $keylessSigningEnabled" + echo "results.defaultOIDCIssuer: $defaultOIDCIssuer" + echo "results.rekorExternalUrl: $rekorExternalUrl" + echo "results.fulcioExternalUrl: $fulcioExternalUrl" + echo "results.tufExternalUrl: $tufExternalUrl" + echo "results.buildIdentity: $buildIdentity" + echo "results.buildIdentityRegexp: $buildIdentityRegexp" + env: + - name: configMapNamespace + value: "$(params.configMapNamespace)" + - name: configMapName + value: "$(params.configMapName)" diff --git a/tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml b/tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml index 8d06266ef..271c12724 100644 --- a/tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml +++ b/tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml @@ -83,6 +83,20 @@ spec: keyless verification. The PUBLIC_KEY param will be ignored if this is provided. default: "" + - name: CERTIFICATE_IDENTITY_REGEXP + type: string + description: >- + Similar to CERTIFICATE_IDENTITY but the value is a regexp that will be matched. + Note that CERTIFICATE_IDENTITY takes precendence over this if both are present. + default: "" + + - name: CERTIFICATE_OIDC_ISSUER_REGEXP + type: string + description: >- + Similar to CERTIFICATE_OIDC_ISSUER but a regexp that will be matched. Note that + CERTIFICATE_OIDC_ISSUER takes precendence over this if both are present. + default: "" + - name: IGNORE_REKOR type: string description: >- @@ -318,20 +332,48 @@ spec: # To keep bash logic as thin as possible we deliberately don't sanitize # these params. If something is wrong or missing let Conforma handle it. - if [ -n "${CERTIFICATE_IDENTITY}" ] || [ -n "${CERTIFICATE_OIDC_ISSUER}" ]; then + + if [ -n "${CERTIFICATE_IDENTITY}" ] || \ + [ -n "${CERTIFICATE_OIDC_ISSUER}" ] || \ + [ -n "${CERTIFICATE_IDENTITY_REGEXP}" ] || \ + [ -n "${CERTIFICATE_OIDC_ISSUER_REGEXP}" ]; then + # If *any* of the above are non-empty assume the intention is to + # try keyless verification + + if [ -n "${CERTIFICATE_IDENTITY}" ]; then + cmd_args+=( + --certificate-identity="${CERTIFICATE_IDENTITY}" + ) + elif [ -n "${CERTIFICATE_IDENTITY_REGEXP}" ]; then + cmd_args+=( + --certificate-identity-regexp="${CERTIFICATE_IDENTITY_REGEXP}" + ) + fi + + if [ -n "${CERTIFICATE_OIDC_ISSUER}" ]; then + cmd_args+=( + --certificate-oidc-issuer="${CERTIFICATE_OIDC_ISSUER}" + ) + elif [ -n "${CERTIFICATE_OIDC_ISSUER_REGEXP}" ]; then + cmd_args+=( + --certificate-oidc-issuer-regexp="${CERTIFICATE_OIDC_ISSUER_REGEXP}" + ) + fi + + # Force --ignore-rekor to false since we need rekor cmd_args+=( - --certificate-identity="${CERTIFICATE_IDENTITY}" - --certificate-oidc-issuer="${CERTIFICATE_OIDC_ISSUER}" + --ignore-rekor=false ) - elif [ -n "${PUBLIC_KEY}" ]; then + else + # Assume traditional signing secret verification cmd_args+=( --public-key="${PUBLIC_KEY}" + --ignore-rekor="${IGNORE_REKOR}" ) fi cmd_args+=( --rekor-url="${REKOR_HOST}" - --ignore-rekor="${IGNORE_REKOR}" --workers="${WORKERS}" --info="${INFO}" --timeout=0 @@ -407,6 +449,10 @@ spec: value: "$(params.CERTIFICATE_IDENTITY)" - name: CERTIFICATE_OIDC_ISSUER value: "$(params.CERTIFICATE_OIDC_ISSUER)" + - name: CERTIFICATE_IDENTITY_REGEXP + value: "$(params.CERTIFICATE_IDENTITY_REGEXP)" + - name: CERTIFICATE_OIDC_ISSUER_REGEXP + value: "$(params.CERTIFICATE_OIDC_ISSUER_REGEXP)" - name: REKOR_HOST value: "$(params.REKOR_HOST)" - name: IGNORE_REKOR diff --git a/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml b/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml index f30b43485..d9845c694 100644 --- a/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml +++ b/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml @@ -90,6 +90,20 @@ spec: keyless verification. The PUBLIC_KEY param will be ignored if this is provided. default: "" + - name: CERTIFICATE_IDENTITY_REGEXP + type: string + description: >- + Similar to CERTIFICATE_IDENTITY but the value is a regexp that will be matched. + Note that CERTIFICATE_IDENTITY takes precendence over this if both are present. + default: "" + + - name: CERTIFICATE_OIDC_ISSUER_REGEXP + type: string + description: >- + Similar to CERTIFICATE_OIDC_ISSUER but a regexp that will be matched. Note that + CERTIFICATE_OIDC_ISSUER takes precendence over this if both are present. + default: "" + - name: IGNORE_REKOR type: string description: >- @@ -270,20 +284,48 @@ spec: # To keep bash logic as thin as possible we deliberately don't sanitize # these params. If something is wrong or missing let Conforma handle it. - if [ -n "${CERTIFICATE_IDENTITY}" ] || [ -n "${CERTIFICATE_OIDC_ISSUER}" ]; then + + if [ -n "${CERTIFICATE_IDENTITY}" ] || \ + [ -n "${CERTIFICATE_OIDC_ISSUER}" ] || \ + [ -n "${CERTIFICATE_IDENTITY_REGEXP}" ] || \ + [ -n "${CERTIFICATE_OIDC_ISSUER_REGEXP}" ]; then + # If *any* of the above are non-empty assume the intention is to + # try keyless verification + + if [ -n "${CERTIFICATE_IDENTITY}" ]; then + cmd_args+=( + --certificate-identity="${CERTIFICATE_IDENTITY}" + ) + elif [ -n "${CERTIFICATE_IDENTITY_REGEXP}" ]; then + cmd_args+=( + --certificate-identity-regexp="${CERTIFICATE_IDENTITY_REGEXP}" + ) + fi + + if [ -n "${CERTIFICATE_OIDC_ISSUER}" ]; then + cmd_args+=( + --certificate-oidc-issuer="${CERTIFICATE_OIDC_ISSUER}" + ) + elif [ -n "${CERTIFICATE_OIDC_ISSUER_REGEXP}" ]; then + cmd_args+=( + --certificate-oidc-issuer-regexp="${CERTIFICATE_OIDC_ISSUER_REGEXP}" + ) + fi + + # Force --ignore-rekor to false since we need rekor cmd_args+=( - --certificate-identity="${CERTIFICATE_IDENTITY}" - --certificate-oidc-issuer="${CERTIFICATE_OIDC_ISSUER}" + --ignore-rekor=false ) - elif [ -n "${PUBLIC_KEY}" ]; then + else + # Assume traditional signing secret verification cmd_args+=( --public-key="${PUBLIC_KEY}" + --ignore-rekor="${IGNORE_REKOR}" ) fi cmd_args+=( --rekor-url="${REKOR_HOST}" - --ignore-rekor="${IGNORE_REKOR}" --workers="${WORKERS}" --info="${INFO}" --timeout=0 @@ -313,6 +355,10 @@ spec: value: "$(params.CERTIFICATE_IDENTITY)" - name: CERTIFICATE_OIDC_ISSUER value: "$(params.CERTIFICATE_OIDC_ISSUER)" + - name: CERTIFICATE_IDENTITY_REGEXP + value: "$(params.CERTIFICATE_IDENTITY_REGEXP)" + - name: CERTIFICATE_OIDC_ISSUER_REGEXP + value: "$(params.CERTIFICATE_OIDC_ISSUER_REGEXP)" - name: REKOR_HOST value: "$(params.REKOR_HOST)" - name: IGNORE_REKOR