From 6476ca58a73ec7e84467b5d8e0dd43b5d633fcd6 Mon Sep 17 00:00:00 2001 From: Scott Hart Date: Fri, 10 Jul 2026 13:53:11 -0400 Subject: [PATCH 1/2] impl(rest): use PQC algorithm when available --- ...den_kitchen_sink_rest_stub_factory_test.cc | 2 + ...lden_thing_admin_rest_stub_factory_test.cc | 2 + google/cloud/BUILD.bazel | 1 + .../cloud/google_cloud_cpp_rest_internal.bzl | 4 + .../google_cloud_cpp_rest_internal.cmake | 10 +- ...gle_cloud_cpp_rest_internal_unit_tests.bzl | 1 + google/cloud/internal/curl_impl.cc | 15 ++- google/cloud/internal/curl_impl.h | 4 + google/cloud/internal/curl_rest_client.cc | 14 ++- google/cloud/internal/curl_rest_client.h | 1 + .../cloud/internal/openssl/ssl_ec_curves.cc | 105 ++++++++++++++++++ google/cloud/internal/ssl_ec_curves.cc | 78 +++++++++++++ google/cloud/internal/ssl_ec_curves.h | 40 +++++++ google/cloud/internal/ssl_ec_curves_test.cc | 71 ++++++++++++ google/cloud/internal/win32/ssl_ec_curves.cc | 62 +++++++++++ 15 files changed, 405 insertions(+), 5 deletions(-) create mode 100644 google/cloud/internal/openssl/ssl_ec_curves.cc create mode 100644 google/cloud/internal/ssl_ec_curves.cc create mode 100644 google/cloud/internal/ssl_ec_curves.h create mode 100644 google/cloud/internal/ssl_ec_curves_test.cc create mode 100644 google/cloud/internal/win32/ssl_ec_curves.cc diff --git a/generator/integration_tests/tests/golden_kitchen_sink_rest_stub_factory_test.cc b/generator/integration_tests/tests/golden_kitchen_sink_rest_stub_factory_test.cc index 596d26013872c..df07885d23ea2 100644 --- a/generator/integration_tests/tests/golden_kitchen_sink_rest_stub_factory_test.cc +++ b/generator/integration_tests/tests/golden_kitchen_sink_rest_stub_factory_test.cc @@ -15,6 +15,7 @@ #include "generator/integration_tests/golden/v1/internal/golden_kitchen_sink_rest_stub_factory.h" #include "generator/integration_tests/test.pb.h" #include "google/cloud/common_options.h" +#include "google/cloud/testing_util/scoped_environment.h" #include "google/cloud/testing_util/scoped_log.h" #include "google/cloud/testing_util/status_matchers.h" #include @@ -30,6 +31,7 @@ using ::testing::HasSubstr; using ::testing::IsEmpty; TEST(GoldenKitchenSinkRestStubFactoryTest, DefaultStubWithoutLogging) { + testing_util::ScopedEnvironment env("GOOGLE_CLOUD_CPP_DISABLE_PQC", "true"); testing_util::ScopedLog log; auto default_stub = CreateDefaultGoldenKitchenSinkRestStub({}); auto const log_lines = log.ExtractLines(); diff --git a/generator/integration_tests/tests/golden_thing_admin_rest_stub_factory_test.cc b/generator/integration_tests/tests/golden_thing_admin_rest_stub_factory_test.cc index cd6f1f166a48c..f232f36e3e106 100644 --- a/generator/integration_tests/tests/golden_thing_admin_rest_stub_factory_test.cc +++ b/generator/integration_tests/tests/golden_thing_admin_rest_stub_factory_test.cc @@ -14,6 +14,7 @@ #include "generator/integration_tests/golden/v1/internal/golden_thing_admin_rest_stub_factory.h" #include "google/cloud/common_options.h" +#include "google/cloud/testing_util/scoped_environment.h" #include "google/cloud/testing_util/scoped_log.h" #include "google/cloud/testing_util/status_matchers.h" #include @@ -29,6 +30,7 @@ using ::testing::HasSubstr; using ::testing::IsEmpty; TEST(GoldenThingAdminRestStubFactoryTest, DefaultStubWithoutLogging) { + testing_util::ScopedEnvironment env("GOOGLE_CLOUD_CPP_DISABLE_PQC", "true"); testing_util::ScopedLog log; auto default_stub = CreateDefaultGoldenThingAdminRestStub({}); auto const log_lines = log.ExtractLines(); diff --git a/google/cloud/BUILD.bazel b/google/cloud/BUILD.bazel index dd4feee33dcf0..b61ab4d89fb02 100644 --- a/google/cloud/BUILD.bazel +++ b/google/cloud/BUILD.bazel @@ -241,6 +241,7 @@ cc_library( "@platforms//os:windows": [ "-DEFAULTLIB:bcrypt.lib", "-DEFAULTLIB:crypt32.lib", + "-DEFAULTLIB:ncrypt.lib", ], "//conditions:default": [], }), diff --git a/google/cloud/google_cloud_cpp_rest_internal.bzl b/google/cloud/google_cloud_cpp_rest_internal.bzl index 94307436e22d8..74036482d375c 100644 --- a/google/cloud/google_cloud_cpp_rest_internal.bzl +++ b/google/cloud/google_cloud_cpp_rest_internal.bzl @@ -75,6 +75,7 @@ google_cloud_cpp_rest_internal_hdrs = [ "internal/rest_retry_loop.h", "internal/rest_set_metadata.h", "internal/sign_using_sha256.h", + "internal/ssl_ec_curves.h", "internal/tracing_http_payload.h", "internal/tracing_rest_client.h", "internal/tracing_rest_response.h", @@ -122,6 +123,7 @@ google_cloud_cpp_rest_internal_srcs = [ "internal/oauth2_universe_domain.cc", "internal/openssl/parse_service_account_p12_file.cc", "internal/openssl/sign_using_sha256.cc", + "internal/openssl/ssl_ec_curves.cc", "internal/populate_rest_options.cc", "internal/rest_carrier.cc", "internal/rest_context.cc", @@ -132,11 +134,13 @@ google_cloud_cpp_rest_internal_srcs = [ "internal/rest_request.cc", "internal/rest_response.cc", "internal/rest_set_metadata.cc", + "internal/ssl_ec_curves.cc", "internal/tracing_http_payload.cc", "internal/tracing_rest_client.cc", "internal/tracing_rest_response.cc", "internal/unified_rest_credentials.cc", "internal/win32/parse_service_account_p12_file.cc", "internal/win32/sign_using_sha256.cc", + "internal/win32/ssl_ec_curves.cc", "internal/win32/win32_helpers.cc", ] diff --git a/google/cloud/google_cloud_cpp_rest_internal.cmake b/google/cloud/google_cloud_cpp_rest_internal.cmake index ab811e240cd4c..6d4ca373d0647 100644 --- a/google/cloud/google_cloud_cpp_rest_internal.cmake +++ b/google/cloud/google_cloud_cpp_rest_internal.cmake @@ -103,6 +103,7 @@ add_library( internal/oauth2_universe_domain.h internal/openssl/parse_service_account_p12_file.cc internal/openssl/sign_using_sha256.cc + internal/openssl/ssl_ec_curves.cc internal/parse_service_account_p12_file.h internal/populate_rest_options.cc internal/populate_rest_options.h @@ -129,6 +130,8 @@ add_library( internal/rest_set_metadata.cc internal/rest_set_metadata.h internal/sign_using_sha256.h + internal/ssl_ec_curves.cc + internal/ssl_ec_curves.h internal/tracing_http_payload.cc internal/tracing_http_payload.h internal/tracing_rest_client.cc @@ -139,6 +142,7 @@ add_library( internal/unified_rest_credentials.h internal/win32/parse_service_account_p12_file.cc internal/win32/sign_using_sha256.cc + internal/win32/ssl_ec_curves.cc internal/win32/win32_helpers.cc internal/win32/win32_helpers.h rest_options.h) @@ -152,7 +156,7 @@ if (WIN32) # We use `setsockopt()` directly, which requires the ws2_32 (Winsock2 for # Windows32?) library on Windows. target_link_libraries(google_cloud_cpp_rest_internal PUBLIC ws2_32 bcrypt - crypt32) + crypt32 ncrypt) else () # We already require OpenSSL for non-Windows platforms. target_compile_definitions( @@ -211,7 +215,8 @@ google_cloud_cpp_add_pkgconfig( WIN32_LIBS ws2_32 bcrypt - crypt32) + crypt32 + ncrypt) # Create and install the CMake configuration files. include(CMakePackageConfigHelpers) @@ -303,6 +308,7 @@ if (BUILD_TESTING) internal/rest_response_test.cc internal/rest_retry_loop_test.cc internal/rest_set_metadata_test.cc + internal/ssl_ec_curves_test.cc internal/tracing_http_payload_test.cc internal/tracing_rest_client_test.cc internal/tracing_rest_response_test.cc diff --git a/google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl b/google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl index c0e1909d4ca6b..b3e69ac6751ec 100644 --- a/google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl +++ b/google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl @@ -66,6 +66,7 @@ google_cloud_cpp_rest_internal_unit_tests = [ "internal/rest_response_test.cc", "internal/rest_retry_loop_test.cc", "internal/rest_set_metadata_test.cc", + "internal/ssl_ec_curves_test.cc", "internal/tracing_http_payload_test.cc", "internal/tracing_rest_client_test.cc", "internal/tracing_rest_response_test.cc", diff --git a/google/cloud/internal/curl_impl.cc b/google/cloud/internal/curl_impl.cc index 9d897bfe59909..57a419a7be27a 100644 --- a/google/cloud/internal/curl_impl.cc +++ b/google/cloud/internal/curl_impl.cc @@ -171,9 +171,15 @@ std::size_t SpillBuffer::MoveTo(absl::Span dst) { CurlImpl::CurlImpl(CurlHandle handle, std::shared_ptr factory, Options const& options) + : CurlImpl(std::move(handle), std::move(factory), options, {}) {} + +CurlImpl::CurlImpl(CurlHandle handle, + std::shared_ptr factory, + Options const& options, std::string pqc_ec_curves) : factory_(std::move(factory)), handle_(std::move(handle)), - multi_(factory_->CreateMultiHandle()) { + multi_(factory_->CreateMultiHandle()), + pqc_ec_curves_(std::move(pqc_ec_curves)) { CurlInitializeOnce(options); logging_enabled_ = google::cloud::internal::Contains( @@ -381,6 +387,13 @@ Status CurlImpl::MakeRequest(HttpMethod method, RestContext& context, if (!status.ok()) return OnTransferError(context, std::move(status)); } +#if CURL_AT_LEAST_VERSION(7, 73, 0) + if (!pqc_ec_curves_.empty()) { + status = handle_.SetOption(CURLOPT_SSL_EC_CURVES, pqc_ec_curves_.c_str()); + if (!status.ok()) return OnTransferError(context, std::move(status)); + } +#endif + if (client_ssl_cert_.has_value()) { #if CURL_AT_LEAST_VERSION(7, 71, 0) status = handle_.SetOption(CURLOPT_SSL_VERIFYPEER, 1L); diff --git a/google/cloud/internal/curl_impl.h b/google/cloud/internal/curl_impl.h index c71078aeeacd4..239f9012fa048 100644 --- a/google/cloud/internal/curl_impl.h +++ b/google/cloud/internal/curl_impl.h @@ -75,6 +75,8 @@ class CurlImpl { CurlImpl(CurlHandle handle, std::shared_ptr factory, Options const& options); + CurlImpl(CurlHandle handle, std::shared_ptr factory, + Options const& options, std::string pqc_ec_curves); ~CurlImpl(); CurlImpl(CurlImpl const&) = delete; @@ -195,6 +197,8 @@ class CurlImpl { // Store pending data between WriteCallback() calls. SpillBuffer spill_; + + std::string pqc_ec_curves_; }; /// Compute the CURLOPT_PROXY setting from @p options. diff --git a/google/cloud/internal/curl_rest_client.cc b/google/cloud/internal/curl_rest_client.cc index 0a993ff7c2a86..3e113b11212db 100644 --- a/google/cloud/internal/curl_rest_client.cc +++ b/google/cloud/internal/curl_rest_client.cc @@ -21,8 +21,10 @@ #include "google/cloud/internal/curl_rest_response.h" #include "google/cloud/internal/oauth2_google_credentials.h" #include "google/cloud/internal/opentelemetry.h" +#include "google/cloud/internal/ssl_ec_curves.h" #include "google/cloud/internal/tracing_rest_client.h" #include "google/cloud/internal/unified_rest_credentials.h" +#include "google/cloud/log.h" #include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" @@ -115,14 +117,22 @@ CurlRestClient::CurlRestClient(std::string endpoint_address, if (options_.has()) { credentials_ = MapCredentials(*options_.get()); } + auto pqc_curves = GetPqcEcCurves(); + if (pqc_curves.ok()) { + pqc_ec_curves_ = *std::move(pqc_curves); + } else { + GCP_LOG(INFO) + << "Post-quantum cryptography (PQC) algorithms not available: " + << pqc_curves.status().message(); + } } StatusOr> CurlRestClient::CreateCurlImpl( RestContext const& context, RestRequest const& request, Options const& options) { auto handle = CurlHandle::MakeFromPool(*handle_factory_); - auto impl = - std::make_unique(std::move(handle), handle_factory_, options); + auto impl = std::make_unique(std::move(handle), handle_factory_, + options, pqc_ec_curves_); if (credentials_) { auto auth_headers = credentials_->AuthenticationHeaders( std::chrono::system_clock::now(), endpoint_address_); diff --git a/google/cloud/internal/curl_rest_client.h b/google/cloud/internal/curl_rest_client.h index d5a3cfdb9500c..75b546308ee95 100644 --- a/google/cloud/internal/curl_rest_client.h +++ b/google/cloud/internal/curl_rest_client.h @@ -79,6 +79,7 @@ class CurlRestClient : public RestClient { std::shared_ptr handle_factory_; std::shared_ptr credentials_; Options options_; + std::string pqc_ec_curves_; }; GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END diff --git a/google/cloud/internal/openssl/ssl_ec_curves.cc b/google/cloud/internal/openssl/ssl_ec_curves.cc new file mode 100644 index 0000000000000..5ffcd6a50c694 --- /dev/null +++ b/google/cloud/internal/openssl/ssl_ec_curves.cc @@ -0,0 +1,105 @@ +// 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. + +#ifndef _WIN32 +#include "google/cloud/internal/ssl_ec_curves.h" +#include +#include +#include +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace rest_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +#if OPENSSL_VERSION_NUMBER >= 0x30500000L || defined(OPENSSL_IS_BORINGSSL) +namespace { +struct OpenSslDeleter { +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + void operator()(STACK_OF(OPENSSL_CSTRING) * ptr) { + sk_OPENSSL_CSTRING_free(ptr); + } +#endif + void operator()(SSL_CTX* ptr) { SSL_CTX_free(ptr); } +}; +} // namespace +#endif + +std::vector AvailableCryptoGroups() { + std::vector groups; +#if OPENSSL_VERSION_NUMBER >= 0x30500000L + auto ctx = + std::unique_ptr(SSL_CTX_new(TLS_method())); + if (!ctx) return groups; + auto names = std::unique_ptr( + sk_OPENSSL_CSTRING_new_null()); + if (!names) return groups; + if (SSL_CTX_get0_implemented_groups(ctx.get(), 1, names.get()) > 0) { + int const num_names = sk_OPENSSL_CSTRING_num(names.get()); + for (int i = 0; i < num_names; ++i) { + char const* name = sk_OPENSSL_CSTRING_value(names.get(), i); + if (name != nullptr) groups.emplace_back(name); + } + } +#else // OPENSSL_VERSION_NUMBER < 0x30500000L + std::vector builtin_groups; + size_t const num_curves = EC_get_builtin_curves(nullptr, 0); + std::vector builtin(num_curves); + EC_get_builtin_curves(builtin.data(), num_curves); + for (auto const& c : builtin) { + char const* name = OBJ_nid2sn(c.nid); + if (name != nullptr) builtin_groups.emplace_back(name); + } +#ifdef OPENSSL_IS_BORINGSSL + // "secp224r1" can be included in the builtin curves, but it results in a + // "CURL error [59]=Couldn't use specified SSL cipher" error. + for (auto const& curve : builtin_groups) { + if (curve == "secp224r1") continue; + groups.emplace_back(curve); + } + + // Unlike OpenSSL, BoringSSL does not provide a way to query the available + // curves, so we have to probe the library using a temporary SSL_CTX. + std::array constexpr kCandidates{ + "X25519", + "X25519MLKEM768", + "X25519Kyber768Draft00", + }; + + auto ctx = + std::unique_ptr(SSL_CTX_new(TLS_method())); + if (!ctx) return groups; + for (auto const& curve : kCandidates) { + // SSL_CTX_set1_curves_list returns 1 if the curve(s) are + // recognized/supported + if (SSL_CTX_set1_curves_list(ctx.get(), curve.data()) == 1) { + groups.emplace_back(curve); + } + } +#else + groups = std::move(builtin_groups); +#endif // OPENSSL_IS_BORINGSSL +#endif // OPENSSL_VERSION_NUMBER >= 0x30500000L + return groups; +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace rest_internal +} // namespace cloud +} // namespace google +#endif // _WIN32 diff --git a/google/cloud/internal/ssl_ec_curves.cc b/google/cloud/internal/ssl_ec_curves.cc new file mode 100644 index 0000000000000..26cf7d0ba4ae4 --- /dev/null +++ b/google/cloud/internal/ssl_ec_curves.cc @@ -0,0 +1,78 @@ +// 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/internal/ssl_ec_curves.h" +#include "google/cloud/internal/getenv.h" +#include "google/cloud/internal/make_status.h" +#include "absl/strings/match.h" +#include "absl/strings/str_join.h" +#include +#include +#include + +namespace google { +namespace cloud { +namespace rest_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +// libcurl encodes version Ma.Mi.Pa as (Ma << 16) | (Mi << 8) | Pa. +// CURLOPT_SSL_EC_CURVES was introduced in libcurl version 7.73.0. +constexpr unsigned int kMinimumLibcurlVersionForEcCurves = + (7U << 16) | (73U << 8) | 0U; + +bool SupportsSslEcCurves() { + auto* vinfo = curl_version_info(CURLVERSION_NOW); + return vinfo != nullptr && + vinfo->version_num >= kMinimumLibcurlVersionForEcCurves; +} + +} // namespace + +StatusOr PrependPqcEcCurve(std::vector groups, + bool supports_ssl_ec_curves) { + if (internal::GetEnv("GOOGLE_CLOUD_CPP_DISABLE_PQC")) { + return absl::StrJoin(groups, ":"); + } + if (!supports_ssl_ec_curves) { + return internal::UnavailableError( + "libcurl version 7.73.0 or later is required to set SSL EC curves."); + } + + std::string const target = "X25519MLKEM768"; + auto it = std::find_if(groups.begin(), groups.end(), + [&target](std::string const& s) { + return absl::EqualsIgnoreCase(s, target); + }); + + if (it == groups.end()) { + return internal::UnavailableError( + "X25519MLKEM768 is not supported by the underlying crypto library."); + } + + std::string exact_name = *it; + groups.erase(it); + groups.insert(groups.begin(), std::move(exact_name)); + + return absl::StrJoin(groups, ":"); +} + +StatusOr GetPqcEcCurves() { + return PrependPqcEcCurve(AvailableCryptoGroups(), SupportsSslEcCurves()); +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace rest_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/internal/ssl_ec_curves.h b/google/cloud/internal/ssl_ec_curves.h new file mode 100644 index 0000000000000..b03059f9e6a79 --- /dev/null +++ b/google/cloud/internal/ssl_ec_curves.h @@ -0,0 +1,40 @@ +// 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. + +#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_SSL_EC_CURVES_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_SSL_EC_CURVES_H + +#include "google/cloud/status_or.h" +#include "google/cloud/version.h" +#include +#include + +namespace google { +namespace cloud { +namespace rest_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +std::vector AvailableCryptoGroups(); + +StatusOr PrependPqcEcCurve(std::vector groups, + bool supports_ssl_ec_curves); + +StatusOr GetPqcEcCurves(); + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace rest_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_SSL_EC_CURVES_H diff --git a/google/cloud/internal/ssl_ec_curves_test.cc b/google/cloud/internal/ssl_ec_curves_test.cc new file mode 100644 index 0000000000000..9760c04d9f2ee --- /dev/null +++ b/google/cloud/internal/ssl_ec_curves_test.cc @@ -0,0 +1,71 @@ +// 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/internal/ssl_ec_curves.h" +#include "google/cloud/testing_util/status_matchers.h" +#include + +namespace google { +namespace cloud { +namespace rest_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::google::cloud::testing_util::StatusIs; +using ::testing::HasSubstr; + +TEST(SslEcCurvesTest, PrependPqcEcCurveNotSupportedVersion) { + std::vector groups = {"P-256", "X25519MLKEM768"}; + auto actual = PrependPqcEcCurve(groups, /*supports_ssl_ec_curves=*/false); + EXPECT_THAT(actual, StatusIs(StatusCode::kUnavailable, + HasSubstr("libcurl version 7.73.0 or later"))); +} + +TEST(SslEcCurvesTest, PrependPqcEcCurveNotSupportedByLibrary) { + std::vector groups = {"P-256", "P-384"}; + auto actual = PrependPqcEcCurve(groups, /*supports_ssl_ec_curves=*/true); + EXPECT_THAT(actual, StatusIs(StatusCode::kUnavailable, + HasSubstr("X25519MLKEM768 is not supported"))); +} + +TEST(SslEcCurvesTest, PrependPqcEcCurveSuccess) { + std::vector groups = {"P-256", "X25519MLKEM768", "P-384"}; + auto actual = PrependPqcEcCurve(groups, /*supports_ssl_ec_curves=*/true); + ASSERT_STATUS_OK(actual); + EXPECT_EQ(*actual, "X25519MLKEM768:P-256:P-384"); +} + +TEST(SslEcCurvesTest, PrependPqcEcCurveCaseInsensitive) { + std::vector groups = {"P-256", "x25519mlkem768", "P-384"}; + auto actual = PrependPqcEcCurve(groups, /*supports_ssl_ec_curves=*/true); + ASSERT_STATUS_OK(actual); + EXPECT_EQ(*actual, "x25519mlkem768:P-256:P-384"); +} + +TEST(SslEcCurvesTest, GetPqcEcCurvesDoesNotCrash) { + // We cannot assume the host machine has PQC support or OpenSSL 3+, but + // calling GetPqcEcCurves() should execute cleanly without crashing. + auto actual = GetPqcEcCurves(); + if (actual.ok()) { + EXPECT_THAT(*actual, HasSubstr("X25519MLKEM768")); + } else { + EXPECT_THAT(actual.status().code(), StatusCode::kUnavailable); + } +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace rest_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/internal/win32/ssl_ec_curves.cc b/google/cloud/internal/win32/ssl_ec_curves.cc new file mode 100644 index 0000000000000..0285958c0914a --- /dev/null +++ b/google/cloud/internal/win32/ssl_ec_curves.cc @@ -0,0 +1,62 @@ +// 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. + +#ifdef _WIN32 +#include "google/cloud/internal/ssl_ec_curves.h" + +// windows.h must be included before ncrypt.h +// clang-format off +#include +#include +// clang-format on + +#include + +namespace google { +namespace cloud { +namespace rest_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +std::vector AvailableCryptoGroups() { + std::vector groups; + NCRYPT_PROV_HANDLE hProvider = 0; + if (FAILED(NCryptOpenStorageProvider(&hProvider, nullptr, 0))) return groups; + + DWORD count = 0; + NCryptAlgorithmName* alg_list = nullptr; + if (SUCCEEDED(NCryptEnumAlgorithms(hProvider, + NCRYPT_SECRET_AGREEMENT_OPERATION, &count, + &alg_list, 0))) { + for (DWORD i = 0; i < count; ++i) { + int size = WideCharToMultiByte(CP_UTF8, 0, alg_list[i].pszName, -1, + nullptr, 0, nullptr, nullptr); + if (size > 1) { + std::string name(size, '\0'); + WideCharToMultiByte(CP_UTF8, 0, alg_list[i].pszName, -1, &name[0], size, + nullptr, nullptr); + name.resize(size - 1); + groups.push_back(std::move(name)); + } + } + NCryptFreeBuffer(alg_list); + } + NCryptFreeObject(hProvider); + return groups; +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace rest_internal +} // namespace cloud +} // namespace google +#endif // _WIN32 From 87b67bdc056d19e449e20cab027497fef949d38d Mon Sep 17 00:00:00 2001 From: Scott Hart Date: Mon, 13 Jul 2026 13:55:20 -0400 Subject: [PATCH 2/2] add showcase build and test to verify REST PQC --- .bazelignore | 4 +- ci/cloudbuild/builds/showcase.sh | 95 ++++++++ ci/cloudbuild/triggers/showcase-ci.yaml | 28 +++ ci/cloudbuild/triggers/showcase-pr.yaml | 29 +++ ci/showcase/.bazelignore | 1 + ci/showcase/.bazelrc | 33 +++ ci/showcase/.bazelversion | 1 + ci/showcase/.gitignore | 7 + ci/showcase/BUILD.bazel.in | 113 +++++++++ ci/showcase/rest_pqc_test.cc | 215 ++++++++++++++++++ ci/showcase/showcase_config.textproto | 18 ++ .../bigtable/tests/data_integration_test.cc | 1 + 12 files changed, 544 insertions(+), 1 deletion(-) create mode 100755 ci/cloudbuild/builds/showcase.sh create mode 100644 ci/cloudbuild/triggers/showcase-ci.yaml create mode 100644 ci/cloudbuild/triggers/showcase-pr.yaml create mode 100644 ci/showcase/.bazelignore create mode 100644 ci/showcase/.bazelrc create mode 100644 ci/showcase/.bazelversion create mode 100644 ci/showcase/.gitignore create mode 100644 ci/showcase/BUILD.bazel.in create mode 100644 ci/showcase/rest_pqc_test.cc create mode 100644 ci/showcase/showcase_config.textproto diff --git a/.bazelignore b/.bazelignore index 98112232070b2..51934bc1a7518 100644 --- a/.bazelignore +++ b/.bazelignore @@ -1,5 +1,7 @@ build-out/ -ci/ +ci/bzlmod_tests/consumer-test +ci/verify_current_targets +ci/verify_deprecated_targets cmake-out/ cmake-build-debug/ cmake-build-coverage/ diff --git a/ci/cloudbuild/builds/showcase.sh b/ci/cloudbuild/builds/showcase.sh new file mode 100755 index 0000000000000..793489636fd3d --- /dev/null +++ b/ci/cloudbuild/builds/showcase.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# +# 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. + +set -euo pipefail + +source "$(dirname "$0")/../../lib/init.sh" +source module ci/cloudbuild/builds/lib/bazel.sh +source module ci/cloudbuild/builds/lib/cloudcxxrc.sh +source module ci/lib/io.sh + +export CC=clang +export CXX=clang++ + +mapfile -t args < <(bazel::common_args) + +io::log_h1 "Running Showcase PQC tests with Bazel" + +cp ci/showcase/BUILD.bazel.in ci/showcase/BUILD.bazel + +# Ensure server and temporary files are cleaned up on script exit +cleanup() { + rm -f ci/showcase/BUILD.bazel + if [[ -n "${showcase_pid:-}" ]]; then + kill "${showcase_pid}" || true + fi +} +trap cleanup EXIT + +SHOWCASE_VERSION="${SHOWCASE_VERSION:-main}" +SHOWCASE_DIR="ci/showcase/googleapis/gapic-showcase" +if [[ ! -f "${SHOWCASE_DIR}/go.mod" ]]; then + io::log_h2 "Downloading googleapis/gapic-showcase (${SHOWCASE_VERSION}) tarball into ${SHOWCASE_DIR}" + mkdir -p "${SHOWCASE_DIR}" + curl -fsSL "https://github.com/googleapis/gapic-showcase/archive/${SHOWCASE_VERSION}.tar.gz" | + tar -C "${SHOWCASE_DIR}" -xzf - --strip-components=1 +fi + +bazel_output_base="$(bazel info output_base)" +protobuf_proto_path="${bazel_output_base}/external/protobuf+/src" +googleapis_proto_path="${bazel_output_base}/external/googleapis+" + +io::log_h2 "Running C++ codegen generator for gapic-showcase echo.proto" +bazel run --action_env=GOOGLE_CLOUD_CPP_ENABLE_CLOG=yes \ + //generator:google-cloud-cpp-codegen -- \ + --protobuf_proto_path="${protobuf_proto_path}" \ + --googleapis_proto_path="${googleapis_proto_path}" \ + --golden_proto_path="${PROJECT_ROOT}/${SHOWCASE_DIR}/schema" \ + --output_path="${PROJECT_ROOT}/ci/showcase" \ + --check_comment_substitutions=false \ + --config_file="${PROJECT_ROOT}/ci/showcase/showcase_config.textproto" + +io::log_h2 "Building gapic-showcase server binary" +mkdir -p "${PROJECT_ROOT}/ci/showcase/bin" +( + cd "${PROJECT_ROOT}/${SHOWCASE_DIR}" + go build -o "${PROJECT_ROOT}/ci/showcase/bin/gapic-showcase" ./cmd/gapic-showcase +) + +SHOWCASE_PORT="${SHOWCASE_PORT:-7469}" +SHOWCASE_CA_CERT="${PROJECT_ROOT}/ci/showcase/showcase.pem" +rm -f "${SHOWCASE_CA_CERT}" + +io::log_h2 "Starting gapic-showcase server on port ${SHOWCASE_PORT} with TLS" +"${PROJECT_ROOT}/ci/showcase/bin/gapic-showcase" run \ + --port=":${SHOWCASE_PORT}" \ + --tls \ + --ca-cert-output-file="${SHOWCASE_CA_CERT}" >/dev/null 2>&1 & +showcase_pid=$! + +# Wait up to 15 seconds for CA cert file to be created and server to be listening +for _ in $(seq 1 30); do + if [[ -r "${SHOWCASE_CA_CERT}" ]] && curl --insecure -s "https://localhost:${SHOWCASE_PORT}" >/dev/null 2>&1; then + break + fi + sleep 0.5 +done + +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 diff --git a/ci/cloudbuild/triggers/showcase-ci.yaml b/ci/cloudbuild/triggers/showcase-ci.yaml new file mode 100644 index 0000000000000..200f5815c7c85 --- /dev/null +++ b/ci/cloudbuild/triggers/showcase-ci.yaml @@ -0,0 +1,28 @@ +# 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. + +filename: ci/cloudbuild/cloudbuild.yaml +github: + name: google-cloud-cpp + owner: googleapis + push: + branch: main +includeBuildLogs: INCLUDE_BUILD_LOGS_WITH_STATUS +name: showcase-ci +substitutions: + _BUILD_NAME: showcase + _DISTRO: fedora-latest-bazel + _TRIGGER_TYPE: ci +tags: +- ci diff --git a/ci/cloudbuild/triggers/showcase-pr.yaml b/ci/cloudbuild/triggers/showcase-pr.yaml new file mode 100644 index 0000000000000..423df0ec0abf5 --- /dev/null +++ b/ci/cloudbuild/triggers/showcase-pr.yaml @@ -0,0 +1,29 @@ +# 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. + +filename: ci/cloudbuild/cloudbuild.yaml +github: + name: google-cloud-cpp + owner: googleapis + pullRequest: + branch: main + commentControl: COMMENTS_ENABLED_FOR_EXTERNAL_CONTRIBUTORS_ONLY +includeBuildLogs: INCLUDE_BUILD_LOGS_WITH_STATUS +name: showcase-pr +substitutions: + _BUILD_NAME: showcase + _DISTRO: fedora-latest-bazel + _TRIGGER_TYPE: pr +tags: +- pr diff --git a/ci/showcase/.bazelignore b/ci/showcase/.bazelignore new file mode 100644 index 0000000000000..ac51a054d2da1 --- /dev/null +++ b/ci/showcase/.bazelignore @@ -0,0 +1 @@ +bazel-* diff --git a/ci/showcase/.bazelrc b/ci/showcase/.bazelrc new file mode 100644 index 0000000000000..08cbd3ab82723 --- /dev/null +++ b/ci/showcase/.bazelrc @@ -0,0 +1,33 @@ +# Copyright 2022 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. + +# Use host-OS-specific config lines from bazelrc files. +build --enable_platform_specific_config=true + +# The project requires C++ >= 17. +build:linux --cxxopt=-std=c++17 +build:macos --cxxopt=-std=c++17 +# Protobuf and gRPC require C++17 to compile the "host" +# targets, such as protoc and the grpc plugin. +build:linux --host_cxxopt=-std=c++17 +build:macos --host_cxxopt=-std=c++17 + +# Do not create the convenience links. They are inconvenient when the build +# runs inside a docker image or if one builds a quickstart and then builds +# the project separately. +build --experimental_convenience_symlinks=ignore + +# TODO(#13311) - remove once gRPC works with Bazel v7 or when gRPC stops using +# `apple_rules`. +common:macos --repo_env=BAZEL_NO_APPLE_CPP_TOOLCHAIN=1 diff --git a/ci/showcase/.bazelversion b/ci/showcase/.bazelversion new file mode 100644 index 0000000000000..df5119ec64e60 --- /dev/null +++ b/ci/showcase/.bazelversion @@ -0,0 +1 @@ +8.7.0 diff --git a/ci/showcase/.gitignore b/ci/showcase/.gitignore new file mode 100644 index 0000000000000..a0e667fcf42d3 --- /dev/null +++ b/ci/showcase/.gitignore @@ -0,0 +1,7 @@ +# Ignore downloaded gapic-showcase tarball contents when running locally. +googleapis/gapic-showcase/ +# Ignore dynamically generated C++ library files when running locally. +google/showcase/ +# Ignore compiled binaries and certificates when running locally. +bin/ +showcase.pem diff --git a/ci/showcase/BUILD.bazel.in b/ci/showcase/BUILD.bazel.in new file mode 100644 index 0000000000000..b6f573faec6f9 --- /dev/null +++ b/ci/showcase/BUILD.bazel.in @@ -0,0 +1,113 @@ +# 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. + +load("@com_google_protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("@grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library") + +package(default_visibility = ["//visibility:private"]) + +licenses(["notice"]) + +proto_library( + name = "showcase_echo_proto", + srcs = ["googleapis/gapic-showcase/schema/google/showcase/v1beta1/echo.proto"], + strip_import_prefix = "googleapis/gapic-showcase/schema", + deps = [ + "@com_google_protobuf//:any_proto", + "@com_google_protobuf//:duration_proto", + "@com_google_protobuf//:timestamp_proto", + "@googleapis//google/api:annotations_proto", + "@googleapis//google/api:client_proto", + "@googleapis//google/api:field_behavior_proto", + "@googleapis//google/api:field_info_proto", + "@googleapis//google/api:routing_proto", + "@googleapis//google/longrunning:operations_proto", + "@googleapis//google/rpc:status_proto", + ], +) + +cc_proto_library( + name = "showcase_echo_cc_proto", + deps = [":showcase_echo_proto"], +) + +cc_grpc_library( + name = "showcase_echo_cc_grpc", + srcs = [":showcase_echo_proto"], + grpc_only = True, + deps = [":showcase_echo_cc_proto"], +) + +filegroup( + name = "echo_srcs", + srcs = glob([ + "google/showcase/v1beta1/internal/*_sources.cc", + ]), +) + +filegroup( + name = "echo_hdrs", + srcs = glob( + include = [ + "google/showcase/v1beta1/*.cc", + "google/showcase/v1beta1/*.h", + "google/showcase/v1beta1/internal/*.cc", + "google/showcase/v1beta1/internal/*.h", + ], + exclude = [ + "google/showcase/v1beta1/internal/*_sources.cc", + ], + ), +) + +cc_library( + name = "showcase_echo_client", + srcs = [":echo_srcs"], + hdrs = [":echo_hdrs"], + copts = [ + "-Ici/showcase", + "-I$(BINDIR)/ci/showcase", + ], + includes = ["."], + strip_include_prefix = "", + deps = [ + ":showcase_echo_cc_grpc", + ":showcase_echo_cc_proto", + "//:common", + "//:grpc_utils", + "//google/cloud:google_cloud_cpp_rest_internal", + "//google/cloud:google_cloud_cpp_rest_protobuf_internal", + "@abseil-cpp//absl/strings", + "@googleapis//google/longrunning:longrunning_cc_grpc", + ], +) + +cc_test( + name = "rest_pqc_test", + srcs = ["rest_pqc_test.cc"], + copts = [ + "-Ici/showcase", + "-I$(BINDIR)/ci/showcase", + ], + tags = ["integration-test"], + deps = [ + ":showcase_echo_client", + "//:common", + "//google/cloud:google_cloud_cpp_rest_internal", + "//google/cloud/testing_util:google_cloud_cpp_testing_private", + "//google/cloud/testing_util:google_cloud_cpp_testing_rest_private", + "@googletest//:gtest_main", + ], +) diff --git a/ci/showcase/rest_pqc_test.cc b/ci/showcase/rest_pqc_test.cc new file mode 100644 index 0000000000000..5c4e718e61804 --- /dev/null +++ b/ci/showcase/rest_pqc_test.cc @@ -0,0 +1,215 @@ +// 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/internal/api_client_header.h" +#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 "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "google/showcase/v1beta1/echo_client.h" +#include "google/showcase/v1beta1/internal/echo_option_defaults.h" +#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 +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace v1beta1 { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::google::cloud::rest_internal::RestClient; +using ::google::cloud::rest_internal::RestContext; +using ::google::cloud::rest_internal::RestRequest; +using ::google::cloud::rest_internal::RestResponse; + +class HeaderInterceptingRestClient : public RestClient { + public: + HeaderInterceptingRestClient( + std::shared_ptr delegate, + std::function const&)> + header_callback) + : delegate_(std::move(delegate)), + header_callback_(std::move(header_callback)) {} + + StatusOr> Delete( + RestContext& context, RestRequest const& request) override { + auto response = delegate_->Delete(context, request); + if (response) header_callback_((*response)->Headers()); + return response; + } + + StatusOr> Get( + RestContext& context, RestRequest const& request) override { + auto response = delegate_->Get(context, request); + if (response) header_callback_((*response)->Headers()); + return response; + } + + StatusOr> Patch( + RestContext& context, RestRequest const& request, + std::vector> const& payload) override { + auto response = delegate_->Patch(context, request, payload); + if (response) header_callback_((*response)->Headers()); + return response; + } + + StatusOr> Post( + RestContext& context, RestRequest const& request, + std::vector> const& payload) override { + auto response = delegate_->Post(context, request, payload); + if (response) header_callback_((*response)->Headers()); + return response; + } + + StatusOr> Post( + RestContext& context, RestRequest const& request, + std::vector> const& form_data) + override { + auto response = delegate_->Post(context, request, form_data); + if (response) header_callback_((*response)->Headers()); + return response; + } + + StatusOr> Put( + RestContext& context, RestRequest const& request, + std::vector> const& payload) override { + auto response = delegate_->Put(context, request, payload); + if (response) header_callback_((*response)->Headers()); + return response; + } + + private: + std::shared_ptr delegate_; + std::function const&)> + header_callback_; +}; + +TEST(EchoRestIntegrationTest, EchoSuccessRestWithPqcVerification) { + 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_NE(test_srcdir, nullptr); + 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("https://localhost:", port); + + auto credentials = MakeAccessTokenCredentials( + "dummy-token", std::chrono::system_clock::now() + std::chrono::hours(1)); + + // Force HTTP/1.1 to make sure showcase multiplexer routes to REST. + // Use https:// prefix for TLS. + auto options = + Options{} + .set(endpoint) + .set(ca_path) + .set(credentials) + .set("1.1"); + options = v1beta1_internal::EchoDefaultOptions(std::move(options)); + + std::multimap intercepted_headers; + auto header_callback = + [&intercepted_headers]( + std::multimap const& headers) { + intercepted_headers = headers; + }; + + // Create the real REST client + auto real_client = rest_internal::MakePooledRestClient(endpoint, options); + ASSERT_NE(real_client, nullptr) << "Failed to create real REST client"; + + // Wrap it with our interceptor + auto intercepting_client = std::make_shared( + std::move(real_client), header_callback); + + // Create DefaultEchoRestStub using the intercepting client + std::shared_ptr stub = + std::make_shared( + intercepting_client, intercepting_client, options); + + // Decorate with metadata decorator (required for X-Goog-Api-Client header) + // We must append "rest/" to the API client header to satisfy showcase REST + // checks. + std::string api_client_header = absl::StrCat( + "rest/1.0.0 ", google::cloud::internal::GeneratedLibClientHeader()); + stub = std::make_shared( + std::move(stub), std::move(api_client_header)); + + // Create ConnectionImpl + auto background = std::make_unique< + rest_internal::AutomaticallyCreatedRestBackgroundThreads>(); + 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 REST!"); + + auto response = client.Echo(request); + ASSERT_TRUE(response.ok()) << response.status().message(); + EXPECT_EQ(response->content(), "Hello from C++ GAPIC REST!"); + + // Verify headers + auto get_header_value = + [](std::multimap const& headers, + std::string const& key) -> std::string { + for (auto const& pair : headers) { + if (absl::EqualsIgnoreCase(pair.first, key)) { + return pair.second; + } + } + return ""; + }; + + std::string tls_group = + get_header_value(intercepted_headers, "x-showcase-tls-group"); + 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()) + << "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")); +} + +} // namespace +} // namespace v1beta1 +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace cloud +} // namespace google diff --git a/ci/showcase/showcase_config.textproto b/ci/showcase/showcase_config.textproto new file mode 100644 index 0000000000000..8d3e73e165cba --- /dev/null +++ b/ci/showcase/showcase_config.textproto @@ -0,0 +1,18 @@ +# proto-file: generator/generator_config.proto +# proto-message: GeneratorConfiguration + +service { + service_proto_path: "google/showcase/v1beta1/echo.proto" + product_path: "google/showcase/v1beta1" + initial_copyright_year: "2026" + generate_rest_transport: true + retryable_status_codes: ["kUnavailable"] + omitted_rpcs: [ + "PagedExpandLegacyMapped", + "PagedExpandLegacy", + "PagedExpand", + "Expand", + "Collect", + "Chat" + ] +} diff --git a/google/cloud/bigtable/tests/data_integration_test.cc b/google/cloud/bigtable/tests/data_integration_test.cc index c190e58b20224..270de0e5ab204 100644 --- a/google/cloud/bigtable/tests/data_integration_test.cc +++ b/google/cloud/bigtable/tests/data_integration_test.cc @@ -569,6 +569,7 @@ TEST_F(DataIntegrationTest, TableReadMultipleCellsBigValue) { } TEST_F(DataIntegrationTest, TableApplyWithLogging) { + testing_util::ScopedEnvironment pqc = {"GOOGLE_CLOUD_CPP_DISABLE_PQC", ""}; // In our ci builds, we set GOOGLE_CLOUD_CPP_ENABLE_TRACING to log our tests, // by default. We should unset this variable and create a fresh client in // order to have a conclusive test.