Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions google-http-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@
<groupId>io.opencensus</groupId>
<artifactId>opencensus-contrib-http-util</artifactId>
</dependency>
<dependency>
<groupId>org.conscrypt</groupId>
<artifactId>conscrypt-openjdk-uber</artifactId>
<version>2.6-alpha5</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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. */
Expand All @@ -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.
*
* <p>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);
}

/**
Expand Down Expand Up @@ -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;

Expand All @@ -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 <a
* href="http://docs.oracle.com/javase/7/docs/api/java/net/doc-files/net-properties.html">system
Expand Down Expand Up @@ -362,14 +401,162 @@ public Builder setHostnameVerifier(HostnameVerifier hostnameVerifier) {
return this;
}

/**
* Sets the security provider to use for SSL context.
*
* <p>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.
*
* <p>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.
*
* <p>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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}

/**
Expand All @@ -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");
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Loading