From 4503decfc498dfb5e82c145778f6e379e0c82006 Mon Sep 17 00:00:00 2001 From: Scott Hart Date: Tue, 28 Jul 2026 12:17:37 -0400 Subject: [PATCH 1/2] impl: add gRPC PQC test --- ci/cloudbuild/builds/showcase.sh | 3 +- ci/showcase/BUILD.bazel.in | 17 ++ ci/showcase/grpc_pqc_test.cc | 287 +++++++++++++++++++++++++++++++ ci/showcase/rest_pqc_test.cc | 25 ++- 4 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 ci/showcase/grpc_pqc_test.cc diff --git a/ci/cloudbuild/builds/showcase.sh b/ci/cloudbuild/builds/showcase.sh index c6ecf5709dd44..faac6395e327a 100755 --- a/ci/cloudbuild/builds/showcase.sh +++ b/ci/cloudbuild/builds/showcase.sh @@ -96,4 +96,5 @@ io::log_h2 "Running showcase tests" bazel test --test_env=SHOWCASE_PORT="${SHOWCASE_PORT}" \ --test_env=SHOWCASE_CA_CERT="${SHOWCASE_CA_CERT}" \ --test_output=errors \ - //ci/showcase:rest_pqc_test + //ci/showcase:rest_pqc_test \ + //ci/showcase:grpc_pqc_test diff --git a/ci/showcase/BUILD.bazel.in b/ci/showcase/BUILD.bazel.in index b6f573faec6f9..16161e68c1b18 100644 --- a/ci/showcase/BUILD.bazel.in +++ b/ci/showcase/BUILD.bazel.in @@ -94,6 +94,23 @@ cc_library( ], ) +cc_test( + name = "grpc_pqc_test", + srcs = ["grpc_pqc_test.cc"], + copts = [ + "-Ici/showcase", + "-I$(BINDIR)/ci/showcase", + ], + tags = ["integration-test"], + deps = [ + ":showcase_echo_client", + "//:common", + "//:grpc_utils", + "//google/cloud/testing_util:google_cloud_cpp_testing_private", + "@googletest//:gtest_main", + ], +) + cc_test( name = "rest_pqc_test", srcs = ["rest_pqc_test.cc"], diff --git a/ci/showcase/grpc_pqc_test.cc b/ci/showcase/grpc_pqc_test.cc new file mode 100644 index 0000000000000..2bba9aff30e03 --- /dev/null +++ b/ci/showcase/grpc_pqc_test.cc @@ -0,0 +1,287 @@ +// 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. + +#include "google/cloud/common_options.h" +#include "google/cloud/credentials.h" +#include "google/cloud/grpc_options.h" +#include "google/cloud/internal/background_threads_impl.h" +#include "google/cloud/internal/unified_grpc_credentials.h" +#include "google/cloud/testing_util/status_matchers.h" +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "google/showcase/v1beta1/echo_client.h" +#include "google/showcase/v1beta1/internal/echo_connection_impl.h" +#include "google/showcase/v1beta1/internal/echo_metadata_decorator.h" +#include "google/showcase/v1beta1/internal/echo_option_defaults.h" +#include "google/showcase/v1beta1/internal/echo_stub.h" +#include "google/showcase/v1beta1/internal/echo_stub_factory.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace v1beta1 { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::google::cloud::testing_util::IsOkAndHolds; +using ::testing::Eq; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Not; +using ::testing::NotNull; + +class HeaderInterceptingEchoStub : public v1beta1_internal::EchoStub { + public: + HeaderInterceptingEchoStub( + std::shared_ptr delegate, + std::function const&)> + metadata_callback) + : delegate_(std::move(delegate)), + metadata_callback_(std::move(metadata_callback)) {} + + ~HeaderInterceptingEchoStub() override = default; + + StatusOr Echo( + grpc::ClientContext& context, Options const& options, + google::showcase::v1beta1::EchoRequest const& request) override { + auto response = delegate_->Echo(context, options, request); + ExtractMetadata(context); + return response; + } + + StatusOr + EchoErrorDetails(grpc::ClientContext& context, Options const& options, + google::showcase::v1beta1::EchoErrorDetailsRequest const& + request) override { + return delegate_->EchoErrorDetails(context, options, request); + } + + StatusOr + FailEchoWithDetails( + grpc::ClientContext& context, Options const& options, + google::showcase::v1beta1::FailEchoWithDetailsRequest const& request) + override { + return delegate_->FailEchoWithDetails(context, options, request); + } + + future> AsyncWait( + google::cloud::CompletionQueue& cq, + std::shared_ptr context, + google::cloud::internal::ImmutableOptions options, + google::showcase::v1beta1::WaitRequest const& request) override { + return delegate_->AsyncWait(cq, std::move(context), std::move(options), + request); + } + + StatusOr Wait( + grpc::ClientContext& context, Options options, + google::showcase::v1beta1::WaitRequest const& request) override { + return delegate_->Wait(context, std::move(options), request); + } + + StatusOr Block( + grpc::ClientContext& context, Options const& options, + google::showcase::v1beta1::BlockRequest const& request) override { + return delegate_->Block(context, options, request); + } + + StatusOr ListLocations( + grpc::ClientContext& context, Options const& options, + google::cloud::location::ListLocationsRequest const& request) override { + return delegate_->ListLocations(context, options, request); + } + + StatusOr GetLocation( + grpc::ClientContext& context, Options const& options, + google::cloud::location::GetLocationRequest const& request) override { + return delegate_->GetLocation(context, options, request); + } + + StatusOr SetIamPolicy( + grpc::ClientContext& context, Options const& options, + google::iam::v1::SetIamPolicyRequest const& request) override { + return delegate_->SetIamPolicy(context, options, request); + } + + StatusOr GetIamPolicy( + grpc::ClientContext& context, Options const& options, + google::iam::v1::GetIamPolicyRequest const& request) override { + return delegate_->GetIamPolicy(context, options, request); + } + + StatusOr TestIamPermissions( + grpc::ClientContext& context, Options const& options, + google::iam::v1::TestIamPermissionsRequest const& request) override { + return delegate_->TestIamPermissions(context, options, request); + } + + StatusOr ListOperations( + grpc::ClientContext& context, Options const& options, + google::longrunning::ListOperationsRequest const& request) override { + return delegate_->ListOperations(context, options, request); + } + + StatusOr GetOperation( + grpc::ClientContext& context, Options const& options, + google::longrunning::GetOperationRequest const& request) override { + return delegate_->GetOperation(context, options, request); + } + + Status DeleteOperation( + grpc::ClientContext& context, Options const& options, + google::longrunning::DeleteOperationRequest const& request) override { + return delegate_->DeleteOperation(context, options, request); + } + + Status CancelOperation( + grpc::ClientContext& context, Options const& options, + google::longrunning::CancelOperationRequest const& request) override { + return delegate_->CancelOperation(context, options, request); + } + + future> AsyncGetOperation( + google::cloud::CompletionQueue& cq, + std::shared_ptr context, + google::cloud::internal::ImmutableOptions options, + google::longrunning::GetOperationRequest const& request) override { + return delegate_->AsyncGetOperation(cq, std::move(context), + std::move(options), request); + } + + future AsyncCancelOperation( + google::cloud::CompletionQueue& cq, + std::shared_ptr context, + google::cloud::internal::ImmutableOptions options, + google::longrunning::CancelOperationRequest const& request) override { + return delegate_->AsyncCancelOperation(cq, std::move(context), + std::move(options), request); + } + + private: + void ExtractMetadata(grpc::ClientContext& context) { + std::multimap metadata; + for (auto const& pair : context.GetServerInitialMetadata()) { + metadata.emplace(std::string(pair.first.data(), pair.first.size()), + std::string(pair.second.data(), pair.second.size())); + } + for (auto const& pair : context.GetServerTrailingMetadata()) { + metadata.emplace(std::string(pair.first.data(), pair.first.size()), + std::string(pair.second.data(), pair.second.size())); + } + metadata_callback_(metadata); + } + + std::shared_ptr delegate_; + std::function const&)> + metadata_callback_; +}; + +TEST(EchoGrpcIntegrationTest, EchoSuccessGrpcWithPqcVerification) { + std::string ca_path; + if (auto* ca_env = std::getenv("SHOWCASE_CA_CERT")) { + ca_path = ca_env; + } else { + auto* test_srcdir = std::getenv("TEST_SRCDIR"); + ASSERT_THAT(test_srcdir, NotNull()); + ca_path = std::string(test_srcdir) + "/_main/ci/showcase/showcase.pem"; + } + + std::ifstream ca_file(ca_path); + ASSERT_TRUE(ca_file.good()) << "Failed to open CA file at " << ca_path; + + std::string port = "7469"; + if (auto* port_env = std::getenv("SHOWCASE_PORT")) { + port = port_env; + } + std::string endpoint = absl::StrCat("localhost:", port); + + auto credentials = MakeAccessTokenCredentials( + "dummy-token", std::chrono::system_clock::now() + std::chrono::hours(1)); + + auto options = Options{} + .set(endpoint) + .set(ca_path) + .set(credentials); + options = v1beta1_internal::EchoDefaultOptions(std::move(options)); + + auto background = internal::MakeBackgroundThreadsFactory(options)(); + auto auth = internal::CreateAuthenticationStrategy(background->cq(), options); + auto real_stub = v1beta1_internal::CreateDefaultEchoStub(auth, options); + + std::multimap intercepted_metadata; + auto metadata_callback = + [&intercepted_metadata]( + std::multimap const& metadata) { + intercepted_metadata = metadata; + }; + + std::shared_ptr stub = + std::make_shared( + std::move(real_stub), std::move(metadata_callback)); + + stub = std::make_shared( + std::move(stub), std::multimap{}); + + auto connection = std::make_shared( + std::move(background), std::move(stub), options); + + auto client = EchoClient(connection); + + ::google::showcase::v1beta1::EchoRequest request; + request.set_content("Hello from C++ GAPIC gRPC!"); + + auto response = client.Echo(request); + ASSERT_STATUS_OK(response); + EXPECT_THAT(response->content(), Eq("Hello from C++ GAPIC gRPC!")); + + auto get_metadata_value = + [](std::multimap const& metadata, + std::string const& key) -> std::string { + for (auto const& pair : metadata) { + if (absl::EqualsIgnoreCase(pair.first, key)) { + return pair.second; + } + } + return ""; + }; + + std::string tls_group = + get_metadata_value(intercepted_metadata, "x-showcase-tls-group"); + std::string supported_groups = get_metadata_value( + intercepted_metadata, "x-showcase-tls-client-supported-groups"); + + EXPECT_THAT(tls_group, Not(IsEmpty())) + << "x-showcase-tls-group metadata not found"; + EXPECT_THAT(supported_groups, Not(IsEmpty())) + << "x-showcase-tls-client-supported-groups metadata not found"; + + // Assert PQC was used. + EXPECT_THAT(tls_group, Eq("X25519MLKEM768")); + EXPECT_THAT(supported_groups, HasSubstr("X25519MLKEM768")); +} + +} // namespace +} // namespace v1beta1 +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace cloud +} // namespace google diff --git a/ci/showcase/rest_pqc_test.cc b/ci/showcase/rest_pqc_test.cc index 5c4e718e61804..1a2ed085fe713 100644 --- a/ci/showcase/rest_pqc_test.cc +++ b/ci/showcase/rest_pqc_test.cc @@ -18,6 +18,7 @@ #include "google/cloud/internal/curl_options.h" // for HttpVersionOption #include "google/cloud/internal/rest_background_threads_impl.h" #include "google/cloud/internal/rest_client.h" +#include "google/cloud/testing_util/status_matchers.h" #include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "google/showcase/v1beta1/echo_client.h" @@ -25,6 +26,7 @@ #include "google/showcase/v1beta1/internal/echo_rest_connection_impl.h" #include "google/showcase/v1beta1/internal/echo_rest_metadata_decorator.h" #include "google/showcase/v1beta1/internal/echo_rest_stub.h" +#include #include #include #include @@ -44,6 +46,12 @@ using ::google::cloud::rest_internal::RestClient; using ::google::cloud::rest_internal::RestContext; using ::google::cloud::rest_internal::RestRequest; using ::google::cloud::rest_internal::RestResponse; +using ::google::cloud::testing_util::IsOkAndHolds; +using ::testing::Eq; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Not; +using ::testing::NotNull; class HeaderInterceptingRestClient : public RestClient { public: @@ -113,7 +121,7 @@ TEST(EchoRestIntegrationTest, EchoSuccessRestWithPqcVerification) { ca_path = ca_env; } else { auto* test_srcdir = std::getenv("TEST_SRCDIR"); - ASSERT_NE(test_srcdir, nullptr); + ASSERT_THAT(test_srcdir, NotNull()); ca_path = std::string(test_srcdir) + "/_main/ci/showcase/showcase.pem"; } @@ -148,7 +156,7 @@ TEST(EchoRestIntegrationTest, EchoSuccessRestWithPqcVerification) { // Create the real REST client auto real_client = rest_internal::MakePooledRestClient(endpoint, options); - ASSERT_NE(real_client, nullptr) << "Failed to create real REST client"; + ASSERT_THAT(real_client, NotNull()) << "Failed to create real REST client"; // Wrap it with our interceptor auto intercepting_client = std::make_shared( @@ -179,8 +187,8 @@ TEST(EchoRestIntegrationTest, EchoSuccessRestWithPqcVerification) { request.set_content("Hello from C++ GAPIC REST!"); auto response = client.Echo(request); - ASSERT_TRUE(response.ok()) << response.status().message(); - EXPECT_EQ(response->content(), "Hello from C++ GAPIC REST!"); + ASSERT_STATUS_OK(response); + EXPECT_THAT(response->content(), Eq("Hello from C++ GAPIC REST!")); // Verify headers auto get_header_value = @@ -199,13 +207,14 @@ TEST(EchoRestIntegrationTest, EchoSuccessRestWithPqcVerification) { std::string supported_groups = get_header_value( intercepted_headers, "x-showcase-tls-client-supported-groups"); - EXPECT_FALSE(tls_group.empty()) << "x-showcase-tls-group header not found"; - EXPECT_FALSE(supported_groups.empty()) + EXPECT_THAT(tls_group, Not(IsEmpty())) + << "x-showcase-tls-group header not found"; + EXPECT_THAT(supported_groups, Not(IsEmpty())) << "x-showcase-tls-client-supported-groups header not found"; // Assert PQC was used. - EXPECT_EQ(tls_group, "X25519MLKEM768"); - EXPECT_TRUE(absl::StrContains(supported_groups, "X25519MLKEM768")); + EXPECT_THAT(tls_group, Eq("X25519MLKEM768")); + EXPECT_THAT(supported_groups, HasSubstr("X25519MLKEM768")); } } // namespace From 9c4d070528b9413dd6b4529fd226fec9aebc39d3 Mon Sep 17 00:00:00 2001 From: Scott Hart Date: Tue, 28 Jul 2026 12:40:40 -0400 Subject: [PATCH 2/2] address review comments --- ci/showcase/grpc_pqc_test.cc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ci/showcase/grpc_pqc_test.cc b/ci/showcase/grpc_pqc_test.cc index 2bba9aff30e03..7f9383dec6b0a 100644 --- a/ci/showcase/grpc_pqc_test.cc +++ b/ci/showcase/grpc_pqc_test.cc @@ -179,14 +179,15 @@ class HeaderInterceptingEchoStub : public v1beta1_internal::EchoStub { private: void ExtractMetadata(grpc::ClientContext& context) { + auto to_string = [](grpc::string_ref ref) { + return ref.empty() ? std::string{} : std::string{ref.data(), ref.size()}; + }; std::multimap metadata; for (auto const& pair : context.GetServerInitialMetadata()) { - metadata.emplace(std::string(pair.first.data(), pair.first.size()), - std::string(pair.second.data(), pair.second.size())); + metadata.emplace(to_string(pair.first), to_string(pair.second)); } for (auto const& pair : context.GetServerTrailingMetadata()) { - metadata.emplace(std::string(pair.first.data(), pair.first.size()), - std::string(pair.second.data(), pair.second.size())); + metadata.emplace(to_string(pair.first), to_string(pair.second)); } metadata_callback_(metadata); }