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..2f4ebbd83 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) { + TrustManagerFactory tmf = SslUtils.getDefaultTrustManagerFactory(securityProvider); + tmf.init((KeyStore) null); + trustManagers = tmf.getTrustManagers(); + } + sslContext.init(null, trustManagers, null); + return sslContext.getSocketFactory(); + } catch (GeneralSecurityException e) { + // Halt execution because the SSLContext cannot be initialized with the requested + // configuration. + throw new IllegalStateException("Failed to initialize SSLSocketFactory.", e); + } + } + /** 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..c6963bbfb 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,68 @@ 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) : getTlsSslContext(); + } + + /** + * 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) + : getDefaultTrustManagerFactory(); + } + + /** + * 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)
+ : getPkixTrustManagerFactory();
+ }
+
+ /**
+ * 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)
+ : getDefaultKeyManagerFactory();
+ }
+
/**
* 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..0bb67255c 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;
@@ -36,8 +39,12 @@
import java.net.InetSocketAddress;
import java.net.URL;
import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
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 +242,70 @@ 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);
+ }
+
+ @Test
+ public void testCreateDefaultSslSocketFactory_invalidProviderFailsFast() {
+ Provider invalidProvider =
+ new Provider("MockInvalidProvider", 1.0, "For testing") {
+ private static final long serialVersionUID = 1L;
+ };
+ NetHttpTransport.Builder builder =
+ new NetHttpTransport.Builder().setSecurityProvider(invalidProvider);
+ try {
+ builder.createDefaultSslSocketFactory();
+ fail("Expected IllegalStateException");
+ } catch (IllegalStateException e) {
+ assertEquals("Failed to initialize SSLSocketFactory.", e.getMessage());
+ assertTrue(e.getCause() instanceof NoSuchAlgorithmException);
+ }
+ }
}
diff --git a/google-http-client/src/test/java/com/google/api/client/util/SslUtilsTest.java b/google-http-client/src/test/java/com/google/api/client/util/SslUtilsTest.java
new file mode 100644
index 000000000..f0fd5a1fc
--- /dev/null
+++ b/google-http-client/src/test/java/com/google/api/client/util/SslUtilsTest.java
@@ -0,0 +1,266 @@
+/*
+ * 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.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.KeyStore;
+import java.security.Provider;
+import java.security.SecureRandom;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.KeyManagerFactorySpi;
+import javax.net.ssl.ManagerFactoryParameters;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLContextSpi;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.SSLSessionContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.TrustManagerFactorySpi;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests {@link SslUtils}. */
+@RunWith(JUnit4.class)
+public class SslUtilsTest {
+
+ private static final TrustManager MOCK_TRUST_MANAGER = new TrustManager() {};
+ private static final KeyManager MOCK_KEY_MANAGER = new KeyManager() {};
+
+ private static final SSLSocketFactory MOCK_SSL_SOCKET_FACTORY =
+ new SSLSocketFactory() {
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return new String[0];
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return new String[0];
+ }
+
+ @Override
+ public Socket createSocket(Socket s, String host, int port, boolean autoClose)
+ throws IOException {
+ return null;
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException {
+ return null;
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
+ throws IOException {
+ return null;
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ return null;
+ }
+
+ @Override
+ public Socket createSocket(
+ InetAddress address, int port, InetAddress localAddress, int localPort)
+ throws IOException {
+ return null;
+ }
+ };
+
+ /** Mock SPI implementation for SSLContext. */
+ public static class MockSslContextSpi extends SSLContextSpi {
+ @Override
+ protected void engineInit(KeyManager[] km, TrustManager[] tm, SecureRandom sr) {}
+
+ @Override
+ protected SSLSocketFactory engineGetSocketFactory() {
+ return MOCK_SSL_SOCKET_FACTORY;
+ }
+
+ @Override
+ protected SSLServerSocketFactory engineGetServerSocketFactory() {
+ return null;
+ }
+
+ @Override
+ protected SSLEngine engineCreateSSLEngine() {
+ return null;
+ }
+
+ @Override
+ protected SSLEngine engineCreateSSLEngine(String host, int port) {
+ return null;
+ }
+
+ @Override
+ protected SSLSessionContext engineGetServerSessionContext() {
+ return null;
+ }
+
+ @Override
+ protected SSLSessionContext engineGetClientSessionContext() {
+ return null;
+ }
+ }
+
+ /** Mock SPI implementation for TrustManagerFactory. */
+ public static class MockTrustManagerFactorySpi extends TrustManagerFactorySpi {
+ @Override
+ protected void engineInit(KeyStore ks) {}
+
+ @Override
+ protected void engineInit(ManagerFactoryParameters mfp) {}
+
+ @Override
+ protected TrustManager[] engineGetTrustManagers() {
+ return new TrustManager[] {MOCK_TRUST_MANAGER};
+ }
+ }
+
+ /** Mock SPI implementation for KeyManagerFactory. */
+ public static class MockKeyManagerFactorySpi extends KeyManagerFactorySpi {
+ @Override
+ protected void engineInit(KeyStore ks, char[] password) {}
+
+ @Override
+ protected void engineInit(ManagerFactoryParameters mfp) {}
+
+ @Override
+ protected KeyManager[] engineGetKeyManagers() {
+ return new KeyManager[] {MOCK_KEY_MANAGER};
+ }
+ }
+
+ // A mock security provider used to verify that SslUtils correctly delegates context and factory
+ // initialization to the configured Provider instance. Using a mock provider avoids having to
+ // load platform-dependent native libraries (such as Conscrypt or OpenSSL) during unit testing.
+ private static final Provider mockProvider =
+ new Provider("MockProvider", 1.0, "For testing") {
+ private static final long serialVersionUID = 1L;
+
+ {
+ put("SSLContext.TLS", MockSslContextSpi.class.getName());
+ put(
+ "TrustManagerFactory." + TrustManagerFactory.getDefaultAlgorithm(),
+ MockTrustManagerFactorySpi.class.getName());
+ put("TrustManagerFactory.PKIX", MockTrustManagerFactorySpi.class.getName());
+ put(
+ "KeyManagerFactory." + KeyManagerFactory.getDefaultAlgorithm(),
+ MockKeyManagerFactorySpi.class.getName());
+ }
+ };
+
+ @Test
+ public void testGetTlsSslContext() throws Exception {
+ SSLContext context = SslUtils.getTlsSslContext();
+ assertNotNull(context);
+ assertEquals("TLS", context.getProtocol());
+ }
+
+ @Test
+ public void testGetTlsSslContext_withCustomProvider() throws Exception {
+ SSLContext context = SslUtils.getTlsSslContext(mockProvider);
+ assertNotNull(context);
+ assertEquals("TLS", context.getProtocol());
+ assertEquals(mockProvider, context.getProvider());
+ assertEquals(MOCK_SSL_SOCKET_FACTORY, context.getSocketFactory());
+ }
+
+ @Test
+ public void testGetDefaultTrustManagerFactory() throws Exception {
+ TrustManagerFactory tmf = SslUtils.getDefaultTrustManagerFactory();
+ assertNotNull(tmf);
+ assertEquals(TrustManagerFactory.getDefaultAlgorithm(), tmf.getAlgorithm());
+ }
+
+ @Test
+ public void testGetDefaultTrustManagerFactory_withCustomProvider() throws Exception {
+ TrustManagerFactory tmf = SslUtils.getDefaultTrustManagerFactory(mockProvider);
+ assertNotNull(tmf);
+ assertEquals(TrustManagerFactory.getDefaultAlgorithm(), tmf.getAlgorithm());
+ assertEquals(mockProvider, tmf.getProvider());
+
+ tmf.init((KeyStore) null);
+ TrustManager[] tms = tmf.getTrustManagers();
+ assertEquals(1, tms.length);
+ assertEquals(MOCK_TRUST_MANAGER, tms[0]);
+ }
+
+ @Test
+ public void testGetPkixTrustManagerFactory() throws Exception {
+ TrustManagerFactory tmf = SslUtils.getPkixTrustManagerFactory();
+ assertNotNull(tmf);
+ assertEquals("PKIX", tmf.getAlgorithm());
+ }
+
+ @Test
+ public void testGetPkixTrustManagerFactory_withCustomProvider() throws Exception {
+ TrustManagerFactory tmf = SslUtils.getPkixTrustManagerFactory(mockProvider);
+ assertNotNull(tmf);
+ assertEquals("PKIX", tmf.getAlgorithm());
+ assertEquals(mockProvider, tmf.getProvider());
+
+ tmf.init((KeyStore) null);
+ TrustManager[] tms = tmf.getTrustManagers();
+ assertEquals(1, tms.length);
+ assertEquals(MOCK_TRUST_MANAGER, tms[0]);
+ }
+
+ @Test
+ public void testGetDefaultKeyManagerFactory() throws Exception {
+ KeyManagerFactory kmf = SslUtils.getDefaultKeyManagerFactory();
+ assertNotNull(kmf);
+ assertEquals(KeyManagerFactory.getDefaultAlgorithm(), kmf.getAlgorithm());
+ }
+
+ @Test
+ public void testGetDefaultKeyManagerFactory_withCustomProvider() throws Exception {
+ KeyManagerFactory kmf = SslUtils.getDefaultKeyManagerFactory(mockProvider);
+ assertNotNull(kmf);
+ assertEquals(KeyManagerFactory.getDefaultAlgorithm(), kmf.getAlgorithm());
+ assertEquals(mockProvider, kmf.getProvider());
+
+ kmf.init((KeyStore) null, new char[0]);
+ KeyManager[] kms = kmf.getKeyManagers();
+ assertEquals(1, kms.length);
+ assertEquals(MOCK_KEY_MANAGER, kms[0]);
+ }
+
+ @Test
+ public void testGetPkixKeyManagerFactory() throws Exception {
+ KeyManagerFactory kmf = SslUtils.getPkixKeyManagerFactory();
+ assertNotNull(kmf);
+ assertEquals("PKIX", kmf.getAlgorithm());
+ }
+
+ @Test
+ public void testInitSslContext() throws Exception {
+ SSLContext context = SslUtils.getTlsSslContext();
+ TrustManagerFactory tmf = SslUtils.getDefaultTrustManagerFactory();
+ tmf.init((KeyStore) null);
+ SSLContext initializedContext = SslUtils.initSslContext(context, null, tmf);
+ assertEquals(context, initializedContext);
+ }
+}
diff --git a/google-http-client/src/test/resources/META-INF/native-image/com.google.http-client/google-http-client/reflect-config.json b/google-http-client/src/test/resources/META-INF/native-image/com.google.http-client/google-http-client/reflect-config.json
index 41fade5e2..638ad6b6d 100644
--- a/google-http-client/src/test/resources/META-INF/native-image/com.google.http-client/google-http-client/reflect-config.json
+++ b/google-http-client/src/test/resources/META-INF/native-image/com.google.http-client/google-http-client/reflect-config.json
@@ -240,5 +240,44 @@
"queryAllPublicConstructors": true,
"queryAllDeclaredMethods": true,
"allDeclaredFields": true
+ },
+ {
+ "name": "com.google.api.client.util.SslUtilsTest$MockSslContextSpi",
+ "queryAllDeclaredConstructors": true,
+ "queryAllPublicConstructors": true,
+ "queryAllDeclaredMethods": true,
+ "allDeclaredFields": true,
+ "methods": [
+ {
+ "name": "