1717package com .google .showcase .v1beta1 .it ;
1818
1919import static com .google .common .truth .Truth .assertThat ;
20+ import static com .google .common .truth .Truth .assertWithMessage ;
21+ import static com .google .showcase .v1beta1 .it .util .TestClientInitializer .DEFAULT_GRPC_ENDPOINT ;
22+ import static com .google .showcase .v1beta1 .it .util .TestClientInitializer .DEFAULT_HTTPJSON_ENDPOINT ;
2023
2124import com .google .api .client .http .javanet .NetHttpTransport ;
2225import com .google .api .gax .core .NoCredentialsProvider ;
2326import com .google .api .gax .grpc .GrpcTransportChannel ;
24- import com .google .api .gax .httpjson .ApiMethodDescriptor ;
25- import com .google .api .gax .httpjson .ForwardingHttpJsonClientCall ;
26- import com .google .api .gax .httpjson .ForwardingHttpJsonClientCallListener ;
27- import com .google .api .gax .httpjson .HttpJsonCallOptions ;
28- import com .google .api .gax .httpjson .HttpJsonChannel ;
29- import com .google .api .gax .httpjson .HttpJsonClientCall ;
30- import com .google .api .gax .httpjson .HttpJsonClientInterceptor ;
3127import com .google .api .gax .httpjson .HttpJsonMetadata ;
3228import com .google .api .gax .httpjson .InstantiatingHttpJsonChannelProvider ;
3329import com .google .api .gax .rpc .FixedTransportChannelProvider ;
3632import com .google .showcase .v1beta1 .EchoRequest ;
3733import com .google .showcase .v1beta1 .EchoResponse ;
3834import com .google .showcase .v1beta1 .EchoSettings ;
35+ import com .google .showcase .v1beta1 .it .util .HttpJsonCapturingClientInterceptor ;
3936import io .grpc .Channel ;
37+ import io .grpc .ChannelCredentials ;
4038import io .grpc .ClientCall ;
4139import io .grpc .ClientInterceptor ;
4240import io .grpc .ForwardingClientCall ;
4341import io .grpc .ForwardingClientCallListener ;
42+ import io .grpc .Grpc ;
4443import io .grpc .ManagedChannel ;
4544import io .grpc .Metadata ;
4645import io .grpc .MethodDescriptor ;
47- import io .grpc .netty .shaded .io .grpc .netty .GrpcSslContexts ;
48- import io .grpc .netty .shaded .io .grpc .netty .NettyChannelBuilder ;
49- import io .grpc .netty .shaded .io .netty .handler .ssl .SslContext ;
50- import io .grpc .netty .shaded .io .netty .handler .ssl .SslContextBuilder ;
51- import io .grpc .netty .shaded .io .netty .handler .ssl .util .InsecureTrustManagerFactory ;
46+ import io .grpc .TlsChannelCredentials ;
47+ import java .io .File ;
48+ import java .io .InputStream ;
49+ import java .nio .file .Files ;
50+ import java .nio .file .Paths ;
51+ import java .security .KeyStore ;
5252import java .security .Provider ;
53+ import java .security .Security ;
54+ import java .security .cert .Certificate ;
55+ import java .security .cert .CertificateFactory ;
5356import java .util .Collections ;
57+ import java .util .List ;
5458import java .util .concurrent .TimeUnit ;
59+ import javax .net .ssl .SSLContext ;
60+ import javax .net .ssl .TrustManagerFactory ;
5561import org .conscrypt .Conscrypt ;
5662import org .junit .jupiter .api .BeforeAll ;
5763import org .junit .jupiter .api .Test ;
5864
5965public class ITPqc {
6066
61- private static final String GRPC_ENDPOINT = "localhost:7470" ;
62- private static final String HTTPJSON_ENDPOINT = "https://localhost:7470" ;
67+ // TLS response header names from Showcase server
68+ private static final String TLS_GROUP_HEADER = "x-showcase-tls-group" ;
69+ private static final String TLS_VERSION_HEADER = "x-showcase-tls-version" ;
70+ private static final String TLS_CIPHER_HEADER = "x-showcase-tls-cipher" ;
71+ private static final String TLS_SUPPORTED_GROUPS_HEADER = "x-showcase-tls-client-supported-groups" ;
72+
73+ // Expected TLS parameters
74+ private static final String EXPECTED_TLS_GROUP = "X25519MLKEM768" ;
75+ private static final String EXPECTED_TLS_VERSION = "TLS 1.3" ;
76+ private static final String EXPECTED_TLS_CIPHER = "TLS_AES_128_GCM_SHA256" ;
77+
78+ private static final String DEFAULT_CA_CERT_PATH = "target/showcase-ca.pem" ;
6379
6480 @ BeforeAll
6581 static void setUp () {
66- // Force Conscrypt and OpenJDK to prefer X25519MLKEM768 for TLS 1.3
67- System .setProperty ("jdk.tls.namedGroups" , "X25519MLKEM768,X25519,secp256r1" );
82+ File certFile = new File (DEFAULT_CA_CERT_PATH );
83+ assertWithMessage ("CA certificate file not found at " + DEFAULT_CA_CERT_PATH )
84+ .that (certFile .isFile ())
85+ .isTrue ();
6886 }
6987
7088 @ Test
7189 void testGrpcPqc () throws Exception {
72- // Build insecure Netty SslContext to bypass certificate validation for testing
73- SslContext sslContext = GrpcSslContexts .configure (
74- SslContextBuilder .forClient ().trustManager (InsecureTrustManagerFactory .INSTANCE )).build ();
7590
76- ManagedChannel channel =
77- NettyChannelBuilder .forTarget (GRPC_ENDPOINT ).sslContext (sslContext ).build ();
91+ // Create channel credentials trusting the custom CA
92+ ChannelCredentials creds =
93+ TlsChannelCredentials .newBuilder ().trustManager (new File (DEFAULT_CA_CERT_PATH )).build ();
94+
95+ ManagedChannel channel = Grpc .newChannelBuilder (DEFAULT_GRPC_ENDPOINT , creds ).build ();
7896 TransportChannel transportChannel = GrpcTransportChannel .create (channel );
7997
8098 GrpcHeaderCapturingInterceptor interceptor = new GrpcHeaderCapturingInterceptor ();
@@ -104,18 +122,17 @@ void testGrpcPqc() throws Exception {
104122 assertThat (capturedHeaders ).isNotNull ();
105123
106124 Metadata .Key <String > groupKey =
107- Metadata .Key .of ("x-showcase-tls-group" , Metadata .ASCII_STRING_MARSHALLER );
125+ Metadata .Key .of (TLS_GROUP_HEADER , Metadata .ASCII_STRING_MARSHALLER );
108126 Metadata .Key <String > versionKey =
109- Metadata .Key .of ("x-showcase-tls-version" , Metadata .ASCII_STRING_MARSHALLER );
127+ Metadata .Key .of (TLS_VERSION_HEADER , Metadata .ASCII_STRING_MARSHALLER );
110128 Metadata .Key <String > cipherKey =
111- Metadata .Key .of ("x-showcase-tls-cipher" , Metadata .ASCII_STRING_MARSHALLER );
129+ Metadata .Key .of (TLS_CIPHER_HEADER , Metadata .ASCII_STRING_MARSHALLER );
112130 Metadata .Key <String > supportedGroupsKey =
113- Metadata .Key .of (
114- "x-showcase-tls-client-supported-groups" , Metadata .ASCII_STRING_MARSHALLER );
131+ Metadata .Key .of (TLS_SUPPORTED_GROUPS_HEADER , Metadata .ASCII_STRING_MARSHALLER );
115132
116- assertThat (capturedHeaders .get (groupKey )).isEqualTo ("X25519MLKEM768" );
117- assertThat (capturedHeaders .get (versionKey )).isEqualTo ("TLS 1.3" );
118- assertThat (capturedHeaders .get (cipherKey )).isEqualTo ("TLS_AES_128_GCM_SHA256" );
133+ assertThat (capturedHeaders .get (groupKey )).isEqualTo (EXPECTED_TLS_GROUP );
134+ assertThat (capturedHeaders .get (versionKey )).isEqualTo (EXPECTED_TLS_VERSION );
135+ assertThat (capturedHeaders .get (cipherKey )).isEqualTo (EXPECTED_TLS_CIPHER );
119136 assertThat (capturedHeaders .get (supportedGroupsKey )).isNotNull ();
120137 } finally {
121138 channel .shutdown ();
@@ -125,15 +142,17 @@ void testGrpcPqc() throws Exception {
125142
126143 @ Test
127144 void testHttpJsonPqc () throws Exception {
128- // Build NetHttpTransport with certificate validation disabled
129- NetHttpTransport transport = new NetHttpTransport .Builder ().doNotValidateCertificate ().build ();
130145
131- HttpJsonHeaderCapturingInterceptor interceptor = new HttpJsonHeaderCapturingInterceptor ();
146+ // Build NetHttpTransport trusting the CA cert
147+ NetHttpTransport transport =
148+ new NetHttpTransport .Builder ().trustCertificates (loadCaCert (DEFAULT_CA_CERT_PATH )).build ();
149+
150+ HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor ();
132151
133152 InstantiatingHttpJsonChannelProvider transportChannelProvider =
134153 EchoSettings .defaultHttpJsonTransportProviderBuilder ()
135154 .setHttpTransport (transport )
136- .setEndpoint (HTTPJSON_ENDPOINT )
155+ .setEndpoint (DEFAULT_HTTPJSON_ENDPOINT . replace ( "http://" , "https://" ) )
137156 .setInterceptorProvider (() -> Collections .singletonList (interceptor ))
138157 .build ();
139158
@@ -148,41 +167,47 @@ void testHttpJsonPqc() throws Exception {
148167 client .echo (EchoRequest .newBuilder ().setContent ("pqc-httpjson-test" ).build ());
149168 assertThat (response .getContent ()).isEqualTo ("pqc-httpjson-test" );
150169
151- HttpJsonMetadata capturedHeaders = interceptor .getCapturedHeaders () ;
170+ HttpJsonMetadata capturedHeaders = interceptor .metadata ;
152171 assertThat (capturedHeaders ).isNotNull ();
153172
154- String negotiatedGroup = getSingleHeaderString (capturedHeaders , "x-showcase-tls-group" );
155- assertThat (negotiatedGroup ).isEqualTo ("X25519MLKEM768" );
173+ String negotiatedGroup = getSingleHeaderString (capturedHeaders , TLS_GROUP_HEADER );
174+ assertThat (negotiatedGroup ).isEqualTo (EXPECTED_TLS_GROUP );
156175
157- String tlsVersion = getSingleHeaderString (capturedHeaders , "x-showcase-tls-version" );
158- assertThat (tlsVersion ).isEqualTo ("TLS 1.3" );
176+ String tlsVersion = getSingleHeaderString (capturedHeaders , TLS_VERSION_HEADER );
177+ assertThat (tlsVersion ).isEqualTo (EXPECTED_TLS_VERSION );
159178
160- String tlsCipher = getSingleHeaderString (capturedHeaders , "x-showcase-tls-cipher" );
161- assertThat (tlsCipher ).isEqualTo ("TLS_AES_128_GCM_SHA256" );
179+ String tlsCipher = getSingleHeaderString (capturedHeaders , TLS_CIPHER_HEADER );
180+ assertThat (tlsCipher ).isEqualTo (EXPECTED_TLS_CIPHER );
162181
163- String supportedGroups =
164- getSingleHeaderString (capturedHeaders , "x-showcase-tls-client-supported-groups" );
182+ String supportedGroups = getSingleHeaderString (capturedHeaders , TLS_SUPPORTED_GROUPS_HEADER );
165183 assertThat (supportedGroups ).isNotNull ();
166184 }
167185 }
168186
169187 @ Test
170188 void testHttpJsonPqc_withExplicitSecurityProvider () throws Exception {
171- Provider explicitConscryptProvider = Conscrypt .newProvider ();
189+ // Explicitly use SunJSSE (JDK default) instead of Conscrypt
190+ Provider sunJsseProvider = Security .getProvider ("SunJSSE" );
191+ assertThat (sunJsseProvider ).isNotNull ();
172192
173- // Build NetHttpTransport specifying the Conscrypt provider explicitly
193+ // Initialize SSLContext and TrustManagerFactory explicitly with SunJSSE provider to trust the CA
194+ SSLContext sslContext = SSLContext .getInstance ("TLS" , sunJsseProvider );
195+ TrustManagerFactory tmf = TrustManagerFactory .getInstance (TrustManagerFactory .getDefaultAlgorithm (), sunJsseProvider );
196+ tmf .init (loadCaCert (DEFAULT_CA_CERT_PATH ));
197+ sslContext .init (null , tmf .getTrustManagers (), null );
198+
199+ // Build NetHttpTransport using the SunJSSE socket factory
174200 NetHttpTransport transport =
175201 new NetHttpTransport .Builder ()
176- .setSecurityProvider (explicitConscryptProvider )
177- .doNotValidateCertificate ()
202+ .setSslSocketFactory (sslContext .getSocketFactory ())
178203 .build ();
179204
180- HttpJsonHeaderCapturingInterceptor interceptor = new HttpJsonHeaderCapturingInterceptor ();
205+ HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor ();
181206
182207 InstantiatingHttpJsonChannelProvider transportChannelProvider =
183208 EchoSettings .defaultHttpJsonTransportProviderBuilder ()
184209 .setHttpTransport (transport )
185- .setEndpoint (HTTPJSON_ENDPOINT )
210+ .setEndpoint (DEFAULT_HTTPJSON_ENDPOINT . replace ( "http://" , "https://" ) )
186211 .setInterceptorProvider (() -> Collections .singletonList (interceptor ))
187212 .build ();
188213
@@ -198,20 +223,26 @@ void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception {
198223 EchoRequest .newBuilder ().setContent ("pqc-httpjson-explicit-provider-test" ).build ());
199224 assertThat (response .getContent ()).isEqualTo ("pqc-httpjson-explicit-provider-test" );
200225
201- HttpJsonMetadata capturedHeaders = interceptor .getCapturedHeaders () ;
226+ HttpJsonMetadata capturedHeaders = interceptor .metadata ;
202227 assertThat (capturedHeaders ).isNotNull ();
203228
204- String negotiatedGroup = getSingleHeaderString (capturedHeaders , "x-showcase-tls-group" );
205- assertThat (negotiatedGroup ).isEqualTo ("X25519MLKEM768" );
229+ String negotiatedGroup = getSingleHeaderString (capturedHeaders , TLS_GROUP_HEADER );
230+ // Under SunJSSE (JDK default), PQC curves are unsupported, so it falls back to classical X25519
231+ assertThat (negotiatedGroup ).isEqualTo ("X25519" );
206232
207- String tlsVersion = getSingleHeaderString (capturedHeaders , "x-showcase-tls-version" );
233+ String tlsVersion = getSingleHeaderString (capturedHeaders , TLS_VERSION_HEADER );
208234 assertThat (tlsVersion ).isEqualTo ("TLS 1.3" );
209235
210- String tlsCipher = getSingleHeaderString (capturedHeaders , "x-showcase-tls-cipher" );
236+ String tlsCipher = getSingleHeaderString (capturedHeaders , TLS_CIPHER_HEADER );
211237 assertThat (tlsCipher ).isEqualTo ("TLS_AES_128_GCM_SHA256" );
212238 }
213239 }
214240
241+ /**
242+ * Captures initial TLS response headers (e.g. x-showcase-tls-group) from the gRPC stream.
243+ * This is required because showcase TLS headers are sent as initial headers rather than trailing metadata (trailers),
244+ * which means the shared utility GrpcCapturingClientInterceptor cannot be used (as it only intercepts trailers).
245+ */
215246 private static class GrpcHeaderCapturingInterceptor implements ClientInterceptor {
216247 private Metadata capturedHeaders ;
217248
@@ -241,38 +272,12 @@ public Metadata getCapturedHeaders() {
241272 }
242273 }
243274
244- private static class HttpJsonHeaderCapturingInterceptor implements HttpJsonClientInterceptor {
245- private HttpJsonMetadata capturedHeaders ;
246-
247- @ Override
248- public <ReqT , RespT > HttpJsonClientCall <ReqT , RespT > interceptCall (
249- ApiMethodDescriptor <ReqT , RespT > method ,
250- HttpJsonCallOptions callOptions ,
251- HttpJsonChannel next ) {
252- return new ForwardingHttpJsonClientCall .SimpleForwardingHttpJsonClientCall <ReqT , RespT >(
253- next .newCall (method , callOptions )) {
254- @ Override
255- public void start (
256- HttpJsonClientCall .Listener <RespT > responseListener , HttpJsonMetadata requestHeaders ) {
257- super .start (
258- new ForwardingHttpJsonClientCallListener .SimpleForwardingHttpJsonClientCallListener <
259- RespT >(responseListener ) {
260- @ Override
261- public void onHeaders (HttpJsonMetadata responseHeaders ) {
262- capturedHeaders = responseHeaders ;
263- super .onHeaders (responseHeaders );
264- }
265- },
266- requestHeaders );
267- }
268- };
269- }
270-
271- public HttpJsonMetadata getCapturedHeaders () {
272- return capturedHeaders ;
273- }
274- }
275-
275+ /**
276+ * Helper class to wrap a standard ManagedChannel with gRPC client interceptors.
277+ * Since EchoClient requires a ManagedChannel (which handles shutdown and awaitTermination lifecycles),
278+ * but ClientInterceptors.intercept() only returns a generic Channel, this class bridges the two by
279+ * forwarding call creation to the intercepted channel, and routing lifecycle calls to the base channel.
280+ */
276281 private static class InterceptedManagedChannel extends ManagedChannel {
277282 private final ManagedChannel delegate ;
278283 private final Channel intercepted ;
@@ -323,8 +328,8 @@ public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedE
323328
324329 private static String getSingleHeaderString (HttpJsonMetadata metadata , String name ) {
325330 Object valueObj = metadata .getHeaders ().get (name );
326- if (valueObj instanceof java . util . List ) {
327- java . util . List <?> list = (java . util . List <?>) valueObj ;
331+ if (valueObj instanceof List ) {
332+ List <?> list = (List <?>) valueObj ;
328333 if (!list .isEmpty ()) {
329334 return String .valueOf (list .get (0 ));
330335 }
@@ -333,4 +338,15 @@ private static String getSingleHeaderString(HttpJsonMetadata metadata, String na
333338 }
334339 return null ;
335340 }
341+
342+ private static KeyStore loadCaCert (String certPath ) throws Exception {
343+ KeyStore trustStore = KeyStore .getInstance (KeyStore .getDefaultType ());
344+ trustStore .load (null , null );
345+ CertificateFactory cf = CertificateFactory .getInstance ("X.509" );
346+ try (InputStream is = Files .newInputStream (Paths .get (certPath ))) {
347+ Certificate cert = cf .generateCertificate (is );
348+ trustStore .setCertificateEntry ("showcase-ca" , cert );
349+ }
350+ return trustStore ;
351+ }
336352}
0 commit comments