Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2026 Google Inc.
*
* 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.net.InetAddress;
import java.net.Socket;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

/**
* An {@link SSLSocketFactory} wrapper that intercepts all socket creation entrypoints (both default
* and bound socket creations) and applies the custom user-provided {@link SslSocketConfigurator}
* callback to the socket before returning it.
*
* <p>This factory delegates all standard socket actions to the underlying default or custom {@link
* SSLSocketFactory} instance. Sockets are intercepted and cast to {@link SSLSocket} for callback
* execution.
*
* @since 2.1.2
*/
final class ConfigurableSSLSocketFactory extends SSLSocketFactory {
private final SSLSocketFactory delegate;
private final SslSocketConfigurator configurator;

ConfigurableSSLSocketFactory(SSLSocketFactory delegate, SslSocketConfigurator configurator) {
this.delegate = delegate;
this.configurator = configurator;
}

private Socket configure(Socket socket) {
if (socket instanceof SSLSocket && configurator != null) {
configurator.configure((SSLSocket) socket);
}
return socket;
}

@Override
public String[] getDefaultCipherSuites() {
return delegate.getDefaultCipherSuites();
}

@Override
public String[] getSupportedCipherSuites() {
return delegate.getSupportedCipherSuites();
}

@Override
public Socket createSocket() throws IOException {
return configure(delegate.createSocket());
}

@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(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 @@ -28,12 +28,15 @@
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.Provider;
import java.security.cert.CertificateFactory;
import java.util.Arrays;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

/**
* Thread-safe HTTP low-level transport based on the {@code java.net} package.
Expand Down Expand Up @@ -84,7 +87,7 @@ private static Proxy defaultProxy() {
private final ConnectionFactory connectionFactory;

/** SSL socket factory or {@code null} for the default. */
private final SSLSocketFactory sslSocketFactory;
final SSLSocketFactory sslSocketFactory;

/** Host name verifier or {@code null} for the default. */
private final HostnameVerifier hostnameVerifier;
Expand Down Expand Up @@ -189,6 +192,12 @@ public static final class Builder {
/** SSL socket factory or {@code null} for the default. */
private SSLSocketFactory sslSocketFactory;

/** Security provider to use or {@code null} for default. */
private Provider securityProvider;

/** Custom SSLSocket configurator or {@code null} to disable callback configuration. */
SslSocketConfigurator sslSocketConfigurator;

/** Host name verifier or {@code null} for the default. */
private HostnameVerifier hostnameVerifier;

Expand Down Expand Up @@ -289,8 +298,9 @@ public Builder trustCertificatesFromStream(InputStream certificateStream)
* @since 1.14
*/
public Builder trustCertificates(KeyStore trustStore) throws GeneralSecurityException {
SSLContext sslContext = SslUtils.getTlsSslContext();
SslUtils.initSslContext(sslContext, trustStore, SslUtils.getPkixTrustManagerFactory());
SSLContext sslContext = SslUtils.getTlsSslContext(securityProvider);
SslUtils.initSslContext(
sslContext, trustStore, SslUtils.getPkixTrustManagerFactory(securityProvider));
return setSslSocketFactory(sslContext.getSocketFactory());
}

Expand All @@ -313,14 +323,14 @@ public Builder trustCertificates(
if (mtlsKeyStore != null && mtlsKeyStore.size() > 0) {
this.isMtls = true;
}
SSLContext sslContext = SslUtils.getTlsSslContext();
SSLContext sslContext = SslUtils.getTlsSslContext(securityProvider);
SslUtils.initSslContext(
sslContext,
trustStore,
SslUtils.getPkixTrustManagerFactory(),
SslUtils.getPkixTrustManagerFactory(securityProvider),
mtlsKeyStore,
mtlsKeyStorePassword,
SslUtils.getDefaultKeyManagerFactory());
SslUtils.getDefaultKeyManagerFactory(securityProvider));
return setSslSocketFactory(sslContext.getSocketFactory());
}

Expand All @@ -345,12 +355,69 @@ public SSLSocketFactory getSslSocketFactory() {
return sslSocketFactory;
}

/** Sets the SSL socket factory or {@code null} for the default. */
/**
* Sets the SSL socket factory or {@code null} for the default.
*
* <p>Note: If a custom {@link SslSocketConfigurator} is also provided, it will wrap and apply
* its configuration callback to all sockets created by this factory.
*/
public Builder setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
this.sslSocketFactory = sslSocketFactory;
return this;
}

/**
* Sets the custom security provider or {@code null} to use the default JRE provider.
*
* <p>When enabling Post-Quantum Cryptography (PQC) transport:
*
* <ul>
* <li>On JDK 8-19: A custom JCA provider (such as Conscrypt or BouncyCastle) must be
* configured via this method, in addition to configuring a custom {@link
* SslSocketConfigurator} callback to select the hybrid/PQC curves using provider-specific
* APIs.
* <li>On JDK 20-26: A custom provider (like Conscrypt) is recommended, but a custom {@link
* SslSocketConfigurator} invoking {@code SSLParameters.setNamedGroups(String[])} directly
* can be used natively without a custom JCA provider if standard JSSE supports the
* curves.
* <li>On JDK 27+: Neither a custom provider nor a configurator is required as PQC algorithms
* are negotiated natively by default.
* </ul>
*
* @param securityProvider provider to use
*/
public Builder setSecurityProvider(Provider securityProvider) {
Comment thread
lqiu96 marked this conversation as resolved.
this.securityProvider = securityProvider;
return this;
}

/**
* Sets the custom {@link SslSocketConfigurator} callback to configure active SSLSockets.
*
* <p>If both a custom {@link SSLSocketFactory} (via {@link
* #setSslSocketFactory(SSLSocketFactory)}) and a custom configurator are set, the configurator
* callback will be applied to all sockets created by the custom socket factory. If no custom
* factory is provided, the configurator will wrap and apply to sockets created by the default
* resolved socket factory.
*
* <p>When enabling Post-Quantum Cryptography (PQC) transport:
*
* <ul>
* <li>On JDK 20-26: Callers can configure a custom {@link SslSocketConfigurator} that sets
* the named groups (such as {@code X25519MLKEM768}) directly on {@code SSLParameters}.
* <li>On JDK 8-19: Callers must provide a custom {@link SslSocketConfigurator} implementation
* to inspect the socket types and invoke provider-specific API extensions (e.g. Conscrypt
* JNI interfaces) to configure the curves.
* </ul>
*
* @param configurator the callback configurator
* @since 2.1.2
*/
public Builder setSslSocketConfigurator(SslSocketConfigurator configurator) {
this.sslSocketConfigurator = configurator;
return this;
}

/** Returns the host name verifier or {@code null} for the default. */
public HostnameVerifier getHostnameVerifier() {
return hostnameVerifier;
Expand All @@ -362,14 +429,63 @@ public Builder setHostnameVerifier(HostnameVerifier hostnameVerifier) {
return this;
}

/**
* Resolves the {@link SSLSocketFactory} to be used by the transport.
*
* <p>If a custom factory has been set via {@link #setSslSocketFactory(SSLSocketFactory)}, it
* will be returned. Otherwise, a default SSL socket factory will be constructed via {@link
* #createDefaultSslSocketFactory()}.
*
* @return the resolved {@link SSLSocketFactory}
*/
SSLSocketFactory resolveSslSocketFactory() {
if (securityProvider == null && sslSocketConfigurator == null) {
return sslSocketFactory;
}
SSLSocketFactory factory =
sslSocketFactory != null ? sslSocketFactory : createDefaultSslSocketFactory();
if (sslSocketConfigurator != null) {
return new ConfigurableSSLSocketFactory(factory, sslSocketConfigurator);
}
return factory;
}

/**
* Constructs a default {@link SSLSocketFactory} configured with the specified {@link Provider}.
*
* <p>This method initializes an {@link SSLContext} and resolves its {@link TrustManagerFactory}
* using the same security provider (if provided), ensuring compatibility for TLS handshakes
* when using custom providers (such as Conscrypt).
*
* @return the initialized default {@link SSLSocketFactory}
*/
SSLSocketFactory createDefaultSslSocketFactory() {
try {
SSLContext sslContext = SslUtils.getTlsSslContext(securityProvider);
TrustManager[] trustManagers = null;
if (securityProvider != null) {
TrustManagerFactory tmf = SslUtils.getDefaultTrustManagerFactory(securityProvider);
tmf.init((KeyStore) null);
trustManagers = tmf.getTrustManagers();
}
sslContext.init(null, trustManagers, null);
return sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
// Halt execution because the SSLContext cannot be initialized with the requested
// configuration.
throw new IllegalStateException("Failed to initialize SSLSocketFactory.", e);
}
}

/** 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 resolvedFactory = resolveSslSocketFactory();
Comment thread
lqiu96 marked this conversation as resolved.
return this.proxy == null
? new NetHttpTransport(connectionFactory, sslSocketFactory, hostnameVerifier, isMtls)
: new NetHttpTransport(this.proxy, sslSocketFactory, hostnameVerifier, isMtls);
? new NetHttpTransport(connectionFactory, resolvedFactory, hostnameVerifier, isMtls)
: new NetHttpTransport(this.proxy, resolvedFactory, hostnameVerifier, isMtls);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright (c) 2026 Google Inc.
*
* 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 javax.net.ssl.SSLSocket;

/**
* A callback interface allowing users to programmatically configure active {@link SSLSocket}
* parameters (e.g., named groups, cipher suites, application protocols) before the TLS handshake
* starts.
*
* <p>Exposing this hook allows users to customize advanced TLS capabilities that are not
* configurable via standard JVM system properties, or require custom security provider APIs (e.g.,
* Conscrypt or BouncyCastle). Typical use cases include:
*
* <ul>
* <li>Enabling Post-Quantum Cryptography (PQC) hybrid curves (e.g., X25519MLKEM768).
* <li>Configuring Application-Layer Protocol Negotiation (ALPN) for HTTP/2.
* <li>Restricting cipher suites or protocol versions dynamically based on environment.
* </ul>
*
* <h3>JDK Version and Provider Compatibility Matrix for PQC</h3>
*
* <ul>
* <li><b>JDK 27+</b>: PQC algorithms are supported natively by default. No custom configurator or
* security provider is required.
* <li><b>JDK 20-26</b>: Callers can configure named groups natively using standard JRE JSSE APIs
* (via {@code SSLParameters.setNamedGroups(String[])}), as shown in the JDK 20+ example
* below.
* <li><b>JDK 8-19</b>: The standard JRE does not expose APIs to set named groups. Callers must
* register a custom security provider (like Conscrypt JNI or BouncyCastle) and configure
* sockets using the provider-specific APIs demonstrated in the examples below.
* </ul>
*
* <h3>Conscrypt Configuration Example</h3>
*
* <pre>{@code
* builder.setSslSocketConfigurator(new SslSocketConfigurator() {
* @Override
* public void configure(SSLSocket socket) {
* if (org.conscrypt.Conscrypt.isConscrypt(socket)) {
* org.conscrypt.Conscrypt.setNamedGroups(socket, new String[] {"X25519MLKEM768", "X25519"});
* }
* }
* });
* }</pre>
*
* <h3>BouncyCastle Configuration Example</h3>
*
* <pre>{@code
* builder.setSslSocketConfigurator(new SslSocketConfigurator() {
* @Override
* public void configure(SSLSocket socket) {
* if (socket instanceof org.bouncycastle.jsse.BCSSLSocket) {
* org.bouncycastle.jsse.BCSSLSocket bcSocket = (org.bouncycastle.jsse.BCSSLSocket) socket;
* org.bouncycastle.jsse.BCSSLParameters bcParams = bcSocket.getParameters();
* bcParams.setNamedGroups(new String[] {"X25519MLKEM768", "X25519"});
* bcSocket.setParameters(bcParams);
* }
* }
* });
* }</pre>
*
* <h3>JDK 20+ Standard JSSE Configuration Example</h3>
*
* <p>Note: This example requires compilation and runtime environment running JDK 20+.
*
* <pre>{@code
* builder.setSslSocketConfigurator(new SslSocketConfigurator() {
* @Override
* public void configure(SSLSocket socket) {
* javax.net.ssl.SSLParameters parameters = socket.getSSLParameters();
* parameters.setNamedGroups(new String[] {"X25519MLKEM768", "X25519"});
* socket.setSSLParameters(parameters);
* }
* });
* }</pre>
*
* @since 2.1.2
*/
public interface SslSocketConfigurator {
/**
* Configures the active TLS socket parameters before the handshake starts.
*
* @param sslSocket the newly created SSLSocket connection
*/
void configure(SSLSocket sslSocket);
}
Loading
Loading