system
@@ -362,14 +384,86 @@ public Builder setHostnameVerifier(HostnameVerifier hostnameVerifier) {
return this;
}
+ /**
+ * Sets the security provider to use for SSL context.
+ *
+ * By default, {@link NetHttpTransport} will attempt to use Conscrypt as the security
+ * provider. If Conscrypt is not available on the system, it will fall back to the default JDK
+ * provider. Configuring a custom security provider here will override this default behavior and
+ * take precedence.
+ *
+ * @param securityProvider security provider to use
+ * @since 1.39
+ */
+ public Builder setSecurityProvider(Provider securityProvider) {
+ this.securityProvider = securityProvider;
+ return this;
+ }
+
+ /**
+ * Resolves the {@link SSLSocketFactory} to use, prioritizing user-configured factory, then
+ * custom security provider, defaulting to Conscrypt, and falling back to JDK.
+ */
+ private SSLSocketFactory resolveSslSocketFactory() {
+ if (sslSocketFactory != null) {
+ return new PqcEnforcingSSLSocketFactory(sslSocketFactory, PQC_GROUPS);
+ }
+ return new PqcEnforcingSSLSocketFactory(createDefaultSslSocketFactory(), PQC_GROUPS);
+ }
+
+ /**
+ * Instantiates and initializes a default {@link SSLSocketFactory} utilizing the resolved
+ * security provider and default TrustManagers. Falls back to the system default socket factory
+ * if initialization fails.
+ */
+ private SSLSocketFactory createDefaultSslSocketFactory() {
+ try {
+ SSLContext sslContext = createSslContext();
+ sslContext.init(null, null, null);
+ return sslContext.getSocketFactory();
+ } catch (Exception e) {
+ // If SSLContext initialization fails entirely, fall back to the JVM's default
+ // system-wide SSLSocketFactory.
+ return (SSLSocketFactory) SSLSocketFactory.getDefault();
+ }
+ }
+
+ /**
+ * Resolves and instantiates the {@link SSLContext} for "TLS", prioritizing the configured
+ * custom security provider, falling back to inserting and loading the Conscrypt provider, and
+ * defaulting to the JRE's standard SSLContext if Conscrypt is unavailable.
+ */
+ private SSLContext createSslContext() throws GeneralSecurityException {
+ // 1. If a custom security provider is configured, use it
+ if (securityProvider != null) {
+ return SSLContext.getInstance("TLS", securityProvider);
+ }
+ // 2. Default: Try Conscrypt (assumed to be available as part of SDK)
+ try {
+ if (Security.getProvider("Conscrypt") == null) {
+ Security.insertProviderAt(Conscrypt.newProvider(), 1);
+ }
+ return SSLContext.getInstance("TLS", "Conscrypt");
+ } catch (Throwable e) {
+ logger.log(
+ Level.WARNING,
+ "Conscrypt security provider not available. Falling back to JDK default.",
+ e);
+ }
+ // 3. Fallback to standard JDK
+ return SSLContext.getInstance("TLS");
+ }
+
/** 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 resolvedSslSocketFactory = resolveSslSocketFactory();
return this.proxy == null
- ? new NetHttpTransport(connectionFactory, sslSocketFactory, hostnameVerifier, isMtls)
- : new NetHttpTransport(this.proxy, sslSocketFactory, hostnameVerifier, isMtls);
+ ? new NetHttpTransport(
+ connectionFactory, resolvedSslSocketFactory, hostnameVerifier, isMtls)
+ : new NetHttpTransport(this.proxy, resolvedSslSocketFactory, hostnameVerifier, isMtls);
}
}
}
diff --git a/google-http-client/src/main/java/com/google/api/client/http/javanet/PqcEnforcingSSLSocketFactory.java b/google-http-client/src/main/java/com/google/api/client/http/javanet/PqcEnforcingSSLSocketFactory.java
new file mode 100644
index 000000000..a06dfa431
--- /dev/null
+++ b/google-http-client/src/main/java/com/google/api/client/http/javanet/PqcEnforcingSSLSocketFactory.java
@@ -0,0 +1,190 @@
+/*
+ * 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.api.client.http.javanet;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import org.conscrypt.Conscrypt;
+
+/**
+ * An {@link SSLSocketFactory} wrapper that configures PQC curves on Conscrypt sockets.
+ *
+ *
If the socket is detected as a Conscrypt socket, it configures the requested named groups
+ * using Conscrypt's direct APIs. If the socket or provider does not support these groups, it falls
+ * back to the default TLS negotiation of the delegate factory.
+ */
+final class PqcEnforcingSSLSocketFactory extends SSLSocketFactory {
+ private final SSLSocketFactory delegate;
+ private final String[] groups;
+
+ PqcEnforcingSSLSocketFactory(SSLSocketFactory delegate, String[] groups) {
+ this.delegate = delegate;
+ this.groups = groups;
+ }
+
+ private static final Logger logger =
+ Logger.getLogger(PqcEnforcingSSLSocketFactory.class.getName());
+
+ private static final boolean CAN_USE_JDK_NAMED_GROUPS_API = checkJdkNamedGroupsApiAvailability();
+
+ private static final AtomicBoolean loggedPqcWarning = new AtomicBoolean(false);
+
+ /**
+ * Checks whether the standard JCA {@link SSLParameters} API contains the {@code setNamedGroups}
+ * method.
+ *
+ *
This capability is required to programmatically enforce or prioritize PQC curves (like
+ * X25519MLKEM768) when using non-Conscrypt security providers (such as Bouncy Castle or standard
+ * JRE SunJSSE).
+ *
+ *
Because this library compiles to Java 8 bytecode target, the standard {@code setNamedGroups}
+ * method (introduced in Java 20) is not available at compile time. This reflection capability
+ * check runs once on class loading to check availability, allowing us to safely skip runtime
+ * reflection calls and avoid throwing expensive {@link NoSuchMethodException}s on older JVMs.
+ */
+ private static boolean checkJdkNamedGroupsApiAvailability() {
+ try {
+ SSLParameters.class.getMethod("setNamedGroups", String[].class);
+ return true;
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ /**
+ * Configures post-quantum cryptography (PQC) hybrid curves on the socket if it is an SSLSocket
+ * managed by Conscrypt.
+ *
+ *
If the socket is an instance of {@link SSLSocket} and Conscrypt is the active provider
+ * backing it, this method invokes Conscrypt's direct APIs to set the prioritized named groups
+ * (curves) to negotiate during the TLS handshake (e.g., X25519MLKEM768 and classical X25519).
+ *
+ *
It catches {@link Throwable} to ensure that if Conscrypt native JNI libraries fail to link
+ * or load (which throws linkage errors like {@link UnsatisfiedLinkError}), the configuration
+ * falls back silently to default TLS negotiation instead of failing the connection.
+ */
+ private Socket configure(Socket socket) {
+ if (socket instanceof SSLSocket) {
+ SSLSocket sslSocket = (SSLSocket) socket;
+ try {
+ // 1. If it is a Conscrypt socket, use Conscrypt's direct JNI API first
+ if (Conscrypt.isConscrypt(sslSocket)) {
+ Conscrypt.setNamedGroups(sslSocket, groups);
+ return socket;
+ }
+ // 2. Otherwise (user-provided security provider or JDK fallback), try standard JSSE
+ // reflection (Java 20+)
+ if (CAN_USE_JDK_NAMED_GROUPS_API) {
+ trySetNamedGroupsViaReflection(sslSocket, groups);
+ } else {
+ if (loggedPqcWarning.compareAndSet(false, true)) {
+ logger.log(
+ Level.WARNING,
+ "The Java 20+ SSLParameters.setNamedGroups API is not available on this JRE, "
+ + "and Conscrypt is not active. Cannot configure PQC curve negotiation; "
+ + "falling back to the standard TLS connection configuration.");
+ }
+ }
+ } catch (Throwable ignored) {
+ // Fall back silently if not supported by the provider/platform
+ }
+ }
+ return socket;
+ }
+
+ /**
+ * Configures named groups (curves) on standard JSSE socket parameters using reflection.
+ *
+ *
We must use reflection to invoke {@code SSLParameters.setNamedGroups} because the
+ * compilation target of this library is Java 8, which does not contain the standard {@code
+ * setNamedGroups} API (introduced in Java 20).
+ *
+ *
Using reflection allows compilation against Java 8 while enabling the application to
+ * dynamically configure post-quantum curve negotiation at runtime if it is executed in a Java 20+
+ * environment with a compliant JCA provider (such as Bouncy Castle or standard SunJSSE).
+ */
+ private static boolean trySetNamedGroupsViaReflection(SSLSocket sslSocket, String[] groups) {
+ if (!CAN_USE_JDK_NAMED_GROUPS_API) {
+ return false;
+ }
+ try {
+ Object sslParameters = sslSocket.getClass().getMethod("getSSLParameters").invoke(sslSocket);
+ if (sslParameters != null) {
+ Method setNamedGroupsMethod =
+ sslParameters.getClass().getMethod("setNamedGroups", String[].class);
+ setNamedGroupsMethod.invoke(sslParameters, (Object) groups);
+ sslSocket
+ .getClass()
+ .getMethod("setSSLParameters", sslParameters.getClass())
+ .invoke(sslSocket, sslParameters);
+ return true;
+ }
+ } catch (Throwable ignored) {
+ // Reflection failed (e.g. method not found or security manager restrictions)
+ }
+ return false;
+ }
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return delegate.getDefaultCipherSuites();
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return delegate.getSupportedCipherSuites();
+ }
+
+ @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() throws IOException {
+ return configure(delegate.createSocket());
+ }
+
+ @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/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..b51ded1ac 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
@@ -38,6 +38,8 @@
import java.security.KeyStore;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.security.Provider;
+import java.security.Security;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -241,4 +243,14 @@ public void handle(HttpExchange httpExchange) throws IOException {
response.disconnect();
}
}
+
+ @Test
+ public void testCustomSecurityProvider() throws Exception {
+ Provider customProvider = new Provider("TestProvider", 1.0, "Test Provider") {};
+ NetHttpTransport transport = new NetHttpTransport.Builder()
+ .setSecurityProvider(customProvider)
+ .build();
+ // Verify it compiles and builds successfully with a custom provider
+ assertTrue(transport != null);
+ }
}
diff --git a/google-http-client/src/test/java/com/google/api/client/http/javanet/PqcEnforcingSSLSocketFactoryTest.java b/google-http-client/src/test/java/com/google/api/client/http/javanet/PqcEnforcingSSLSocketFactoryTest.java
new file mode 100644
index 000000000..9e998e733
--- /dev/null
+++ b/google-http-client/src/test/java/com/google/api/client/http/javanet/PqcEnforcingSSLSocketFactoryTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.api.client.http.javanet;
+
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import javax.net.ssl.HandshakeCompletedListener;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+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 PqcEnforcingSSLSocketFactoryTest {
+
+ private static final String[] TEST_GROUPS = new String[] {"X25519MLKEM768", "X25519"};
+
+ @Test
+ public void testDelegationAndNonSslSocket() throws Exception {
+ Socket plainSocket = new Socket();
+ FakeSSLSocketFactory delegate = new FakeSSLSocketFactory(plainSocket);
+ PqcEnforcingSSLSocketFactory factory = new PqcEnforcingSSLSocketFactory(delegate, TEST_GROUPS);
+
+ Socket result = factory.createSocket();
+ assertSame(plainSocket, result);
+ assertTrue(delegate.createSocketCalled);
+ }
+
+ @Test
+ public void testConfigureNonConscryptSslSocket() throws Exception {
+ FakeSSLSocket sslSocket = new FakeSSLSocket();
+ FakeSSLSocketFactory delegate = new FakeSSLSocketFactory(sslSocket);
+ PqcEnforcingSSLSocketFactory factory = new PqcEnforcingSSLSocketFactory(delegate, TEST_GROUPS);
+
+ Socket result = factory.createSocket();
+ assertSame(sslSocket, result);
+ assertTrue(delegate.createSocketCalled);
+ }
+
+ @Test
+ public void testConfigureConscryptSslSocket() throws Exception {
+ try {
+ java.security.Provider provider = org.conscrypt.Conscrypt.newProvider();
+ SSLContext sslContext = SSLContext.getInstance("TLS", provider);
+ sslContext.init(null, null, null);
+ } catch (Throwable t) {
+ org.junit.Assume.assumeNoException(
+ "Conscrypt JNI library is not available on this platform", t);
+ }
+
+ java.security.Provider provider = org.conscrypt.Conscrypt.newProvider();
+ SSLContext sslContext = SSLContext.getInstance("TLS", provider);
+ sslContext.init(null, null, null);
+ SSLSocketFactory conscryptFactory = sslContext.getSocketFactory();
+
+ PqcEnforcingSSLSocketFactory factory =
+ new PqcEnforcingSSLSocketFactory(conscryptFactory, TEST_GROUPS);
+
+ Socket socket = factory.createSocket();
+ try {
+ if (socket instanceof SSLSocket) {
+ SSLSocket sslSocket = (SSLSocket) socket;
+ assertTrue(org.conscrypt.Conscrypt.isConscrypt(sslSocket));
+ }
+ } finally {
+ socket.close();
+ }
+ }
+
+ private static class FakeSSLSocketFactory extends SSLSocketFactory {
+ private final Socket socketToReturn;
+ boolean createSocketCalled = false;
+
+ FakeSSLSocketFactory(Socket socketToReturn) {
+ this.socketToReturn = socketToReturn;
+ }
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return new String[0];
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return new String[0];
+ }
+
+ @Override
+ public Socket createSocket() throws IOException {
+ createSocketCalled = true;
+ return socketToReturn;
+ }
+
+ @Override
+ public Socket createSocket(Socket s, String host, int port, boolean autoClose)
+ throws IOException {
+ createSocketCalled = true;
+ return socketToReturn;
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException {
+ createSocketCalled = true;
+ return socketToReturn;
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
+ throws IOException {
+ createSocketCalled = true;
+ return socketToReturn;
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ createSocketCalled = true;
+ return socketToReturn;
+ }
+
+ @Override
+ public Socket createSocket(
+ InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
+ createSocketCalled = true;
+ return socketToReturn;
+ }
+ }
+
+ private static class FakeSSLSocket extends SSLSocket {
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return new String[0];
+ }
+
+ @Override
+ public String[] getEnabledCipherSuites() {
+ return new String[0];
+ }
+
+ @Override
+ public void setEnabledCipherSuites(String[] suites) {}
+
+ @Override
+ public String[] getSupportedProtocols() {
+ return new String[0];
+ }
+
+ @Override
+ public String[] getEnabledProtocols() {
+ return new String[0];
+ }
+
+ @Override
+ public void setEnabledProtocols(String[] protocols) {}
+
+ @Override
+ public SSLSession getSession() {
+ return null;
+ }
+
+ @Override
+ public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {}
+
+ @Override
+ public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {}
+
+ @Override
+ public void startHandshake() throws IOException {}
+
+ @Override
+ public void setUseClientMode(boolean mode) {}
+
+ @Override
+ public boolean getUseClientMode() {
+ return false;
+ }
+
+ @Override
+ public void setNeedClientAuth(boolean need) {}
+
+ @Override
+ public boolean getNeedClientAuth() {
+ return false;
+ }
+
+ @Override
+ public void setWantClientAuth(boolean want) {}
+
+ @Override
+ public boolean getWantClientAuth() {
+ return false;
+ }
+
+ @Override
+ public void setEnableSessionCreation(boolean flag) {}
+
+ @Override
+ public boolean getEnableSessionCreation() {
+ return false;
+ }
+ }
+}