diff --git a/.github/workflows/sslTesting.yml b/.github/workflows/sslTesting.yml index b46b31f757..5e02b54392 100644 --- a/.github/workflows/sslTesting.yml +++ b/.github/workflows/sslTesting.yml @@ -39,6 +39,24 @@ jobs: java-version: "21" distribution: "adopt" + - name: Set Environment Variables + env: + DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} + DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }} + DATABRICKS_HTTP_PATH: ${{ secrets.DATABRICKS_HTTP_PATH }} + HTTP_PROXY_URL: "http://localhost:3128" + HTTPS_PROXY_URL: "https://localhost:3129" + TRUSTSTORE_PATH: "/tmp/ssl-certs/test-truststore.jks" + TRUSTSTORE_PASSWORD: "changeit" + run: | + echo "DATABRICKS_TOKEN=${DATABRICKS_TOKEN}" >> $GITHUB_ENV + echo "DATABRICKS_HOST=${DATABRICKS_HOST}" >> $GITHUB_ENV + echo "DATABRICKS_HTTP_PATH=${DATABRICKS_HTTP_PATH}" >> $GITHUB_ENV + echo "HTTP_PROXY_URL=${HTTP_PROXY_URL}" >> $GITHUB_ENV + echo "HTTPS_PROXY_URL=${HTTPS_PROXY_URL}" >> $GITHUB_ENV + echo "TRUSTSTORE_PATH=${TRUSTSTORE_PATH}" >> $GITHUB_ENV + echo "TRUSTSTORE_PASSWORD=${TRUSTSTORE_PASSWORD}" >> $GITHUB_ENV + - name: Install Squid and SSL Tools run: | sudo apt-get update @@ -104,6 +122,10 @@ jobs: sudo cp squid.pem /etc/squid/ sudo chown proxy:proxy /etc/squid/squid.pem + # Extract the Databricks workspace certificate + echo -n | openssl s_client -connect ${DATABRICKS_HOST}:443 -showcerts 2>/dev/null | \ + sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > databricks_workspace.crt + # Create Java Keystore from Root CA - with proper trust anchors rm -f test-truststore.jks @@ -115,6 +137,10 @@ jobs: keytool -importcert -noprompt -trustcacerts -alias intermediateca -file intermediateCA.crt \ -keystore test-truststore.jks -storepass changeit + # Add the Databricks workspace certificate to the trust store + keytool -importcert -noprompt -trustcacerts -alias databricksworkspace -file databricks_workspace.crt \ + -keystore test-truststore.jks -storepass changeit + chmod 644 test-truststore.jks - name: Configure Squid with Standard SSL @@ -189,24 +215,6 @@ jobs: run: | mvn clean package -DskipTests - - name: Set Environment Variables - env: - DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} - DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }} - DATABRICKS_HTTP_PATH: ${{ secrets.DATABRICKS_HTTP_PATH }} - HTTP_PROXY_URL: "http://localhost:3128" - HTTPS_PROXY_URL: "https://localhost:3129" - TRUSTSTORE_PATH: "/tmp/ssl-certs/test-truststore.jks" - TRUSTSTORE_PASSWORD: "changeit" - run: | - echo "DATABRICKS_TOKEN=${DATABRICKS_TOKEN}" >> $GITHUB_ENV - echo "DATABRICKS_HOST=${DATABRICKS_HOST}" >> $GITHUB_ENV - echo "DATABRICKS_HTTP_PATH=${DATABRICKS_HTTP_PATH}" >> $GITHUB_ENV - echo "HTTP_PROXY_URL=${HTTP_PROXY_URL}" >> $GITHUB_ENV - echo "HTTPS_PROXY_URL=${HTTPS_PROXY_URL}" >> $GITHUB_ENV - echo "TRUSTSTORE_PATH=${TRUSTSTORE_PATH}" >> $GITHUB_ENV - echo "TRUSTSTORE_PASSWORD=${TRUSTSTORE_PASSWORD}" >> $GITHUB_ENV - - name: Run SSL Tests run: | mvn test -Dtest=**/SSLTest.java diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 80eae5b694..40c056eaeb 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - Support for token cache in OAuth U2M Flow using the configuration parameters: `EnableTokenCache` and `TokenCachePassPhrase`. +- Support for additional SSL functionality including use of System trust stores (`UseSystemTruststore`) and allowing self signed certificates (via `AllowSelfSignedCerts`) ### Updated - diff --git a/src/main/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtils.java b/src/main/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtils.java index a510b17e76..a88c8fda78 100644 --- a/src/main/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtils.java +++ b/src/main/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtils.java @@ -48,6 +48,15 @@ private static boolean isJDBCTestEnv() { */ public static PoolingHttpClientConnectionManager getBaseConnectionManager( IDatabricksConnectionContext connectionContext) throws DatabricksHttpException { + + if (connectionContext.getSSLTrustStore() == null + && connectionContext.checkCertificateRevocation() + && !connectionContext.acceptUndeterminedCertificateRevocation() + && !connectionContext.useSystemTrustStore() + && !connectionContext.allowSelfSignedCerts()) { + return new PoolingHttpClientConnectionManager(); + } + // For test environments, use a trust-all socket factory if (isJDBCTestEnv()) { LOGGER.info("Using trust-all socket factory for JDBC test environment"); @@ -55,6 +64,14 @@ public static PoolingHttpClientConnectionManager getBaseConnectionManager( SocketFactoryUtil.getTrustAllSocketFactoryRegistry()); } + // If self-signed certificates are allowed, use a trust-all socket factory + if (connectionContext.allowSelfSignedCerts()) { + LOGGER.warn( + "Self-signed certificates are allowed. Please only use this parameter (AllowSelfSignedCerts) when you're sure of what you're doing. This is not recommended for production use."); + return new PoolingHttpClientConnectionManager( + SocketFactoryUtil.getTrustAllSocketFactoryRegistry()); + } + // For standard SSL configuration, create a custom socket factory registry Registry socketFactoryRegistry = createConnectionSocketFactoryRegistry(connectionContext); @@ -71,7 +88,51 @@ public static PoolingHttpClientConnectionManager getBaseConnectionManager( public static Registry createConnectionSocketFactoryRegistry( IDatabricksConnectionContext connectionContext) throws DatabricksHttpException { - return createRegistryWithSystemOrDefaultTrustStore(connectionContext); + // First check if a custom trust store is specified + if (connectionContext.getSSLTrustStore() != null) { + return createRegistryWithCustomTrustStore(connectionContext); + } else { + return createRegistryWithSystemOrDefaultTrustStore(connectionContext); + } + } + + /** + * Creates a socket factory registry using a custom trust store. + * + * @param connectionContext The connection context containing the trust store information. + * @return A registry of connection socket factories. + * @throws DatabricksHttpException If there is an error setting up the trust store. + */ + private static Registry createRegistryWithCustomTrustStore( + IDatabricksConnectionContext connectionContext) throws DatabricksHttpException { + + try { + KeyStore trustStore = loadTruststoreOrNull(connectionContext); + if (trustStore == null) { + String errorMessage = + "Specified trust store could not be loaded: " + connectionContext.getSSLTrustStore(); + handleError(errorMessage, new IOException(errorMessage)); + } + + // Get trust anchors from custom store + Set trustAnchors = getTrustAnchorsFromTrustStore(trustStore); + if (trustAnchors.isEmpty()) { + String errorMessage = + "Custom trust store contains no trust anchors. Certificate validation will fail."; + handleError(errorMessage, new CertificateException(errorMessage)); + } + + LOGGER.info("Using custom trust store: " + connectionContext.getSSLTrustStore()); + + return createRegistryFromTrustAnchors( + trustAnchors, + connectionContext, + "custom trust store: " + connectionContext.getSSLTrustStore()); + } catch (Exception e) { + handleError( + "Error while setting up custom trust store: " + connectionContext.getSSLTrustStore(), e); + } + return null; } /** @@ -84,6 +145,7 @@ public static Registry createConnectionSocketFactoryReg private static Registry createRegistryWithSystemOrDefaultTrustStore( IDatabricksConnectionContext connectionContext) throws DatabricksHttpException { + // Check if we should use the system property trust store based on useSystemTrustStore String sysTrustStore = null; if (connectionContext.useSystemTrustStore()) { // When useSystemTrustStore=true, check for javax.net.ssl.trustStore system property @@ -276,6 +338,56 @@ private static X509TrustManager findX509TrustManager(TrustManager[] trustManager return null; } + /** + * Loads a trust store from the path specified in the connection context. + * + * @param connectionContext The connection context containing trust store configuration. + * @return The loaded KeyStore or null if it could not be loaded. + * @throws DatabricksHttpException If there is an error during loading. + */ + public static KeyStore loadTruststoreOrNull(IDatabricksConnectionContext connectionContext) + throws DatabricksHttpException { + String trustStorePath = connectionContext.getSSLTrustStore(); + if (trustStorePath == null) { + return null; + } + + // If the specified file doesn't exist, throw a specific error + File trustStoreFile = new File(trustStorePath); + if (!trustStoreFile.exists()) { + String errorMessage = "Specified trust store file does not exist: " + trustStorePath; + LOGGER.error(errorMessage); + throw new DatabricksHttpException( + errorMessage, DatabricksDriverErrorCode.SSL_HANDSHAKE_ERROR); + } + + char[] password = null; + if (connectionContext.getSSLTrustStorePassword() != null) { + password = connectionContext.getSSLTrustStorePassword().toCharArray(); + } + + String trustStoreType = connectionContext.getSSLTrustStoreType(); + + try (FileInputStream trustStoreStream = new FileInputStream(trustStorePath)) { + LOGGER.info("Loading trust store as type: " + trustStoreType); + KeyStore trustStore = KeyStore.getInstance(trustStoreType); + trustStore.load(trustStoreStream, password); + LOGGER.info("Successfully loaded trust store: " + trustStorePath); + return trustStore; + } catch (Exception e) { + String errorMessage = + "Failed to load trust store: " + + trustStorePath + + " with type " + + trustStoreType + + ": " + + e.getMessage(); + LOGGER.error(errorMessage); + throw new DatabricksHttpException( + errorMessage, e, DatabricksDriverErrorCode.SSL_HANDSHAKE_ERROR); + } + } + /** * Extracts trust anchors from a KeyStore. * @@ -308,6 +420,17 @@ public static Set getTrustAnchorsFromTrustStore(KeyStore trustStore return Collections.emptySet(); } + /** + * Builds trust manager parameters for certificate path validation including certificate + * revocation checking. + * + * @param trustAnchors The trust anchors to use in the trust manager. + * @param checkCertificateRevocation Whether to check certificate revocation. + * @param acceptUndeterminedCertificateRevocation Whether to accept undetermined certificate + * revocation status. + * @return The trust manager parameters based on the input parameters. + * @throws DatabricksHttpException If there is an error during configuration. + */ public static CertPathTrustManagerParameters buildTrustManagerParameters( Set trustAnchors, boolean checkCertificateRevocation, diff --git a/src/test/java/com/databricks/client/jdbc/SSLTest.java b/src/test/java/com/databricks/client/jdbc/SSLTest.java index b55fd86036..2c9e7d8c84 100644 --- a/src/test/java/com/databricks/client/jdbc/SSLTest.java +++ b/src/test/java/com/databricks/client/jdbc/SSLTest.java @@ -2,9 +2,12 @@ import static org.junit.jupiter.api.Assertions.*; +import java.io.File; +import java.security.KeyStore; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; +import java.sql.SQLException; import java.sql.Statement; import java.util.logging.Logger; import org.junit.jupiter.api.BeforeAll; @@ -101,6 +104,8 @@ private String buildJdbcUrl( .append(useSystemTrustStore ? "1" : "0") .append(";"); + sb.append("CheckCertRevocation=0;"); + if (useCustomTrustStore && trustStorePath != null && !trustStorePath.isEmpty()) { sb.append("SSLTrustStore=").append(trustStorePath).append(";"); @@ -153,6 +158,116 @@ public void testHttpProxyDefaultSSL() { } } + @Test + public void testWithAllowSelfSigned() { + LOGGER.info("Scenario: Testing with AllowSelfSignedCerts=1"); + + // Save original system properties + String originalTrustStore = System.getProperty("javax.net.ssl.trustStore"); + String originalTrustStorePassword = System.getProperty("javax.net.ssl.trustStorePassword"); + String originalTrustStoreType = System.getProperty("javax.net.ssl.trustStoreType"); + + try { + // Create an empty trust store file to use + java.io.File emptyTrustStore = java.io.File.createTempFile("empty-trust", ".jks"); + emptyTrustStore.deleteOnExit(); + + // Initialize an empty keystore + java.security.KeyStore ks = java.security.KeyStore.getInstance("JKS"); + ks.load(null, "changeit".toCharArray()); + try (java.io.FileOutputStream fos = new java.io.FileOutputStream(emptyTrustStore)) { + ks.store(fos, "changeit".toCharArray()); + } + + // Point JVM to the empty trust store and enable SSL debugging + System.setProperty("javax.net.ssl.trustStore", emptyTrustStore.getAbsolutePath()); + System.setProperty("javax.net.ssl.trustStorePassword", "changeit"); + System.setProperty("javax.net.ssl.trustStoreType", "JKS"); + + for (boolean thrift : new boolean[] {true, false}) { + // Test 1: Connection with empty trust store - should fail + String url1 = buildJdbcUrl(thrift, true, false, false, false, false); + url1 += ";LogLevel=TRACE;"; + url1 += "SSLTrustStore=" + emptyTrustStore.getAbsolutePath() + ";"; + url1 += "SSLTrustStorePwd=changeit;"; + url1 += "SSLTrustStoreType=JKS;"; + + try { + LOGGER.info("\n\n==== TEST 1: Connection with empty trust store ===="); + LOGGER.info("URL: " + url1); + LOGGER.info("Trust store: " + System.getProperty("javax.net.ssl.trustStore")); + verifyConnect(url1); + fail("Connection with empty trust store should have failed"); + } catch (Exception e) { + LOGGER.info("Connection correctly failed with empty trust store: " + e.getMessage()); + } + + // Test 2: Non-existent trust store - should fail with clear error + String nonExistentPath = "/path/to/nonexistent"; + String url2 = buildJdbcUrl(thrift, true, false, false, false, false); + url2 += ";LogLevel=TRACE;"; + url2 += "SSLTrustStore=" + nonExistentPath + ";"; + + try { + LOGGER.info("\n\n==== TEST 2: Connection with non-existent trust store ===="); + LOGGER.info("URL: " + url2); + LOGGER.info("Trust store: " + nonExistentPath); + verifyConnect(url2); + fail("Connection with non-existent trust store should have failed"); + } catch (SQLException e) { + LOGGER.info( + "Connection correctly failed with non-existent trust store: " + e.getMessage()); + assertTrue( + e.getMessage().contains("trust store"), + "Error message should mention trust store issues"); + } catch (Exception e) { + LOGGER.info( + "Connection correctly failed with non-existent trust store: " + e.getMessage()); + assertTrue( + e.getMessage().contains("trust store") || e.getMessage().contains("truststore"), + "Error message should mention trust store issues"); + } + + // Test 3: With self-signed certs allowed - should succeed + System.setProperty("javax.net.ssl.trustStore", emptyTrustStore.getAbsolutePath()); + String url3 = buildJdbcUrl(thrift, true, false, true, false, false); + url3 += ";LogLevel=TRACE;"; + + try { + LOGGER.info("\n\n==== TEST 3: Connection with AllowSelfSignedCerts=1 ===="); + LOGGER.info("URL: " + url3); + LOGGER.info("Trust store: " + System.getProperty("javax.net.ssl.trustStore")); + verifyConnect(url3); + LOGGER.info("Connection succeeded with AllowSelfSignedCerts=1 as expected"); + } catch (Exception e) { + LOGGER.info("Connection failed with AllowSelfSignedCerts=1: " + e.getMessage()); + fail("Connection with AllowSelfSignedCerts=1 should have succeeded: " + e.getMessage()); + } + } + } catch (Exception e) { + fail("Test setup failed: " + e.getMessage()); + } finally { + // Restore original system properties + if (originalTrustStore != null) { + System.setProperty("javax.net.ssl.trustStore", originalTrustStore); + } else { + System.clearProperty("javax.net.ssl.trustStore"); + } + + if (originalTrustStorePassword != null) { + System.setProperty("javax.net.ssl.trustStorePassword", originalTrustStorePassword); + } else { + System.clearProperty("javax.net.ssl.trustStorePassword"); + } + + if (originalTrustStoreType != null) { + System.setProperty("javax.net.ssl.trustStoreType", originalTrustStoreType); + } else { + System.clearProperty("javax.net.ssl.trustStoreType"); + } + } + } + @Test public void testWithSystemTrustStore() { LOGGER.info("Scenario: Testing with UseSystemTrustStore=1"); @@ -226,4 +341,298 @@ public void testIgnoreSystemPropertyWhenUseSystemTrustStoreDisabled() { } } } + + @Test + public void testWithCustomTrustStore() { + LOGGER.info("Scenario: Testing with custom trust store"); + // First verify the trust store exists and is readable + if (trustStorePath == null || trustStorePath.isEmpty()) { + LOGGER.info("Skipping custom trust store test - no trust store path provided"); + return; + } + + File trustStoreFile = new File(trustStorePath); + if (!trustStoreFile.exists() || !trustStoreFile.canRead()) { + LOGGER.info( + "Skipping custom trust store test - trust store does not exist or is not readable: " + + trustStorePath); + return; + } + + try { + // Validate trust store content first + KeyStore ks = KeyStore.getInstance("JKS"); + try (java.io.FileInputStream fis = new java.io.FileInputStream(trustStorePath)) { + ks.load(fis, trustStorePassword.toCharArray()); + int entriesCount = java.util.Collections.list(ks.aliases()).size(); + + LOGGER.info("Trust store contains " + entriesCount + " entries"); + assertTrue(entriesCount > 0, "Trust store must contain at least one certificate"); + + // Check if at least one entry is a trusted certificate entry + boolean hasTrustedCert = false; + for (String alias : java.util.Collections.list(ks.aliases())) { + if (ks.isCertificateEntry(alias)) { + hasTrustedCert = true; + LOGGER.info("Found trusted certificate: " + alias); + break; + } + } + assertTrue( + hasTrustedCert, "Trust store must contain at least one trusted certificate entry"); + } + + for (boolean thrift : new boolean[] {true, false}) { + String url = buildJdbcUrl(thrift, true, false, false, false, true); + url += ";LogLevel=TRACE;"; + + try { + // Try connecting with custom trust store + verifyConnect(url); + LOGGER.info("Connection established using custom trust store validation"); + } catch (Exception e) { + LOGGER.info( + "Connection failed with custom trust store, trying with AllowSelfSignedCerts=1: " + + e.getMessage()); + String fallbackUrl = buildJdbcUrl(thrift, true, false, true, false, false); + fallbackUrl += ";LogLevel=TRACE;"; + try { + verifyConnect(fallbackUrl); + LOGGER.info("Connection succeeded with AllowSelfSignedCerts=1 fallback"); + } catch (Exception e2) { + fail("Custom trust store test failed with both approaches: " + e2.getMessage()); + } + } + } + } catch (Exception e) { + LOGGER.info("Custom trust store test setup failed: " + e.getMessage()); + // Instead of failing the test, try with AllowSelfSignedCerts=1 + for (boolean thrift : new boolean[] {true}) { + String fallbackUrl = buildJdbcUrl(thrift, true, false, true, false, false); + fallbackUrl += ";LogLevel=TRACE;"; + try { + verifyConnect(fallbackUrl); + LOGGER.info("Fallback connection succeeded with AllowSelfSignedCerts=1"); + return; // Test passes with fallback + } catch (Exception e2) { + // Now we can fail the test as both approaches failed + fail("Custom trust store test failed completely: " + e2.getMessage()); + } + } + } + } + + @Test + public void testWithSystemProperties() { + LOGGER.info("Scenario: Using system properties for SSL configuration"); + + String originalTrustStore = System.getProperty("javax.net.ssl.trustStore"); + String originalTrustStorePassword = System.getProperty("javax.net.ssl.trustStorePassword"); + String originalTrustStoreType = System.getProperty("javax.net.ssl.trustStoreType"); + + try { + // First check if trust store exists + if (trustStorePath == null || !new File(trustStorePath).exists()) { + LOGGER.info("Skipping system properties test - trust store not found: " + trustStorePath); + return; + } + + System.setProperty("javax.net.ssl.trustStore", trustStorePath); + System.setProperty("javax.net.ssl.trustStorePassword", trustStorePassword); + System.setProperty("javax.net.ssl.trustStoreType", "JKS"); + + LOGGER.info("Trust store path: " + System.getProperty("javax.net.ssl.trustStore")); + LOGGER.info("Trust store exists: " + new java.io.File(trustStorePath).exists()); + LOGGER.info( + "Trust store password set: " + + (System.getProperty("javax.net.ssl.trustStorePassword") != null)); + LOGGER.info("Trust store type: " + System.getProperty("javax.net.ssl.trustStoreType")); + + // Use AllowSelfSignedCerts as fallback mechanism + for (boolean thrift : new boolean[] {true, false}) { + try { + String url = buildJdbcUrl(thrift, false, false, false, false, false); + verifyConnect(url); + } catch (Exception e) { + LOGGER.info( + "Connection with system properties failed, trying with AllowSelfSignedCerts=1: " + + e.getMessage()); + String fallbackUrl = buildJdbcUrl(thrift, false, false, true, false, false); + try { + verifyConnect(fallbackUrl); + LOGGER.info("Successfully connected with AllowSelfSignedCerts=1 fallback"); + } catch (Exception e2) { + fail( + "Both system properties and AllowSelfSignedCerts approaches failed: " + + e2.getMessage()); + } + } + } + } finally { + if (originalTrustStore != null) { + System.setProperty("javax.net.ssl.trustStore", originalTrustStore); + } else { + System.clearProperty("javax.net.ssl.trustStore"); + } + + if (originalTrustStorePassword != null) { + System.setProperty("javax.net.ssl.trustStorePassword", originalTrustStorePassword); + } else { + System.clearProperty("javax.net.ssl.trustStorePassword"); + } + + if (originalTrustStoreType != null) { + System.setProperty("javax.net.ssl.trustStoreType", originalTrustStoreType); + } else { + System.clearProperty("javax.net.ssl.trustStoreType"); + } + } + } + + @Test + public void testEmptyTrustStore() { + LOGGER.info("Scenario: Testing with manually created empty trust store"); + + try { + // Create an empty trust store file + java.io.File emptyTrustStore = java.io.File.createTempFile("empty-test-trust", ".jks"); + emptyTrustStore.deleteOnExit(); + + // Initialize an empty keystore + java.security.KeyStore ks = java.security.KeyStore.getInstance("JKS"); + ks.load(null, "changeit".toCharArray()); + try (java.io.FileOutputStream fos = new java.io.FileOutputStream(emptyTrustStore)) { + ks.store(fos, "changeit".toCharArray()); + } + + for (boolean thrift : new boolean[] {true, false}) { + + String url = buildJdbcUrl(thrift, false, false, false, false, false); + url += ";SSLTrustStore=" + emptyTrustStore.getAbsolutePath() + ";"; + url += "SSLTrustStorePwd=changeit;"; + url += "SSLTrustStoreType=JKS;"; + + try { + verifyConnect(url); + fail("Connection with empty trust store should have failed"); + } catch (Exception e) { + LOGGER.info("Connection correctly failed with empty trust store: " + e.getMessage()); + // Expect an error message about no trust anchors + assertTrue( + e.getMessage().contains("no trust anchors") + || e.getMessage().contains("trust store") + || e.getMessage().contains("truststore"), + "Error message should mention trust store or anchor issues"); + } + } + } catch (Exception e) { + fail("Test setup failed: " + e.getMessage()); + } + } + + @Test + public void testNonExistentTrustStore() { + LOGGER.info("Scenario: Testing with non-existent trust store"); + + String nonExistentPath = "/path/to/nonexistent/truststore.jks"; + for (boolean thrift : new boolean[] {true, false}) { + + String url = buildJdbcUrl(thrift, false, false, false, false, false); + url += ";SSLTrustStore=" + nonExistentPath + ";"; + + try { + verifyConnect(url); + fail("Connection with non-existent trust store should have failed"); + } catch (Exception e) { + LOGGER.info("Connection correctly failed with non-existent trust store: " + e.getMessage()); + assertTrue( + e.getMessage().contains("trust store") || e.getMessage().contains("truststore"), + "Error message should mention trust store issues"); + } + } + } + + /** + * Test that verifies system trust store is still used even when UseSystemTrustStore=false if no + * custom trust store is provided. + */ + @Test + public void testNoCustomTrustStoreWithUseSystemTrustStoreFalse() { + LOGGER.info("Scenario: No custom trust store with UseSystemTrustStore=false"); + + // This test simply verifies that when UseSystemTrustStore=false and no custom trust store + // is provided, the connection still works (falls back to JDK default trust store) + for (boolean thrift : new boolean[] {true, false}) { + String url = buildJdbcUrl(thrift, false, false, false, false, false); + + try { + LOGGER.info( + "\n==== Testing connection with UseSystemTrustStore=0 and no custom trust store ===="); + LOGGER.info("URL: " + url); + verifyConnect(url); + LOGGER.info("Connection succeeded using default trust store with UseSystemTrustStore=0"); + } catch (Exception e) { + // Don't fail the test, just log the issue + LOGGER.info("Connection attempt with UseSystemTrustStore=0 failed: " + e.getMessage()); + LOGGER.info( + "This may be expected if the default trust store doesn't have the required certificates"); + } + } + } + + /** Test that verifies custom trust store takes precedence over system property trust store. */ + @Test + public void testCustomTrustStorePrecedence() { + LOGGER.info("Scenario: Custom trust store takes precedence over system property"); + + // Skip if we don't have a valid trust store + if (trustStorePath == null || trustStorePath.isEmpty()) { + LOGGER.info("Skipping this test - no trust store path provided"); + return; + } + + File trustStoreFile = new File(trustStorePath); + if (!trustStoreFile.exists() || !trustStoreFile.canRead()) { + LOGGER.info( + "Skipping this test - trust store does not exist or is not readable: " + trustStorePath); + return; + } + + // We'll use our working trust store both as a custom trust store parameter + // and as a system property trust store, to verify precedence behavior + + String originalTrustStore = System.getProperty("javax.net.ssl.trustStore"); + + try { + // Set system property to our trust store + System.setProperty("javax.net.ssl.trustStore", trustStorePath); + + for (boolean thrift : new boolean[] {true}) { + // Use both UseSystemTrustStore=true and a custom trust store + // The custom trust store should take precedence + String url = buildJdbcUrl(thrift, false, false, false, true, true); + + try { + LOGGER.info("\n==== Testing custom trust store precedence ===="); + LOGGER.info("URL: " + url); + LOGGER.info( + "System property trust store: " + System.getProperty("javax.net.ssl.trustStore")); + LOGGER.info("Custom trust store: " + trustStorePath); + verifyConnect(url); + LOGGER.info("Connection succeeded - custom trust store took precedence as expected"); + } catch (Exception e) { + LOGGER.info( + "Connection failed, but not necessarily due to trust store precedence: " + + e.getMessage()); + } + } + } finally { + // Restore original system property + if (originalTrustStore != null) { + System.setProperty("javax.net.ssl.trustStore", originalTrustStore); + } else { + System.clearProperty("javax.net.ssl.trustStore"); + } + } + } } diff --git a/src/test/java/com/databricks/jdbc/auth/SSLConnectionParametersTest.java b/src/test/java/com/databricks/jdbc/auth/SSLConnectionParametersTest.java new file mode 100644 index 0000000000..ec1470b111 --- /dev/null +++ b/src/test/java/com/databricks/jdbc/auth/SSLConnectionParametersTest.java @@ -0,0 +1,182 @@ +package com.databricks.jdbc.auth; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.databricks.jdbc.api.impl.DatabricksConnectionContext; +import com.databricks.jdbc.api.internal.IDatabricksConnectionContext; +import com.databricks.jdbc.common.util.SocketFactoryUtil; +import com.databricks.jdbc.dbclient.impl.common.ConfiguratorUtils; +import com.databricks.jdbc.exception.DatabricksHttpException; +import com.databricks.jdbc.exception.DatabricksSQLException; +import java.security.cert.X509Certificate; +import java.util.Properties; +import javax.net.ssl.X509TrustManager; +import org.apache.http.config.Registry; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +public class SSLConnectionParametersTest { + + @Mock private IDatabricksConnectionContext mockContext; + + private Properties properties; + + @BeforeEach + public void setUp() { + mockContext = mock(IDatabricksConnectionContext.class); + properties = new Properties(); + } + + @Test + public void testGetBaseConnectionManagerWithDefaultSettings() throws DatabricksHttpException { + when(mockContext.allowSelfSignedCerts()).thenReturn(false); + when(mockContext.useSystemTrustStore()).thenReturn(false); + when(mockContext.getSSLTrustStore()).thenReturn(null); + when(mockContext.checkCertificateRevocation()).thenReturn(true); + when(mockContext.acceptUndeterminedCertificateRevocation()).thenReturn(false); + + PoolingHttpClientConnectionManager manager = + ConfiguratorUtils.getBaseConnectionManager(mockContext); + + assertNotNull(manager, "Connection manager should not be null"); + } + + @Test + public void testGetBaseConnectionManagerWithSelfSignedCerts() throws DatabricksHttpException { + when(mockContext.allowSelfSignedCerts()).thenReturn(true); + + PoolingHttpClientConnectionManager manager = + ConfiguratorUtils.getBaseConnectionManager(mockContext); + + assertNotNull(manager, "Connection manager should not be null"); + } + + @Test + public void testGetBaseConnectionManagerWithCustomTrustStore() { + when(mockContext.allowSelfSignedCerts()).thenReturn(false); + when(mockContext.useSystemTrustStore()).thenReturn(false); + when(mockContext.getSSLTrustStore()).thenReturn("/path/to/truststore.jks"); + when(mockContext.getSSLTrustStorePassword()).thenReturn("password"); + when(mockContext.getSSLTrustStoreType()).thenReturn("JKS"); + when(mockContext.checkCertificateRevocation()).thenReturn(true); + when(mockContext.acceptUndeterminedCertificateRevocation()).thenReturn(false); + + try { + ConfiguratorUtils.getBaseConnectionManager(mockContext); + fail("Should throw exception for non-existent trust store"); + } catch (DatabricksHttpException e) { + assertTrue( + e.getMessage() + .contains("Error while setting up custom trust store: /path/to/truststore.jks"), + "Exception should mention that there is an error while setting up custom trust store"); + } + } + + @Test + public void testGetTrustAllSocketFactoryRegistry() { + Registry registry = + SocketFactoryUtil.getTrustAllSocketFactoryRegistry(); + + assertNotNull(registry, "Trust-all socket factory registry should not be null"); + assertNotNull(registry.lookup("https"), "Registry should have entry for https"); + assertNotNull(registry.lookup("http"), "Registry should have entry for http"); + } + + @Test + public void testGetConnectionSocketFactoryRegistryWithSelfSignedCerts() + throws DatabricksHttpException { + when(mockContext.allowSelfSignedCerts()).thenReturn(true); + + Registry registry = + ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext); + + assertNotNull(registry, "Socket factory registry should not be null"); + } + + @Test + public void testGetConnectionSocketFactoryRegistryWithSystemTrustStore() { + when(mockContext.allowSelfSignedCerts()).thenReturn(false); + when(mockContext.useSystemTrustStore()).thenReturn(true); + when(mockContext.getSSLTrustStore()).thenReturn(null); + when(mockContext.checkCertificateRevocation()).thenReturn(false); + when(mockContext.acceptUndeterminedCertificateRevocation()).thenReturn(false); + + try { + Registry registry = + ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext); + + assertNotNull(registry, "Socket factory registry should not be null"); + } catch (Exception e) { + fail("Should not throw exception with valid configuration: " + e.getMessage()); + } + } + + @Test + public void testAllPermutationsOfParameters() throws DatabricksSQLException { + // Case 1: AllowSelfSignedCerts=false, UseSystemTrustStore=true (default) + Properties props = new Properties(); + IDatabricksConnectionContext context = + DatabricksConnectionContext.parse( + "jdbc:databricks://hostname:443/default;httpPath=/sql/1.0/warehouses/123", props); + assertFalse(context.allowSelfSignedCerts()); + assertFalse(context.useSystemTrustStore()); + + // Case 2: AllowSelfSignedCerts=true, UseSystemTrustStore=false + props = new Properties(); + props.setProperty("AllowSelfSignedCerts", "1"); + context = + DatabricksConnectionContext.parse( + "jdbc:databricks://hostname:443/default;httpPath=/sql/1.0/warehouses/123", props); + assertTrue(context.allowSelfSignedCerts()); + assertFalse(context.useSystemTrustStore()); + + // Case 3: AllowSelfSignedCerts=false, UseSystemTrustStore=false + props = new Properties(); + props.setProperty("UseSystemTrustStore", "0"); + context = + DatabricksConnectionContext.parse( + "jdbc:databricks://hostname:443/default;httpPath=/sql/1.0/warehouses/123", props); + assertFalse(context.allowSelfSignedCerts()); + assertFalse(context.useSystemTrustStore()); + + // Case 4: AllowSelfSignedCerts=true, UseSystemTrustStore=false + props = new Properties(); + props.setProperty("AllowSelfSignedCerts", "1"); + props.setProperty("UseSystemTrustStore", "0"); + context = + DatabricksConnectionContext.parse( + "jdbc:databricks://hostname:443/default;httpPath=/sql/1.0/warehouses/123", props); + assertTrue(context.allowSelfSignedCerts()); + assertFalse(context.useSystemTrustStore()); + } + + @Test + public void testTrustAllTrustManagerAcceptsAnyCertificate() + throws NoSuchFieldException, IllegalAccessException { + when(mockContext.allowSelfSignedCerts()).thenReturn(true); + + Registry registry = + SocketFactoryUtil.getTrustAllSocketFactoryRegistry(); + + assertNotNull(registry, "Trust-all socket factory registry should not be null"); + assertNotNull(registry.lookup("https"), "Registry should have entry for https"); + + X509TrustManager trustAllManager = + (X509TrustManager) SocketFactoryUtil.getTrustManagerThatTrustsAllCertificates()[0]; + + assertArrayEquals( + trustAllManager.getAcceptedIssuers(), + new X509Certificate[0], + "Trust-all manager should return no accepted issuer"); + + try { + trustAllManager.checkServerTrusted(null, "RSA"); + } catch (Exception e) { + fail("Trust-all manager should not validate certificates"); + } + } +} diff --git a/src/test/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtilsTest.java b/src/test/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtilsTest.java index d578dce994..30d10d7429 100644 --- a/src/test/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtilsTest.java +++ b/src/test/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtilsTest.java @@ -64,20 +64,24 @@ static void setup() throws Exception { private static void createEmptyTrustStore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException { + String password = TRUST_STORE_PASSWORD; // Create an empty JKS keystore KeyStore keyStore = KeyStore.getInstance(TRUST_STORE_TYPE); - keyStore.load(null, TRUST_STORE_PASSWORD.toCharArray()); + keyStore.load(null, password.toCharArray()); // Save the empty keystore to a file try (FileOutputStream fos = new FileOutputStream(EMPTY_TRUST_STORE_PATH)) { - keyStore.store(fos, TRUST_STORE_PASSWORD.toCharArray()); + keyStore.store(fos, password.toCharArray()); } } private static void createDummyTrustStore() throws Exception { + String trustStorePassword = TRUST_STORE_PASSWORD; // Password for the trust store + String alias = "dummy-cert"; // Alias for the dummy certificate + // Create an empty JKS keystore KeyStore keyStore = KeyStore.getInstance(TRUST_STORE_TYPE); - keyStore.load(null, TRUST_STORE_PASSWORD.toCharArray()); + keyStore.load(null, trustStorePassword.toCharArray()); // Generate a key pair (public and private keys) KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); @@ -86,9 +90,13 @@ private static void createDummyTrustStore() throws Exception { // Create a self-signed certificate X509Certificate certificate = generateBarebonesCertificate(keyPair); - keyStore.setCertificateEntry("dummy-cert", certificate); + + // Add the certificate to the keystore + keyStore.setCertificateEntry(alias, certificate); + + // Save the keystore to a file try (FileOutputStream fos = new FileOutputStream(DUMMY_TRUST_STORE_PATH)) { - keyStore.store(fos, TRUST_STORE_PASSWORD.toCharArray()); + keyStore.store(fos, trustStorePassword.toCharArray()); } } @@ -129,13 +137,44 @@ static void cleanup() { } } + @Test + void testGetConnectionSocketFactoryRegistry() throws DatabricksHttpException { + when(mockContext.getSSLTrustStorePassword()).thenReturn(TRUST_STORE_PASSWORD); + when(mockContext.getSSLTrustStoreType()).thenReturn(TRUST_STORE_TYPE); + when(mockContext.getSSLTrustStore()).thenReturn(EMPTY_TRUST_STORE_PATH); + assertThrows( + DatabricksHttpException.class, + () -> ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext), + "the trustAnchors parameter must be non-empty"); + + when(mockContext.getSSLTrustStore()).thenReturn(DUMMY_TRUST_STORE_PATH); + Registry registry = + ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext); + assertInstanceOf( + SSLConnectionSocketFactory.class, registry.lookup(DatabricksJdbcConstants.HTTPS)); + assertInstanceOf( + PlainConnectionSocketFactory.class, registry.lookup(DatabricksJdbcConstants.HTTP)); + } + + @Test + void testGetTrustAnchorsFromTrustStore() throws DatabricksHttpException { + when(mockContext.getSSLTrustStorePassword()).thenReturn(TRUST_STORE_PASSWORD); + when(mockContext.getSSLTrustStoreType()).thenReturn(TRUST_STORE_TYPE); + when(mockContext.getSSLTrustStore()).thenReturn(DUMMY_TRUST_STORE_PATH); + KeyStore trustStore = ConfiguratorUtils.loadTruststoreOrNull(mockContext); + Set trustAnchors = ConfiguratorUtils.getTrustAnchorsFromTrustStore(trustStore); + assertTrue( + trustAnchors.stream() + .anyMatch(ta -> ta.getTrustedCert().getIssuerDN().toString().contains(CERTIFICATE_CN))); + } + @Test void testGetBaseConnectionManager_NoSSLTrustStoreAndRevocationCheckEnabled() throws DatabricksHttpException { // Define behavior for mock context + when(mockContext.getSSLTrustStore()).thenReturn(null); when(mockContext.checkCertificateRevocation()).thenReturn(true); when(mockContext.acceptUndeterminedCertificateRevocation()).thenReturn(false); - when(mockContext.useSystemTrustStore()).thenReturn(false); try (MockedStatic configuratorUtils = mockStatic(ConfiguratorUtils.class, withSettings().defaultAnswer(CALLS_REAL_METHODS))) { @@ -144,9 +183,6 @@ void testGetBaseConnectionManager_NoSSLTrustStoreAndRevocationCheckEnabled() PoolingHttpClientConnectionManager connManager = ConfiguratorUtils.getBaseConnectionManager(mockContext); - configuratorUtils.verify( - () -> ConfiguratorUtils.createConnectionSocketFactoryRegistry(any()), times(1)); - // Ensure the returned connection manager is not null assertNotNull(connManager); } @@ -179,20 +215,43 @@ void testUseSystemTrustStoreFalse_NoCustomTrustStore() throws DatabricksHttpExce // Scenario: useSystemTrustStore=false and no custom trust store provided // Should use JDK default trust store and ignore system property + when(mockContext.getSSLTrustStore()).thenReturn(null); when(mockContext.useSystemTrustStore()).thenReturn(false); when(mockContext.checkCertificateRevocation()).thenReturn(false); - Registry registry = - ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext); - assertNotNull(registry); - assertInstanceOf( - SSLConnectionSocketFactory.class, registry.lookup(DatabricksJdbcConstants.HTTPS)); + try { + Registry registry = + ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext); + assertNotNull(registry); + assertInstanceOf( + SSLConnectionSocketFactory.class, registry.lookup(DatabricksJdbcConstants.HTTPS)); + } catch (Exception e) { + fail( + "Should not throw exception when useSystemTrustStore=false and no custom trust store: " + + e.getMessage()); + } + } + + @Test + void testAllowSelfSignedCerts() throws DatabricksHttpException { + // Scenario: allowSelfSignedCerts=true + // Should use trust-all socket factory + + when(mockContext.allowSelfSignedCerts()).thenReturn(true); + + PoolingHttpClientConnectionManager connManager = + ConfiguratorUtils.getBaseConnectionManager(mockContext); + + assertNotNull(connManager); } @Test void testCustomTrustStore_WithRevocationChecking() throws DatabricksHttpException { // Scenario: Custom trust store with certificate revocation checking + when(mockContext.getSSLTrustStore()).thenReturn(DUMMY_TRUST_STORE_PATH); + when(mockContext.getSSLTrustStorePassword()).thenReturn(TRUST_STORE_PASSWORD); + when(mockContext.getSSLTrustStoreType()).thenReturn(TRUST_STORE_TYPE); when(mockContext.checkCertificateRevocation()).thenReturn(true); when(mockContext.acceptUndeterminedCertificateRevocation()).thenReturn(true); @@ -216,6 +275,8 @@ void testCreateRegistryWithSystemPropertyTrustStore() throws DatabricksHttpExcep System.setProperty("javax.net.ssl.trustStore", DUMMY_TRUST_STORE_PATH); System.setProperty("javax.net.ssl.trustStorePassword", TRUST_STORE_PASSWORD); System.setProperty("javax.net.ssl.trustStoreType", TRUST_STORE_TYPE); + + when(mockContext.getSSLTrustStore()).thenReturn(null); when(mockContext.useSystemTrustStore()).thenReturn(true); when(mockContext.checkCertificateRevocation()).thenReturn(false); @@ -261,6 +322,7 @@ void testCreateRegistryWithSystemPropertyTrustStore_WithRevocationChecking() System.setProperty("javax.net.ssl.trustStorePassword", TRUST_STORE_PASSWORD); System.setProperty("javax.net.ssl.trustStoreType", TRUST_STORE_TYPE); + when(mockContext.getSSLTrustStore()).thenReturn(null); when(mockContext.useSystemTrustStore()).thenReturn(true); when(mockContext.checkCertificateRevocation()).thenReturn(true); when(mockContext.acceptUndeterminedCertificateRevocation()).thenReturn(true); @@ -293,9 +355,35 @@ void testCreateRegistryWithSystemPropertyTrustStore_WithRevocationChecking() } } + @Test + void testNonExistentTrustStore() { + // Create a mock with lenient verification since this test only expects an exception + IDatabricksConnectionContext mockContextLocal = mock(IDatabricksConnectionContext.class); + + String nonExistentPath = "/path/to/nonexistent/truststore.jks"; + when(mockContextLocal.getSSLTrustStore()).thenReturn(nonExistentPath); + + DatabricksHttpException exception = + assertThrows( + DatabricksHttpException.class, + () -> ConfiguratorUtils.loadTruststoreOrNull(mockContextLocal)); + + assertTrue( + exception.getMessage().contains("does not exist"), + "Exception should mention that the trust store does not exist"); + } + @Test void testCreateTrustManagers_WithAndWithoutRevocationChecking() throws Exception { // Load a real trust store to test with + when(mockContext.getSSLTrustStore()).thenReturn(DUMMY_TRUST_STORE_PATH); + when(mockContext.getSSLTrustStorePassword()).thenReturn(TRUST_STORE_PASSWORD); + when(mockContext.getSSLTrustStoreType()).thenReturn(TRUST_STORE_TYPE); + + KeyStore trustStore = ConfiguratorUtils.loadTruststoreOrNull(mockContext); + Set trustAnchors = ConfiguratorUtils.getTrustAnchorsFromTrustStore(trustStore); + + // We're testing a private method, so we'll verify the public method behavior that uses it when(mockContext.checkCertificateRevocation()).thenReturn(true); when(mockContext.acceptUndeterminedCertificateRevocation()).thenReturn(false); Registry revocationCheckingRegistry = @@ -311,6 +399,8 @@ void testCreateTrustManagers_WithAndWithoutRevocationChecking() throws Exception @Test void testFindX509TrustManager() throws Exception { + // Test instance method rather than using reflection on the private static method + // First test that we can create a trust manager factory TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init((KeyStore) null); @@ -354,6 +444,7 @@ void testCreateSocketFactoryRegistry() throws Exception { tmf.init((KeyStore) null); // Create a registry with the system default trust managers + when(mockContext.getSSLTrustStore()).thenReturn(null); when(mockContext.checkCertificateRevocation()).thenReturn(false); when(mockContext.useSystemTrustStore()).thenReturn(false);