From 6722f91a1d1b60c74cb8753fb8d6f71153643912 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Thu, 11 Jun 2026 17:17:48 +0000 Subject: [PATCH 1/8] feat(bigquery-jdbc): respect JVM trustStore properties and default to JVM cacerts with google.p12 fallback --- .../jdbc/BigQueryJdbcProxyUtility.java | 127 ++++++++++++++++-- .../jdbc/BigQueryJdbcProxyUtilityTest.java | 10 +- 2 files changed, 123 insertions(+), 14 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java index 7c495e801537..82444753847d 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java @@ -18,8 +18,10 @@ import static com.google.cloud.bigquery.storage.v1.stub.BigQueryReadStubSettings.defaultGrpcTransportProviderBuilder; +import com.google.api.client.googleapis.GoogleUtils; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.apache.v5.Apache5HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.gax.rpc.TransportChannelProvider; import com.google.auth.http.HttpTransportFactory; import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException; @@ -35,11 +37,15 @@ import java.net.SocketAddress; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy; @@ -136,17 +142,27 @@ static HttpTransportOptions getHttpTransportOptions( boolean hasProxyOrSsl = proxyProperties.containsKey(BigQueryJdbcUrlUtility.PROXY_HOST_PROPERTY_NAME) || sslTrustStorePath != null; - boolean hasTimeoutConfig = connectTimeout != null || readTimeout != null; - - if (!hasProxyOrSsl && !hasTimeoutConfig) { - return null; - } HttpTransportOptions.Builder httpTransportOptionsBuilder = HttpTransportOptions.newBuilder(); if (hasProxyOrSsl) { httpTransportOptionsBuilder.setHttpTransportFactory( getHttpTransportFactory( proxyProperties, sslTrustStorePath, sslTrustStorePassword, callerClassName)); + } else { + // Default to NetHttpTransport configured with a MergedTrustManager that trusts + // both the JVM's default trust store and Google's bundled certificate store. + httpTransportOptionsBuilder.setHttpTransportFactory( + () -> { + try { + SSLContext sslContext = createMergedSslContext(); + return new NetHttpTransport.Builder() + .setSslSocketFactory(sslContext.getSocketFactory()) + .build(); + } catch (GeneralSecurityException | IOException e) { + throw new BigQueryJdbcRuntimeException( + "Failed to configure SSL for HTTP transport", e); + } + }); } if (connectTimeout != null) { @@ -196,15 +212,19 @@ private static HttpTransportFactory getHttpTransportFactory( SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext); - httpClientBuilder.setConnectionManager( - PoolingHttpClientConnectionManagerBuilder.create() - .setSSLSocketFactory(sslSocketFactory) - .build()); + setSslSocketFactory(httpClientBuilder, sslContext); } catch (IOException | GeneralSecurityException e) { throw new BigQueryJdbcRuntimeException( "Failed to configure SSL TrustStore for HTTP transport", e); } + } else { + // Default to MergedTrustManager when no custom SSLTrustStore is specified, ensuring standard + // JVM properties (like javax.net.ssl.trustStore) and google.p12 fallback are respected. + try { + setSslSocketFactory(httpClientBuilder, createMergedSslContext()); + } catch (IOException | GeneralSecurityException e) { + throw new BigQueryJdbcRuntimeException("Failed to configure SSL for HTTP transport", e); + } } addAuthToProxyIfPresent(proxyProperties, httpClientBuilder, callerClassName); @@ -318,4 +338,91 @@ private static HttpConnectProxiedSocketAddress getHttpConnectProxiedSocketAddres } return builder.build(); } + + private static SSLContext createMergedSslContext() throws GeneralSecurityException, IOException { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] {createMergedTrustManager()}, null); + return sslContext; + } + + private static void setSslSocketFactory( + HttpClientBuilder httpClientBuilder, SSLContext sslContext) { + SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext); + httpClientBuilder.setConnectionManager( + PoolingHttpClientConnectionManagerBuilder.create() + .setSSLSocketFactory(sslSocketFactory) + .build()); + } + + private static X509TrustManager createMergedTrustManager() + throws GeneralSecurityException, IOException { + // 1. Get default JVM TrustManager + TrustManagerFactory defaultTmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + defaultTmf.init((KeyStore) null); + X509TrustManager defaultTm = findX509TrustManager(defaultTmf); + + // 2. Get Google TrustManager + KeyStore googleKeystore = GoogleUtils.getCertificateTrustStore(); + TrustManagerFactory googleTmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + googleTmf.init(googleKeystore); + X509TrustManager googleTm = findX509TrustManager(googleTmf); + + if (defaultTm == null || googleTm == null) { + throw new IllegalStateException("Could not find X509TrustManager"); + } + + return new MergedTrustManager(defaultTm, googleTm); + } + + private static X509TrustManager findX509TrustManager(TrustManagerFactory tmf) { + for (TrustManager tm : tmf.getTrustManagers()) { + if (tm instanceof X509TrustManager) { + return (X509TrustManager) tm; + } + } + return null; + } + + private static class MergedTrustManager implements X509TrustManager { + private final X509TrustManager defaultTm; + private final X509TrustManager googleTm; + + public MergedTrustManager(X509TrustManager defaultTm, X509TrustManager googleTm) { + this.defaultTm = defaultTm; + this.googleTm = googleTm; + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + X509Certificate[] defaultIssuers = defaultTm.getAcceptedIssuers(); + X509Certificate[] googleIssuers = googleTm.getAcceptedIssuers(); + X509Certificate[] result = new X509Certificate[defaultIssuers.length + googleIssuers.length]; + System.arraycopy(defaultIssuers, 0, result, 0, defaultIssuers.length); + System.arraycopy(googleIssuers, 0, result, defaultIssuers.length, googleIssuers.length); + return result; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + try { + defaultTm.checkClientTrusted(chain, authType); + } catch (CertificateException e) { + googleTm.checkClientTrusted(chain, authType); + } + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + try { + defaultTm.checkServerTrusted(chain, authType); + } catch (CertificateException e) { + // Fall back to Google's trusted certs + googleTm.checkServerTrusted(chain, authType); + } + } + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtilityTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtilityTest.java index ea62166e0112..c8e613f08941 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtilityTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtilityTest.java @@ -161,7 +161,7 @@ public void testGetHttpTransportOptionsWithNonAuthenticatedProxy() { } @Test - public void testGetHttpTransportOptionsWithNoProxySettingsReturnsNull() { + public void testGetHttpTransportOptionsWithNoProxySettingsReturnsDefaultOptions() { String connection_uri = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" + "ProjectId=TestProject" @@ -172,7 +172,8 @@ public void testGetHttpTransportOptionsWithNoProxySettingsReturnsNull() { HttpTransportOptions result = BigQueryJdbcProxyUtility.getHttpTransportOptions( proxyProperties, null, null, null, null, "TestClass"); - assertNull(result); + assertNotNull(result); + assertNotNull(result.getHttpTransportFactory()); } private String getTestResourcePath(String resourceName) throws URISyntaxException { @@ -299,11 +300,12 @@ public void testGetTransportChannelProvider_noProxyNoSsl_returnsNull() { } @Test - public void testGetHttpTransportOptions_noProxyNoSsl_returnsNull() { + public void testGetHttpTransportOptions_noProxyNoSsl_returnsDefaultOptions() { HttpTransportOptions options = BigQueryJdbcProxyUtility.getHttpTransportOptions( Collections.emptyMap(), null, null, null, null, "TestClass"); - assertNull(options); + assertNotNull(options); + assertNotNull(options.getHttpTransportFactory()); } @Test From 54b2c0ca2296feeeccf7da8d5d1a763235ee8ea1 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Thu, 11 Jun 2026 17:46:59 +0000 Subject: [PATCH 2/8] simplify logic --- .../jdbc/BigQueryJdbcProxyUtility.java | 126 ++---------------- 1 file changed, 9 insertions(+), 117 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java index 82444753847d..71295f8beb47 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java @@ -18,7 +18,6 @@ import static com.google.cloud.bigquery.storage.v1.stub.BigQueryReadStubSettings.defaultGrpcTransportProviderBuilder; -import com.google.api.client.googleapis.GoogleUtils; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.apache.v5.Apache5HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; @@ -37,15 +36,11 @@ import java.net.SocketAddress; import java.security.GeneralSecurityException; import java.security.KeyStore; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy; @@ -149,20 +144,10 @@ static HttpTransportOptions getHttpTransportOptions( getHttpTransportFactory( proxyProperties, sslTrustStorePath, sslTrustStorePassword, callerClassName)); } else { - // Default to NetHttpTransport configured with a MergedTrustManager that trusts - // both the JVM's default trust store and Google's bundled certificate store. + // Default to NetHttpTransport which automatically respects the JVM's default trust store + // (cacerts or javax.net.ssl.trustStore). httpTransportOptionsBuilder.setHttpTransportFactory( - () -> { - try { - SSLContext sslContext = createMergedSslContext(); - return new NetHttpTransport.Builder() - .setSslSocketFactory(sslContext.getSocketFactory()) - .build(); - } catch (GeneralSecurityException | IOException e) { - throw new BigQueryJdbcRuntimeException( - "Failed to configure SSL for HTTP transport", e); - } - }); + () -> new NetHttpTransport.Builder().build()); } if (connectTimeout != null) { @@ -193,10 +178,8 @@ private static HttpTransportFactory getHttpTransportFactory( proxyProperties.get(BigQueryJdbcUrlUtility.PROXY_PORT_PROPERTY_NAME))); HttpRoutePlanner httpRoutePlanner = new DefaultProxyRoutePlanner(proxyHostDetails); httpClientBuilder.setRoutePlanner(httpRoutePlanner); - addAuthToProxyIfPresent(proxyProperties, httpClientBuilder, callerClassName); - } else { - httpClientBuilder.useSystemProperties(); } + httpClientBuilder.useSystemProperties(); if (sslTrustStorePath != null) { try (FileInputStream trustStoreStream = new FileInputStream(sslTrustStorePath)) { @@ -212,19 +195,15 @@ private static HttpTransportFactory getHttpTransportFactory( SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - setSslSocketFactory(httpClientBuilder, sslContext); + SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext); + httpClientBuilder.setConnectionManager( + PoolingHttpClientConnectionManagerBuilder.create() + .setSSLSocketFactory(sslSocketFactory) + .build()); } catch (IOException | GeneralSecurityException e) { throw new BigQueryJdbcRuntimeException( "Failed to configure SSL TrustStore for HTTP transport", e); } - } else { - // Default to MergedTrustManager when no custom SSLTrustStore is specified, ensuring standard - // JVM properties (like javax.net.ssl.trustStore) and google.p12 fallback are respected. - try { - setSslSocketFactory(httpClientBuilder, createMergedSslContext()); - } catch (IOException | GeneralSecurityException e) { - throw new BigQueryJdbcRuntimeException("Failed to configure SSL for HTTP transport", e); - } } addAuthToProxyIfPresent(proxyProperties, httpClientBuilder, callerClassName); @@ -338,91 +317,4 @@ private static HttpConnectProxiedSocketAddress getHttpConnectProxiedSocketAddres } return builder.build(); } - - private static SSLContext createMergedSslContext() throws GeneralSecurityException, IOException { - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, new TrustManager[] {createMergedTrustManager()}, null); - return sslContext; - } - - private static void setSslSocketFactory( - HttpClientBuilder httpClientBuilder, SSLContext sslContext) { - SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext); - httpClientBuilder.setConnectionManager( - PoolingHttpClientConnectionManagerBuilder.create() - .setSSLSocketFactory(sslSocketFactory) - .build()); - } - - private static X509TrustManager createMergedTrustManager() - throws GeneralSecurityException, IOException { - // 1. Get default JVM TrustManager - TrustManagerFactory defaultTmf = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - defaultTmf.init((KeyStore) null); - X509TrustManager defaultTm = findX509TrustManager(defaultTmf); - - // 2. Get Google TrustManager - KeyStore googleKeystore = GoogleUtils.getCertificateTrustStore(); - TrustManagerFactory googleTmf = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - googleTmf.init(googleKeystore); - X509TrustManager googleTm = findX509TrustManager(googleTmf); - - if (defaultTm == null || googleTm == null) { - throw new IllegalStateException("Could not find X509TrustManager"); - } - - return new MergedTrustManager(defaultTm, googleTm); - } - - private static X509TrustManager findX509TrustManager(TrustManagerFactory tmf) { - for (TrustManager tm : tmf.getTrustManagers()) { - if (tm instanceof X509TrustManager) { - return (X509TrustManager) tm; - } - } - return null; - } - - private static class MergedTrustManager implements X509TrustManager { - private final X509TrustManager defaultTm; - private final X509TrustManager googleTm; - - public MergedTrustManager(X509TrustManager defaultTm, X509TrustManager googleTm) { - this.defaultTm = defaultTm; - this.googleTm = googleTm; - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - X509Certificate[] defaultIssuers = defaultTm.getAcceptedIssuers(); - X509Certificate[] googleIssuers = googleTm.getAcceptedIssuers(); - X509Certificate[] result = new X509Certificate[defaultIssuers.length + googleIssuers.length]; - System.arraycopy(defaultIssuers, 0, result, 0, defaultIssuers.length); - System.arraycopy(googleIssuers, 0, result, defaultIssuers.length, googleIssuers.length); - return result; - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - try { - defaultTm.checkClientTrusted(chain, authType); - } catch (CertificateException e) { - googleTm.checkClientTrusted(chain, authType); - } - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - try { - defaultTm.checkServerTrusted(chain, authType); - } catch (CertificateException e) { - // Fall back to Google's trusted certs - googleTm.checkServerTrusted(chain, authType); - } - } - } } From ce6f7fc84821d4ca626631b1a3494503b0d4e4a4 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Thu, 11 Jun 2026 17:51:48 +0000 Subject: [PATCH 3/8] revert changes --- .../cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java index 71295f8beb47..c9b3b321578b 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java @@ -144,8 +144,6 @@ static HttpTransportOptions getHttpTransportOptions( getHttpTransportFactory( proxyProperties, sslTrustStorePath, sslTrustStorePassword, callerClassName)); } else { - // Default to NetHttpTransport which automatically respects the JVM's default trust store - // (cacerts or javax.net.ssl.trustStore). httpTransportOptionsBuilder.setHttpTransportFactory( () -> new NetHttpTransport.Builder().build()); } @@ -178,8 +176,10 @@ private static HttpTransportFactory getHttpTransportFactory( proxyProperties.get(BigQueryJdbcUrlUtility.PROXY_PORT_PROPERTY_NAME))); HttpRoutePlanner httpRoutePlanner = new DefaultProxyRoutePlanner(proxyHostDetails); httpClientBuilder.setRoutePlanner(httpRoutePlanner); + addAuthToProxyIfPresent(proxyProperties, httpClientBuilder, callerClassName); + } else { + httpClientBuilder.useSystemProperties(); } - httpClientBuilder.useSystemProperties(); if (sslTrustStorePath != null) { try (FileInputStream trustStoreStream = new FileInputStream(sslTrustStorePath)) { From 1de12aafc410a73b99063054d7386def4bf63c4b Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Thu, 11 Jun 2026 18:23:52 +0000 Subject: [PATCH 4/8] address pr review --- .../cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java index c9b3b321578b..ac67c213231f 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java @@ -144,8 +144,8 @@ static HttpTransportOptions getHttpTransportOptions( getHttpTransportFactory( proxyProperties, sslTrustStorePath, sslTrustStorePassword, callerClassName)); } else { - httpTransportOptionsBuilder.setHttpTransportFactory( - () -> new NetHttpTransport.Builder().build()); + final HttpTransport defaultTransport = new NetHttpTransport.Builder().build(); + httpTransportOptionsBuilder.setHttpTransportFactory(() -> defaultTransport); } if (connectTimeout != null) { @@ -177,9 +177,8 @@ private static HttpTransportFactory getHttpTransportFactory( HttpRoutePlanner httpRoutePlanner = new DefaultProxyRoutePlanner(proxyHostDetails); httpClientBuilder.setRoutePlanner(httpRoutePlanner); addAuthToProxyIfPresent(proxyProperties, httpClientBuilder, callerClassName); - } else { - httpClientBuilder.useSystemProperties(); } + httpClientBuilder.useSystemProperties(); if (sslTrustStorePath != null) { try (FileInputStream trustStoreStream = new FileInputStream(sslTrustStorePath)) { From 0f696a375b32aa29123448da2d6187e7e4239742 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Fri, 12 Jun 2026 12:24:10 +0000 Subject: [PATCH 5/8] remove `final` --- .../google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java index ac67c213231f..496c91a7efa4 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java @@ -144,7 +144,7 @@ static HttpTransportOptions getHttpTransportOptions( getHttpTransportFactory( proxyProperties, sslTrustStorePath, sslTrustStorePassword, callerClassName)); } else { - final HttpTransport defaultTransport = new NetHttpTransport.Builder().build(); + HttpTransport defaultTransport = new NetHttpTransport.Builder().build(); httpTransportOptionsBuilder.setHttpTransportFactory(() -> defaultTransport); } From 16713a8c6dee0bf04e8df73181f4a5e9c88c9a65 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Fri, 12 Jun 2026 17:04:06 +0000 Subject: [PATCH 6/8] add integration test --- .../jdbc/BigQueryJdbcProxyUtility.java | 4 +- .../jdbc/it/ITLocalSslValidationTest.java | 253 ++++++++++++++++++ .../jdbc/it/suites/ITPresubmitTests.java | 2 + .../src/test/resources/localhost-keystore.jks | Bin 0 -> 2215 bytes .../test/resources/localhost-truststore.jks | Bin 0 -> 927 bytes 5 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITLocalSslValidationTest.java create mode 100644 java-bigquery-jdbc/src/test/resources/localhost-keystore.jks create mode 100644 java-bigquery-jdbc/src/test/resources/localhost-truststore.jks diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java index 496c91a7efa4..983eda9760f8 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java @@ -59,6 +59,7 @@ final class BigQueryJdbcProxyUtility { new BigQueryJdbcCustomLogger(BigQueryJdbcProxyUtility.class.getName()); static final String validPortRegex = "^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"; + private static final HttpTransport DEFAULT_TRANSPORT = new NetHttpTransport.Builder().build(); private BigQueryJdbcProxyUtility() {} @@ -144,8 +145,7 @@ static HttpTransportOptions getHttpTransportOptions( getHttpTransportFactory( proxyProperties, sslTrustStorePath, sslTrustStorePassword, callerClassName)); } else { - HttpTransport defaultTransport = new NetHttpTransport.Builder().build(); - httpTransportOptionsBuilder.setHttpTransportFactory(() -> defaultTransport); + httpTransportOptionsBuilder.setHttpTransportFactory(() -> DEFAULT_TRANSPORT); } if (connectTimeout != null) { diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITLocalSslValidationTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITLocalSslValidationTest.java new file mode 100644 index 000000000000..df4b7e8c25c1 --- /dev/null +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITLocalSslValidationTest.java @@ -0,0 +1,253 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.bigquery.jdbc.it; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.cloud.bigquery.jdbc.utils.URIBuilder; +import com.google.common.io.CharStreams; +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsParameters; +import com.sun.net.httpserver.HttpsServer; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ITLocalSslValidationTest { + private static final String HOST = "localhost"; + private static final String PASSWORD = "changeit"; + private static final String KEYSTORE_RESOURCE = "/localhost-keystore.jks"; + private static final String TRUSTSTORE_PATH = "src/test/resources/localhost-truststore.jks"; + private static final String SUCCESS_MARKER = "SUBPROCESS_RESULT: SUCCESS"; + private static final String FAILURE_MARKER_PREFIX = "SUBPROCESS_RESULT: FAILURE - "; + private static final String PKIX_ERROR_MSG = "PKIX path building failed"; + + private static MockHttpsServer mockServer; + private static int port; + + public static class MockHttpsServer { + private final HttpsServer server; + + public MockHttpsServer(int port) throws Exception { + server = HttpsServer.create(new InetSocketAddress(HOST, port), 0); + SSLContext sslContext = SSLContext.getInstance("TLS"); + + KeyStore ks = KeyStore.getInstance("JKS"); + try (InputStream stream = getClass().getResourceAsStream(KEYSTORE_RESOURCE)) { + if (stream == null) { + throw new IllegalStateException( + "Keystore resource " + KEYSTORE_RESOURCE + " not found on classpath!"); + } + ks.load(stream, PASSWORD.toCharArray()); + } + + KeyManagerFactory kmf = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, PASSWORD.toCharArray()); + + sslContext.init(kmf.getKeyManagers(), null, null); + + server.setHttpsConfigurator( + new HttpsConfigurator(sslContext) { + @Override + public void configure(HttpsParameters params) { + try { + SSLContext context = getSSLContext(); + SSLParameters sslParams = context.getDefaultSSLParameters(); + params.setSSLParameters(sslParams); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + }); + + server.createContext( + "/", + exchange -> { + String path = exchange.getRequestURI().getPath(); + String response; + if (path.contains("/queries")) { + response = + "{\n" + + " \"kind\": \"bigquery#queryResponse\",\n" + + " \"jobComplete\": true,\n" + + " \"rows\": [],\n" + + " \"totalRows\": \"0\",\n" + + " \"schema\": {\n" + + " \"fields\": []\n" + + " }\n" + + "}"; + } else { + response = + "{\n" + + " \"kind\": \"bigquery#job\",\n" + + " \"status\": {\n" + + " \"state\": \"DONE\"\n" + + " },\n" + + " \"jobReference\": {\n" + + " \"projectId\": \"dummy\",\n" + + " \"jobId\": \"dummy-job\"\n" + + " },\n" + + " \"configuration\": {\n" + + " \"query\": {\n" + + " \"query\": \"SELECT 1\"\n" + + " }\n" + + " },\n" + + " \"statistics\": {\n" + + " \"query\": {\n" + + " \"statementType\": \"SELECT\"\n" + + " }\n" + + " }\n" + + "}"; + } + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, response.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes()); + } + }); + } + + public void start() { + server.start(); + } + + public void stop() { + server.stop(0); + } + + public int getPort() { + return server.getAddress().getPort(); + } + } + + private static class ProcessResult { + final int exitCode; + final String stdout; + + ProcessResult(int exitCode, String stdout) { + this.exitCode = exitCode; + this.stdout = stdout; + } + } + + @BeforeAll + public static void setUp() throws Exception { + mockServer = new MockHttpsServer(0); + mockServer.start(); + port = mockServer.getPort(); + } + + @AfterAll + public static void tearDown() { + if (mockServer == null) { + return; + } + mockServer.stop(); + } + + private ProcessResult runSubprocess(String trustStore, String password) throws Exception { + String javaHome = System.getProperty("java.home"); + String javaBin = javaHome + File.separator + "bin" + File.separator + "java"; + String classpath = System.getProperty("java.class.path"); + String className = ITLocalSslValidationTest.class.getCanonicalName(); + + List command = new ArrayList<>(); + command.add(javaBin); + if (trustStore != null) { + command.add("-Djavax.net.ssl.trustStore=" + trustStore); + } + if (password != null) { + command.add("-Djavax.net.ssl.trustStorePassword=" + password); + } + command.add("-cp"); + command.add(classpath); + command.add(className); + command.add(String.valueOf(port)); + + ProcessBuilder builder = new ProcessBuilder(command); + builder.redirectErrorStream(true); + Process process = builder.start(); + + String output; + try (InputStreamReader reader = + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) { + output = CharStreams.toString(reader); + } + + int exitCode = process.waitFor(); + return new ProcessResult(exitCode, output); + } + + @Test + public void testDefaultSslFailsForSelfSigned() throws Exception { + ProcessResult result = runSubprocess(null, null); + assertEquals(1, result.exitCode, "Subprocess should fail. Output:\n" + result.stdout); + assertTrue(result.stdout.contains(PKIX_ERROR_MSG)); + } + + @Test + public void testCustomTrustStoreSucceeds() throws Exception { + String trustStorePath = new File(TRUSTSTORE_PATH).getAbsolutePath(); + ProcessResult result = runSubprocess(trustStorePath, PASSWORD); + + assertEquals(0, result.exitCode, "Subprocess failed. Output:\n" + result.stdout); + assertTrue(result.stdout.contains(SUCCESS_MARKER)); + assertFalse( + result.stdout.contains(PKIX_ERROR_MSG), + "Handshake failed with SSL error: " + result.stdout); + } + + public static void main(String[] args) { + int port = Integer.parseInt(args[0]); + String baseUri = "jdbc:bigquery://https://" + HOST + ":" + port + ";"; + String url = + new URIBuilder(baseUri) + .append("EndpointOverrides", "BIGQUERY=https://" + HOST + ":" + port) + .append("ProjectId", "dummy") + .append("OAuthType", 2) + .append("OAuthAccessToken", "dummy-token") + .toString(); + try (Connection connection = DriverManager.getConnection(url); + Statement statement = connection.createStatement()) { + statement.execute("SELECT 1"); + System.out.println(SUCCESS_MARKER); + System.exit(0); + } catch (Throwable e) { + System.out.println(FAILURE_MARKER_PREFIX + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/suites/ITPresubmitTests.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/suites/ITPresubmitTests.java index cdcece31a279..44e37f6888e2 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/suites/ITPresubmitTests.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/suites/ITPresubmitTests.java @@ -23,6 +23,7 @@ import com.google.cloud.bigquery.jdbc.it.ITConnectionTest; import com.google.cloud.bigquery.jdbc.it.ITDatabaseMetadataTest; import com.google.cloud.bigquery.jdbc.it.ITDriverTest; +import com.google.cloud.bigquery.jdbc.it.ITLocalSslValidationTest; import com.google.cloud.bigquery.jdbc.it.ITResultSetMetadataTest; import com.google.cloud.bigquery.jdbc.it.ITStatementTest; import org.junit.platform.suite.api.SelectClasses; @@ -37,6 +38,7 @@ ITConnectionPoolingTest.class, ITDatabaseMetadataTest.class, ITDriverTest.class, + ITLocalSslValidationTest.class, ITResultSetMetadataTest.class, ITStatementTest.class }) diff --git a/java-bigquery-jdbc/src/test/resources/localhost-keystore.jks b/java-bigquery-jdbc/src/test/resources/localhost-keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..a5a04b2fe56ebfb02819999555f64e28e6efa50e GIT binary patch literal 2215 zcmcJQ`8(8$7sux_#y%L0B{4ImCRZ`~jK;oX=f;Tadx-4Y2xa*)iV$j$RQB~+6P2|R zg=TQC#xk~Sjhd_}5#{UN=ef_ff8hJW`Qde*bDr~@<$0fIElCnB8WH;6%=m>9n7n`oO-i*_elao2d$|= zF+TLxqT?3m5k;vUF?0nNF(95IhkAZm-i?bX zE~K;OA;IYSb9j5$!roI*!v><(iisFvm@!jsMz>D{~YyVQynxmJ?t*-OCXlNE#JXWL_4Lwt<1Z4tz2gx#ILvJz%h zZrlhz2*E;S67Qg<_hxfqk@)^stnO;yrh2fQ*C&Pbyw-=hO*s7z=HJ92Y7#3f7GGma(L0J6bhdfO$hoR-98rlBT)q9Y2mdA!p zGttJJQu=Pmq6d1Zca7k0l%62vE#+9uJ`!T=sw8J9JS%X!h6RpJk-rp&xktO8YgVP% zuVPYp^^QJo^uvy5rYN2UZ4nx*>+!-yS-o!b==k^M4w%83UYh4e``3ScAJeH{WUCan z&Fji)OC(8#mW;iLxQLTzv%@mTC=M$HL}0@7AVlj`rHqr2G>#4S~bn1+fa&2X33*Y@{l>qL*lwqaTv07PlXwH`g zk?IY+&_hAExyIZ}M;deAxLj@T)!5)!dSb!a@@h-VwT_H2#~#t4sTcPVD}LO|E+~y^ zc@aw0(x%a9p{M}EKP2=%<94wdSI$rg^xK5@{3ctFMVBt0sh2GNM;bZp6_h|V2!z80 zq;NQZ6sT?y2NZxpdAN%%!%sgyYXu5$@t#aFN%|%L0Pq9RbRzJByj&bMFfJt2#_C_- zK|=L){{?O&lw?Z~I>5G^jC zs0=C-6bXt%P)XJC09Ag$|8H>$aP-$^0Zs@91rR!T696BS0sxS%WTU4ex;QbjBbXhw zP6B?UV3=uc!`9JU?*O?D>HJLomOK#A`sb)}M6HW0M~D;R?fXYoD~D;hz2k|bsObV> z_GHWxEn#eKG z`a!h*+1Ul_*2niyt){|`^4wlAW6!@gvh!QMHsNXB)eDcpFZN}6#OtH-78$V@C$_uO z9@u$_3WRlw$xM8`hs=Nn&8*}GcV{@XUXjo8yAk-{;@w)ApnbQ(9-$Gme08V7GAZ&x zBzMNnDk`Dy1x;FqD^Pktx9XJV1&mBbWyRuhP@sNA=D=kr2LJ&|u^a6ZYl2G2pMAB~l_Q@OqNA9lo<2H$iJa zt=o7VC#12eTvT5c>diJDGq)yM-Rf$q>bW>vJ-&+EdMcQe9Ibw4b?k`wb$?xKCyyC+ z(=3xS(TS8yBwI=a!qJrR8+*`{B$!d@*pVenqVx}Ij5txO29diJu(n911@ z#$gv`@(nZO2kHbW;}T|b&(BZKNj2m(;06hB3$uiz7MB{&)u2IeM4eg=akMlPl%Mn;B%?QYwzI;x2INXh?v z<5|$exa`%Y(DHUU=woq*Z#fzY}w8W*UZY+#D*~!L z+?^-*>UPaGTS3L?8;}0J9eyh&tzxRpO`e>4*_KDn$dz`rEm)!Vcxl5P!_2}TXVO*7 z%nwIB<0-dt&O3eb!Gp&kJGah~-RUhDrNf?DbAA7>jj!hJ__|s5blR%Nn@Unn^d~hq z%g%gqr8ec(kHg(_!c#?giw=va-TFF9xQ{3A-rMQd2jwGGmQe-v9Xd zNBFPG>8#qT_r|w8p`5+%=R4WP`3FubII!g^Ja$~6pPV4CwrBaWCvWm{otO2WEoEY6 zWMEvZXdrJO3ye5fJ{B<+k$LW~e0BzAIyJF)o>=phZ}O}jx68L)XX7Ka0Ku zxie Date: Fri, 12 Jun 2026 17:09:25 +0000 Subject: [PATCH 7/8] make resource as classpath instead of hardcoding path --- .../bigquery/jdbc/it/ITLocalSslValidationTest.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITLocalSslValidationTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITLocalSslValidationTest.java index df4b7e8c25c1..7f122985431d 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITLocalSslValidationTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITLocalSslValidationTest.java @@ -30,6 +30,7 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.net.InetSocketAddress; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.sql.Connection; @@ -48,7 +49,7 @@ public class ITLocalSslValidationTest { private static final String HOST = "localhost"; private static final String PASSWORD = "changeit"; private static final String KEYSTORE_RESOURCE = "/localhost-keystore.jks"; - private static final String TRUSTSTORE_PATH = "src/test/resources/localhost-truststore.jks"; + private static final String TRUSTSTORE_RESOURCE = "/localhost-truststore.jks"; private static final String SUCCESS_MARKER = "SUBPROCESS_RESULT: SUCCESS"; private static final String FAILURE_MARKER_PREFIX = "SUBPROCESS_RESULT: FAILURE - "; private static final String PKIX_ERROR_MSG = "PKIX path building failed"; @@ -219,7 +220,12 @@ public void testDefaultSslFailsForSelfSigned() throws Exception { @Test public void testCustomTrustStoreSucceeds() throws Exception { - String trustStorePath = new File(TRUSTSTORE_PATH).getAbsolutePath(); + URL trustStoreUrl = getClass().getResource(TRUSTSTORE_RESOURCE); + if (trustStoreUrl == null) { + throw new IllegalStateException( + "Truststore resource " + TRUSTSTORE_RESOURCE + " not found on classpath!"); + } + String trustStorePath = new File(trustStoreUrl.toURI()).getAbsolutePath(); ProcessResult result = runSubprocess(trustStorePath, PASSWORD); assertEquals(0, result.exitCode, "Subprocess failed. Output:\n" + result.stdout); From 898f09143d7e85483c7ec85a2d5eafa0ee3ed80c Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Fri, 12 Jun 2026 17:15:09 +0000 Subject: [PATCH 8/8] address pr feedback --- .../jdbc/it/ITLocalSslValidationTest.java | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITLocalSslValidationTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITLocalSslValidationTest.java index 7f122985431d..6ccffb703547 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITLocalSslValidationTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITLocalSslValidationTest.java @@ -38,6 +38,8 @@ import java.sql.Statement; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; @@ -132,10 +134,11 @@ public void configure(HttpsParameters params) { + " }\n" + "}"; } + byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "application/json"); - exchange.sendResponseHeaders(200, response.length()); + exchange.sendResponseHeaders(200, responseBytes.length); try (OutputStream os = exchange.getResponseBody()) { - os.write(response.getBytes()); + os.write(responseBytes); } }); } @@ -201,14 +204,24 @@ private ProcessResult runSubprocess(String trustStore, String password) throws E builder.redirectErrorStream(true); Process process = builder.start(); - String output; - try (InputStreamReader reader = - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) { - output = CharStreams.toString(reader); + String output = ""; + boolean finished = false; + try { + try (InputStreamReader reader = + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) { + output = CharStreams.toString(reader); + } + finished = process.waitFor(10, TimeUnit.SECONDS); + if (!finished) { + throw new TimeoutException("Subprocess timed out after 10 seconds"); + } + int exitCode = process.exitValue(); + return new ProcessResult(exitCode, output); + } finally { + if (!finished && process.isAlive()) { + process.destroyForcibly(); + } } - - int exitCode = process.waitFor(); - return new ProcessResult(exitCode, output); } @Test