@@ -91,6 +91,28 @@ public class PqcConnectivityTest {
9191 * <li>Spins up the hermetic <code>PqcTestServer</code> instance.</li>
9292 * </ol>
9393 */
94+ /**
95+ * Configures the integration test harness environment before test cases are executed.
96+ *
97+ * <p><b>Detailed Security & Keystore Configuration Architecture:</b></p>
98+ * <ul>
99+ * <li><b>What is a Keystore (PKCS12):</b> A PKCS12 keystore (<code>pqctest.p12</code>) is a secure key database
100+ * containing the server's private key and its self-signed public certificate. The server uses it during
101+ * the TLS handshake to prove its identity and establish a secure channel.</li>
102+ * <li><b>How Encryption Works:</b> The certificate itself does not encrypt message data directly. Instead,
103+ * during the TLS handshake, the client and server negotiate a symmetric session key using post-quantum
104+ * cryptography (ML-KEM). This session key is then used to encrypt and decrypt all sent/received HTTP/gRPC data.</li>
105+ * <li><b>Why a Custom Temporary Truststore is Required:</b> Because the server uses a self-signed test certificate,
106+ * it is not signed by any public Certificate Authority (CA) trusted by the standard JRE truststore (<code>cacerts</code>).
107+ * Without registering a custom truststore containing this certificate, standard JRE TLS clients will reject the connection
108+ * with an <code>SSLHandshakeException</code>. We extract the certificate to a temporary file and point
109+ * <code>javax.net.ssl.trustStore</code> to it, thereby trusting it scope-specifically for this test run without
110+ * polluting or mutating the user's system-wide JRE truststore.</li>
111+ * <li><b>JCA Provider Registration:</b> Registers <code>BouncyCastleJsseProvider</code> at provider position 1.
112+ * This registers Bouncy Castle as the primary security provider, causing all standard default <code>SSLContext</code>
113+ * and vanilla client factories to utilize Bouncy Castle JSSE and negotiate PQC automatically.</li>
114+ * </ul>
115+ */
94116 @ BeforeAll
95117 public static void setup () throws Exception {
96118 System .setProperty ("javax.net.debug" , "all" );
@@ -103,31 +125,33 @@ public static void setup() throws Exception {
103125 isPqcSupported = false ;
104126 }
105127
106- // Extract the test certificate keystore from the classpath and save it to a temporary file
128+ // 1. Load the self-signed server validation certificate/ keystore from test resources.
107129 java .security .KeyStore ks = java .security .KeyStore .getInstance ("PKCS12" );
108130 try (InputStream is = PqcTestServer .class .getResourceAsStream ("/pqctest.p12" )) {
109131 if (is == null ) {
110132 throw new RuntimeException ("pqctest.p12 not found in classpath" );
111133 }
112134 ks .load (is , "password" .toCharArray ());
113135 }
136+
137+ // 2. Save the keystore to a temporary file so the JRE's JSSE property system can access its absolute path.
114138 java .io .File tempFile = java .io .File .createTempFile ("pqctest" , ".p12" );
115139 tempFile .deleteOnExit ();
116140 try (java .io .FileOutputStream fos = new java .io .FileOutputStream (tempFile )) {
117141 ks .store (fos , "password" .toCharArray ());
118142 }
119143
120- // Configure JVM default JSSE trust store system properties to trust our test server
144+ // 3. Configure JVM default JSSE trust store system properties to trust the self-signed validation certificate.
121145 System .setProperty ("javax.net.ssl.trustStore" , tempFile .getAbsolutePath ());
122146 System .setProperty ("javax.net.ssl.trustStorePassword" , "password" );
123147 System .setProperty ("javax.net.ssl.trustStoreType" , "PKCS12" );
124148
149+ // 4. Register Bouncy Castle JSSE globally at position 1 to intercept default TLS handshakes.
150+ // Note: Bouncy Castle JSSE utilizes this server-scoped property to configure the accepted TLS 1.3 curves
151+ // on Java 17, since standard JRE 17 SSLParameters lacks programmatic namedGroup configuration APIs.
152+ System .setProperty ("org.bouncycastle.jsse.server.namedGroups" , "MLKEM768" );
125153 Security .addProvider (new BouncyCastleProvider ());
126- if (isPqcSupported ) {
127- Security .insertProviderAt (new BouncyCastleJsseProvider (), 1 );
128- } else {
129- Security .addProvider (new BouncyCastleJsseProvider ());
130- }
154+ Security .insertProviderAt (new BouncyCastleJsseProvider (), 1 );
131155
132156 server = new PqcTestServer ();
133157 server .start ();
@@ -138,6 +162,8 @@ public static void teardown() {
138162 if (server != null ) {
139163 server .stop ();
140164 }
165+ // Clear Bouncy Castle system properties in teardown to prevent side-effects/leakage to other test cases in the JVM.
166+ System .clearProperty ("org.bouncycastle.jsse.server.namedGroups" );
141167 Security .removeProvider ("BCJSSE" );
142168 Security .removeProvider ("BC" );
143169 }
@@ -186,10 +212,24 @@ public void testHttpPqc() throws Exception {
186212 com .google .api .client .http .HttpRequest request = transportFromChannel .createRequestFactory ().buildGetRequest (
187213 new com .google .api .client .http .GenericUrl ("https://localhost:" + server .getHttpPort () + "/test" ));
188214
189- HttpResponse response = request .execute ();
190- assertEquals (200 , response .getStatusCode ());
191- String content = response .parseAsString ();
192- assertEquals ("PQC HTTP OK" , content .trim ());
215+ // In Snapshot Mode, the connection succeeds natively via PQC auto-upgrade.
216+ // In Release Mode, because the server strictly expects MLKEM768 and the release client lacks PQC wrapping,
217+ // the connection attempt MUST fail during the handshake. We assert this connection failure.
218+ try {
219+ HttpResponse response = request .execute ();
220+ if (!isPqcSupported ) {
221+ org .junit .jupiter .api .Assertions .fail ("Expected legacy HTTP client connection to fail because PQC is unsupported!" );
222+ }
223+ assertEquals (200 , response .getStatusCode ());
224+ String content = response .parseAsString ();
225+ assertEquals ("PQC HTTP OK" , content .trim ());
226+ } catch (Exception e ) {
227+ if (isPqcSupported ) {
228+ throw e ; // Should never fail in Snapshot Mode
229+ }
230+ // Exception is expected and welcomed in Release Mode!
231+ System .out .println ("Verified: Legacy release HTTP client connection successfully rejected as expected: " + e .getMessage ());
232+ }
193233 }
194234
195235 @ Test
@@ -205,6 +245,9 @@ public void testGrpcPqc() throws Exception {
205245 .setEndpoint ("localhost:" + server .getGrpcPort ())
206246 .setHeaderProvider (() -> java .util .Collections .emptyMap ());
207247
248+ // In Snapshot Mode, we dynamically inject the Netty JJSSE provider channel configurator to enable PQC.
249+ // In Release Mode, we skip this configuration, forcing the classical client to connect without PQC,
250+ // which should cause the strictly-configured server to reject the connection.
208251 if (isPqcSupported ) {
209252 providerBuilder .setChannelConfigurator (new com .google .api .core .ApiFunction <io .grpc .ManagedChannelBuilder , io .grpc .ManagedChannelBuilder >() {
210253 @ Override
@@ -261,14 +304,28 @@ public io.grpc.ManagedChannelBuilder apply(io.grpc.ManagedChannelBuilder builder
261304 }
262305
263306 InstantiatingGrpcChannelProvider provider = providerBuilder .build ();
264-
265307 io .grpc .Channel channel = ((com .google .api .gax .grpc .GrpcTransportChannel ) provider .getTransportChannel ()).getChannel ();
266308
267- byte [] response = io .grpc .stub .ClientCalls .blockingUnaryCall (
268- channel , method , io .grpc .CallOptions .DEFAULT , "Hello" .getBytes ());
269-
270- assertEquals ("PQC gRPC OK" , new String (response ).trim ());
271- ((io .grpc .ManagedChannel ) channel ).shutdown ();
309+ // Note: Because this test module only depends on core gax-grpc and grpc-stub
310+ // without pulling in a concrete generated service client library (e.g., PubSub or Spanner),
311+ // using a standard low-level gRPC blocking stubs call (ClientCalls.blockingUnaryCall) is the standard,
312+ // compile-safe way to trigger and assert raw channel TLS handshakes directly.
313+ try {
314+ byte [] response = io .grpc .stub .ClientCalls .blockingUnaryCall (
315+ channel , method , io .grpc .CallOptions .DEFAULT , "Hello" .getBytes ());
316+ if (!isPqcSupported ) {
317+ org .junit .jupiter .api .Assertions .fail ("Expected legacy gRPC client connection to fail because PQC is unsupported!" );
318+ }
319+ assertEquals ("PQC gRPC OK" , new String (response ).trim ());
320+ } catch (Exception e ) {
321+ if (isPqcSupported ) {
322+ throw e ; // Should never fail in Snapshot Mode
323+ }
324+ // Exception is expected and welcomed in Release Mode!
325+ System .out .println ("Verified: Legacy release gRPC client connection successfully rejected as expected: " + e .getMessage ());
326+ } finally {
327+ ((io .grpc .ManagedChannel ) channel ).shutdown ();
328+ }
272329 }
273330
274331 @ Test
@@ -285,7 +342,18 @@ public void testBigQueryPqc() throws Exception {
285342 // This will trigger a request to https://localhost:httpPort/bigquery/v2/projects/test-project/datasets
286343 // Under-the-hood, the default factory wraps NetHttpTransport with our programmatic PqcTlsSocketFactory,
287344 // and negotiates hybrid ML-KEM-768 successfully!
288- bigquery .listDatasets ();
345+ try {
346+ bigquery .listDatasets ();
347+ if (!isPqcSupported ) {
348+ org .junit .jupiter .api .Assertions .fail ("Expected legacy BigQuery client call to fail because PQC is unsupported!" );
349+ }
350+ } catch (Exception e ) {
351+ if (isPqcSupported ) {
352+ throw e ; // Should never fail in Snapshot Mode
353+ }
354+ // Exception is expected and welcomed in Release Mode!
355+ System .out .println ("Verified: Legacy release BigQuery client call successfully rejected as expected: " + e .getMessage ());
356+ }
289357 }
290358
291359 private static class ByteMarshaller implements io .grpc .MethodDescriptor .Marshaller <byte []> {
0 commit comments