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..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 @@ -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. @@ -77,10 +85,21 @@ 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"; + 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. */ @@ -92,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); } /** @@ -186,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; @@ -208,6 +240,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 +401,162 @@ 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.addProvider(Conscrypt.newProvider()); + } + 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/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"); + } } /** 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); + } }