3131package com .google .api .gax .httpjson ;
3232
3333import static org .junit .jupiter .api .Assertions .assertEquals ;
34+ import static org .junit .jupiter .api .Assertions .assertNotNull ;
35+ import static org .junit .jupiter .api .Assertions .fail ;
3436
37+ import com .google .api .gax .core .NoCredentialsProvider ;
38+ import com .google .api .gax .rpc .ApiException ;
39+ import com .google .api .gax .rpc .StatusCode ;
40+ import com .google .cloud .NoCredentials ;
41+ import com .google .cloud .bigquery .*;
42+ import com .google .cloud .translate .v3 .*;
43+ import io .grpc .netty .shaded .io .grpc .netty .NettyChannelBuilder ;
44+ import io .grpc .netty .shaded .io .netty .handler .ssl .ApplicationProtocolConfig ;
45+ import io .grpc .netty .shaded .io .netty .handler .ssl .SslContext ;
46+ import io .grpc .netty .shaded .io .netty .handler .ssl .SslContextBuilder ;
47+ import io .grpc .netty .shaded .io .netty .handler .ssl .SslProvider ;
48+ import io .grpc .netty .shaded .io .netty .handler .ssl .util .InsecureTrustManagerFactory ;
3549import java .io .InputStream ;
36- import java .security .Security ;
50+ import java .security .KeyStore ;
51+ import java .util .ArrayList ;
52+ import java .util .List ;
3753import org .junit .jupiter .api .AfterAll ;
3854import org .junit .jupiter .api .BeforeAll ;
3955import org .junit .jupiter .api .Test ;
87103 * paths remain fully robust and operational.
88104 * </ul>
89105 */
90- public class PqcConnectivityTest {
106+ public abstract class PqcConnectivityTest {
91107
92108 private static Process serverProcess ;
93- protected static int httpPort ;
94- protected static int grpcPort ;
109+ protected static int httpPqcPort ;
110+ protected static int httpClassicalPort ;
111+ protected static int grpcPqcPort ;
112+ protected static int grpcClassicalPort ;
95113 private static boolean isPqcSupported ;
114+ private static KeyStore ks ;
96115
97116 /**
98117 * Configures the integration test harness environment before test cases are executed.
@@ -144,42 +163,29 @@ protected boolean clientSupportsPqc() {
144163 return true ;
145164 }
146165
166+ protected abstract boolean grpcTestShouldSucceed ();
167+
147168 @ BeforeAll
148169 public static void setup () throws Exception {
149170
150- // Dynamically detect if PQC auto-upgrade wrapping is supported by current classpath
171+ // Dynamically detect if PQC auto-upgrade wrapping is supported by current
172+ // classpath
151173 // dependencies (Snapshot vs Release)
152174 try {
153- Class .forName ("com.google.api.client.http.javanet.PqcDelegatingSSLSocketFactory " );
175+ Class .forName ("com.google.api.client.http.javanet.PqcPeerHostSSLSocketFactory " );
154176 isPqcSupported = true ;
155177 } catch (ClassNotFoundException e ) {
156178 isPqcSupported = false ;
157179 }
158180
159- // 1. Load the self-signed server validation certificate/keystore from test resources.
160- java .security .KeyStore ks = java .security .KeyStore .getInstance ("PKCS12" );
181+ ks = KeyStore .getInstance ("PKCS12" );
161182 try (InputStream is = PqcConnectivityTest .class .getResourceAsStream ("/pqctest.p12" )) {
162183 if (is == null ) {
163184 throw new RuntimeException ("pqctest.p12 not found in classpath" );
164185 }
165186 ks .load (is , "password" .toCharArray ());
166187 }
167188
168- // 2. Save the keystore to a temporary file so the JRE's JSSE property system can access its
169- // absolute path.
170- java .io .File tempFile = java .io .File .createTempFile ("pqctest" , ".p12" );
171- tempFile .deleteOnExit ();
172- try (java .io .FileOutputStream fos = new java .io .FileOutputStream (tempFile )) {
173- ks .store (fos , "password" .toCharArray ());
174- }
175-
176- // 3. Configure JVM default JSSE trust store system properties to trust the self-signed
177- // validation certificate.
178- // This allows the client to trust the certificate issued by our local server
179- System .setProperty ("javax.net.ssl.trustStore" , tempFile .getAbsolutePath ());
180- System .setProperty ("javax.net.ssl.trustStorePassword" , "password" );
181- System .setProperty ("javax.net.ssl.trustStoreType" , "PKCS12" );
182-
183189 // 6. Spawn PqcTestServer in a separate background process to ensure physical
184190 // JVM runtime isolation!
185191 ProcessBuilder pb =
@@ -200,22 +206,30 @@ public static void setup() throws Exception {
200206 serverProcess .getInputStream (), java .nio .charset .StandardCharsets .UTF_8 ));
201207
202208 String line ;
203- boolean httpPortFound = false ;
204- boolean grpcPortFound = false ;
209+ boolean httpPqcFound = false ;
210+ boolean httpClassicalFound = false ;
211+ boolean grpcPqcFound = false ;
212+ boolean grpcClassicalFound = false ;
205213
206214 // Wait for the server process to output its HTTP and gRPC ports
207215 long startTime = System .currentTimeMillis ();
208216 while ((line = reader .readLine ()) != null ) {
209217 System .out .println ("[SERVER-OUT] " + line );
210- if (line .startsWith ("HTTP_PORT: " )) {
211- httpPort = Integer .parseInt (line .substring (11 ).trim ());
212- httpPortFound = true ;
213- } else if (line .startsWith ("GRPC_PORT: " )) {
214- grpcPort = Integer .parseInt (line .substring (11 ).trim ());
215- grpcPortFound = true ;
218+ if (line .startsWith ("HTTP_PQC_PORT: " )) {
219+ httpPqcPort = Integer .parseInt (line .substring (15 ).trim ());
220+ httpPqcFound = true ;
221+ } else if (line .startsWith ("HTTP_CLASSICAL_PORT: " )) {
222+ httpClassicalPort = Integer .parseInt (line .substring (21 ).trim ());
223+ httpClassicalFound = true ;
224+ } else if (line .startsWith ("GRPC_PQC_PORT: " )) {
225+ grpcPqcPort = Integer .parseInt (line .substring (15 ).trim ());
226+ grpcPqcFound = true ;
227+ } else if (line .startsWith ("GRPC_CLASSICAL_PORT: " )) {
228+ grpcClassicalPort = Integer .parseInt (line .substring (21 ).trim ());
229+ grpcClassicalFound = true ;
216230 }
217231
218- if (httpPortFound && grpcPortFound ) {
232+ if (httpPqcFound && httpClassicalFound && grpcPqcFound && grpcClassicalFound ) {
219233 break ;
220234 }
221235
@@ -227,7 +241,7 @@ public static void setup() throws Exception {
227241 }
228242 }
229243
230- if (!httpPortFound || !grpcPortFound ) {
244+ if (!httpPqcFound || !httpClassicalFound || ! grpcPqcFound || ! grpcClassicalFound ) {
231245 throw new RuntimeException ("PqcTestServer failed to initialize ephemeral ports!" );
232246 }
233247
@@ -255,41 +269,130 @@ public static void teardown() {
255269 // clean exit
256270 serverProcess .destroyForcibly ();
257271 }
258- if (isPqcSupported ) {
259- Security .removeProvider ("BCJSSE" );
260- Security .removeProvider ("BC" );
261- }
262272 }
263273
264- public void runTests () throws Exception {
265- assertEquals (isPqcSupported , clientSupportsPqc ());
266- testHttpPqc ();
267- testGrpcPqc ();
268- testBigQueryPqc ();
274+ private void runGrpcTest (int port , boolean shouldSucceed ) throws Exception {
275+ TranslationServiceSettings .Builder settingsBuilder =
276+ TranslationServiceSettings .newBuilder ()
277+ .setEndpoint ("localhost:" + port )
278+ .setCredentialsProvider (NoCredentialsProvider .create ());
279+
280+ com .google .api .gax .grpc .InstantiatingGrpcChannelProvider .Builder channelProviderBuilder =
281+ ((com .google .api .gax .grpc .InstantiatingGrpcChannelProvider )
282+ settingsBuilder .getTransportChannelProvider ())
283+ .toBuilder ();
284+ channelProviderBuilder .setChannelConfigurator (
285+ new com .google .api .core .ApiFunction <
286+ io .grpc .ManagedChannelBuilder , io .grpc .ManagedChannelBuilder >() {
287+ @ Override
288+ public io .grpc .ManagedChannelBuilder apply (io .grpc .ManagedChannelBuilder builder ) {
289+ if (clientSupportsPqc ()) {
290+ configureGrpcChannelForPqc (builder );
291+ } else {
292+ configureGrpcChannelForClassical (builder );
293+ }
294+ return builder ;
295+ }
296+ });
297+ settingsBuilder .setTransportChannelProvider (channelProviderBuilder .build ());
298+
299+ TranslationServiceSettings settings = settingsBuilder .build ();
300+
301+ try (TranslationServiceClient client = TranslationServiceClient .create (settings )) {
302+ List <String > contents = new ArrayList <>();
303+ contents .add ("house" );
304+ TranslateTextRequest request =
305+ TranslateTextRequest .newBuilder ()
306+ .setParent ("projects/test-project" )
307+ .addAllContents (contents )
308+ .build ();
309+
310+ try {
311+ TranslateTextResponse response = client .translateText (request );
312+ if (!shouldSucceed ) {
313+ fail ("Expected gRPC call to fail!" );
314+ }
315+ assertNotNull (response );
316+ } catch (ApiException e ) {
317+ if (shouldSucceed ) {
318+ fail ("Expected gRPC call to succeed, but failed: " + e .getMessage (), e );
319+ }
320+ }
321+ }
269322 }
270323
271- @ Test
272- public void testHttpPqc () throws Exception {}
324+
273325
274326 @ Test
275- public void testGrpcPqc () throws Exception {}
327+ public void testGrpcPqcServerEnforced () throws Exception {
328+ runGrpcTest (grpcPqcPort , grpcTestShouldSucceed ());
329+ }
276330
277331 @ Test
278- public void testBigQueryPqc () throws Exception {}
332+ public void testGrpcClassicalServer () throws Exception {
333+ runGrpcTest (grpcClassicalPort , true );
334+ }
279335
280- private static class ByteMarshaller implements io .grpc .MethodDescriptor .Marshaller <byte []> {
281- @ Override
282- public InputStream stream (byte [] value ) {
283- return new java .io .ByteArrayInputStream (value );
336+
337+ private static void configureGrpcChannelForPqc (io .grpc .ManagedChannelBuilder <?> builder ) {
338+ if (!(builder instanceof NettyChannelBuilder )) {
339+ throw new IllegalArgumentException (
340+ "Unsupported channel builder type: " + builder .getClass ().getName ());
284341 }
342+ NettyChannelBuilder nettyBuilder = (NettyChannelBuilder ) builder ;
285343
286- @ Override
287- public byte [] parse (InputStream stream ) {
288- try {
289- return com .google .common .io .ByteStreams .toByteArray (stream );
290- } catch (java .io .IOException e ) {
291- throw new RuntimeException (e );
292- }
344+ // Ensures HTTP/2 is used as it's required by gRPC
345+ ApplicationProtocolConfig apn =
346+ new ApplicationProtocolConfig (
347+ ApplicationProtocolConfig .Protocol .ALPN ,
348+ ApplicationProtocolConfig .SelectorFailureBehavior .NO_ADVERTISE ,
349+ ApplicationProtocolConfig .SelectedListenerFailureBehavior .ACCEPT ,
350+ "h2" );
351+
352+ try {
353+ java .security .Provider bcProvider = new org .bouncycastle .jce .provider .BouncyCastleProvider ();
354+ java .security .Provider bcJsseProvider =
355+ new org .bouncycastle .jsse .provider .BouncyCastleJsseProvider (bcProvider );
356+
357+ SslContext shadedSslContext =
358+ SslContextBuilder .forClient ()
359+ .sslProvider (SslProvider .JDK )
360+ .sslContextProvider (bcJsseProvider )
361+ .protocols ("TLSv1.3" )
362+ .applicationProtocolConfig (apn )
363+ .trustManager (InsecureTrustManagerFactory .INSTANCE )
364+ .build ();
365+
366+ nettyBuilder .sslContext (shadedSslContext );
367+ } catch (Exception e ) {
368+ throw new RuntimeException ("Failed to configure shaded gRPC Netty channel for PQC" , e );
369+ }
370+ }
371+
372+ private static void configureGrpcChannelForClassical (io .grpc .ManagedChannelBuilder <?> builder ) {
373+ if (!(builder instanceof NettyChannelBuilder )) {
374+ throw new IllegalArgumentException (
375+ "Unsupported channel builder type: " + builder .getClass ().getName ());
376+ }
377+ NettyChannelBuilder nettyBuilder = (NettyChannelBuilder ) builder ;
378+
379+ ApplicationProtocolConfig apn =
380+ new ApplicationProtocolConfig (
381+ ApplicationProtocolConfig .Protocol .ALPN ,
382+ ApplicationProtocolConfig .SelectorFailureBehavior .NO_ADVERTISE ,
383+ ApplicationProtocolConfig .SelectedListenerFailureBehavior .ACCEPT ,
384+ "h2" );
385+
386+ try {
387+ SslContext shadedSslContext =
388+ SslContextBuilder .forClient ()
389+ .applicationProtocolConfig (apn )
390+ .trustManager (InsecureTrustManagerFactory .INSTANCE )
391+ .build ();
392+
393+ nettyBuilder .sslContext (shadedSslContext );
394+ } catch (Exception e ) {
395+ throw new RuntimeException ("Failed to configure shaded gRPC Netty channel for Classical" , e );
293396 }
294397 }
295398}
0 commit comments