diff --git a/google-http-client/src/main/java/com/google/api/client/http/javanet/ConfigurableSSLSocketFactory.java b/google-http-client/src/main/java/com/google/api/client/http/javanet/ConfigurableSSLSocketFactory.java new file mode 100644 index 000000000..971f4ebec --- /dev/null +++ b/google-http-client/src/main/java/com/google/api/client/http/javanet/ConfigurableSSLSocketFactory.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026 Google Inc. + * + * 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.api.client.http.javanet; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * An {@link SSLSocketFactory} wrapper that intercepts all socket creation entrypoints (both default + * and bound socket creations) and applies the custom user-provided {@link SslSocketConfigurator} + * callback to the socket before returning it. + * + *
This factory delegates all standard socket actions to the underlying default or custom {@link + * SSLSocketFactory} instance. Sockets are intercepted and cast to {@link SSLSocket} for callback + * execution. + * + * @since 2.1.2 + */ +final class ConfigurableSSLSocketFactory extends SSLSocketFactory { + private final SSLSocketFactory delegate; + private final SslSocketConfigurator configurator; + + ConfigurableSSLSocketFactory(SSLSocketFactory delegate, SslSocketConfigurator configurator) { + this.delegate = delegate; + this.configurator = configurator; + } + + private Socket configure(Socket socket) { + if (socket instanceof SSLSocket && configurator != null) { + configurator.configure((SSLSocket) socket); + } + return socket; + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + return configure(delegate.createSocket()); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) + throws IOException { + return configure(delegate.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + return configure(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException { + return configure(delegate.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return configure(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) + throws IOException { + return configure(delegate.createSocket(address, port, localAddress, localPort)); + } +} diff --git a/google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpTransport.java b/google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpTransport.java index 2a0ae6c1f..a5651409d 100644 --- a/google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpTransport.java +++ b/google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpTransport.java @@ -28,12 +28,15 @@ import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.Provider; import java.security.cert.CertificateFactory; import java.util.Arrays; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; /** * Thread-safe HTTP low-level transport based on the {@code java.net} package. @@ -84,7 +87,7 @@ private static Proxy defaultProxy() { private final ConnectionFactory connectionFactory; /** SSL socket factory or {@code null} for the default. */ - private final SSLSocketFactory sslSocketFactory; + final SSLSocketFactory sslSocketFactory; /** Host name verifier or {@code null} for the default. */ private final HostnameVerifier hostnameVerifier; @@ -189,6 +192,12 @@ public static final class Builder { /** SSL socket factory or {@code null} for the default. */ private SSLSocketFactory sslSocketFactory; + /** Security provider to use or {@code null} for default. */ + private Provider securityProvider; + + /** Custom SSLSocket configurator or {@code null} to disable callback configuration. */ + SslSocketConfigurator sslSocketConfigurator; + /** Host name verifier or {@code null} for the default. */ private HostnameVerifier hostnameVerifier; @@ -289,8 +298,9 @@ public Builder trustCertificatesFromStream(InputStream certificateStream) * @since 1.14 */ public Builder trustCertificates(KeyStore trustStore) throws GeneralSecurityException { - SSLContext sslContext = SslUtils.getTlsSslContext(); - SslUtils.initSslContext(sslContext, trustStore, SslUtils.getPkixTrustManagerFactory()); + SSLContext sslContext = SslUtils.getTlsSslContext(securityProvider); + SslUtils.initSslContext( + sslContext, trustStore, SslUtils.getPkixTrustManagerFactory(securityProvider)); return setSslSocketFactory(sslContext.getSocketFactory()); } @@ -313,14 +323,14 @@ public Builder trustCertificates( if (mtlsKeyStore != null && mtlsKeyStore.size() > 0) { this.isMtls = true; } - SSLContext sslContext = SslUtils.getTlsSslContext(); + SSLContext sslContext = SslUtils.getTlsSslContext(securityProvider); SslUtils.initSslContext( sslContext, trustStore, - SslUtils.getPkixTrustManagerFactory(), + SslUtils.getPkixTrustManagerFactory(securityProvider), mtlsKeyStore, mtlsKeyStorePassword, - SslUtils.getDefaultKeyManagerFactory()); + SslUtils.getDefaultKeyManagerFactory(securityProvider)); return setSslSocketFactory(sslContext.getSocketFactory()); } @@ -345,12 +355,69 @@ public SSLSocketFactory getSslSocketFactory() { return sslSocketFactory; } - /** Sets the SSL socket factory or {@code null} for the default. */ + /** + * Sets the SSL socket factory or {@code null} for the default. + * + *
Note: If a custom {@link SslSocketConfigurator} is also provided, it will wrap and apply + * its configuration callback to all sockets created by this factory. + */ public Builder setSslSocketFactory(SSLSocketFactory sslSocketFactory) { this.sslSocketFactory = sslSocketFactory; return this; } + /** + * Sets the custom security provider or {@code null} to use the default JRE provider. + * + *
When enabling Post-Quantum Cryptography (PQC) transport: + * + *
If both a custom {@link SSLSocketFactory} (via {@link + * #setSslSocketFactory(SSLSocketFactory)}) and a custom configurator are set, the configurator + * callback will be applied to all sockets created by the custom socket factory. If no custom + * factory is provided, the configurator will wrap and apply to sockets created by the default + * resolved socket factory. + * + *
When enabling Post-Quantum Cryptography (PQC) transport: + * + *
If a custom factory has been set via {@link #setSslSocketFactory(SSLSocketFactory)}, it + * will be returned. Otherwise, a default SSL socket factory will be constructed via {@link + * #createDefaultSslSocketFactory()}. + * + * @return the resolved {@link SSLSocketFactory} + */ + SSLSocketFactory resolveSslSocketFactory() { + if (securityProvider == null && sslSocketConfigurator == null) { + return sslSocketFactory; + } + SSLSocketFactory factory = + sslSocketFactory != null ? sslSocketFactory : createDefaultSslSocketFactory(); + if (sslSocketConfigurator != null) { + return new ConfigurableSSLSocketFactory(factory, sslSocketConfigurator); + } + return factory; + } + + /** + * Constructs a default {@link SSLSocketFactory} configured with the specified {@link Provider}. + * + *
This method initializes an {@link SSLContext} and resolves its {@link TrustManagerFactory} + * using the same security provider (if provided), ensuring compatibility for TLS handshakes + * when using custom providers (such as Conscrypt). + * + * @return the initialized default {@link SSLSocketFactory} + */ + SSLSocketFactory createDefaultSslSocketFactory() { + try { + SSLContext sslContext = SslUtils.getTlsSslContext(securityProvider); + TrustManager[] trustManagers = null; + if (securityProvider != null) { + try { + TrustManagerFactory tmf = SslUtils.getDefaultTrustManagerFactory(securityProvider); + tmf.init((KeyStore) null); + trustManagers = tmf.getTrustManagers(); + } catch (Exception e) { + // Ignore and fall back to default + } + } + sslContext.init(null, trustManagers, null); + return sslContext.getSocketFactory(); + } catch (Exception e) { + return (SSLSocketFactory) SSLSocketFactory.getDefault(); + } + } + /** Returns a new instance of {@link NetHttpTransport} based on the options. */ public NetHttpTransport build() { if (System.getProperty(SHOULD_USE_PROXY_FLAG) != null) { setProxy(defaultProxy()); } + SSLSocketFactory resolvedFactory = resolveSslSocketFactory(); return this.proxy == null - ? new NetHttpTransport(connectionFactory, sslSocketFactory, hostnameVerifier, isMtls) - : new NetHttpTransport(this.proxy, sslSocketFactory, hostnameVerifier, isMtls); + ? new NetHttpTransport(connectionFactory, resolvedFactory, hostnameVerifier, isMtls) + : new NetHttpTransport(this.proxy, resolvedFactory, hostnameVerifier, isMtls); } } } diff --git a/google-http-client/src/main/java/com/google/api/client/http/javanet/SslSocketConfigurator.java b/google-http-client/src/main/java/com/google/api/client/http/javanet/SslSocketConfigurator.java new file mode 100644 index 000000000..0159ca315 --- /dev/null +++ b/google-http-client/src/main/java/com/google/api/client/http/javanet/SslSocketConfigurator.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2026 Google Inc. + * + * 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.api.client.http.javanet; + +import javax.net.ssl.SSLSocket; + +/** + * A callback interface allowing users to programmatically configure active {@link SSLSocket} + * parameters (e.g., named groups, cipher suites, application protocols) before the TLS handshake + * starts. + * + *
Exposing this hook allows users to customize advanced TLS capabilities that are not + * configurable via standard JVM system properties, or require custom security provider APIs (e.g., + * Conscrypt or BouncyCastle). Typical use cases include: + * + *
{@code
+ * builder.setSslSocketConfigurator(new SslSocketConfigurator() {
+ * @Override
+ * public void configure(SSLSocket socket) {
+ * if (org.conscrypt.Conscrypt.isConscrypt(socket)) {
+ * org.conscrypt.Conscrypt.setNamedGroups(socket, new String[] {"X25519MLKEM768", "X25519"});
+ * }
+ * }
+ * });
+ * }
+ *
+ * {@code
+ * builder.setSslSocketConfigurator(new SslSocketConfigurator() {
+ * @Override
+ * public void configure(SSLSocket socket) {
+ * if (socket instanceof org.bouncycastle.jsse.BCSSLSocket) {
+ * org.bouncycastle.jsse.BCSSLSocket bcSocket = (org.bouncycastle.jsse.BCSSLSocket) socket;
+ * org.bouncycastle.jsse.BCSSLParameters bcParams = bcSocket.getParameters();
+ * bcParams.setNamedGroups(new String[] {"X25519MLKEM768", "X25519"});
+ * bcSocket.setParameters(bcParams);
+ * }
+ * }
+ * });
+ * }
+ *
+ * Note: This example requires compilation and runtime environment running JDK 20+. + * + *
{@code
+ * builder.setSslSocketConfigurator(new SslSocketConfigurator() {
+ * @Override
+ * public void configure(SSLSocket socket) {
+ * javax.net.ssl.SSLParameters parameters = socket.getSSLParameters();
+ * parameters.setNamedGroups(new String[] {"X25519MLKEM768", "X25519"});
+ * socket.setSSLParameters(parameters);
+ * }
+ * });
+ * }
+ *
+ * @since 2.1.2
+ */
+public interface SslSocketConfigurator {
+ /**
+ * Configures the active TLS socket parameters before the handshake starts.
+ *
+ * @param sslSocket the newly created SSLSocket connection
+ */
+ void configure(SSLSocket sslSocket);
+}
diff --git a/google-http-client/src/main/java/com/google/api/client/util/SslUtils.java b/google-http-client/src/main/java/com/google/api/client/util/SslUtils.java
index a578c7383..a807aa09e 100644
--- a/google-http-client/src/main/java/com/google/api/client/util/SslUtils.java
+++ b/google-http-client/src/main/java/com/google/api/client/util/SslUtils.java
@@ -18,6 +18,7 @@
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
@@ -54,6 +55,70 @@ public static SSLContext getTlsSslContext() throws NoSuchAlgorithmException {
return SSLContext.getInstance("TLS");
}
+ /**
+ * Returns the SSL context for "TLS" algorithm using the specified provider.
+ *
+ * If a custom provider (e.g., Conscrypt) is configured, the context must be loaded from it to + * enable the provider's specific TLS parameters and curve groups (such as PQC). + * + * @param provider the security provider, or {@code null} to use the default JRE provider + * @since 2.1.2 + */ + public static SSLContext getTlsSslContext(Provider provider) throws NoSuchAlgorithmException { + return provider != null + ? SSLContext.getInstance("TLS", provider) + : SSLContext.getInstance("TLS"); + } + + /** + * Returns the default trust manager factory using the specified provider. + * + *
Aligning the trust manager factory's provider with the SSLContext's provider is necessary to + * prevent handshake failures. For example, if Conscrypt is used for the SSLContext but the + * default SunJSSE TrustManager is used, TLS 1.3 handshakes will fail with "Unknown authType: + * GENERIC" because SunJSSE does not recognize Conscrypt's "GENERIC" authentication type string. + * + * @param provider the security provider, or {@code null} to use the default JRE provider + * @since 2.1.2 + */ + public static TrustManagerFactory getDefaultTrustManagerFactory(Provider provider) + throws NoSuchAlgorithmException { + return provider != null + ? TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm(), provider) + : TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + } + + /** + * Returns the PKIX trust manager factory using the specified provider. + * + *
Aligning the trust manager factory's provider with the SSLContext's provider is necessary to + * prevent handshake failures. For example, if Conscrypt is used for the SSLContext but the + * default SunJSSE TrustManager is used, TLS 1.3 handshakes will fail with "Unknown authType: + * GENERIC" because SunJSSE does not recognize Conscrypt's "GENERIC" authentication type string. + * + * @param provider the security provider, or {@code null} to use the default JRE provider + * @since 2.1.2 + */ + public static TrustManagerFactory getPkixTrustManagerFactory(Provider provider) + throws NoSuchAlgorithmException { + return provider != null + ? TrustManagerFactory.getInstance("PKIX", provider) + : TrustManagerFactory.getInstance("PKIX"); + } + + /** + * Returns the default key manager factory using the specified provider. + * + * @param provider the security provider, or {@code null} to use the default JRE provider + * @since 2.1.2 + */ + public static KeyManagerFactory getDefaultKeyManagerFactory(Provider provider) + throws NoSuchAlgorithmException { + return provider != null + ? KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm(), provider) + : KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + } + /** * Returns the default trust manager factory. * diff --git a/google-http-client/src/test/java/com/google/api/client/http/javanet/ConfigurableSSLSocketFactoryTest.java b/google-http-client/src/test/java/com/google/api/client/http/javanet/ConfigurableSSLSocketFactoryTest.java new file mode 100644 index 000000000..48a894b43 --- /dev/null +++ b/google-http-client/src/test/java/com/google/api/client/http/javanet/ConfigurableSSLSocketFactoryTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026 Google Inc. + * + * 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.api.client.http.javanet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.net.Socket; +import java.util.concurrent.atomic.AtomicInteger; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ConfigurableSSLSocketFactoryTest { + + @Test + public void testCreateSocketDelegatesAndConfigures() throws IOException { + SSLSocketFactory delegate = (SSLSocketFactory) SSLSocketFactory.getDefault(); + final AtomicInteger callbackCount = new AtomicInteger(0); + SslSocketConfigurator configurator = + new SslSocketConfigurator() { + @Override + public void configure(SSLSocket socket) { + callbackCount.incrementAndGet(); + } + }; + + ConfigurableSSLSocketFactory factory = new ConfigurableSSLSocketFactory(delegate, configurator); + + Socket result = factory.createSocket(); + assertTrue(result instanceof SSLSocket); + assertEquals(1, callbackCount.get()); + } + + @Test + public void testGetCipherSuitesDelegates() { + SSLSocketFactory delegate = (SSLSocketFactory) SSLSocketFactory.getDefault(); + ConfigurableSSLSocketFactory factory = new ConfigurableSSLSocketFactory(delegate, null); + + assertEquals(delegate.getDefaultCipherSuites().length, factory.getDefaultCipherSuites().length); + assertEquals( + delegate.getSupportedCipherSuites().length, factory.getSupportedCipherSuites().length); + } +} diff --git a/google-http-client/src/test/java/com/google/api/client/http/javanet/NetHttpTransportTest.java b/google-http-client/src/test/java/com/google/api/client/http/javanet/NetHttpTransportTest.java index 87c5337c6..84de06303 100644 --- a/google-http-client/src/test/java/com/google/api/client/http/javanet/NetHttpTransportTest.java +++ b/google-http-client/src/test/java/com/google/api/client/http/javanet/NetHttpTransportTest.java @@ -16,10 +16,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpTransport; import com.google.api.client.testing.http.HttpTesting; import com.google.api.client.testing.http.javanet.MockHttpURLConnection; @@ -38,6 +41,8 @@ import java.security.KeyStore; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -235,10 +240,53 @@ public void handle(HttpExchange httpExchange) throws IOException { HttpTransport transport = new NetHttpTransport(); GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar"); testUrl.setPort(server.getPort()); - com.google.api.client.http.HttpResponse response = - transport.createRequestFactory().buildGetRequest(testUrl).execute(); - // disconnect should not wait to read the entire content + HttpResponse response = transport.createRequestFactory().buildGetRequest(testUrl).execute(); response.disconnect(); } } + + @Test + public void testBuilderDefaultSslSocketConfiguratorIsNull() { + NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); + assertNull(builder.sslSocketConfigurator); + } + + @Test + public void testBuilderConfigureSslSocketConfigurator() { + SslSocketConfigurator customConfigurator = + new SslSocketConfigurator() { + @Override + public void configure(SSLSocket socket) {} + }; + NetHttpTransport.Builder builder = + new NetHttpTransport.Builder().setSslSocketConfigurator(customConfigurator); + assertEquals(customConfigurator, builder.sslSocketConfigurator); + } + + @Test + public void testResolveSslSocketFactoryWithDefault() { + NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); + SSLSocketFactory factory = builder.resolveSslSocketFactory(); + assertNull(factory); + } + + @Test + public void testResolveSslSocketFactoryWithCustom() { + SslSocketConfigurator customConfigurator = + new SslSocketConfigurator() { + @Override + public void configure(SSLSocket socket) {} + }; + NetHttpTransport.Builder builder = + new NetHttpTransport.Builder().setSslSocketConfigurator(customConfigurator); + SSLSocketFactory factory = builder.resolveSslSocketFactory(); + assertTrue(factory instanceof ConfigurableSSLSocketFactory); + } + + @Test + public void testCreateDefaultSslSocketFactory() { + NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); + SSLSocketFactory factory = builder.createDefaultSslSocketFactory(); + assertNotNull(factory); + } }