Instead use {@link Builder} to modify behavior.
*/
public NetHttpTransport() {
- this((ConnectionFactory) null, null, null, false);
+ this((ConnectionFactory) null, getDefaultSslSocketFactory(), null, false);
}
/**
@@ -171,7 +186,8 @@ protected NetHttpRequest buildRequest(String method, String url) throws IOExcept
secureConnection.setHostnameVerifier(hostnameVerifier);
}
if (sslSocketFactory != null) {
- secureConnection.setSSLSocketFactory(sslSocketFactory);
+ secureConnection.setSSLSocketFactory(
+ new PqcPeerHostSSLSocketFactory(sslSocketFactory, connUrl.getHost()));
}
}
return new NetHttpRequest(connection);
@@ -294,6 +310,40 @@ public Builder trustCertificates(KeyStore trustStore) throws GeneralSecurityExce
return setSslSocketFactory(sslContext.getSocketFactory());
}
+ /**
+ * Sets the SSL socket factory based on a root certificate trust store and a specific security
+ * provider.
+ *
+ * @param trustStore certificate trust store
+ * @param provider security provider to use for SSL context
+ * @since 1.39
+ */
+ public Builder trustCertificates(KeyStore trustStore, Provider provider)
+ throws GeneralSecurityException {
+ SSLContext sslContext = SslUtils.getTlsSslContext(provider);
+ SslUtils.initSslContext(sslContext, trustStore, SslUtils.getPkixTrustManagerFactory());
+ return setSslSocketFactory(sslContext.getSocketFactory());
+ }
+
+ /**
+ * Sets the SSL socket factory based on a root certificate trust store and a specific security
+ * provider name.
+ *
+ * @param trustStore certificate trust store
+ * @param providerName security provider name to use for SSL context
+ * @since 1.39
+ */
+ public Builder trustCertificates(KeyStore trustStore, String providerName)
+ throws GeneralSecurityException {
+ try {
+ SSLContext sslContext = SslUtils.getTlsSslContext(providerName);
+ SslUtils.initSslContext(sslContext, trustStore, SslUtils.getPkixTrustManagerFactory());
+ return setSslSocketFactory(sslContext.getSocketFactory());
+ } catch (NoSuchProviderException e) {
+ throw new GeneralSecurityException(e);
+ }
+ }
+
/**
* {@link Beta}
* Sets the SSL socket factory based on a root certificate trust store and a client certificate
@@ -367,9 +417,11 @@ public NetHttpTransport build() {
if (System.getProperty(SHOULD_USE_PROXY_FLAG) != null) {
setProxy(defaultProxy());
}
+ SSLSocketFactory factory =
+ sslSocketFactory != null ? sslSocketFactory : getDefaultSslSocketFactory();
return this.proxy == null
- ? new NetHttpTransport(connectionFactory, sslSocketFactory, hostnameVerifier, isMtls)
- : new NetHttpTransport(this.proxy, sslSocketFactory, hostnameVerifier, isMtls);
+ ? new NetHttpTransport(connectionFactory, factory, hostnameVerifier, isMtls)
+ : new NetHttpTransport(this.proxy, factory, hostnameVerifier, isMtls);
}
}
}
diff --git a/google-http-client/src/main/java/com/google/api/client/http/javanet/PqcPeerHostSSLSocketFactory.java b/google-http-client/src/main/java/com/google/api/client/http/javanet/PqcPeerHostSSLSocketFactory.java
new file mode 100644
index 000000000..e988b9e45
--- /dev/null
+++ b/google-http-client/src/main/java/com/google/api/client/http/javanet/PqcPeerHostSSLSocketFactory.java
@@ -0,0 +1,121 @@
+/*
+ * 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.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * A custom {@link SSLSocketFactory} wrapper designed to ensure that the peer hostname is preserved
+ * during connection establishment.
+ *
+ *
When secure connections are initiated via Java's default {@code HttpURLConnection}, some + * socket-creation flows only provide an {@link InetAddress} instead of the DNS hostname. Under + * hybrid TLS configurations—such as Post-Quantum Cryptography (PQC)—underlying JSSE security + * providers (Conscrypt or Bouncy Castle JSSE) rely on the peer hostname string to enable proper + * Server Name Indication (SNI) extensions, negotiate PQC cipher suites, and perform endpoint + * identification. + * + *
This wrapper intercepts socket creation requests, manually establishes the TCP socket
+ * connection to the target address, and wraps it using the delegate's hostname-aware factory
+ * method.
+ */
+class PqcPeerHostSSLSocketFactory extends SSLSocketFactory {
+
+ private final SSLSocketFactory delegate;
+ private final String host;
+
+ /**
+ * Constructs a new {@link PqcPeerHostSSLSocketFactory} wrapping the provided delegate.
+ *
+ * @param delegate the underlying {@link SSLSocketFactory}
+ * @param host the peer hostname to propagate to the delegate socket factory
+ */
+ PqcPeerHostSSLSocketFactory(SSLSocketFactory delegate, String host) {
+ this.delegate = delegate;
+ this.host = host;
+ }
+
+ @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 configureSocket(delegate.createSocket(s, host, port, autoClose));
+ }
+
+ @Override
+ public Socket createSocket() throws IOException {
+ return configureSocket(delegate.createSocket());
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
+ return configureSocket(delegate.createSocket(host, port));
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localAddress, int localPort)
+ throws IOException, UnknownHostException {
+ return configureSocket(delegate.createSocket(host, port, localAddress, localPort));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port) throws IOException {
+ Socket plainSocket = new Socket();
+ plainSocket.connect(new InetSocketAddress(address, port));
+ return configureSocket(delegate.createSocket(plainSocket, this.host, port, true));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
+ throws IOException {
+ Socket plainSocket = new Socket();
+ plainSocket.bind(new InetSocketAddress(localAddress, localPort));
+ plainSocket.connect(new InetSocketAddress(address, port));
+ return configureSocket(delegate.createSocket(plainSocket, this.host, port, true));
+ }
+
+ @org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
+ private Socket configureSocket(Socket socket) {
+ if (socket instanceof javax.net.ssl.SSLSocket) {
+ javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) socket;
+ try {
+ javax.net.ssl.SSLParameters params = sslSocket.getSSLParameters();
+ if (params != null) {
+ java.util.List
* Returns an SSL context in which all X.509 certificates are trusted.
@@ -191,4 +271,229 @@ public boolean verify(String arg0, SSLSession arg1) {
}
private SslUtils() {}
+
+ @org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
+ private static class PqcEnforcingSSLEngine extends javax.net.ssl.SSLEngine {
+ private final javax.net.ssl.SSLEngine delegate;
+
+ PqcEnforcingSSLEngine(javax.net.ssl.SSLEngine delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void setSSLParameters(javax.net.ssl.SSLParameters params) {
+ delegate.setSSLParameters(params);
+ Object objEngine = delegate;
+ if (objEngine instanceof org.bouncycastle.jsse.BCSSLEngine) {
+ org.bouncycastle.jsse.BCSSLEngine bcEngine = (org.bouncycastle.jsse.BCSSLEngine) objEngine;
+ org.bouncycastle.jsse.BCSSLParameters bcParams = bcEngine.getParameters();
+ bcParams.setNamedGroups(new String[] {"X25519MLKEM768"});
+ bcEngine.setParameters(bcParams);
+ }
+ }
+
+ @Override
+ public void setHandshakeApplicationProtocolSelector(
+ BiFunction
The validation framework operates via an end-to-end hermetic handshake architecture: + * + *
+ * +---------------------------------------+ +-----------------------------------------+ + * | Vanilla App Client Code | | PqcTestServer (Enforces MLKEM768)| + * | (e.g. BigQueryOptions.getDefaultInst) | +-----------------------------------------+ + * +---------------------------------------+ ^ + * | | + * v | + * +---------------------------------------+ | + * | google-cloud-core-http | | + * | (DefaultHttpTransportFactory) | | + * +---------------------------------------+ | + * | | + * v | + * +---------------------------------------+ | + * | google-http-java-client | | + * | (SslUtils.getTlsSslContext() JJSSE) | | + * +---------------------------------------+ | + * | | + * v | + * +---------------------------------------+ | + * | PqcDelegatingSSLSocketFactory | | + * | (Wraps default BCSSLSocketFactory) | | + * +---------------------------------------+ | + * | | + * +-----------------[TLSv1.3 MLKEM768 Hybrid Handshake] + *+ * + *
google-http-java-client (which contains
+ * PqcDelegatingSSLSocketFactory).
+ * Harness Execution Flow: + * + *
pqctest.p12) from the
+ * classpath to a localized temp file to guarantee isolated execution.
+ * javax.net.ssl.trustStore
+ * ) to point to the extracted certificate, enabling clean default SSLContext
+ * verification.
+ * BouncyCastleJsseProvider at position 1. This
+ * automatically causes all standard vanilla clients instantiating default SSLContext
+ * to negotiate PQC.
+ * PqcTestServer in a separate JVM process.
+ * Detailed Security & Keystore Configuration Architecture: + * + *
pqctest.p12) is a
+ * secure key database containing the server's private key and its self-signed public
+ * certificate. The server uses it during the TLS handshake to prove its identity and
+ * establish a secure channel.
+ * cacerts). Without registering a
+ * custom truststore containing this certificate, standard JRE TLS clients will reject the
+ * connection with an SSLHandshakeException. We extract the certificate to a
+ * temporary file and point javax.net.ssl.trustStore to it, thereby trusting it
+ * scope-specifically for this test run without polluting or mutating the user's system-wide
+ * JRE truststore.
+ * BouncyCastleJsseProvider at
+ * provider position 1. This registers Bouncy Castle as the primary security provider,
+ * causing all standard default SSLContext and vanilla client factories to
+ * utilize Bouncy Castle JSSE and negotiate PQC automatically.
+ *