From 3aa54c849e7d444133ede0d9810b37b625ce6df6 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Mon, 29 Jun 2026 20:42:24 +0000 Subject: [PATCH 1/2] feat(javanet): support Conscrypt security provider and PQC hybrid negotiation Adds fallback logic in NetHttpTransport to default to Conscrypt for SSLContext and fall back to the standard JDK TLS if Conscrypt is not available. Enables setting a custom security provider on the builder which takes precedence. Enforces PQC hybrid named groups (X25519MLKEM768, X25519) on Conscrypt sockets via a wrapped SSLSocketFactory. TAG=agy CONV=385b9ab5-874c-4c9a-b331-66dab51fef61 --- google-http-client/pom.xml | 6 + .../client/http/javanet/NetHttpTransport.java | 170 +++++++++++++++++- .../http/javanet/NetHttpTransportTest.java | 10 ++ 3 files changed, 184 insertions(+), 2 deletions(-) diff --git a/google-http-client/pom.xml b/google-http-client/pom.xml index 16a8dd127..02b3d17ec 100644 --- a/google-http-client/pom.xml +++ b/google-http-client/pom.xml @@ -163,6 +163,12 @@ io.opencensus opencensus-contrib-http-util + + org.conscrypt + conscrypt-openjdk-uber + 2.6-alpha5 + provided + com.google.guava 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..743e133ed 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 @@ -23,17 +23,25 @@ import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; +import java.net.Socket; import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.Provider; +import java.security.Security; import java.security.cert.CertificateFactory; import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; +import org.conscrypt.Conscrypt; /** * Thread-safe HTTP low-level transport based on the {@code java.net} package. @@ -81,6 +89,10 @@ private static Proxy defaultProxy() { private static final String SHOULD_USE_PROXY_FLAG = "com.google.api.client.should_use_proxy"; + private static final Logger logger = Logger.getLogger(NetHttpTransport.class.getName()); + + private static final String[] PQC_GROUPS = new String[] {"X25519MLKEM768", "X25519"}; + private final ConnectionFactory connectionFactory; /** SSL socket factory or {@code null} for the default. */ @@ -208,6 +220,13 @@ public static final class Builder { /** Whether the transport is mTLS. Default value is {@code false}. */ private boolean isMtls; + /** + * Security provider to use for SSL context, or {@code null} for the default fallback. If not + * set, {@link NetHttpTransport} defaults to using Conscrypt (if available) and falls back to + * the default JDK provider. + */ + private Provider securityProvider; + /** * Sets the HTTP proxy or {@code null} to use the proxy settings from system @@ -362,14 +381,161 @@ 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() { + SSLSocketFactory resolvedFactory = sslSocketFactory; + if (resolvedFactory == null) { + try { + SSLContext sslContext = null; + // 1. If a custom security provider is configured, use it + if (securityProvider != null) { + sslContext = SSLContext.getInstance("TLS", securityProvider); + } else { + // 2. Default: Try Conscrypt (assumed to be available as part of SDK) + try { + if (Security.getProvider("Conscrypt") == null) { + Security.insertProviderAt(Conscrypt.newProvider(), 1); + } + sslContext = SSLContext.getInstance("TLS", "Conscrypt"); + } catch (NoClassDefFoundError | Exception e) { + logger.log( + Level.WARNING, + "Conscrypt security provider not available. Falling back to JDK default.", + e); + } + } + + if (sslContext == null) { + // 3. Fallback to standard JDK + sslContext = SSLContext.getInstance("TLS"); + } + + // Initialize SSLContext: + // - First param (KeyManager[]): null (use default JDK key managers) + // - Second param (TrustManager[]): null (use default JDK trust managers) + // - Third param (SecureRandom): null (use default JDK secure random) + sslContext.init(null, null, null); + resolvedFactory = sslContext.getSocketFactory(); + } catch (Exception e) { + // If SSLContext initialization fails entirely, fall back to the JVM's default + // system-wide SSLSocketFactory. + resolvedFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + } + } + + // Wrap factory to enforce PQC hybrid groups + return new PqcEnforcingSSLSocketFactory(resolvedFactory, PQC_GROUPS); + } + /** 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); + } + } + /** + * An {@link SSLSocketFactory} wrapper that enforces Post-Quantum Cryptography (PQC) hybrid named + * groups (such as X25519MLKEM768) on compatible sockets. + * + *

This wrapper is applied to the final resolved {@link SSLSocketFactory} before the transport + * is built. This ensures that even if the factory was configured and initialized via custom trust + * stores (e.g. {@link Builder#trustCertificates} which internally invokes {@code + * SslUtils.initSslContext}), the resulting sockets are still intercepted and configured to + * request PQC hybrid groups. + * + *

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. + */ + private static class PqcEnforcingSSLSocketFactory extends SSLSocketFactory { + private final SSLSocketFactory delegate; + private final String[] groups; + + PqcEnforcingSSLSocketFactory(SSLSocketFactory delegate, String[] groups) { + this.delegate = delegate; + this.groups = groups; + } + + private Socket configure(Socket socket) { + if (socket instanceof SSLSocket) { + SSLSocket sslSocket = (SSLSocket) socket; + try { + if (Conscrypt.isConscrypt(sslSocket)) { + Conscrypt.setNamedGroups(sslSocket, groups); + } + } catch (NoClassDefFoundError | Exception ignored) { + // Fall back silently if Conscrypt classes are not loaded or the socket is not Conscrypt + } + } + return socket; + } + + @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..c04365b91 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 @@ -36,6 +36,7 @@ import java.net.InetSocketAddress; import java.net.URL; import java.security.KeyStore; +import java.security.Provider; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.junit.Test; @@ -241,4 +242,13 @@ 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); + } } From 3497a47f55e5c4378cb32dd947d830ebee751b94 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Tue, 30 Jun 2026 19:57:43 +0000 Subject: [PATCH 2/2] feat(pqc): prioritize Conscrypt TrustManagerFactory and SSLContext by default, supporting default constructors TAG=agy CONV=385b9ab5-874c-4c9a-b331-66dab51fef61 --- .../client/http/javanet/NetHttpTransport.java | 35 +++++++++++++++---- .../com/google/api/client/util/SslUtils.java | 23 +++++++----- 2 files changed, 43 insertions(+), 15 deletions(-) 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 743e133ed..53574f1d0 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 @@ -85,6 +85,13 @@ private static Proxy defaultProxy() { static { Arrays.sort(SUPPORTED_METHODS); + try { + if (Security.getProvider("Conscrypt") == null) { + Security.addProvider(Conscrypt.newProvider()); + } + } catch (NoClassDefFoundError | Exception ignored) { + // Conscrypt not available on classpath, fall back silently + } } private static final String SHOULD_USE_PROXY_FLAG = "com.google.api.client.should_use_proxy"; @@ -104,13 +111,16 @@ private static Proxy defaultProxy() { /** Whether the transport is mTLS. Default value is {@code false}. */ private final boolean isMtls; - /** - * Constructor with the default behavior. - * - *

Instead use {@link Builder} to modify behavior. - */ public NetHttpTransport() { - this((ConnectionFactory) null, null, null, false); + this(new Builder()); + } + + private NetHttpTransport(Builder builder) { + this( + builder.connectionFactory, + builder.resolveSslSocketFactory(), + builder.hostnameVerifier, + builder.isMtls); } /** @@ -198,6 +208,16 @@ protected NetHttpRequest buildRequest(String method, String url) throws IOExcept */ public static final class Builder { + static { + try { + if (Security.getProvider("Conscrypt") == null) { + Security.addProvider(org.conscrypt.Conscrypt.newProvider()); + } + } catch (NoClassDefFoundError | Exception ignored) { + // Conscrypt not available on classpath, fall back silently + } + } + /** SSL socket factory or {@code null} for the default. */ private SSLSocketFactory sslSocketFactory; @@ -413,7 +433,7 @@ private SSLSocketFactory resolveSslSocketFactory() { // 2. Default: Try Conscrypt (assumed to be available as part of SDK) try { if (Security.getProvider("Conscrypt") == null) { - Security.insertProviderAt(Conscrypt.newProvider(), 1); + Security.addProvider(Conscrypt.newProvider()); } sslContext = SSLContext.getInstance("TLS", "Conscrypt"); } catch (NoClassDefFoundError | Exception e) { @@ -458,6 +478,7 @@ public NetHttpTransport build() { : new NetHttpTransport(this.proxy, resolvedSslSocketFactory, hostnameVerifier, isMtls); } } + /** * An {@link SSLSocketFactory} wrapper that enforces Post-Quantum Cryptography (PQC) hybrid named * groups (such as X25519MLKEM768) on compatible sockets. 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..21c4094a6 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 @@ -51,17 +51,20 @@ public static SSLContext getSslContext() throws NoSuchAlgorithmException { * @since 1.14 */ public static SSLContext getTlsSslContext() throws NoSuchAlgorithmException { - return SSLContext.getInstance("TLS"); + try { + return SSLContext.getInstance("TLS", "Conscrypt"); + } catch (Exception e) { + return SSLContext.getInstance("TLS"); + } } - /** - * Returns the default trust manager factory. - * - * @since 1.14 - */ public static TrustManagerFactory getDefaultTrustManagerFactory() throws NoSuchAlgorithmException { - return TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + try { + return TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm(), "Conscrypt"); + } catch (Exception e) { + return TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + } } /** @@ -70,7 +73,11 @@ public static TrustManagerFactory getDefaultTrustManagerFactory() * @since 1.14 */ public static TrustManagerFactory getPkixTrustManagerFactory() throws NoSuchAlgorithmException { - return TrustManagerFactory.getInstance("PKIX"); + try { + return TrustManagerFactory.getInstance("PKIX", "Conscrypt"); + } catch (Exception e) { + return TrustManagerFactory.getInstance("PKIX"); + } } /**