Skip to content

Commit cf211ea

Browse files
authored
fix(gax-httpjson): use Conscrypt TrustManagerFactory for mTLS SSLContext (#14556)
> [!IMPORTANT] > **This must merge before the `agentic-identities-bound-token` feature branch is merged to `main`.** Without it, HTTP/JSON clients fail by default wherever mTLS is enabled automatically (see below). ## Problem When mTLS is active, `InstantiatingHttpJsonChannelProvider.configureMtls()` builds a Conscrypt `SSLContext` but initializes it with the JDK (SunJSSE) PKIX `TrustManagerFactory`. On TLS 1.3, Conscrypt passes authType `"GENERIC"` to the trust manager. SunJSSE's end-entity checks reject that authType for server certificates that are CA-issued and carry a KeyUsage extension, which includes Google front ends. So every mTLS HTTP/JSON handshake fails with: ``` javax.net.ssl.SSLHandshakeException: Unknown authType: GENERIC Caused by: java.security.cert.CertificateException: Unknown authType: GENERIC ``` The bug is already on `main`, but there it only triggers when `GOOGLE_API_USE_CLIENT_CERTIFICATE=true` is set explicitly. On this feature branch, #13995 enables mTLS automatically whenever a workload certificate config is present, so on Cloud Run (agent identity) **every HTTP/JSON client fails by default**. gRPC isn't affected. The non-mTLS path isn't affected either, because google-http-client already pairs Conscrypt with a provider-matched trust manager there. A second provider mismatch shows up on JDK 26: the mTLS `SSLContext` also used the JDK's default (`SunX509`) `KeyManagerFactory`. Since JDK 26 ([JDK-8359956](https://bugs.openjdk.org/browse/JDK-8359956)), that key manager applies algorithm constraints to the client certificate. Called from a Conscrypt handshake, it rejects valid certificates (e.g. `SHA256withRSA`), so the client sends an empty certificate chain and the server aborts with `certificate_required`. ## Fix Use Conscrypt's own PKIX `TrustManagerFactory` (`TrustManagerFactory.getInstance("PKIX", conscryptProvider)`) for the mTLS `SSLContext`. I called the JDK API directly rather than `SslUtils.getPkixTrustManagerFactory(Provider)`, which only exists in google-http-client 2.2.0+. Likewise, use Conscrypt's own `KeyManagerFactory` (`KeyManagerFactory.getInstance("PKIX", conscryptProvider)`), so the trust manager and key manager both come from the same provider as the `SSLContext`. Conscrypt's trust manager loads the same default trust store as the JDK. Verified on JDK 21: 174 anchors in both, the same set, and both honor `-Djavax.net.ssl.trustStore` overrides identically. ## Testing - New `InstantiatingHttpJsonChannelProviderTls13Test`: a local JDK TLS 1.3 server presents a CA-issued leaf with KeyUsage and requires a client certificate. The test asserts that the transport completes the request and presents its client certificate. - Without the fix: fails with `Unknown authType: GENERIC`. - With the fix: passes. - Skips when Conscrypt native or TLS 1.3 is unavailable. - Full `gax-httpjson` suite on the current `agentic-identities-bound-token` (with #13995 merged): 194/194 pass on JDK 8, 11, 17, 21, 25 and 26. On JDK 26, the Tls13 test fails without the `KeyManagerFactory` change: the client sends an empty certificate chain and times out. - End-to-end with #13995's certificate-rotation support, on JDK 21 with Conscrypt active: 22 scenarios with real KMS and BigQuery Storage GAPIC clients against local mTLS servers, where the certificate rotates on disk. - With this fix: all 22 pass. That includes HTTP/JSON rotation, in-flight calls during refresh, and stress with `close()`. The refreshed transport still uses Conscrypt's socket factory. - Without this fix: every HTTP/JSON mTLS scenario fails with `Unknown authType: GENERIC`. Limiting the test server to TLS 1.2 makes the error go away, which confirms that the TLS 1.3 authType is the trigger. - Logs: https://paste.googleplex.com/5563956459077632 (with fix), https://paste.googleplex.com/5467233782988800 (without fix, JDK 21 set) - Live on Cloud Run (agent identity), combined #13873 + #13995 build, default env: | | gRPC | HTTP/JSON | |---|---|---| | Before | ✅ | ❌ `Unknown authType: GENERIC` | | After | ✅ | ✅ authenticated over mTLS with a bound token |
1 parent 6f8311e commit cf211ea

5 files changed

Lines changed: 368 additions & 6 deletions

File tree

‎sdk-platform-java/gax-java/gax-httpjson/BUILD.bazel‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ java_library(
5353
srcs = glob(["src/test/java/**/*.java"]),
5454
javacopts = _JAVA_COPTS,
5555
plugins = ["//:auto_value_plugin"],
56+
resources = glob([
57+
"src/test/resources/com/google/api/gax/httpjson/tls13TestCa.pem",
58+
"src/test/resources/com/google/api/gax/httpjson/tls13TestServerCertAndKey.pem",
59+
]),
5660
visibility = ["//visibility:public"],
5761
deps = [":gax_httpjson"] + _COMPILE_DEPS + _TEST_COMPILE_DEPS,
5862
)

‎sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@
5353
import java.util.function.Supplier;
5454
import java.util.logging.Level;
5555
import java.util.logging.Logger;
56+
import javax.net.ssl.KeyManagerFactory;
5657
import javax.net.ssl.SSLContext;
58+
import javax.net.ssl.TrustManagerFactory;
5759
import org.jspecify.annotations.NullMarked;
5860
import org.jspecify.annotations.Nullable;
5961

@@ -218,18 +220,28 @@ private NetHttpTransport.Builder configureMtls(NetHttpTransport.Builder builder)
218220
// Fall back to standard JDK JSSE if Conscrypt provider is unavailable
219221
return builder;
220222
}
221-
// Explicitly initialize SSLContext with the Conscrypt provider so that the client certificate
222-
// key managers
223-
// and trust manager factory (TMF) are bound to Conscrypt's TLS implementation (supporting PQC
224-
// key exchange).
223+
// NetHttpTransport.Builder.trustCertificates() in google-http-client initializes with the
224+
// default SunX509 KeyManagerFactory (via SslUtils.getDefaultKeyManagerFactory()), which is not
225+
// supported by Conscrypt. Explicitly initialize SSLContext with the Conscrypt provider so that
226+
// the client certificate key managers and trust manager factory (TMF) are bound to Conscrypt's
227+
// TLS implementation (supporting PQC key exchange).
225228
SSLContext sslContext = SSLContext.getInstance("TLS", conscryptProvider);
229+
// The TrustManagerFactory must come from the same provider as the SSLContext. On TLS 1.3,
230+
// Conscrypt passes authType "GENERIC" to the trust manager, which the JDK (SunJSSE) PKIX
231+
// trust manager rejects for CA-issued server certificates that carry a KeyUsage extension
232+
// (e.g. Google front ends), failing the handshake with "Unknown authType: GENERIC".
233+
// Conscrypt's trust manager loads the same default trust store as the JDK.
234+
// The KeyManagerFactory must also come from Conscrypt: since JDK 26 (JDK-8359956) the JDK's
235+
// SunX509 key manager applies algorithm constraints to the client certificate, and when called
236+
// from a Conscrypt handshake it rejects valid certificates (e.g. SHA256withRSA), so no client
237+
// certificate is sent.
226238
SslUtils.initSslContext(
227239
sslContext,
228240
null,
229-
SslUtils.getPkixTrustManagerFactory(),
241+
TrustManagerFactory.getInstance("PKIX", conscryptProvider),
230242
mtlsKeyStore,
231243
"",
232-
SslUtils.getDefaultKeyManagerFactory());
244+
KeyManagerFactory.getInstance("PKIX", conscryptProvider));
233245
builder.setSslSocketFactory(sslContext.getSocketFactory());
234246
return builder;
235247
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.gax.httpjson;
31+
32+
import static com.google.common.truth.Truth.assertThat;
33+
34+
import com.google.api.client.http.GenericUrl;
35+
import com.google.api.client.http.HttpResponse;
36+
import com.google.api.client.http.HttpTransport;
37+
import com.google.api.client.util.SecurityUtils;
38+
import com.google.api.gax.rpc.mtls.CertificateBasedAccess;
39+
import com.google.api.gax.rpc.testing.FakeMtlsProvider;
40+
import java.io.BufferedReader;
41+
import java.io.File;
42+
import java.io.FileOutputStream;
43+
import java.io.InputStream;
44+
import java.io.InputStreamReader;
45+
import java.io.OutputStream;
46+
import java.net.InetAddress;
47+
import java.nio.charset.StandardCharsets;
48+
import java.security.KeyStore;
49+
import java.security.cert.CertificateFactory;
50+
import java.security.cert.X509Certificate;
51+
import java.util.concurrent.ExecutorService;
52+
import java.util.concurrent.Executors;
53+
import java.util.concurrent.Future;
54+
import java.util.concurrent.TimeUnit;
55+
import javax.net.ssl.KeyManagerFactory;
56+
import javax.net.ssl.SSLContext;
57+
import javax.net.ssl.SSLServerSocket;
58+
import javax.net.ssl.SSLSocket;
59+
import javax.net.ssl.TrustManager;
60+
import javax.net.ssl.X509TrustManager;
61+
import org.junit.jupiter.api.AfterEach;
62+
import org.junit.jupiter.api.BeforeEach;
63+
import org.junit.jupiter.api.Test;
64+
import org.junit.jupiter.api.condition.EnabledIf;
65+
import org.junit.jupiter.api.io.TempDir;
66+
import org.mockito.Mockito;
67+
68+
/**
69+
* Handshake test for the Conscrypt-backed mTLS {@link HttpTransport} built by {@link
70+
* InstantiatingHttpJsonChannelProvider}.
71+
*
72+
* <p>Regression test for "Unknown authType: GENERIC": on TLS 1.3, Conscrypt passes the authType
73+
* {@code "GENERIC"} to its trust manager. If the trust manager comes from the JDK (SunJSSE) rather
74+
* than Conscrypt, it rejects server certificates that are issued by a trusted CA and carry a
75+
* KeyUsage extension (as Google front ends' certificates do), and every mTLS HTTP/JSON handshake
76+
* fails. A self-signed server certificate that is itself the trust anchor does not reproduce the
77+
* issue, so the server here presents a CA-issued leaf with KeyUsage.
78+
*
79+
* <p>The transport always uses the default trust store, so this test points {@code
80+
* javax.net.ssl.trustStore} at a temporary store containing only the test CA, and restores the
81+
* previous value afterwards.
82+
*/
83+
@EnabledIf(
84+
value = "isConscryptAndTls13Available",
85+
disabledReason = "Conscrypt native library or TLS 1.3 is unavailable on this platform")
86+
class InstantiatingHttpJsonChannelProviderTls13Test {
87+
88+
private static final String RESOURCE_DIR = "com/google/api/gax/httpjson/";
89+
private static final String TRUST_STORE_PROPERTY = "javax.net.ssl.trustStore";
90+
private static final String TRUST_STORE_PASSWORD_PROPERTY = "javax.net.ssl.trustStorePassword";
91+
private static final String TRUST_STORE_PASSWORD = "changeit";
92+
93+
@TempDir File tempDir;
94+
95+
private String previousTrustStore;
96+
private String previousTrustStorePassword;
97+
private ExecutorService serverExecutor;
98+
99+
/** Condition for {@link EnabledIf}; must be static because it is used at class level. */
100+
static boolean isConscryptAndTls13Available() {
101+
return HttpJsonConscryptUtils.getConscryptProvider() != null && isTls13Supported();
102+
}
103+
104+
@BeforeEach
105+
void setUp() throws Exception {
106+
previousTrustStore = System.getProperty(TRUST_STORE_PROPERTY);
107+
previousTrustStorePassword = System.getProperty(TRUST_STORE_PASSWORD_PROPERTY);
108+
File trustStoreFile = writeTrustStoreWithTestCa();
109+
System.setProperty(TRUST_STORE_PROPERTY, trustStoreFile.getAbsolutePath());
110+
System.setProperty(TRUST_STORE_PASSWORD_PROPERTY, TRUST_STORE_PASSWORD);
111+
112+
serverExecutor = Executors.newSingleThreadExecutor();
113+
}
114+
115+
@AfterEach
116+
void tearDown() {
117+
restoreProperty(TRUST_STORE_PROPERTY, previousTrustStore);
118+
restoreProperty(TRUST_STORE_PASSWORD_PROPERTY, previousTrustStorePassword);
119+
if (serverExecutor != null) {
120+
serverExecutor.shutdownNow();
121+
}
122+
}
123+
124+
@Test
125+
void createHttpTransport_withMtlsAndConscrypt_completesTls13HandshakeWithCaIssuedServerCert()
126+
throws Exception {
127+
CertificateBasedAccess certificateBasedAccess = Mockito.mock(CertificateBasedAccess.class);
128+
Mockito.when(certificateBasedAccess.useMtlsClientCertificate()).thenReturn(true);
129+
InstantiatingHttpJsonChannelProvider channelProvider =
130+
InstantiatingHttpJsonChannelProvider.newBuilder()
131+
.setEndpoint("localhost:443")
132+
.setMtlsProvider(
133+
new FakeMtlsProvider(FakeMtlsProvider.createTestMtlsKeyStore(), "", false))
134+
.setCertificateBasedAccess(certificateBasedAccess)
135+
.build();
136+
HttpTransport transport = channelProvider.createHttpTransport();
137+
assertThat(transport).isNotNull();
138+
139+
final SSLServerSocket serverSocket = createTls13ServerSocket();
140+
try {
141+
Future<Boolean> clientCertificatePresented =
142+
serverExecutor.submit(() -> serveSingleRequest(serverSocket));
143+
144+
HttpResponse response =
145+
transport
146+
.createRequestFactory()
147+
.buildGetRequest(
148+
new GenericUrl("https://localhost:" + serverSocket.getLocalPort() + "/"))
149+
.execute();
150+
try {
151+
assertThat(response.getStatusCode()).isEqualTo(200);
152+
assertThat(response.parseAsString()).isEqualTo("ok");
153+
} finally {
154+
response.disconnect();
155+
}
156+
// execute() is synchronous so the server task is already done; timeout guards against hangs.
157+
assertThat(clientCertificatePresented.get(10, TimeUnit.SECONDS)).isTrue();
158+
} finally {
159+
serverSocket.close();
160+
}
161+
}
162+
163+
/** Accepts one connection, answers one HTTP request, and reports if a client cert was sent. */
164+
private static boolean serveSingleRequest(SSLServerSocket serverSocket) throws Exception {
165+
SSLSocket socket = (SSLSocket) serverSocket.accept();
166+
try {
167+
socket.startHandshake();
168+
boolean clientCertificatePresented = socket.getSession().getPeerCertificates().length > 0;
169+
BufferedReader reader =
170+
new BufferedReader(
171+
new InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII));
172+
String line;
173+
while ((line = reader.readLine()) != null && !line.isEmpty()) {
174+
// Drain request headers.
175+
}
176+
OutputStream out = socket.getOutputStream();
177+
out.write(
178+
("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
179+
.getBytes(StandardCharsets.US_ASCII));
180+
out.flush();
181+
return clientCertificatePresented;
182+
} finally {
183+
socket.close();
184+
}
185+
}
186+
187+
/**
188+
* Creates a JDK TLS 1.3 server that presents a CA-issued leaf certificate with KeyUsage and
189+
* requires (but does not validate) a client certificate.
190+
*/
191+
private static SSLServerSocket createTls13ServerSocket() throws Exception {
192+
KeyStore serverKeyStore;
193+
InputStream certAndKey = openResource("tls13TestServerCertAndKey.pem");
194+
try {
195+
serverKeyStore = SecurityUtils.createMtlsKeyStore(certAndKey);
196+
} finally {
197+
certAndKey.close();
198+
}
199+
KeyManagerFactory keyManagerFactory =
200+
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
201+
keyManagerFactory.init(serverKeyStore, new char[0]);
202+
203+
SSLContext serverContext = SSLContext.getInstance("TLSv1.3");
204+
serverContext.init(
205+
keyManagerFactory.getKeyManagers(), new TrustManager[] {new AcceptAllTrustManager()}, null);
206+
SSLServerSocket serverSocket =
207+
(SSLServerSocket)
208+
serverContext
209+
.getServerSocketFactory()
210+
.createServerSocket(0, 1, InetAddress.getLoopbackAddress());
211+
serverSocket.setEnabledProtocols(new String[] {"TLSv1.3"});
212+
serverSocket.setNeedClientAuth(true);
213+
return serverSocket;
214+
}
215+
216+
private File writeTrustStoreWithTestCa() throws Exception {
217+
X509Certificate caCertificate;
218+
InputStream caPem = openResource("tls13TestCa.pem");
219+
try {
220+
caCertificate =
221+
(X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(caPem);
222+
} finally {
223+
caPem.close();
224+
}
225+
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
226+
trustStore.load(null, null);
227+
trustStore.setCertificateEntry("gax-test-ca", caCertificate);
228+
File trustStoreFile = new File(tempDir, "truststore");
229+
OutputStream out = new FileOutputStream(trustStoreFile);
230+
try {
231+
trustStore.store(out, TRUST_STORE_PASSWORD.toCharArray());
232+
} finally {
233+
out.close();
234+
}
235+
return trustStoreFile;
236+
}
237+
238+
private static InputStream openResource(String name) {
239+
InputStream stream =
240+
InstantiatingHttpJsonChannelProviderTls13Test.class
241+
.getClassLoader()
242+
.getResourceAsStream(RESOURCE_DIR + name);
243+
if (stream == null) {
244+
throw new IllegalStateException("Missing test resource: " + RESOURCE_DIR + name);
245+
}
246+
return stream;
247+
}
248+
249+
private static boolean isTls13Supported() {
250+
try {
251+
SSLContext.getInstance("TLSv1.3");
252+
return true;
253+
} catch (Exception e) {
254+
return false;
255+
}
256+
}
257+
258+
private static void restoreProperty(String key, String value) {
259+
if (value == null) {
260+
System.clearProperty(key);
261+
} else {
262+
System.setProperty(key, value);
263+
}
264+
}
265+
266+
/** Server-side trust manager: the test only checks that a client certificate is presented. */
267+
private static final class AcceptAllTrustManager implements X509TrustManager {
268+
@Override
269+
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
270+
271+
@Override
272+
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
273+
274+
@Override
275+
public X509Certificate[] getAcceptedIssuers() {
276+
return new X509Certificate[0];
277+
}
278+
}
279+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDHzCCAgegAwIBAgIUZEyXXbTT3KXPPzN6Z6b30/GQ26wwDQYJKoZIhvcNAQEL
3+
BQAwFjEUMBIGA1UEAwwLR0FYIFRlc3QgQ0EwIBcNMjYwOTMwMDMzNzM5WhgPMjEy
4+
NjA5MDYwMzM3MzlaMBYxFDASBgNVBAMMC0dBWCBUZXN0IENBMIIBIjANBgkqhkiG
5+
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkLx+Kh78x1ktPpvGf3ITPS7xwr7rU6Pt/Z34
6+
AKodCSBCrt/d3BhkGiy8w3IqWN7mIy2ujdoKeUau4NJiYdz3A16XZ1nhvp7J3tK5
7+
a6SSxO95lVwy70nafTO8ApFYKawKLKNWvGqChM95AI6+Q7dTzzGArpwJxCK0p7dp
8+
4bKjhpSOZaW46VdpCsLwoPJmXNlDpAL5O3nrvdnfilHVCqWHoAZk7ESwQcwm8vP6
9+
5suLUSvpagPfMRabFTEv91PsmVhB8KjSu8/kGPAC1F7RLyZK31a0Kb5wNwW+69cb
10+
kxjUEhNux7/SHTF250kF2vQ+nFrnUQZSGM8cHU8qpeaOsTFDRwIDAQABo2MwYTAd
11+
BgNVHQ4EFgQU7pHELECvL5hdK8Y3N1x6GZSzSjkwHwYDVR0jBBgwFoAU7pHELECv
12+
L5hdK8Y3N1x6GZSzSjkwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw
13+
DQYJKoZIhvcNAQELBQADggEBACCyzuu0K4HF0iMu1rj+adlpYUzl6uwkKBuL6fRJ
14+
sTxe0ftgWWcFXJV+P6T+maGu7DMANqbADpWqQbAHMNbSXxPSfGgOd2tS+jg/OOef
15+
go1MhsMUpgxSN9PvWAfhiVIhgF7pdjdVP2qiObx7F/Qo/Xmr0MEy61mmeCbWENBj
16+
jC7DsCo5SWo6kMjvb6dz5G+f+4LElQKCQbBGAOm00XlERE/W7WfUMBzNFhLCHrVs
17+
CRq5lIGxek8DEIC8oXtt/7g0KoKVkOCNj2ZXljEQgi4awoYHqlTRXw/nlctJJ8xs
18+
DLmvfUObHIo66rliDG1Pco/Ce9FVewUJz2WQXJK/mGuuesA=
19+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)