From e7fb5ab4501fe697df0a9b096f51b88f85cc3d0c Mon Sep 17 00:00:00 2001 From: Stanislav Koshchienko Date: Wed, 23 Sep 2026 20:04:42 +0200 Subject: [PATCH 1/5] Add test for PQC (X25519MLKEM768) key exchange verification --- test/e2e/pqc_test.go | 186 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 test/e2e/pqc_test.go diff --git a/test/e2e/pqc_test.go b/test/e2e/pqc_test.go new file mode 100644 index 00000000..43a6a04c --- /dev/null +++ b/test/e2e/pqc_test.go @@ -0,0 +1,186 @@ +/* +Copyright 2025. + +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. +*/ + +package e2e + +import ( + "context" + "os/exec" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + imagev1 "github.com/openshift/api/image/v1" + routev1 "github.com/openshift/api/route/v1" + webserversv1alpha1 "github.com/web-servers/jws-operator/api/v1alpha1" + "github.com/web-servers/jws-operator/test/utils" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("WebServerControllerTest", Ordered, func() { + SetDefaultEventuallyTimeout(2 * time.Minute) + SetDefaultEventuallyPollingInterval(time.Second) + + ctx := context.Background() + name := "pqc-key-exchange-test" + appName := "pqc-key-exchange" + testURI := "/health" + imageStreamName := "pqc-test" + imageStreamNamespace := namespace + + helperRouteName := "pqc-domain-helper" + host := "" + + imgStream := &imagev1.ImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: imageStreamName, + Namespace: namespace, + }, + Spec: imagev1.ImageStreamSpec{ + Tags: []imagev1.TagReference{ + { + Name: "latest", + From: &corev1.ObjectReference{ + Kind: "DockerImage", + Name: testImg, + }, + }, + }, + }, + } + + helperRoute := &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: helperRouteName, + Namespace: namespace, + }, + Spec: routev1.RouteSpec{ + Subdomain: "pqc", + To: routev1.RouteTargetReference{ + Name: "pqc-dummy", + Kind: "Service", + }, + }, + } + + webserver := &webserversv1alpha1.WebServer{ + TypeMeta: metav1.TypeMeta{ + Kind: "WebServer", + APIVersion: "web.servers.org/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: webserversv1alpha1.WebServerSpec{ + ApplicationName: appName, + Replicas: 1, + UseSessionClustering: false, + TLSConfig: webserversv1alpha1.TLSConfig{ + RouteHostname: "Will be set later", + TLSSecret: "test-tls-secret", + }, + WebImageStream: &webserversv1alpha1.WebImageStreamSpec{ + ImageStreamName: imageStreamName, + ImageStreamNamespace: imageStreamNamespace, + }, + }, + } + + BeforeAll(func() { + createImageStream(imgStream) + + Expect(k8sClient.Create(ctx, helperRoute)).Should(Succeed()) + + foundRoute := &routev1.Route{} + Eventually(func() bool { + if k8sClient.Get(ctx, types.NamespacedName{Name: helperRouteName, Namespace: namespace}, foundRoute) != nil { + return false + } + host = utils.GetHost(foundRoute) + return host != "" + }, "1m", "1s").Should(BeTrue()) + + webserver.Spec.TLSConfig.RouteHostname = "tls:pqctest-" + namespace + "." + host[4:] + + createWebServer(webserver) + }) + + AfterAll(func() { + deleteWebServer(webserver) + + Expect(k8sClient.Delete(ctx, helperRoute)).Should(Succeed()) + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{Name: helperRouteName, Namespace: namespace}, &routev1.Route{}) + return apierrors.IsNotFound(err) + }, "2m", "5s").Should(BeTrue(), "the helper route should be deleted") + + deleteImageStream(imgStream) + }) + + // The goal of the test is to check if JWS supports PQC key exchange, and it can be done without a change to the Operator. + // To forces the client to request PQC key during each handshake is to set X25519MLKEM768 flag + Context("PQCKeyExchangeTest", func() { + It("TLS handshake over PQC key exchange with openssl", func() { + routeHost := webserver.Spec.TLSConfig.RouteHostname[4:] + + var output string + Eventually(func() bool { + cmd := exec.Command("openssl", "s_client", + "-connect", routeHost+":443", + "-groups", "X25519MLKEM768", + "-servername", routeHost, + ) + cmd.Stdin = strings.NewReader("") + + out, err := cmd.CombinedOutput() + output = string(out) + if err != nil { + thetest.Logf("openssl s_client error: %v", err) + return false + } + return strings.Contains(output, "X25519MLKEM768") + }, "3m", "10s").Should(BeTrue(), + "Expected X25519MLKEM768 in TLS negotiation. openssl output: "+output) + }) + + It("curl HTTP GET to /health over PQC", func() { + routeHost := webserver.Spec.TLSConfig.RouteHostname[4:] + + var output string + Eventually(func() bool { + cmd := exec.Command("curl", "-k", "-s", "-o", "/dev/null", "-w", "%{http_code}", + "--curves", "X25519MLKEM768", + "https://"+routeHost+testURI, + ) + + out, err := cmd.CombinedOutput() + output = string(out) + if err != nil { + thetest.Logf("curl PQC error: %v", err) + return false + } + return strings.TrimSpace(output) == "200" + }, "3m", "10s").Should(BeTrue(), + "Expected HTTP 200 over PQC connection, got: "+output) + }) + }) +}) From 4dddfe3c8642fa7f55fcbe62c74cf82e08972130 Mon Sep 17 00:00:00 2001 From: Stanislav Koshchienko Date: Tue, 29 Sep 2026 14:07:35 +0200 Subject: [PATCH 2/5] Rework of outout + Go client do HTTP GET request --- test/e2e/pqc_test.go | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/test/e2e/pqc_test.go b/test/e2e/pqc_test.go index 43a6a04c..d7596892 100644 --- a/test/e2e/pqc_test.go +++ b/test/e2e/pqc_test.go @@ -18,6 +18,8 @@ package e2e import ( "context" + "crypto/tls" + "net/http" "os/exec" "strings" "time" @@ -154,11 +156,12 @@ var _ = Describe("WebServerControllerTest", Ordered, func() { out, err := cmd.CombinedOutput() output = string(out) if err != nil { + output = output + err.Error() thetest.Logf("openssl s_client error: %v", err) return false } return strings.Contains(output, "X25519MLKEM768") - }, "3m", "10s").Should(BeTrue(), + }, "1m", "20s").Should(BeTrue(), "Expected X25519MLKEM768 in TLS negotiation. openssl output: "+output) }) @@ -175,11 +178,39 @@ var _ = Describe("WebServerControllerTest", Ordered, func() { out, err := cmd.CombinedOutput() output = string(out) if err != nil { + output = output + err.Error() thetest.Logf("curl PQC error: %v", err) return false } return strings.TrimSpace(output) == "200" - }, "3m", "10s").Should(BeTrue(), + }, "1m", "20s").Should(BeTrue(), + "Expected HTTP 200 over PQC connection, got: "+output) + }) + + It("Go HTTP client with X25519MLKEM768", func() { + routeHost := webserver.Spec.TLSConfig.RouteHostname[4:] + + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + CurvePreferences: []tls.CurveID{tls.X25519MLKEM768}, + }, + }, + } + + var output string + Eventually(func() bool { + resp, err := httpClient.Get("https://" + routeHost + testURI) + if err != nil { + output = output + err.Error() + thetest.Logf("Go PQC client error: %v", err) + return false + } + resp.Body.Close() + output = resp.Status + return resp.StatusCode == http.StatusOK + }, "1m", "20s").Should(BeTrue(), "Expected HTTP 200 over PQC connection, got: "+output) }) }) From 098d53bfa81d49505886f91ddea29cc08d4c207c Mon Sep 17 00:00:00 2001 From: Stanislav Koshchienko Date: Tue, 29 Sep 2026 14:25:26 +0200 Subject: [PATCH 3/5] Change to SS --- test/e2e/pqc_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/pqc_test.go b/test/e2e/pqc_test.go index d7596892..71d7c382 100644 --- a/test/e2e/pqc_test.go +++ b/test/e2e/pqc_test.go @@ -170,7 +170,7 @@ var _ = Describe("WebServerControllerTest", Ordered, func() { var output string Eventually(func() bool { - cmd := exec.Command("curl", "-k", "-s", "-o", "/dev/null", "-w", "%{http_code}", + cmd := exec.Command("curl", "-k", "-sS", "-o", "/dev/null", "-w", "%{http_code}", "--curves", "X25519MLKEM768", "https://"+routeHost+testURI, ) From 8f8a90650e491b0b18aaae4d042e9b15be3b9f20 Mon Sep 17 00:00:00 2001 From: Stanislav Koshchienko Date: Tue, 29 Sep 2026 15:45:30 +0200 Subject: [PATCH 4/5] Correct return --- test/e2e/pqc_test.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/e2e/pqc_test.go b/test/e2e/pqc_test.go index 71d7c382..84e9d5d2 100644 --- a/test/e2e/pqc_test.go +++ b/test/e2e/pqc_test.go @@ -161,8 +161,9 @@ var _ = Describe("WebServerControllerTest", Ordered, func() { return false } return strings.Contains(output, "X25519MLKEM768") - }, "1m", "20s").Should(BeTrue(), - "Expected X25519MLKEM768 in TLS negotiation. openssl output: "+output) + }, "1m", "20s").Should(BeTrue(), func() string { + return "Expected X25519MLKEM768 in TLS negotiation. openssl output: " + output + }) }) It("curl HTTP GET to /health over PQC", func() { @@ -183,8 +184,9 @@ var _ = Describe("WebServerControllerTest", Ordered, func() { return false } return strings.TrimSpace(output) == "200" - }, "1m", "20s").Should(BeTrue(), - "Expected HTTP 200 over PQC connection, got: "+output) + }, "1m", "20s").Should(BeTrue(), func() string { + return "Expected HTTP 200 over PQC connection, got: " + output + }) }) It("Go HTTP client with X25519MLKEM768", func() { @@ -210,8 +212,9 @@ var _ = Describe("WebServerControllerTest", Ordered, func() { resp.Body.Close() output = resp.Status return resp.StatusCode == http.StatusOK - }, "1m", "20s").Should(BeTrue(), - "Expected HTTP 200 over PQC connection, got: "+output) + }, "1m", "20s").Should(BeTrue(), func() string { + return "Expected HTTP 200 over PQC connection, got: " + output + }) }) }) }) From 85106fda51e51058b552f244994f0410f9c3b3bd Mon Sep 17 00:00:00 2001 From: Stanislav Koshchienko Date: Tue, 29 Sep 2026 16:08:12 +0200 Subject: [PATCH 5/5] Fix linter --- test/e2e/pqc_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/pqc_test.go b/test/e2e/pqc_test.go index 84e9d5d2..603e6868 100644 --- a/test/e2e/pqc_test.go +++ b/test/e2e/pqc_test.go @@ -209,7 +209,7 @@ var _ = Describe("WebServerControllerTest", Ordered, func() { thetest.Logf("Go PQC client error: %v", err) return false } - resp.Body.Close() + _ = resp.Body.Close() output = resp.Status return resp.StatusCode == http.StatusOK }, "1m", "20s").Should(BeTrue(), func() string {