From 7cc05de773c7a444ac3855d60614ac12fe3e86a3 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Wed, 19 Aug 2026 01:57:29 +0000 Subject: [PATCH 1/5] test(showcase): verify PQC X25519MLKEM768 transport with TLS Add an integration test target for gapic-showcase running with Auto-TLS and restricted key exchange groups (--tls-groups 0x11ec). This verifies that the HTTP (reqwest) and gRPC (tonic) transports in google-cloud-rust successfully perform post-quantum hybrid key exchange using X25519MLKEM768. --- tests/showcase/src/lib.rs | 6 +- tests/showcase/src/pqc.rs | 207 ++++++++++++++++++++++++++++++++++++ tests/showcase/tests/pqc.rs | 25 +++++ 3 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 tests/showcase/src/pqc.rs create mode 100644 tests/showcase/tests/pqc.rs diff --git a/tests/showcase/src/lib.rs b/tests/showcase/src/lib.rs index 6c871b5f8a..396f04c6a1 100644 --- a/tests/showcase/src/lib.rs +++ b/tests/showcase/src/lib.rs @@ -25,6 +25,7 @@ use tokio::process::Command; mod compliance; mod echo; mod identity; +pub mod pqc; #[cfg(google_cloud_unstable_gapic_streaming)] mod streaming; @@ -35,8 +36,7 @@ mod streaming; /// ```shell /// go list -m -f '{{.Version}}' github.com/googleapis/gapic-showcase@main /// ``` -const SHOWCASE_NAME: &str = - "github.com/googleapis/gapic-showcase/cmd/gapic-showcase@v0.37.1-0.20260210150911-3fd9cb2f682d"; +const SHOWCASE_NAME: &str = "github.com/googleapis/gapic-showcase/cmd/gapic-showcase@v0.43.0"; pub async fn run() -> Result<()> { let _guard = google_cloud_test_utils::tracing::enable_tracing(); @@ -73,7 +73,7 @@ pub async fn run() -> Result<()> { Ok(()) } -async fn install() -> Result { +pub(crate) async fn install() -> Result { for backoff in [Some(5), Some(10), Some(20), None] { let error = match install_attempt().await { Ok(path) => return Ok(path), diff --git a/tests/showcase/src/pqc.rs b/tests/showcase/src/pqc.rs new file mode 100644 index 0000000000..f369c36404 --- /dev/null +++ b/tests/showcase/src/pqc.rs @@ -0,0 +1,207 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// https://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. + +//! Post-Quantum Cryptography (PQC) Transport Verification Tests. +//! +//! This module verifies that `google-cloud-rust` transports support and negotiate +//! post-quantum hybrid key exchange algorithms (specifically `X25519MLKEM768`, +//! IANA group ID `0x11ec` / 4588) when connecting over TLS 1.3. +//! +//! # Verification Mechanism +//! +//! An isolated instance of `gapic-showcase` is spawned with: +//! * `--tls`: Enables Auto-TLS, generating in-memory CA and server certificates. +//! * `--ca-cert-output-file `: Exports the self-signed CA certificate PEM. +//! * `--tls-groups 0x11ec`: Strictly restricts server-accepted key exchange groups +//! to `X25519MLKEM768`. +//! +//! If the client's TLS stack (`aws-lc-rs` via `rustls`) does not offer and negotiate +//! `X25519MLKEM768` during the TLS 1.3 `ClientHello`, the server rejects the handshake +//! with a TLS `HandshakeFailure` alert. +//! +//! # Scope of Tests +//! +//! 1. **HTTP/REST Unary (`reqwest`)**: Verifies HTTPS unary RPC execution. +//! 2. **gRPC Streaming (`tonic`)**: Verifies bidirectional streaming RPC execution +//! over HTTP/2 TLS. + +use super::{Anonymous, NeverRetry}; +use crate::Result; +use anyhow::Error; +use google_cloud_gax::options::RequestOptionsBuilder; +use google_cloud_gax::retry_policy::{AlwaysRetry, RetryPolicyExt}; +use google_cloud_showcase_v1beta1::client::{Echo, Testing}; +#[cfg(google_cloud_unstable_gapic_streaming)] +use google_cloud_showcase_v1beta1::model::EchoRequest; +use std::path::PathBuf; +use std::process::Stdio; +use std::time::Duration; +use tokio::process::Command; + +/// Dedicated port for the PQC Showcase server to prevent collisions with the standard +/// showcase test suite (running concurrently on port `:7469`). +const PQC_PORT: &str = ":7471"; +/// Dedicated fallback port to prevent port collisions with the default `:1337`. +const PQC_FALLBACK_PORT: &str = ":1339"; +const PQC_ENDPOINT: &str = "https://localhost:7471"; + +/// Main entry point for the PQC integration test suite. +/// +/// Spawns an isolated `gapic-showcase` server configured with Auto-TLS and pinned to +/// `0x11ec` (`X25519MLKEM768`), configures CA trust, and runs transport verifications. +pub async fn run() -> Result<()> { + let _guard = google_cloud_test_utils::tracing::enable_tracing(); + + let path = super::install().await?; + let showcase: PathBuf = [path.as_str(), "bin", "gapic-showcase"].iter().collect(); + + let ca_cert_path = std::env::temp_dir().join("showcase_pqc_ca.pem"); + if ca_cert_path.exists() { + let _ = std::fs::remove_file(&ca_cert_path); + } + + tracing::info!("starting {showcase:?} with Auto-TLS (PQC enabled and pinned to 0x11ec)"); + let child = Command::new(&showcase) + .args([ + "run", + "--port", + PQC_PORT, + "--fallback-port", + PQC_FALLBACK_PORT, + "--tls", + "--ca-cert-output-file", + ca_cert_path.to_str().unwrap(), + "--tls-groups", + "0x11ec", + ]) + .stdin(Stdio::null()) + .kill_on_drop(true) + .spawn() + .map_err(anyhow::Error::from)?; + tracing::info!("started showcase PQC server: {child:?}"); + + // Wait for the Showcase server to write its autogenerated CA certificate and become ready. + let mut ready = false; + for _ in 0..50 { + if let Ok(pem) = std::fs::read(&ca_cert_path) + && !pem.is_empty() + { + // SAFETY: Setting environment variable before issuing network requests in the test process. + // This configures `rustls-native-certs` to trust the autogenerated Showcase CA. + unsafe { + std::env::set_var("SSL_CERT_FILE", &ca_cert_path); + } + if wait_until_ready(PQC_ENDPOINT).await.is_ok() { + ready = true; + break; + } + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + + if !ready { + tracing::error!("showcase PQC server is not ready {child:?}"); + } + + tracing::info!("testing PQC transport (HTTP unary)"); + test_pqc_http().await?; + + #[cfg(google_cloud_unstable_gapic_streaming)] + { + tracing::info!("testing PQC transport (gRPC streaming)"); + test_pqc_grpc().await?; + } + + Ok(()) +} + +async fn wait_until_ready(endpoint: &str) -> Result<()> { + for _ in 0..50 { + if let Ok(client) = Testing::builder() + .with_endpoint(endpoint) + .with_credentials(Anonymous::new().build()) + .build() + .await + && client + .list_sessions() + .with_retry_policy(AlwaysRetry.with_attempt_limit(1)) + .with_attempt_timeout(Duration::from_millis(500)) + .send() + .await + .is_ok() + { + return Ok(()); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + Err(Error::msg(format!( + "showcase server at {endpoint} is not ready" + ))) +} + +/// Verifies that HTTP unary RPCs (`reqwest`) succeed over a TLS channel requiring `X25519MLKEM768`. +async fn test_pqc_http() -> Result<()> { + let client = Echo::builder() + .with_endpoint(PQC_ENDPOINT) + .with_credentials(Anonymous::new().build()) + .with_retry_policy(NeverRetry) + .with_tracing() + .build() + .await?; + + const TEXT: &str = "testing PQC transport compliance (HTTP unary)"; + let response = client.echo().set_content(TEXT).send().await?; + assert_eq!(response.content, TEXT); + + tracing::info!("Verified HTTP unary RPC over PQC TLS channel"); + Ok(()) +} + +/// Verifies that gRPC bidirectional streaming RPCs (`tonic`) succeed over a TLS channel requiring `X25519MLKEM768`. +#[cfg(google_cloud_unstable_gapic_streaming)] +async fn test_pqc_grpc() -> Result<()> { + let client = Echo::builder() + .with_endpoint(PQC_ENDPOINT) + .with_credentials(Anonymous::new().build()) + .with_retry_policy(NeverRetry) + .with_tracing() + .build() + .await?; + + const TOTAL_MESSAGES: usize = 5; + let (sender, mut receiver) = client.chat().build(); + + for i in 0..TOTAL_MESSAGES { + sender + .send(EchoRequest::new().set_content(format!("pqc-grpc-msg-{i}"))) + .await?; + } + drop(sender); + + let mut received = Vec::new(); + while let Some(res) = receiver.recv().await { + received.push(res?.content); + } + + let expected: Vec = (0..TOTAL_MESSAGES) + .map(|i| format!("pqc-grpc-msg-{i}")) + .collect(); + assert_eq!( + received, expected, + "gRPC streaming message exchange over PQC TLS channel must match" + ); + + tracing::info!("Verified gRPC streaming over PQC TLS channel (0x11ec)"); + Ok(()) +} diff --git a/tests/showcase/tests/pqc.rs b/tests/showcase/tests/pqc.rs new file mode 100644 index 0000000000..023292f860 --- /dev/null +++ b/tests/showcase/tests/pqc.rs @@ -0,0 +1,25 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// https://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. + +#[cfg(all(test, feature = "run-showcase-tests"))] +mod pqc { + use google_cloud_test_utils::errors::anydump; + + #[tokio::test] + async fn run() -> anyhow::Result<()> { + integration_tests_showcase::pqc::run() + .await + .inspect_err(anydump) + } +} From 33487447fae0732fa2fedebb492e86c8927d3fc3 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Wed, 19 Aug 2026 02:57:24 +0000 Subject: [PATCH 2/5] use new gapic-showcase version --- tests/showcase/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/showcase/src/lib.rs b/tests/showcase/src/lib.rs index 396f04c6a1..52b87275bd 100644 --- a/tests/showcase/src/lib.rs +++ b/tests/showcase/src/lib.rs @@ -36,7 +36,7 @@ mod streaming; /// ```shell /// go list -m -f '{{.Version}}' github.com/googleapis/gapic-showcase@main /// ``` -const SHOWCASE_NAME: &str = "github.com/googleapis/gapic-showcase/cmd/gapic-showcase@v0.43.0"; +const SHOWCASE_NAME: &str = "github.com/googleapis/gapic-showcase/cmd/gapic-showcase@v0.43.1-0.20260817230810-0c88ce83d259"; pub async fn run() -> Result<()> { let _guard = google_cloud_test_utils::tracing::enable_tracing(); From c7106d350e0e999338b8101a3ad086b47e94cd63 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Wed, 19 Aug 2026 03:33:32 +0000 Subject: [PATCH 3/5] format --- tests/showcase/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/showcase/src/lib.rs b/tests/showcase/src/lib.rs index 52b87275bd..9a264b9277 100644 --- a/tests/showcase/src/lib.rs +++ b/tests/showcase/src/lib.rs @@ -36,7 +36,8 @@ mod streaming; /// ```shell /// go list -m -f '{{.Version}}' github.com/googleapis/gapic-showcase@main /// ``` -const SHOWCASE_NAME: &str = "github.com/googleapis/gapic-showcase/cmd/gapic-showcase@v0.43.1-0.20260817230810-0c88ce83d259"; +const SHOWCASE_NAME: &str = + "github.com/googleapis/gapic-showcase/cmd/gapic-showcase@v0.43.1-0.20260817230810-0c88ce83d259"; pub async fn run() -> Result<()> { let _guard = google_cloud_test_utils::tracing::enable_tracing(); From 99d409696420436b23371111707506f186ed35f1 Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Wed, 19 Aug 2026 03:43:20 +0000 Subject: [PATCH 4/5] test(showcase): address PR review comments for PQC test Update the PQC integration test to: - Use a unique CA certificate filename including the process ID to prevent temp file collisions across test runs. - Avoid unwrap() on path to string conversions by safely propagating errors. - Fail fast and return an explicit error immediately if the server fails readiness checks. --- tests/showcase/src/pqc.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/showcase/src/pqc.rs b/tests/showcase/src/pqc.rs index f369c36404..d37fc6bf00 100644 --- a/tests/showcase/src/pqc.rs +++ b/tests/showcase/src/pqc.rs @@ -66,7 +66,8 @@ pub async fn run() -> Result<()> { let path = super::install().await?; let showcase: PathBuf = [path.as_str(), "bin", "gapic-showcase"].iter().collect(); - let ca_cert_path = std::env::temp_dir().join("showcase_pqc_ca.pem"); + let ca_cert_path = + std::env::temp_dir().join(format!("showcase_pqc_ca_{}.pem", std::process::id())); if ca_cert_path.exists() { let _ = std::fs::remove_file(&ca_cert_path); } @@ -81,7 +82,9 @@ pub async fn run() -> Result<()> { PQC_FALLBACK_PORT, "--tls", "--ca-cert-output-file", - ca_cert_path.to_str().unwrap(), + ca_cert_path + .to_str() + .ok_or_else(|| Error::msg("temp dir path is not valid UTF-8"))?, "--tls-groups", "0x11ec", ]) @@ -111,7 +114,9 @@ pub async fn run() -> Result<()> { } if !ready { - tracing::error!("showcase PQC server is not ready {child:?}"); + return Err(Error::msg(format!( + "showcase PQC server is not ready: {child:?}" + ))); } tracing::info!("testing PQC transport (HTTP unary)"); From 33546cc4752a4b0fd179fe7e730028559817717a Mon Sep 17 00:00:00 2001 From: Suzy Mueller Date: Wed, 19 Aug 2026 20:42:47 +0000 Subject: [PATCH 5/5] add comment --- tests/showcase/src/pqc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/showcase/src/pqc.rs b/tests/showcase/src/pqc.rs index d37fc6bf00..8ef94de47e 100644 --- a/tests/showcase/src/pqc.rs +++ b/tests/showcase/src/pqc.rs @@ -86,7 +86,7 @@ pub async fn run() -> Result<()> { .to_str() .ok_or_else(|| Error::msg("temp dir path is not valid UTF-8"))?, "--tls-groups", - "0x11ec", + "0x11ec", // X25519MLKEM768 ]) .stdin(Stdio::null()) .kill_on_drop(true)