Skip to content

Commit bd8f7e8

Browse files
committed
test(showcase): clean up ITPqc and build script, enforce CA cert presence, and reuse TestClientInitializer constants
TAG=agy CONV=385b9ab5-874c-4c9a-b331-66dab51fef61
1 parent c00f8a5 commit bd8f7e8

3 files changed

Lines changed: 156 additions & 213 deletions

File tree

build-with-local-http-client.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ HTTP_CLIENT_DIR="${HTTP_CLIENT_DIR:-${PARENT_DIR}/google-http-java-client}"
2626
HTTP_CLIENT_BRANCH="${HTTP_CLIENT_BRANCH:-pqc-support-conscrypt}"
2727

2828

29+
HTTP_CLIENT_VERSION="${HTTP_CLIENT_VERSION:-2.1.2-SNAPSHOT}"
30+
2931
echo "========================================================================="
3032
echo "Building and installing google-http-java-client snapshot..."
3133
echo "Using path: ${HTTP_CLIENT_DIR}"
@@ -38,7 +40,7 @@ if [ ! -d "${HTTP_CLIENT_DIR}" ]; then
3840
fi
3941

4042
# Check if the snapshot jar is already built in the local maven repository
41-
M2_JAR_PATH="${HOME}/.m2/repository/com/google/http-client/google-http-client/2.1.2-SNAPSHOT/google-http-client-2.1.2-SNAPSHOT.jar"
43+
M2_JAR_PATH="${HOME}/.m2/repository/com/google/http-client/google-http-client/${HTTP_CLIENT_VERSION}/google-http-client-${HTTP_CLIENT_VERSION}.jar"
4244

4345
if [ -f "${M2_JAR_PATH}" ] && [ "${FORCE_REBUILD}" != "true" ]; then
4446
echo "Found existing google-http-client snapshot at ${M2_JAR_PATH}."

java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITPqc.java

Lines changed: 139 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,13 @@
1717
package com.google.showcase.v1beta1.it;
1818

1919
import 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

2124
import com.google.api.client.http.javanet.NetHttpTransport;
2225
import com.google.api.gax.core.NoCredentialsProvider;
2326
import 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;
3127
import com.google.api.gax.httpjson.HttpJsonMetadata;
3228
import com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider;
3329
import com.google.api.gax.rpc.FixedTransportChannelProvider;
@@ -36,87 +32,110 @@
3632
import com.google.showcase.v1beta1.EchoRequest;
3733
import com.google.showcase.v1beta1.EchoResponse;
3834
import com.google.showcase.v1beta1.EchoSettings;
35+
import com.google.showcase.v1beta1.it.util.HttpJsonCapturingClientInterceptor;
3936
import io.grpc.Channel;
37+
import io.grpc.ChannelCredentials;
4038
import io.grpc.ClientCall;
4139
import io.grpc.ClientInterceptor;
4240
import io.grpc.ForwardingClientCall;
4341
import io.grpc.ForwardingClientCallListener;
42+
import io.grpc.Grpc;
4443
import io.grpc.ManagedChannel;
4544
import io.grpc.Metadata;
4645
import 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;
5252
import java.security.Provider;
53+
import java.security.Security;
54+
import java.security.cert.Certificate;
55+
import java.security.cert.CertificateFactory;
5356
import java.util.Collections;
57+
import java.util.List;
5458
import java.util.concurrent.TimeUnit;
59+
import javax.net.ssl.SSLContext;
60+
import javax.net.ssl.TrustManagerFactory;
5561
import org.conscrypt.Conscrypt;
5662
import org.junit.jupiter.api.BeforeAll;
5763
import org.junit.jupiter.api.Test;
5864

5965
public 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();
75-
76-
ManagedChannel channel =
77-
NettyChannelBuilder.forTarget(GRPC_ENDPOINT).sslContext(sslContext).build();
78-
TransportChannel transportChannel = GrpcTransportChannel.create(channel);
79-
80-
GrpcHeaderCapturingInterceptor interceptor = new GrpcHeaderCapturingInterceptor();
81-
82-
EchoSettings settings =
83-
EchoSettings.newBuilder()
84-
.setCredentialsProvider(NoCredentialsProvider.create())
85-
.setTransportChannelProvider(FixedTransportChannelProvider.create(transportChannel))
86-
.build();
87-
88-
// Add interceptor to capture headers
89-
ManagedChannel interceptedChannel = new InterceptedManagedChannel(channel, interceptor);
90-
TransportChannel interceptedTransportChannel = GrpcTransportChannel.create(interceptedChannel);
91-
92-
settings =
93-
settings.toBuilder()
94-
.setTransportChannelProvider(
95-
FixedTransportChannelProvider.create(interceptedTransportChannel))
96-
.build();
9790

98-
try (EchoClient client = EchoClient.create(settings)) {
99-
EchoResponse response =
100-
client.echo(EchoRequest.newBuilder().setContent("pqc-grpc-test").build());
101-
assertThat(response.getContent()).isEqualTo("pqc-grpc-test");
102-
103-
Metadata capturedHeaders = interceptor.getCapturedHeaders();
104-
assertThat(capturedHeaders).isNotNull();
105-
106-
Metadata.Key<String> groupKey =
107-
Metadata.Key.of("x-showcase-tls-group", Metadata.ASCII_STRING_MARSHALLER);
108-
Metadata.Key<String> versionKey =
109-
Metadata.Key.of("x-showcase-tls-version", Metadata.ASCII_STRING_MARSHALLER);
110-
Metadata.Key<String> cipherKey =
111-
Metadata.Key.of("x-showcase-tls-cipher", Metadata.ASCII_STRING_MARSHALLER);
112-
Metadata.Key<String> supportedGroupsKey =
113-
Metadata.Key.of(
114-
"x-showcase-tls-client-supported-groups", Metadata.ASCII_STRING_MARSHALLER);
115-
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");
119-
assertThat(capturedHeaders.get(supportedGroupsKey)).isNotNull();
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();
96+
try {
97+
TransportChannel transportChannel = GrpcTransportChannel.create(channel);
98+
99+
GrpcHeaderCapturingInterceptor interceptor = new GrpcHeaderCapturingInterceptor();
100+
101+
EchoSettings settings =
102+
EchoSettings.newBuilder()
103+
.setCredentialsProvider(NoCredentialsProvider.create())
104+
.setTransportChannelProvider(FixedTransportChannelProvider.create(transportChannel))
105+
.build();
106+
107+
// Add interceptor to capture headers
108+
ManagedChannel interceptedChannel = new InterceptedManagedChannel(channel, interceptor);
109+
TransportChannel interceptedTransportChannel = GrpcTransportChannel.create(interceptedChannel);
110+
111+
settings =
112+
settings.toBuilder()
113+
.setTransportChannelProvider(
114+
FixedTransportChannelProvider.create(interceptedTransportChannel))
115+
.build();
116+
117+
try (EchoClient client = EchoClient.create(settings)) {
118+
EchoResponse response =
119+
client.echo(EchoRequest.newBuilder().setContent("pqc-grpc-test").build());
120+
assertThat(response.getContent()).isEqualTo("pqc-grpc-test");
121+
122+
Metadata capturedHeaders = interceptor.getCapturedHeaders();
123+
assertThat(capturedHeaders).isNotNull();
124+
125+
Metadata.Key<String> groupKey =
126+
Metadata.Key.of(TLS_GROUP_HEADER, Metadata.ASCII_STRING_MARSHALLER);
127+
Metadata.Key<String> versionKey =
128+
Metadata.Key.of(TLS_VERSION_HEADER, Metadata.ASCII_STRING_MARSHALLER);
129+
Metadata.Key<String> cipherKey =
130+
Metadata.Key.of(TLS_CIPHER_HEADER, Metadata.ASCII_STRING_MARSHALLER);
131+
Metadata.Key<String> supportedGroupsKey =
132+
Metadata.Key.of(TLS_SUPPORTED_GROUPS_HEADER, Metadata.ASCII_STRING_MARSHALLER);
133+
134+
assertThat(capturedHeaders.get(groupKey)).isEqualTo(EXPECTED_TLS_GROUP);
135+
assertThat(capturedHeaders.get(versionKey)).isEqualTo(EXPECTED_TLS_VERSION);
136+
assertThat(capturedHeaders.get(cipherKey)).isEqualTo(EXPECTED_TLS_CIPHER);
137+
assertThat(capturedHeaders.get(supportedGroupsKey)).isNotNull();
138+
}
120139
} finally {
121140
channel.shutdown();
122141
channel.awaitTermination(10, TimeUnit.SECONDS);
@@ -125,15 +144,17 @@ void testGrpcPqc() throws Exception {
125144

126145
@Test
127146
void testHttpJsonPqc() throws Exception {
128-
// Build NetHttpTransport with certificate validation disabled
129-
NetHttpTransport transport = new NetHttpTransport.Builder().doNotValidateCertificate().build();
130147

131-
HttpJsonHeaderCapturingInterceptor interceptor = new HttpJsonHeaderCapturingInterceptor();
148+
// Build NetHttpTransport trusting the CA cert
149+
NetHttpTransport transport =
150+
new NetHttpTransport.Builder().trustCertificates(loadCaCert(DEFAULT_CA_CERT_PATH)).build();
151+
152+
HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor();
132153

133154
InstantiatingHttpJsonChannelProvider transportChannelProvider =
134155
EchoSettings.defaultHttpJsonTransportProviderBuilder()
135156
.setHttpTransport(transport)
136-
.setEndpoint(HTTPJSON_ENDPOINT)
157+
.setEndpoint(DEFAULT_HTTPJSON_ENDPOINT.replace("http://", "https://"))
137158
.setInterceptorProvider(() -> Collections.singletonList(interceptor))
138159
.build();
139160

@@ -148,41 +169,47 @@ void testHttpJsonPqc() throws Exception {
148169
client.echo(EchoRequest.newBuilder().setContent("pqc-httpjson-test").build());
149170
assertThat(response.getContent()).isEqualTo("pqc-httpjson-test");
150171

151-
HttpJsonMetadata capturedHeaders = interceptor.getCapturedHeaders();
172+
HttpJsonMetadata capturedHeaders = interceptor.metadata;
152173
assertThat(capturedHeaders).isNotNull();
153174

154-
String negotiatedGroup = getSingleHeaderString(capturedHeaders, "x-showcase-tls-group");
155-
assertThat(negotiatedGroup).isEqualTo("X25519MLKEM768");
175+
String negotiatedGroup = getSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER);
176+
assertThat(negotiatedGroup).isEqualTo(EXPECTED_TLS_GROUP);
156177

157-
String tlsVersion = getSingleHeaderString(capturedHeaders, "x-showcase-tls-version");
158-
assertThat(tlsVersion).isEqualTo("TLS 1.3");
178+
String tlsVersion = getSingleHeaderString(capturedHeaders, TLS_VERSION_HEADER);
179+
assertThat(tlsVersion).isEqualTo(EXPECTED_TLS_VERSION);
159180

160-
String tlsCipher = getSingleHeaderString(capturedHeaders, "x-showcase-tls-cipher");
161-
assertThat(tlsCipher).isEqualTo("TLS_AES_128_GCM_SHA256");
181+
String tlsCipher = getSingleHeaderString(capturedHeaders, TLS_CIPHER_HEADER);
182+
assertThat(tlsCipher).isEqualTo(EXPECTED_TLS_CIPHER);
162183

163-
String supportedGroups =
164-
getSingleHeaderString(capturedHeaders, "x-showcase-tls-client-supported-groups");
184+
String supportedGroups = getSingleHeaderString(capturedHeaders, TLS_SUPPORTED_GROUPS_HEADER);
165185
assertThat(supportedGroups).isNotNull();
166186
}
167187
}
168188

169189
@Test
170190
void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception {
171-
Provider explicitConscryptProvider = Conscrypt.newProvider();
191+
// Explicitly use SunJSSE (JDK default) instead of Conscrypt
192+
Provider sunJsseProvider = Security.getProvider("SunJSSE");
193+
assertThat(sunJsseProvider).isNotNull();
172194

173-
// Build NetHttpTransport specifying the Conscrypt provider explicitly
195+
// Initialize SSLContext and TrustManagerFactory explicitly with SunJSSE provider to trust the CA
196+
SSLContext sslContext = SSLContext.getInstance("TLS", sunJsseProvider);
197+
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm(), sunJsseProvider);
198+
tmf.init(loadCaCert(DEFAULT_CA_CERT_PATH));
199+
sslContext.init(null, tmf.getTrustManagers(), null);
200+
201+
// Build NetHttpTransport using the SunJSSE socket factory
174202
NetHttpTransport transport =
175203
new NetHttpTransport.Builder()
176-
.setSecurityProvider(explicitConscryptProvider)
177-
.doNotValidateCertificate()
204+
.setSslSocketFactory(sslContext.getSocketFactory())
178205
.build();
179206

180-
HttpJsonHeaderCapturingInterceptor interceptor = new HttpJsonHeaderCapturingInterceptor();
207+
HttpJsonCapturingClientInterceptor interceptor = new HttpJsonCapturingClientInterceptor();
181208

182209
InstantiatingHttpJsonChannelProvider transportChannelProvider =
183210
EchoSettings.defaultHttpJsonTransportProviderBuilder()
184211
.setHttpTransport(transport)
185-
.setEndpoint(HTTPJSON_ENDPOINT)
212+
.setEndpoint(DEFAULT_HTTPJSON_ENDPOINT.replace("http://", "https://"))
186213
.setInterceptorProvider(() -> Collections.singletonList(interceptor))
187214
.build();
188215

@@ -198,20 +225,26 @@ void testHttpJsonPqc_withExplicitSecurityProvider() throws Exception {
198225
EchoRequest.newBuilder().setContent("pqc-httpjson-explicit-provider-test").build());
199226
assertThat(response.getContent()).isEqualTo("pqc-httpjson-explicit-provider-test");
200227

201-
HttpJsonMetadata capturedHeaders = interceptor.getCapturedHeaders();
228+
HttpJsonMetadata capturedHeaders = interceptor.metadata;
202229
assertThat(capturedHeaders).isNotNull();
203230

204-
String negotiatedGroup = getSingleHeaderString(capturedHeaders, "x-showcase-tls-group");
205-
assertThat(negotiatedGroup).isEqualTo("X25519MLKEM768");
231+
String negotiatedGroup = getSingleHeaderString(capturedHeaders, TLS_GROUP_HEADER);
232+
// Under SunJSSE (JDK default), PQC curves are unsupported, so it falls back to classical X25519
233+
assertThat(negotiatedGroup).isEqualTo("X25519");
206234

207-
String tlsVersion = getSingleHeaderString(capturedHeaders, "x-showcase-tls-version");
235+
String tlsVersion = getSingleHeaderString(capturedHeaders, TLS_VERSION_HEADER);
208236
assertThat(tlsVersion).isEqualTo("TLS 1.3");
209237

210-
String tlsCipher = getSingleHeaderString(capturedHeaders, "x-showcase-tls-cipher");
238+
String tlsCipher = getSingleHeaderString(capturedHeaders, TLS_CIPHER_HEADER);
211239
assertThat(tlsCipher).isEqualTo("TLS_AES_128_GCM_SHA256");
212240
}
213241
}
214242

243+
/**
244+
* Captures initial TLS response headers (e.g. x-showcase-tls-group) from the gRPC stream.
245+
* This is required because showcase TLS headers are sent as initial headers rather than trailing metadata (trailers),
246+
* which means the shared utility GrpcCapturingClientInterceptor cannot be used (as it only intercepts trailers).
247+
*/
215248
private static class GrpcHeaderCapturingInterceptor implements ClientInterceptor {
216249
private Metadata capturedHeaders;
217250

@@ -241,38 +274,12 @@ public Metadata getCapturedHeaders() {
241274
}
242275
}
243276

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-
277+
/**
278+
* Helper class to wrap a standard ManagedChannel with gRPC client interceptors.
279+
* Since EchoClient requires a ManagedChannel (which handles shutdown and awaitTermination lifecycles),
280+
* but ClientInterceptors.intercept() only returns a generic Channel, this class bridges the two by
281+
* forwarding call creation to the intercepted channel, and routing lifecycle calls to the base channel.
282+
*/
276283
private static class InterceptedManagedChannel extends ManagedChannel {
277284
private final ManagedChannel delegate;
278285
private final Channel intercepted;
@@ -323,8 +330,8 @@ public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedE
323330

324331
private static String getSingleHeaderString(HttpJsonMetadata metadata, String name) {
325332
Object valueObj = metadata.getHeaders().get(name);
326-
if (valueObj instanceof java.util.List) {
327-
java.util.List<?> list = (java.util.List<?>) valueObj;
333+
if (valueObj instanceof List) {
334+
List<?> list = (List<?>) valueObj;
328335
if (!list.isEmpty()) {
329336
return String.valueOf(list.get(0));
330337
}
@@ -333,4 +340,15 @@ private static String getSingleHeaderString(HttpJsonMetadata metadata, String na
333340
}
334341
return null;
335342
}
343+
344+
private static KeyStore loadCaCert(String certPath) throws Exception {
345+
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
346+
trustStore.load(null, null);
347+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
348+
try (InputStream is = Files.newInputStream(Paths.get(certPath))) {
349+
Certificate cert = cf.generateCertificate(is);
350+
trustStore.setCertificateEntry("showcase-ca", cert);
351+
}
352+
return trustStore;
353+
}
336354
}

0 commit comments

Comments
 (0)