diff --git a/.gitignore b/.gitignore index b7c3b9ec7..ab5f461d1 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ nosetests.xml .DS_Store **/.classpath **/.checkstyle +**/.vscode/ # Python utilities *.pyc diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 8fc8df860..63685111e 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -27,14 +27,26 @@ source ${scriptDir}/common.sh mvn -version echo ${JOB_TYPE} -# attempt to install 3 times with exponential backoff (starting with 10 seconds) retry_with_backoff 3 10 \ mvn install -B -V -ntp \ -DskipTests=true \ -Dclirr.skip=true \ + -Dcheckstyle.skip=true \ -Denforcer.skip=true \ -Dmaven.javadoc.skip=true \ -Dgcloud.download.skip=true \ + -pl !pqc-test,!pqc-test/pqc-test-common,!pqc-test/pqc-test-snapshot,!pqc-test/pqc-test-release \ + -T 1C + +retry_with_backoff 3 10 \ + mvn install -B -V -ntp \ + -DskipTests=true \ + -Dclirr.skip=true \ + -Dcheckstyle.skip=true \ + -Denforcer.skip=true \ + -Dmaven.javadoc.skip=true \ + -Dgcloud.download.skip=true \ + -pl pqc-test,pqc-test/pqc-test-common,pqc-test/pqc-test-snapshot,pqc-test/pqc-test-release \ -T 1C # if GOOGLE_APPLICATION_CREDENTIALS is specified as a relative path, prepend Kokoro root directory onto it @@ -47,7 +59,7 @@ set +e case ${JOB_TYPE} in test) - mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true + mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true -Dcheckstyle.skip=true RETURN_CODE=$? ;; lint) @@ -65,6 +77,7 @@ integration) -DtrimStackTrace=false \ -Dclirr.skip=true \ -Denforcer.skip=true \ + -Dcheckstyle.skip=true \ -fae \ verify RETURN_CODE=$? diff --git a/google-http-client-appengine/pom.xml b/google-http-client-appengine/pom.xml index dbf5456df..cef48c68a 100644 --- a/google-http-client-appengine/pom.xml +++ b/google-http-client-appengine/pom.xml @@ -21,7 +21,7 @@ org.codehaus.mojo.signature - java17 + java18 1.0 diff --git a/google-http-client/pom.xml b/google-http-client/pom.xml index 6cb84e024..974d5a7ed 100644 --- a/google-http-client/pom.xml +++ b/google-http-client/pom.xml @@ -164,6 +164,22 @@ opencensus-contrib-http-util + + org.bouncycastle + bcprov-jdk18on + ${project.bouncycastle.version} + + + org.codehaus.mojo + animal-sniffer-annotations + true + + + org.bouncycastle + bctls-jdk18on + ${project.bouncycastle.version} + + com.google.guava guava-testlib 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..252f39a24 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 @@ -28,6 +28,8 @@ import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.NoSuchProviderException; +import java.security.Provider; import java.security.cert.CertificateFactory; import java.util.Arrays; import javax.net.ssl.HostnameVerifier; @@ -92,13 +94,26 @@ private static Proxy defaultProxy() { /** Whether the transport is mTLS. Default value is {@code false}. */ private final boolean isMtls; + /** + * Returns the default SSL socket factory, which is PQC-enabled if Bouncy Castle JJSSE is on the + * classpath. + */ + private static SSLSocketFactory getDefaultSslSocketFactory() { + try { + SSLContext sslContext = SslUtils.getTlsSslContext(); + return sslContext.getSocketFactory(); + } catch (Exception e) { + return null; // Fallback to default HttpsURLConnection behavior + } + } + /** * Constructor with the default behavior. * *

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 serverNames = new java.util.ArrayList<>(); + serverNames.add(new javax.net.ssl.SNIHostName(this.host)); + params.setServerNames(serverNames); + sslSocket.setSSLParameters(params); + } + } catch (Exception e) { + // Ignore + } + } + return socket; + } +} 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..e20bf2a83 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 @@ -17,16 +17,24 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.List; +import java.util.function.BiFunction; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; /** * SSL utilities. @@ -46,12 +54,59 @@ public static SSLContext getSslContext() throws NoSuchAlgorithmException { } /** - * Returns the SSL context for "TLS" algorithm. + * Returns the SSL context for "TLS" algorithm using Bouncy Castle JJSSE provider + * scope-specifically. * - * @since 1.14 + * @since 2.1.1 */ public static SSLContext getTlsSslContext() throws NoSuchAlgorithmException { - return SSLContext.getInstance("TLS"); + BouncyCastleProvider cryptoProvider = new BouncyCastleProvider(); + + BouncyCastleJsseProvider provider = new BouncyCastleJsseProvider(cryptoProvider); + + SSLContext bcContext = SSLContext.getInstance("TLS", provider); + + try { + // 4. Initialize the Bouncy Castle SSLContext with default managers. + bcContext.init(null, null, null); + } catch (GeneralSecurityException e) { + // Print diagnostic trace to help understand why Bouncy Castle JSSE failed to initialize. + e.printStackTrace(); + // 5. Retrieve standard JJSSE default context if BC JJSSE initialization fails. + SSLContext fallbackContext = SSLContext.getInstance("TLS"); + try { + // Initialize the fallback context with default managers as well. + fallbackContext.init(null, null, null); + } catch (GeneralSecurityException ex) { + // TODO: Log + } + return fallbackContext; + } + + // 6. Return the raw Bouncy Castle SSLContext. + return new SSLContext( + new PqcEnforcingSSLContextSpi(bcContext), + bcContext.getProvider(), + bcContext.getProtocol()) {}; + } + + /** + * Returns the SSL context for "TLS" algorithm using the specified provider. + * + * @since 1.39 + */ + public static SSLContext getTlsSslContext(Provider provider) throws NoSuchAlgorithmException { + return SSLContext.getInstance("TLS", provider); + } + + /** + * Returns the SSL context for "TLS" algorithm using the specified provider name. + * + * @since 2.1.1 + */ + public static SSLContext getTlsSslContext(String providerName) + throws NoSuchAlgorithmException, NoSuchProviderException { + return SSLContext.getInstance("TLS", providerName); } /** @@ -106,8 +161,8 @@ public static KeyManagerFactory getPkixKeyManagerFactory() throws NoSuchAlgorith public static SSLContext initSslContext( SSLContext sslContext, KeyStore trustStore, TrustManagerFactory trustManagerFactory) throws GeneralSecurityException { - trustManagerFactory.init(trustStore); - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + sslContext.init( + null, getCompatibleTrustManagers(sslContext, trustStore, trustManagerFactory), null); return sslContext; } @@ -137,13 +192,38 @@ public static SSLContext initSslContext( String mtlsKeyStorePassword, KeyManagerFactory keyManagerFactory) throws GeneralSecurityException { - trustManagerFactory.init(trustStore); keyManagerFactory.init(mtlsKeyStore, mtlsKeyStorePassword.toCharArray()); sslContext.init( - keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); + keyManagerFactory.getKeyManagers(), + getCompatibleTrustManagers(sslContext, trustStore, trustManagerFactory), + null); return sslContext; } + /** + * Resolves trust managers compatible with the active security provider. If the SSLContext is + * managed by the Bouncy Castle JJSSE provider, it retrieves Bouncy Castle's native trust managers + * instead of standard JDK trust managers. This prevents JCA trust manager wrapping mismatches and + * unresolved peer host certificate exceptions on strict JVMs (e.g., Java 8/21). + */ + private static TrustManager[] getCompatibleTrustManagers( + SSLContext sslContext, KeyStore trustStore, TrustManagerFactory trustManagerFactory) + throws GeneralSecurityException { + if (sslContext.getProvider() instanceof BouncyCastleJsseProvider) { + try { + TrustManagerFactory bcTmf = + TrustManagerFactory.getInstance( + trustManagerFactory.getAlgorithm(), sslContext.getProvider()); + bcTmf.init(trustStore); + return bcTmf.getTrustManagers(); + } catch (KeyStoreException | NoSuchAlgorithmException e) { + // Fallback to default trust managers + } + } + trustManagerFactory.init(trustStore); + return trustManagerFactory.getTrustManagers(); + } + /** * {@link Beta}
* 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, String> selector) { + delegate.setHandshakeApplicationProtocolSelector( + (engine, protocols) -> selector.apply(this, protocols)); + } + + @Override + public BiFunction, String> getHandshakeApplicationProtocolSelector() { + return delegate.getHandshakeApplicationProtocolSelector(); + } + + @Override + public String getApplicationProtocol() { + return delegate.getApplicationProtocol(); + } + + @Override + public String getHandshakeApplicationProtocol() { + return delegate.getHandshakeApplicationProtocol(); + } + + @Override + public javax.net.ssl.SSLParameters getSSLParameters() { + return delegate.getSSLParameters(); + } + + @Override + public void beginHandshake() throws javax.net.ssl.SSLException { + delegate.beginHandshake(); + } + + @Override + public void closeInbound() throws javax.net.ssl.SSLException { + delegate.closeInbound(); + } + + @Override + public void closeOutbound() { + delegate.closeOutbound(); + } + + @Override + public java.lang.Runnable getDelegatedTask() { + return delegate.getDelegatedTask(); + } + + @Override + public java.lang.String[] getEnabledCipherSuites() { + return delegate.getEnabledCipherSuites(); + } + + @Override + public java.lang.String[] getEnabledProtocols() { + return delegate.getEnabledProtocols(); + } + + @Override + public javax.net.ssl.SSLEngineResult.HandshakeStatus getHandshakeStatus() { + return delegate.getHandshakeStatus(); + } + + @Override + public boolean getNeedClientAuth() { + return delegate.getNeedClientAuth(); + } + + @Override + public javax.net.ssl.SSLSession getSession() { + return delegate.getSession(); + } + + @Override + public java.lang.String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public java.lang.String[] getSupportedProtocols() { + return delegate.getSupportedProtocols(); + } + + @Override + public boolean getUseClientMode() { + return delegate.getUseClientMode(); + } + + @Override + public boolean getWantClientAuth() { + return delegate.getWantClientAuth(); + } + + @Override + public boolean isInboundDone() { + return delegate.isInboundDone(); + } + + @Override + public boolean isOutboundDone() { + return delegate.isOutboundDone(); + } + + @Override + public void setEnabledCipherSuites(java.lang.String[] suites) { + delegate.setEnabledCipherSuites(suites); + } + + @Override + public void setEnabledProtocols(java.lang.String[] protocols) { + delegate.setEnabledProtocols(protocols); + } + + @Override + public void setNeedClientAuth(boolean need) { + delegate.setNeedClientAuth(need); + } + + @Override + public void setUseClientMode(boolean mode) { + delegate.setUseClientMode(mode); + } + + @Override + public void setWantClientAuth(boolean want) { + delegate.setWantClientAuth(want); + } + + @Override + public javax.net.ssl.SSLEngineResult unwrap( + java.nio.ByteBuffer src, java.nio.ByteBuffer[] dsts, int offset, int length) + throws javax.net.ssl.SSLException { + return delegate.unwrap(src, dsts, offset, length); + } + + @Override + public javax.net.ssl.SSLEngineResult wrap( + java.nio.ByteBuffer[] srcs, int offset, int length, java.nio.ByteBuffer dst) + throws javax.net.ssl.SSLException { + return delegate.wrap(srcs, offset, length, dst); + } + + @Override + public boolean getEnableSessionCreation() { + return delegate.getEnableSessionCreation(); + } + + @Override + public void setEnableSessionCreation(boolean flag) { + delegate.setEnableSessionCreation(flag); + } + + @Override + public javax.net.ssl.SSLSession getHandshakeSession() { + return delegate.getHandshakeSession(); + } + } + + @org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + private static class PqcEnforcingSSLContextSpi extends javax.net.ssl.SSLContextSpi { + private final javax.net.ssl.SSLContext delegate; + + PqcEnforcingSSLContextSpi(javax.net.ssl.SSLContext delegate) { + this.delegate = delegate; + } + + @Override + protected javax.net.ssl.SSLEngine engineCreateSSLEngine() { + return new PqcEnforcingSSLEngine(delegate.createSSLEngine()); + } + + @Override + protected javax.net.ssl.SSLEngine engineCreateSSLEngine(java.lang.String host, int port) { + return new PqcEnforcingSSLEngine(delegate.createSSLEngine(host, port)); + } + + @Override + protected javax.net.ssl.SSLSessionContext engineGetClientSessionContext() { + return delegate.getClientSessionContext(); + } + + @Override + protected javax.net.ssl.SSLSessionContext engineGetServerSessionContext() { + return delegate.getServerSessionContext(); + } + + @Override + protected javax.net.ssl.SSLServerSocketFactory engineGetServerSocketFactory() { + return delegate.getServerSocketFactory(); + } + + @Override + protected javax.net.ssl.SSLSocketFactory engineGetSocketFactory() { + return delegate.getSocketFactory(); + } + + @Override + protected void engineInit( + javax.net.ssl.KeyManager[] km, + javax.net.ssl.TrustManager[] tm, + java.security.SecureRandom sr) + throws java.security.KeyManagementException { + delegate.init(km, tm, sr); + } + } } diff --git a/pom.xml b/pom.xml index ca68c89db..72e60c3bb 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,7 @@ google-http-client-bom + pqc-test @@ -280,6 +281,12 @@ opencensus-testing ${project.opencensus.version} + + org.codehaus.mojo + animal-sniffer-annotations + 1.24 + true + @@ -295,8 +302,8 @@ maven-compiler-plugin 3.13.0 - 1.7 - 1.7 + 1.8 + 1.8 @@ -437,7 +444,7 @@ maven-javadoc-plugin none - 7 + 8 @@ -547,6 +554,7 @@ - google-api-java-client/google-api-client-assembly/android-properties (make the filenames match the version here) - Internally, update the default features.json file --> + 1.80 2.1.1-SNAPSHOT 2.0.32 UTF-8 @@ -605,7 +613,7 @@ animal-sniffer - [1.7,) + [1.8,) @@ -621,7 +629,7 @@ org.codehaus.mojo.signature - java17 + java18 1.0 diff --git a/pqc-test/.vscode/settings.json b/pqc-test/.vscode/settings.json new file mode 100644 index 000000000..49ad98fbe --- /dev/null +++ b/pqc-test/.vscode/settings.json @@ -0,0 +1,33 @@ +{ + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#118019", + "activityBar.background": "#118019", + "activityBar.foreground": "#e7e7e7", + "activityBar.inactiveForeground": "#e7e7e799", + "activityBarBadge.background": "#261ac5", + "activityBarBadge.foreground": "#e7e7e7", + "commandCenter.border": "#e7e7e799", + "sash.hoverBorder": "#118019", + "statusBar.background": "#0b5310", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#118019", + "statusBarItem.remoteBackground": "#0b5310", + "statusBarItem.remoteForeground": "#e7e7e7", + "titleBar.activeBackground": "#0b5310", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#0b531099", + "titleBar.inactiveForeground": "#e7e7e799" + }, + "peacock.color": "#0b5310", + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/*.class": true + }, + "java.autobuild.enabled": true, + "java.import.maven.enabled": true, + "java.configuration.updateBuildConfiguration": "interactive", +} \ No newline at end of file diff --git a/pqc-test/pom.xml b/pqc-test/pom.xml new file mode 100644 index 000000000..1fe163c6f --- /dev/null +++ b/pqc-test/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + + com.google.cloud + google-cloud-shared-config + 1.17.0 + + + com.google.api + pqc-test-parent + pom + 2.81.0-SNAPSHOT + + + pqc-test-common + pqc-test-snapshot + pqc-test-release + + diff --git a/pqc-test/pqc-test-common/pom.xml b/pqc-test/pqc-test-common/pom.xml new file mode 100644 index 000000000..3af1ab9f8 --- /dev/null +++ b/pqc-test/pqc-test-common/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + com.google.api + pqc-test-parent + 2.81.0-SNAPSHOT + ../pom.xml + + + pqc-test-common + + + + org.junit.jupiter + junit-jupiter-api + 5.10.2 + + + io.grpc + grpc-netty + 1.81.0 + + + io.grpc + grpc-stub + 1.81.0 + + + org.bouncycastle + bcprov-jdk18on + 1.84 + + + org.bouncycastle + bctls-jdk18on + 1.84 + + + com.google.cloud + google-cloud-bigquery + 2.66.0 + + + com.google.cloud + google-cloud-translate + 2.92.0 + + + diff --git a/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/httpjson/PqcConnectivityTest.java b/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/httpjson/PqcConnectivityTest.java new file mode 100644 index 000000000..18ec1609e --- /dev/null +++ b/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/httpjson/PqcConnectivityTest.java @@ -0,0 +1,467 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.httpjson; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode; +import com.google.cloud.NoCredentials; +import com.google.cloud.bigquery.*; +import com.google.cloud.translate.v3.*; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.Security; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * PqcConnectivityTest serves as the base integration validation suite for confirming transparent, + * zero-config Post-Quantum Cryptography (PQC) auto-upgrades across all Google Cloud Java SDK + * transports. + * + *

Design and Architectural Workflow

+ * + *

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]
+ * 
+ * + * + */ +public abstract class PqcConnectivityTest { + + private static Process serverProcess; + protected static int httpPort; + protected static int grpcPort; + private static boolean isPqcSupported; + protected static javax.net.ssl.SSLContext clientSslContext; + private static KeyStore ks; + + /** + * Configures the integration test harness environment before test cases are executed. + * + *

Harness Execution Flow: + * + *

    + *
  1. Extracts the secure PKCS12 validation certificate (pqctest.p12) from the + * classpath to a localized temp file to guarantee isolated execution. + *
  2. Configures JVM standard truststore system properties (javax.net.ssl.trustStore + * ) to point to the extracted certificate, enabling clean default SSLContext + * verification. + *
  3. Inspects the runtime classpath to determine if PQC wrapper auto-upgrades are active. + *
  4. If PQC is supported, registers BouncyCastleJsseProvider at position 1. This + * automatically causes all standard vanilla clients instantiating default SSLContext + * to negotiate PQC. + *
  5. Spins up the hermetic PqcTestServer in a separate JVM process. + *
+ */ + /** + * Configures the integration test harness environment before test cases are executed. + * + *

Detailed Security & Keystore Configuration Architecture: + * + *

+ */ + protected boolean clientSupportsPqc() { + return true; + } + + protected abstract boolean httpTestShouldSucceed(); + + protected abstract boolean grpcTestShouldSucceed(); + + protected abstract boolean bigqueryTestShouldSucceed(); + + @BeforeAll + public static void setup() throws Exception { + + // Dynamically detect if PQC auto-upgrade wrapping is supported by current + // classpath + // dependencies (Snapshot vs Release) + try { + Class.forName("com.google.api.client.http.javanet.PqcPeerHostSSLSocketFactory"); + isPqcSupported = true; + } catch (ClassNotFoundException e) { + isPqcSupported = false; + } + + ks = KeyStore.getInstance("PKCS12"); + try (InputStream is = PqcConnectivityTest.class.getResourceAsStream("/pqctest.p12")) { + if (is == null) { + throw new RuntimeException("pqctest.p12 not found in classpath"); + } + ks.load(is, "password".toCharArray()); + } + + // 3. Register JCA providers globally for name-based gRPC Netty client lookup. + if (isPqcSupported) { + if (java.security.Security.getProvider("BC") == null) { + java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + if (java.security.Security.getProvider("BCJSSE") == null) { + java.security.Security.insertProviderAt(new org.bouncycastle.jsse.provider.BouncyCastleJsseProvider(), 1); + } + clientSslContext = javax.net.ssl.SSLContext.getInstance("TLSv1.3", + new org.bouncycastle.jsse.provider.BouncyCastleJsseProvider()); + } else { + clientSslContext = javax.net.ssl.SSLContext.getInstance("TLSv1.3"); + } + // Initialize with null managers to force the JVM to resolve standard truststore + // system properties! + clientSslContext.init(null, null, null); + + // 6. Spawn PqcTestServer in a separate background process to ensure physical + // JVM runtime isolation! + ProcessBuilder pb = + new ProcessBuilder( + "java", + "-cp", + System.getProperty("java.class.path"), + "com.google.api.gax.pqc.PqcTestServer"); + + // Force merging of error stream to ease debugging in test output + pb.redirectErrorStream(true); + serverProcess = pb.start(); + + // Read server's stdout to dynamically capture the allocated ephemeral ports + java.io.BufferedReader reader = + new java.io.BufferedReader( + new java.io.InputStreamReader( + serverProcess.getInputStream(), java.nio.charset.StandardCharsets.UTF_8)); + + String line; + boolean httpPortFound = false; + boolean grpcPortFound = false; + + // Wait for the server process to output its HTTP and gRPC ports + long startTime = System.currentTimeMillis(); + while ((line = reader.readLine()) != null) { + System.out.println("[SERVER-OUT] " + line); + if (line.startsWith("HTTP_PORT: ")) { + httpPort = Integer.parseInt(line.substring(11).trim()); + httpPortFound = true; + } else if (line.startsWith("GRPC_PORT: ")) { + grpcPort = Integer.parseInt(line.substring(11).trim()); + grpcPortFound = true; + } + + if (httpPortFound && grpcPortFound) { + break; + } + + // Ephemeral port detection timeout (10 seconds) to fail-fast on server startup + // errors + if (System.currentTimeMillis() - startTime > 10000) { + throw new RuntimeException( + "Timeout waiting for PqcTestServer ephemeral ports to be printed!"); + } + } + + if (!httpPortFound || !grpcPortFound) { + throw new RuntimeException("PqcTestServer failed to initialize ephemeral ports!"); + } + + // Start a background thread to continuously drain the server's stdout + Thread drainThread = + new Thread( + () -> { + try { + String l; + while ((l = reader.readLine()) != null) { + System.out.println("[SERVER-OUT] " + l); + } + } catch (java.io.IOException e) { + // Ignore stream closed + } + }); + drainThread.setDaemon(true); + drainThread.start(); + } + + @AfterAll + public static void teardown() { + if (serverProcess != null) { + // Forcibly destroy the background process and close standard streams to allow + // clean exit + serverProcess.destroyForcibly(); + } + if (isPqcSupported) { + Security.removeProvider("BCJSSE"); + Security.removeProvider("BC"); + } + } + + @Test + public void testHttpPqc() throws Exception { + TranslationServiceSettings.Builder settingsBuilder = + TranslationServiceSettings.newHttpJsonBuilder() + .setEndpoint("localhost:" + httpPort) + .setCredentialsProvider(NoCredentialsProvider.create()); + + com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider.Builder channelProviderBuilder = ((com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider) settingsBuilder + .getTransportChannelProvider()) + .toBuilder(); + + com.google.api.client.http.HttpTransport transport = new com.google.api.client.http.javanet.NetHttpTransport.Builder() + .trustCertificates(ks) + .build(); + + channelProviderBuilder.setHttpTransport(transport); + settingsBuilder.setTransportChannelProvider(channelProviderBuilder.build()); + + TranslationServiceSettings settings = settingsBuilder.build(); + + try (TranslationServiceClient client = TranslationServiceClient.create(settings)) { + List contents = new ArrayList<>(); + contents.add("house"); + TranslateTextRequest request = + TranslateTextRequest.newBuilder() + .setParent("projects/test-project") + .addAllContents(contents) + .build(); + + try { + TranslateTextResponse response = client.translateText(request); + if (!httpTestShouldSucceed()) { + fail("Expected HTTP call to fail in Release due to PQC enforcement"); + } + assertEquals("mocked translated text", response.getTranslations(0).getTranslatedText()); + } catch (ApiException e) { + if (httpTestShouldSucceed()) { + fail("Expected HTTP call to succeed, but failed with: " + e.getStatusCode().getCode(), e); + } + StatusCode.Code code = e.getStatusCode().getCode(); + if (code != StatusCode.Code.UNAVAILABLE && code != StatusCode.Code.UNKNOWN) { + fail( + "Expected HTTP call to fail with UNAVAILABLE or UNKNOWN, but failed with: " + code, + e); + } + } + } + } + + @Test + public void testGrpcPqc() throws Exception { + TranslationServiceSettings.Builder settingsBuilder = + TranslationServiceSettings.newBuilder() + .setEndpoint("localhost:" + grpcPort) + .setCredentialsProvider(NoCredentialsProvider.create()); + + if (clientSupportsPqc()) { + com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.Builder channelProviderBuilder = ((com.google.api.gax.grpc.InstantiatingGrpcChannelProvider) settingsBuilder + .getTransportChannelProvider()) + .toBuilder(); + channelProviderBuilder.setChannelConfigurator( + new com.google.api.core.ApiFunction() { + @Override + public io.grpc.ManagedChannelBuilder apply(io.grpc.ManagedChannelBuilder builder) { + configureGrpcChannelForPqc(builder); + return builder; + } + }); + settingsBuilder.setTransportChannelProvider(channelProviderBuilder.build()); + } + + TranslationServiceSettings settings = settingsBuilder.build(); + + try (TranslationServiceClient client = TranslationServiceClient.create(settings)) { + List contents = new ArrayList<>(); + contents.add("house"); + TranslateTextRequest request = + TranslateTextRequest.newBuilder() + .setParent("projects/test-project") + .addAllContents(contents) + .build(); + + try { + TranslateTextResponse response = client.translateText(request); + if (!grpcTestShouldSucceed()) { + fail("Expected gRPC call to fail!"); + } + assertNotNull(response); + } catch (ApiException e) { + if (grpcTestShouldSucceed()) { + fail( + "Expected gRPC call to succeed, but failed: " + + e.getMessage(), + e); + } + } + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static void configureGrpcChannelForPqc(io.grpc.ManagedChannelBuilder builder) { + String builderClassName = builder.getClass().getName(); + try { + Class apnClass = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig"); + Class apnProtocolClass = Class + .forName("io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig$Protocol"); + Class apnSelectorBehaviorClass = Class + .forName("io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig$SelectorFailureBehavior"); + Class apnSelectedListenerBehaviorClass = Class.forName( + "io.grpc.netty.shaded.io.netty.handler.ssl.ApplicationProtocolConfig$SelectedListenerFailureBehavior"); + + Object alpnEnum = Enum.valueOf((Class) apnProtocolClass, "ALPN"); + Object noAdvertiseEnum = Enum.valueOf((Class) apnSelectorBehaviorClass, "NO_ADVERTISE"); + Object acceptEnum = Enum.valueOf((Class) apnSelectedListenerBehaviorClass, "ACCEPT"); + + Object apn = apnClass + .getConstructor(apnProtocolClass, apnSelectorBehaviorClass, apnSelectedListenerBehaviorClass, String[].class) + .newInstance(alpnEnum, noAdvertiseEnum, acceptEnum, new String[] { "h2" }); + + Class sslContextBuilderClass = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder"); + Class sslProviderClass = Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.SslProvider"); + Object jdkProviderEnum = Enum.valueOf((Class) sslProviderClass, "JDK"); + + Object sslContextBuilder = sslContextBuilderClass.getMethod("forClient").invoke(null); + sslContextBuilderClass.getMethod("sslProvider", sslProviderClass).invoke(sslContextBuilder, jdkProviderEnum); + sslContextBuilderClass.getMethod("protocols", String[].class).invoke(sslContextBuilder, + new Object[] { new String[] { "TLSv1.3" } }); + sslContextBuilderClass.getMethod("applicationProtocolConfig", apnClass).invoke(sslContextBuilder, apn); + + Class insecureTrustManagerFactoryClass = Class + .forName("io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory"); + Object trustManagerFactory = insecureTrustManagerFactoryClass.getField("INSTANCE").get(null); + sslContextBuilderClass.getMethod("trustManager", javax.net.ssl.TrustManagerFactory.class) + .invoke(sslContextBuilder, trustManagerFactory); + + Object shadedSslContext = sslContextBuilderClass.getMethod("build").invoke(sslContextBuilder); + + builder.getClass().getMethod("sslContext", Class.forName("io.grpc.netty.shaded.io.netty.handler.ssl.SslContext")) + .invoke(builder, shadedSslContext); + + } catch (Exception e) { + throw new RuntimeException("Failed to configure gRPC channel for PQC", e); + } + } + + @Test + public void testBigQueryPqc() throws Exception { + + // Vanilla BigQuery Client instantiation. + com.google.api.client.http.HttpTransport transport = new com.google.api.client.http.javanet.NetHttpTransport.Builder() + .trustCertificates(ks) + .build(); + + com.google.cloud.http.HttpTransportOptions transportOptions = com.google.cloud.http.HttpTransportOptions + .newBuilder() + .setHttpTransportFactory(() -> transport) + .build(); + + BigQueryOptions bigqueryOptions = + BigQueryOptions.newBuilder() + .setProjectId("test-project") + .setHost("https://localhost:" + httpPort) + .setCredentials(NoCredentials.getInstance()) + .setTransportOptions(transportOptions) + .build(); + + BigQuery bigquery = bigqueryOptions.getService(); + + // This will trigger a request to + // https://localhost:httpPort/bigquery/v2/projects/test-project/datasets + try { + bigquery.listDatasets(); + if (bigqueryTestShouldSucceed()) { + return; + } + fail("Expected BigQuery client call to fail!"); + } catch (Exception e) { + if (bigqueryTestShouldSucceed()) { + fail("Expected BigQuery client call to succeed!", e); + } + } + } +} diff --git a/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/pqc/PqcTestServer.java b/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/pqc/PqcTestServer.java new file mode 100644 index 000000000..60ffa67a6 --- /dev/null +++ b/pqc-test/pqc-test-common/src/main/java/com/google/api/gax/pqc/PqcTestServer.java @@ -0,0 +1,475 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.pqc; + +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsParameters; +import com.sun.net.httpserver.HttpsServer; +import io.grpc.Server; +import io.grpc.netty.NettyServerBuilder; +import io.netty.handler.ssl.IdentityCipherSuiteFilter; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.JdkSslContext; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.security.KeyStore; +import java.security.Security; +import java.util.List; +import java.util.function.BiFunction; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; + +/** + * PqcTestServer is a specialized test harness designed to validate Post-Quantum Cryptography (PQC) + * transport enforcement in the Google Cloud Java SDK. + */ +public class PqcTestServer { + + private HttpsServer httpServer; + private Server grpcServer; + private int httpPort; + private int grpcPort; + + public void start() throws Exception { + + KeyStore ks = KeyStore.getInstance("PKCS12"); + try (InputStream is = getClass().getResourceAsStream("/pqctest.p12")) { + if (is == null) { + throw new RuntimeException("pqctest.p12 not found in classpath"); + } + ks.load(is, "password".toCharArray()); + } + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, "password".toCharArray()); + + javax.net.ssl.TrustManagerFactory tmf = + javax.net.ssl.TrustManagerFactory.getInstance( + javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + + BouncyCastleProvider bcProvider = new BouncyCastleProvider(); + BouncyCastleJsseProvider bcJsseProvider = new BouncyCastleJsseProvider(bcProvider); + SSLContext bcContext = SSLContext.getInstance("TLSv1.3", bcJsseProvider); + bcContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + SSLContext sslContext = + new SSLContext( + new PqcEnforcingSSLContextSpi(bcContext), + bcContext.getProvider(), + bcContext.getProtocol()) {}; + + httpServer = HttpsServer.create(new InetSocketAddress(0), 0); + httpServer.setHttpsConfigurator( + new HttpsConfigurator(sslContext) { + @Override + public void configure(HttpsParameters params) { + SSLParameters sslparams = getSSLContext().getDefaultSSLParameters(); + sslparams.setProtocols(new String[] {"TLSv1.3"}); + params.setSSLParameters(sslparams); + } + }); + + httpServer.createContext( + "/test", + exchange -> { + String response = "PQC HTTP OK"; + exchange.sendResponseHeaders(200, response.length()); + exchange.getResponseBody().write(response.getBytes()); + exchange.getResponseBody().close(); + }); + + httpServer.createContext( + "/bigquery/v2/projects/test-project/datasets", + exchange -> { + String response = "{\"kind\": \"bigquery#datasetList\"}"; + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, response.length()); + exchange.getResponseBody().write(response.getBytes()); + exchange.getResponseBody().close(); + }); + + httpServer.createContext( + "/v3/", + exchange -> { + if ("POST".equalsIgnoreCase(exchange.getRequestMethod())) { + String response = + "{\"translations\": [{\"translatedText\": \"mocked translated text\"}]}"; + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, response.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes()); + } + } + }); + + httpServer.start(); + httpPort = httpServer.getAddress().getPort(); + + ApplicationProtocolConfig apn = new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + "h2" + ); + + io.netty.handler.ssl.SslContext nettySslContext = new JdkSslContext( + sslContext, + false, + null, + IdentityCipherSuiteFilter.INSTANCE, + apn, + ClientAuth.NONE + ); + + io.grpc.MethodDescriptor method = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.UNARY) + .setFullMethodName("Greeter/SayHello") + .setRequestMarshaller(new ByteMarshaller()) + .setResponseMarshaller(new ByteMarshaller()) + .build(); + + io.grpc.ServerServiceDefinition serviceDef = + io.grpc.ServerServiceDefinition.builder("Greeter") + .addMethod( + method, + io.grpc.stub.ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("PQC gRPC OK".getBytes()); + responseObserver.onCompleted(); + })) + .build(); + + io.grpc.MethodDescriptor translateMethod = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.UNARY) + .setFullMethodName("google.cloud.translation.v3.TranslationService/TranslateText") + .setRequestMarshaller(new ByteMarshaller()) + .setResponseMarshaller(new ByteMarshaller()) + .build(); + + io.grpc.ServerServiceDefinition translationServiceDef = + io.grpc.ServerServiceDefinition.builder("google.cloud.translation.v3.TranslationService") + .addMethod( + translateMethod, + io.grpc.stub.ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext(new byte[0]); // Empty proto response + responseObserver.onCompleted(); + })) + .build(); + + grpcServer = + NettyServerBuilder.forPort(0) + .sslContext(nettySslContext) + .addService(serviceDef) + .addService(translationServiceDef) + .build() + .start(); + grpcPort = grpcServer.getPort(); + } + + public void stop() { + if (httpServer != null) httpServer.stop(0); + if (grpcServer != null) grpcServer.shutdown(); + Security.removeProvider("BC"); + Security.removeProvider("BCJSSE"); + } + + public int getHttpPort() { + return httpPort; + } + + public int getGrpcPort() { + return grpcPort; + } + + private static class ByteMarshaller implements io.grpc.MethodDescriptor.Marshaller { + @Override + public InputStream stream(byte[] value) { + return new java.io.ByteArrayInputStream(value); + } + + @Override + public byte[] parse(InputStream stream) { + try { + return com.google.common.io.ByteStreams.toByteArray(stream); + } catch (java.io.IOException e) { + throw new RuntimeException(e); + } + } + } + + public static void main(String[] args) throws Exception { + PqcTestServer server = new PqcTestServer(); + server.start(); + + System.out.println("HTTP_PORT: " + server.getHttpPort()); + System.out.println("GRPC_PORT: " + server.getGrpcPort()); + System.out.flush(); + + try { + while (System.in.read() != -1) { + Thread.sleep(1000); + } + } catch (Exception e) { + } finally { + server.stop(); + } + } + + @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, String> selector) { + delegate.setHandshakeApplicationProtocolSelector((engine, protocols) -> selector.apply(this, protocols)); + } + + @Override + public BiFunction, String> getHandshakeApplicationProtocolSelector() { + return delegate.getHandshakeApplicationProtocolSelector(); + } + + @Override + public String getApplicationProtocol() { + return delegate.getApplicationProtocol(); + } + + @Override + public String getHandshakeApplicationProtocol() { + return delegate.getHandshakeApplicationProtocol(); + } + + @Override + public javax.net.ssl.SSLParameters getSSLParameters() { + return delegate.getSSLParameters(); + } + + @Override + public void beginHandshake() throws javax.net.ssl.SSLException { + delegate.beginHandshake(); + } + + @Override + public void closeInbound() throws javax.net.ssl.SSLException { + delegate.closeInbound(); + } + + @Override + public void closeOutbound() { + delegate.closeOutbound(); + } + + @Override + public java.lang.Runnable getDelegatedTask() { + return delegate.getDelegatedTask(); + } + + @Override + public java.lang.String[] getEnabledCipherSuites() { + return delegate.getEnabledCipherSuites(); + } + + @Override + public java.lang.String[] getEnabledProtocols() { + return delegate.getEnabledProtocols(); + } + + @Override + public javax.net.ssl.SSLEngineResult.HandshakeStatus getHandshakeStatus() { + return delegate.getHandshakeStatus(); + } + + @Override + public boolean getNeedClientAuth() { + return delegate.getNeedClientAuth(); + } + + @Override + public javax.net.ssl.SSLSession getSession() { + return delegate.getSession(); + } + + @Override + public java.lang.String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public java.lang.String[] getSupportedProtocols() { + return delegate.getSupportedProtocols(); + } + + @Override + public boolean getUseClientMode() { + return delegate.getUseClientMode(); + } + + @Override + public boolean getWantClientAuth() { + return delegate.getWantClientAuth(); + } + + @Override + public boolean isInboundDone() { + return delegate.isInboundDone(); + } + + @Override + public boolean isOutboundDone() { + return delegate.isOutboundDone(); + } + + @Override + public void setEnabledCipherSuites(java.lang.String[] suites) { + delegate.setEnabledCipherSuites(suites); + } + + @Override + public void setEnabledProtocols(java.lang.String[] protocols) { + delegate.setEnabledProtocols(protocols); + } + + @Override + public void setNeedClientAuth(boolean need) { + delegate.setNeedClientAuth(need); + } + + @Override + public void setUseClientMode(boolean mode) { + delegate.setUseClientMode(mode); + } + + @Override + public void setWantClientAuth(boolean want) { + delegate.setWantClientAuth(want); + } + + @Override + public javax.net.ssl.SSLEngineResult unwrap( + java.nio.ByteBuffer src, java.nio.ByteBuffer[] dsts, int offset, int length) + throws javax.net.ssl.SSLException { + return delegate.unwrap(src, dsts, offset, length); + } + + @Override + public javax.net.ssl.SSLEngineResult wrap( + java.nio.ByteBuffer[] srcs, int offset, int length, java.nio.ByteBuffer dst) + throws javax.net.ssl.SSLException { + return delegate.wrap(srcs, offset, length, dst); + } + + @Override + public boolean getEnableSessionCreation() { + return delegate.getEnableSessionCreation(); + } + + @Override + public void setEnableSessionCreation(boolean flag) { + delegate.setEnableSessionCreation(flag); + } + + @Override + public javax.net.ssl.SSLSession getHandshakeSession() { + return delegate.getHandshakeSession(); + } + } + + private static class PqcEnforcingSSLContextSpi extends javax.net.ssl.SSLContextSpi { + private final javax.net.ssl.SSLContext delegate; + + PqcEnforcingSSLContextSpi(javax.net.ssl.SSLContext delegate) { + this.delegate = delegate; + } + + @Override + protected javax.net.ssl.SSLEngine engineCreateSSLEngine() { + return new PqcEnforcingSSLEngine(delegate.createSSLEngine()); + } + + @Override + protected javax.net.ssl.SSLEngine engineCreateSSLEngine(java.lang.String host, int port) { + return new PqcEnforcingSSLEngine(delegate.createSSLEngine(host, port)); + } + + @Override + protected javax.net.ssl.SSLSessionContext engineGetClientSessionContext() { + return delegate.getClientSessionContext(); + } + + @Override + protected javax.net.ssl.SSLSessionContext engineGetServerSessionContext() { + return delegate.getServerSessionContext(); + } + + @Override + protected javax.net.ssl.SSLServerSocketFactory engineGetServerSocketFactory() { + return delegate.getServerSocketFactory(); + } + + @Override + protected javax.net.ssl.SSLSocketFactory engineGetSocketFactory() { + return delegate.getSocketFactory(); + } + + @Override + protected void engineInit( + javax.net.ssl.KeyManager[] km, + javax.net.ssl.TrustManager[] tm, + java.security.SecureRandom sr) + throws java.security.KeyManagementException { + } + } +} diff --git a/pqc-test/pqc-test-common/src/main/resources/pqctest.p12 b/pqc-test/pqc-test-common/src/main/resources/pqctest.p12 new file mode 100644 index 000000000..92c74c66d Binary files /dev/null and b/pqc-test/pqc-test-common/src/main/resources/pqctest.p12 differ diff --git a/pqc-test/pqc-test-release/pom.xml b/pqc-test/pqc-test-release/pom.xml new file mode 100644 index 000000000..fe034f36b --- /dev/null +++ b/pqc-test/pqc-test-release/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + + com.google.api + pqc-test-parent + 2.81.0-SNAPSHOT + ../pom.xml + + + pqc-test-release + + + + com.google.api + pqc-test-common + 2.81.0-SNAPSHOT + + + com.google.cloud + google-cloud-bigquery + 2.66.0 + + + com.google.cloud + google-cloud-translate + 2.92.0 + + + org.junit.jupiter + junit-jupiter-engine + 5.10.2 + test + + + diff --git a/pqc-test/pqc-test-release/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java b/pqc-test/pqc-test-release/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java new file mode 100644 index 000000000..78172e983 --- /dev/null +++ b/pqc-test/pqc-test-release/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.httpjson; + +public class RunPqcTest extends PqcConnectivityTest { + + @Override + protected boolean clientSupportsPqc() { + return false; + } + + @Override + protected boolean httpTestShouldSucceed() { + return false; + } + + @Override + protected boolean grpcTestShouldSucceed() { + return false; + } + + @Override + protected boolean bigqueryTestShouldSucceed() { + return false; + } +} diff --git a/pqc-test/pqc-test-snapshot/pom.xml b/pqc-test/pqc-test-snapshot/pom.xml new file mode 100644 index 000000000..d4f089c92 --- /dev/null +++ b/pqc-test/pqc-test-snapshot/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + + com.google.api + pqc-test-parent + 2.81.0-SNAPSHOT + ../pom.xml + + + pqc-test-snapshot + + + + + com.google.http-client + google-http-client + 2.1.1-SNAPSHOT + + + com.google.http-client + google-http-client-gson + 2.1.1-SNAPSHOT + + + com.google.http-client + google-http-client-jackson2 + 2.1.1-SNAPSHOT + + + com.google.http-client + google-http-client-apache-v2 + 2.1.1-SNAPSHOT + + + com.google.http-client + google-http-client-appengine + 2.1.1-SNAPSHOT + + + + + + + com.google.api + pqc-test-common + 2.81.0-SNAPSHOT + + + org.junit.jupiter + junit-jupiter-engine + 5.10.2 + test + + + com.google.cloud + google-cloud-bigquery + 2.66.0 + test + + + com.google.cloud + google-cloud-translate + 2.92.0 + + + + diff --git a/pqc-test/pqc-test-snapshot/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java b/pqc-test/pqc-test-snapshot/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java new file mode 100644 index 000000000..fe2130639 --- /dev/null +++ b/pqc-test/pqc-test-snapshot/src/test/java/com/google/api/gax/httpjson/RunPqcTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.httpjson; + +public class RunPqcTest extends PqcConnectivityTest { + + @Override + protected boolean clientSupportsPqc() { + return true; + } + + @Override + protected boolean httpTestShouldSucceed() { + return true; + } + + @Override + protected boolean grpcTestShouldSucceed() { + return true; + } + + @Override + protected boolean bigqueryTestShouldSucceed() { + return true; + } +}