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.0</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 @@ -28,12 +28,17 @@
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.SSLSocketFactory;
import org.conscrypt.Conscrypt;

/**
* Thread-safe HTTP low-level transport based on the {@code java.net} package.
Expand Down Expand Up @@ -81,6 +86,16 @@ 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());

/**
* Post-Quantum Cryptography (PQC) hybrid key exchange groups ordered by preference.
*
* <p>"X25519MLKEM768" is the primary hybrid post-quantum key exchange algorithm combining X25519
* elliptic curve with ML-KEM-768. "X25519" is provided as a classical fallback group.
*/
private static final String[] PQC_GROUPS = new String[] {"X25519MLKEM768", "X25519"};

private final ConnectionFactory connectionFactory;

/** SSL socket factory or {@code null} for the default. */
Expand Down Expand Up @@ -208,6 +223,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 +384,86 @@ 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() {
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <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.
*/
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.
*
* <p>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).
*
* <p>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.
*
* <p>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).
*
* <p>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.
*
* <p>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).
*
* <p>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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Loading
Loading