|
21 | 21 |
|
22 | 22 | import com.google.api.client.http.javanet.NetHttpTransport; |
23 | 23 | import com.google.api.gax.core.NoCredentialsProvider; |
| 24 | +import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; |
24 | 25 | import com.google.api.gax.httpjson.HttpJsonConscryptUtils; |
25 | 26 | import com.google.api.gax.httpjson.HttpJsonMetadata; |
26 | 27 | import com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider; |
|
29 | 30 | import com.google.showcase.v1beta1.EchoResponse; |
30 | 31 | import com.google.showcase.v1beta1.EchoSettings; |
31 | 32 | import com.google.showcase.v1beta1.it.util.HttpJsonCapturingClientInterceptor; |
| 33 | +import io.grpc.CallOptions; |
| 34 | +import io.grpc.Channel; |
| 35 | +import io.grpc.ClientCall; |
| 36 | +import io.grpc.ClientInterceptor; |
| 37 | +import io.grpc.ForwardingClientCall; |
| 38 | +import io.grpc.ForwardingClientCallListener; |
| 39 | +import io.grpc.Metadata; |
| 40 | +import io.grpc.MethodDescriptor; |
| 41 | +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; |
| 42 | +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; |
32 | 43 | import java.io.File; |
33 | 44 | import java.io.InputStream; |
34 | 45 | import java.nio.file.Files; |
@@ -266,4 +277,104 @@ private static KeyStore loadCaCert(String certPath) throws Exception { |
266 | 277 | } |
267 | 278 | return trustStore; |
268 | 279 | } |
| 280 | + |
| 281 | + /** |
| 282 | + * Integration test to verify Post-Quantum Cryptography (PQC) TLS negotiation for gRPC clients. |
| 283 | + * |
| 284 | + * <p>In gRPC-Java 1.83.0+, the default Netty transport (`grpc-netty-shaded`) bundles BoringSSL |
| 285 | + * (`netty-tcnative-boringssl-static`) with built-in PQC hybrid key exchange support (e.g., |
| 286 | + * X25519MLKEM768). No custom socket configurator or security provider swapping is needed. |
| 287 | + * |
| 288 | + * <p>Because the local Showcase test server uses a self-signed CA certificate (written to {@link |
| 289 | + * #DEFAULT_CA_CERT_PATH}), we configure the gRPC transport channel builder directly to trust this |
| 290 | + * certificate via {@link GrpcSslContexts#forClient()}. This avoids mutating global JVM system |
| 291 | + * properties in {@code setUp()} and ensures HTTP/JSON tests remain completely isolated. |
| 292 | + */ |
| 293 | + @Test |
| 294 | + void testGrpcPqc_withTls() throws Exception { |
| 295 | + GrpcTlsCapturingClientInterceptor interceptor = new GrpcTlsCapturingClientInterceptor(); |
| 296 | + |
| 297 | + InstantiatingGrpcChannelProvider transportChannelProvider = |
| 298 | + EchoSettings.defaultGrpcTransportProviderBuilder() |
| 299 | + .setEndpoint(SECURE_ENDPOINT) |
| 300 | + .setInterceptorProvider(() -> Collections.singletonList(interceptor)) |
| 301 | + .setChannelConfigurator( |
| 302 | + managedChannelBuilder -> { |
| 303 | + if (managedChannelBuilder instanceof NettyChannelBuilder) { |
| 304 | + try { |
| 305 | + // Explicitly trust the self-signed CA certificate created by the local |
| 306 | + // Showcase TLS connection without altering JVM-wide SSL trust stores. |
| 307 | + ((NettyChannelBuilder) managedChannelBuilder) |
| 308 | + .sslContext( |
| 309 | + GrpcSslContexts.forClient() |
| 310 | + .trustManager(new File(DEFAULT_CA_CERT_PATH)) |
| 311 | + .build()); |
| 312 | + } catch (Exception e) { |
| 313 | + throw new RuntimeException("Failed to configure gRPC SSL context", e); |
| 314 | + } |
| 315 | + } |
| 316 | + return managedChannelBuilder; |
| 317 | + }) |
| 318 | + .build(); |
| 319 | + |
| 320 | + EchoSettings settings = |
| 321 | + EchoSettings.newBuilder() |
| 322 | + .setCredentialsProvider(NoCredentialsProvider.create()) |
| 323 | + .setTransportChannelProvider(transportChannelProvider) |
| 324 | + .build(); |
| 325 | + |
| 326 | + try (EchoClient client = EchoClient.create(settings)) { |
| 327 | + EchoResponse response = |
| 328 | + client.echo(EchoRequest.newBuilder().setContent("pqc-grpc-tls-test").build()); |
| 329 | + assertThat(response.getContent()).isEqualTo("pqc-grpc-tls-test"); |
| 330 | + |
| 331 | + Metadata capturedHeaders = interceptor.capturedMetadata; |
| 332 | + assertThat(capturedHeaders).isNotNull(); |
| 333 | + |
| 334 | + // Verify that TLS 1.3 key exchange negotiated the expected PQC hybrid group (X25519MLKEM768). |
| 335 | + String negotiatedGroup = getGrpcSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER); |
| 336 | + assertThat(negotiatedGroup).isEqualTo(EXPECTED_PQC_GROUP); |
| 337 | + } |
| 338 | + } |
| 339 | + |
| 340 | + /** |
| 341 | + * Private gRPC client interceptor to capture the response headers from the Showcase server to |
| 342 | + * verify the PQC algorithm. |
| 343 | + */ |
| 344 | + private static class GrpcTlsCapturingClientInterceptor implements ClientInterceptor { |
| 345 | + final Metadata capturedMetadata = new Metadata(); |
| 346 | + |
| 347 | + @Override |
| 348 | + public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall( |
| 349 | + MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) { |
| 350 | + return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>( |
| 351 | + next.newCall(method, callOptions)) { |
| 352 | + @Override |
| 353 | + public void start(Listener<RespT> responseListener, Metadata headers) { |
| 354 | + super.start( |
| 355 | + new ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT>( |
| 356 | + responseListener) { |
| 357 | + @Override |
| 358 | + public void onHeaders(Metadata headers) { |
| 359 | + capturedMetadata.merge(headers); |
| 360 | + super.onHeaders(headers); |
| 361 | + } |
| 362 | + }, |
| 363 | + headers); |
| 364 | + } |
| 365 | + }; |
| 366 | + } |
| 367 | + } |
| 368 | + |
| 369 | + /** |
| 370 | + * Private helper method required to extract a single string header from gRPC {@link Metadata}. |
| 371 | + * |
| 372 | + * @param metadata the captured gRPC response metadata |
| 373 | + * @param name the case-insensitive HTTP/2 header name |
| 374 | + * @return the string header value, or {@code null} if not present |
| 375 | + */ |
| 376 | + private static String getGrpcSingleHeaderString(Metadata metadata, String name) { |
| 377 | + Metadata.Key<String> key = Metadata.Key.of(name, Metadata.ASCII_STRING_MARSHALLER); |
| 378 | + return metadata.get(key); |
| 379 | + } |
269 | 380 | } |
0 commit comments