diff --git a/core/src/main/java/com/google/cloud/sql/core/ConscryptWorkaroundDelegatingTrustManger.java b/core/src/main/java/com/google/cloud/sql/core/ConscryptWorkaroundDelegatingTrustManger.java index 3caa91235..66e9b1d13 100644 --- a/core/src/main/java/com/google/cloud/sql/core/ConscryptWorkaroundDelegatingTrustManger.java +++ b/core/src/main/java/com/google/cloud/sql/core/ConscryptWorkaroundDelegatingTrustManger.java @@ -17,8 +17,12 @@ package com.google.cloud.sql.core; import java.net.Socket; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.X509ExtendedTrustManager; @@ -33,6 +37,30 @@ *
See https://github.com/google/conscrypt/issues/1033#issuecomment-982701272 */ class ConscryptWorkaroundDelegatingTrustManger extends X509ExtendedTrustManager { + private static final boolean CONSCRYPT_TLS; + + static { + // Provider name is "Conscrypt", hardcoded string in the library source: + // https://github.com/google/conscrypt/blob/655ad5069e1cb4d1989b8117eaf090371885af99/openjdk/src/main/java/org/conscrypt/Platform.java#L149 + Provider p = Security.getProvider("Conscrypt"); + if (p != null) { + try { + SSLContext ctx = SSLContext.getInstance("TLS"); + Provider prov = ctx.getProvider(); + CONSCRYPT_TLS = "Conscrypt".equals(prov.getName()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Unable to load algorithm TLS", e); + } + } else { + CONSCRYPT_TLS = false; + } + } + + /** Returns true if the Conscrypt Java Crypto Extension is installed. */ + static boolean isWorkaroundNeeded() { + return CONSCRYPT_TLS; + } + private final X509ExtendedTrustManager tm; ConscryptWorkaroundDelegatingTrustManger(X509ExtendedTrustManager tm) { diff --git a/core/src/main/java/com/google/cloud/sql/core/ConscryptWorkaroundTrustManagerFactory.java b/core/src/main/java/com/google/cloud/sql/core/ConscryptWorkaroundTrustManagerFactory.java deleted file mode 100644 index 2e87088fe..000000000 --- a/core/src/main/java/com/google/cloud/sql/core/ConscryptWorkaroundTrustManagerFactory.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2024 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.cloud.sql.core; - -import java.security.NoSuchAlgorithmException; -import java.security.Provider; -import java.security.Security; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; - -/** - * This is a workaround for a known bug in Conscrypt crypto in how it handles X509 auth type. - * OpenJDK interpres the X509 certificate auth type as "UNKNOWN" while Conscrypt interpret the same - * certificate as auth type "GENERIC". This incompatibility causes problems in the JDK. - * - *
This adapter works around the issue by creating wrappers around all TrustManager instances. It - * replaces "GENERIC" auth type with "UNKNOWN" auth type before delegating calls. - * - *
In the JVM, we need to implement 3 classes to make sure that we are capturing all of the - * TrustManager instances created when Conscrypt is the Java Crypto provider so that we can wrap - * them with the ConscryptWorkaroundTrustManager: - * - *
class ConscryptWorkaroundTrustManagerFactory extends TrustManagerFactory - has a bunch of - * final methods that delegate to a TrustManagerFactorySpi. class - * ConscryptWorkaroundTrustManagerFactorySpi implements TrustManagerFactorySpi - can actually - * intercept and delegate calls related to trust managers and wrap them with - * ConscryptWorkaroundTrustManager ConscryptWorkaroundTrustManager - the workaround for the - * Conscrypt bug. - * - *
See https://github.com/google/conscrypt/issues/1033#issuecomment-982701272 - */ -class ConscryptWorkaroundTrustManagerFactory extends TrustManagerFactory { - private static final boolean CONSCRYPT_TLS; - - static { - // Provider name is "Conscrypt", hardcoded string in the library source: - // https://github.com/google/conscrypt/blob/655ad5069e1cb4d1989b8117eaf090371885af99/openjdk/src/main/java/org/conscrypt/Platform.java#L149 - Provider p = Security.getProvider("Conscrypt"); - if (p != null) { - try { - SSLContext ctx = SSLContext.getInstance("TLS"); - Provider prov = ctx.getProvider(); - CONSCRYPT_TLS = "Conscrypt".equals(prov.getName()); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("Unable to load algorithm TLS", e); - } - } else { - CONSCRYPT_TLS = false; - } - } - - /** Returns true if the Conscrypt Java Crypto Extension is installed. */ - static boolean isWorkaroundNeeded() { - return CONSCRYPT_TLS; - } - - static ConscryptWorkaroundTrustManagerFactory newInstance() throws NoSuchAlgorithmException { - TrustManagerFactory delegate = TrustManagerFactory.getInstance("X.509"); - return new ConscryptWorkaroundTrustManagerFactory(delegate); - } - - private ConscryptWorkaroundTrustManagerFactory(TrustManagerFactory delegate) { - super( - new ConscryptWorkaroundTrustManagerFactorySpi(delegate), - delegate.getProvider(), - delegate.getAlgorithm()); - } -} diff --git a/core/src/main/java/com/google/cloud/sql/core/DefaultConnectionInfoRepository.java b/core/src/main/java/com/google/cloud/sql/core/DefaultConnectionInfoRepository.java index eed705ba1..ff77fc764 100644 --- a/core/src/main/java/com/google/cloud/sql/core/DefaultConnectionInfoRepository.java +++ b/core/src/main/java/com/google/cloud/sql/core/DefaultConnectionInfoRepository.java @@ -379,22 +379,8 @@ private SslData createSslData( KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(authKeyStore, new char[0]); - KeyStore trustedKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustedKeyStore.load(null, null); - trustedKeyStore.setCertificateEntry("instance", instanceMetadata.getInstanceCaCertificate()); - TrustManagerFactory tmf; - - // Note: This is a workaround for Conscrypt bug #1033 - // Conscrypt is the JCE provider on some Google Cloud runtimes like DataProc. - // https://github.com/google/conscrypt/issues/1033 - // - if (ConscryptWorkaroundTrustManagerFactory.isWorkaroundNeeded()) { - tmf = ConscryptWorkaroundTrustManagerFactory.newInstance(); - } else { - tmf = TrustManagerFactory.getInstance("X.509"); - } - - tmf.init(trustedKeyStore); + TrustManagerFactory tmf = + InstanceCheckingTrustManagerFactory.newInstance(instanceName, instanceMetadata); SSLContext sslContext; diff --git a/core/src/main/java/com/google/cloud/sql/core/InstanceCheckingTrustManagerFactory.java b/core/src/main/java/com/google/cloud/sql/core/InstanceCheckingTrustManagerFactory.java new file mode 100644 index 000000000..22b1ee221 --- /dev/null +++ b/core/src/main/java/com/google/cloud/sql/core/InstanceCheckingTrustManagerFactory.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024 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.cloud.sql.core; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import javax.net.ssl.TrustManagerFactory; + +/** + * Implement custom server certificate trust checks specific to Cloud SQL. + * + *
In the JVM, we need to implement 3 classes to make sure that we are capturing all the + * TrustManager instances created by the Java Crypto provider so that the connector will: + * + *
class InstanceCheckingTrustManagerFactory extends TrustManagerFactory - has a bunch of final + * methods that delegate to a TrustManagerFactorySpi. + * + *
class InstanceCheckingTrustManagerFactorySpi implements TrustManagerFactorySpi - can actually + * intercept requests for the list of TrustManager instances and wrap them with our replacement + * TrustManager. + * + *
class ConscryptWorkaroundTrustManager - the workaround for the Conscrypt bug. + * + *
class InstanceCheckingTrustManager - delegates TLS checks to the default provider and then + * checks that the Subject CN field contains the Cloud SQL instance ID. + */ +class InstanceCheckingTrustManagerFactory extends TrustManagerFactory { + + static InstanceCheckingTrustManagerFactory newInstance( + CloudSqlInstanceName instanceName, InstanceMetadata instanceMetadata) + throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException { + + TrustManagerFactory delegate = TrustManagerFactory.getInstance("X.509"); + KeyStore trustedKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustedKeyStore.load(null, null); + trustedKeyStore.setCertificateEntry("instance", instanceMetadata.getInstanceCaCertificate()); + + InstanceCheckingTrustManagerFactory tmf = + new InstanceCheckingTrustManagerFactory(instanceName, delegate); + + tmf.init(trustedKeyStore); + + return tmf; + } + + private InstanceCheckingTrustManagerFactory( + CloudSqlInstanceName instanceName, TrustManagerFactory delegate) { + super( + new InstanceCheckingTrustManagerFactorySpi(instanceName, delegate), + delegate.getProvider(), + delegate.getAlgorithm()); + } +} diff --git a/core/src/main/java/com/google/cloud/sql/core/ConscryptWorkaroundTrustManagerFactorySpi.java b/core/src/main/java/com/google/cloud/sql/core/InstanceCheckingTrustManagerFactorySpi.java similarity index 65% rename from core/src/main/java/com/google/cloud/sql/core/ConscryptWorkaroundTrustManagerFactorySpi.java rename to core/src/main/java/com/google/cloud/sql/core/InstanceCheckingTrustManagerFactorySpi.java index 36408a756..407de9c41 100644 --- a/core/src/main/java/com/google/cloud/sql/core/ConscryptWorkaroundTrustManagerFactorySpi.java +++ b/core/src/main/java/com/google/cloud/sql/core/InstanceCheckingTrustManagerFactorySpi.java @@ -26,19 +26,16 @@ import javax.net.ssl.X509ExtendedTrustManager; /** - * This is a workaround for a known bug in Conscrypt crypto in how it handles X509 auth type. - * OpenJDK interpres the X509 certificate auth type as "UNKNOWN" while Conscrypt interpret the same - * certificate as auth type "GENERIC". This incompatibility causes problems in the JDK. - * - *
This adapter works around the issue by creating wrappers around all TrustManager instances. It - * replaces "GENERIC" auth type with "UNKNOWN" auth type before delegating calls. - * - *
See https://github.com/google/conscrypt/issues/1033#issuecomment-982701272 + * Part of the InstanceCheckingTrustManagerFactory that implements custom CloudSQL server + * certificate checks. */ -class ConscryptWorkaroundTrustManagerFactorySpi extends TrustManagerFactorySpi { +class InstanceCheckingTrustManagerFactorySpi extends TrustManagerFactorySpi { private final TrustManagerFactory delegate; + private final CloudSqlInstanceName instanceName; - ConscryptWorkaroundTrustManagerFactorySpi(TrustManagerFactory delegate) { + InstanceCheckingTrustManagerFactorySpi( + CloudSqlInstanceName instanceName, TrustManagerFactory delegate) { + this.instanceName = instanceName; this.delegate = delegate; } @@ -59,10 +56,17 @@ protected TrustManager[] engineGetTrustManagers() { TrustManager[] delegates = new TrustManager[tms.length]; for (int i = 0; i < tms.length; i++) { if (tms[i] instanceof X509ExtendedTrustManager) { - delegates[i] = - new ConscryptWorkaroundDelegatingTrustManger((X509ExtendedTrustManager) tms[i]); - } else { + X509ExtendedTrustManager tm = (X509ExtendedTrustManager) tms[i]; + // Note: This is a workaround for Conscrypt bug #1033 + // Conscrypt is the JCE provider on some Google Cloud runtimes like DataProc. + // https://github.com/google/conscrypt/issues/1033 + if (ConscryptWorkaroundDelegatingTrustManger.isWorkaroundNeeded()) { + tm = new ConscryptWorkaroundDelegatingTrustManger(tm); + } + + delegates[i] = new InstanceCheckingTrustManger(instanceName, tm); + } else { delegates[i] = tms[i]; } } diff --git a/core/src/main/java/com/google/cloud/sql/core/InstanceCheckingTrustManger.java b/core/src/main/java/com/google/cloud/sql/core/InstanceCheckingTrustManger.java new file mode 100644 index 000000000..69d41d8d2 --- /dev/null +++ b/core/src/main/java/com/google/cloud/sql/core/InstanceCheckingTrustManger.java @@ -0,0 +1,128 @@ +/* + * Copyright 2024 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.cloud.sql.core; + +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; + +/** + * This is a workaround for a known bug in Conscrypt crypto in how it handles X509 auth type. + * OpenJDK interpres the X509 certificate auth type as "UNKNOWN" while Conscrypt interpret the same + * certificate as auth type "GENERIC". This incompatibility causes problems in the JDK. + * + *
This adapter works around the issue by creating wrappers around all TrustManager instances. It + * replaces "GENERIC" auth type with "UNKNOWN" auth type before delegating calls. + * + *
See https://github.com/google/conscrypt/issues/1033#issuecomment-982701272
+ */
+class InstanceCheckingTrustManger extends X509ExtendedTrustManager {
+ private final X509ExtendedTrustManager tm;
+ private final CloudSqlInstanceName instanceName;
+
+ public InstanceCheckingTrustManger(
+ CloudSqlInstanceName instanceName, X509ExtendedTrustManager tm) {
+ this.instanceName = instanceName;
+ this.tm = tm;
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
+ throws CertificateException {
+ tm.checkClientTrusted(chain, authType, socket);
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
+ throws CertificateException {
+ tm.checkClientTrusted(chain, authType, engine);
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ tm.checkClientTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
+ throws CertificateException {
+ tm.checkServerTrusted(chain, authType, socket);
+ checkCertificateChain(chain);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
+ throws CertificateException {
+ tm.checkServerTrusted(chain, authType, engine);
+ checkCertificateChain(chain);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ tm.checkServerTrusted(chain, authType);
+ checkCertificateChain(chain);
+ }
+
+ private void checkCertificateChain(X509Certificate[] chain) throws CertificateException {
+ if (chain.length == 0) {
+ throw new CertificateException("No server certificates in chain");
+ }
+ if (chain[0].getSubjectX500Principal() == null) {
+ throw new CertificateException("Subject is missing");
+ }
+
+ String cn = null;
+
+ try {
+ String subject = chain[0].getSubjectX500Principal().getName();
+ LdapName subjectName = new LdapName(subject);
+ for (Rdn rdn : subjectName.getRdns()) {
+ if ("CN".equals(rdn.getType())) {
+ cn = (String) rdn.getValue();
+ }
+ }
+ } catch (InvalidNameException e) {
+ throw new CertificateException("Exception parsing the server certificate subject field", e);
+ }
+
+ if (cn == null) {
+ throw new CertificateException("Server certificate subject does not contain a value for CN");
+ }
+
+ // parse CN from subject. CN always comes last in the list.
+ String instName = this.instanceName.getProjectId() + ":" + this.instanceName.getInstanceId();
+ if (!instName.equals(cn)) {
+ throw new CertificateException(
+ "Server certificate CN does not match instance name. Server certificate CN="
+ + cn
+ + " Expected instance name: "
+ + instName);
+ }
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return tm.getAcceptedIssuers();
+ }
+}
diff --git a/core/src/test/java/com/google/cloud/sql/core/CloudSqlCoreTestingBase.java b/core/src/test/java/com/google/cloud/sql/core/CloudSqlCoreTestingBase.java
index fb1fca9e0..ffc371f9d 100644
--- a/core/src/test/java/com/google/cloud/sql/core/CloudSqlCoreTestingBase.java
+++ b/core/src/test/java/com/google/cloud/sql/core/CloudSqlCoreTestingBase.java
@@ -62,6 +62,8 @@ public class CloudSqlCoreTestingBase {
ListenableFuture