Skip to content

Commit 89dd484

Browse files
authored
Add core SSL trust store resolution and fallback logic (PR 2 of 3) (#786)
1 parent 727a245 commit 89dd484

File tree

15 files changed

+1026
-222
lines changed

15 files changed

+1026
-222
lines changed

.github/workflows/prCheck.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ jobs:
7474

7575
- name: Check Unit Tests
7676
shell: bash
77-
run: mvn test -Dtest='!**/integration/**,!**/DatabricksDriverExamples.java,!**/ProxyTest.java,!**/LoggingTest.java'
77+
run: mvn test -Dtest='!**/integration/**,!**/DatabricksDriverExamples.java,!**/ProxyTest.java,!**/LoggingTest.java,!**/SSLTest.java'
7878

7979
- name: Install xmllint
8080
if: runner.os == 'Linux'

.github/workflows/sslTesting.yml

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# ===================================================================
2+
# GitHub Action: SSL Certificate Validation Test with Squid Proxy
3+
#
4+
# Purpose:
5+
# This workflow simulates real-world SSL trust chain configurations
6+
# to validate JDBC driver support for:
7+
# - Custom trust stores
8+
# - System trust stores
9+
# - Self-signed certificate handling
10+
# - Revocation and fallback behavior
11+
#
12+
# How:
13+
# - Generates a Root CA, Intermediate CA, and signs a server cert (mirroring real world use-cases)
14+
# - Starts a Squid HTTPS proxy using the signed cert
15+
# - Creates a Java truststore with the correct anchors
16+
# - Optionally installs the Root CA into system trust store
17+
# - Runs targeted JDBC integration tests using SSLTest.java
18+
# ===================================================================1
19+
20+
name: SSL Certificate Validation Test with Squid Proxy
21+
22+
on:
23+
workflow_dispatch:
24+
pull_request:
25+
26+
jobs:
27+
ssl-test:
28+
runs-on:
29+
group: databricks-protected-runner-group
30+
labels: linux-ubuntu-latest
31+
32+
steps:
33+
- name: Checkout
34+
uses: actions/checkout@v4
35+
36+
- name: Set Up Java
37+
uses: actions/setup-java@v4
38+
with:
39+
java-version: "21"
40+
distribution: "adopt"
41+
42+
- name: Install Squid and SSL Tools
43+
run: |
44+
sudo apt-get update
45+
sudo apt-get install -y squid openssl libnss3-tools ca-certificates
46+
47+
- name: Create Root CA and Certificates
48+
run: |
49+
mkdir -p /tmp/ssl-certs
50+
cd /tmp/ssl-certs
51+
52+
# Generate Root CA
53+
openssl genrsa -out rootCA.key 4096
54+
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 365 -out rootCA.crt \
55+
-subj "/C=US/ST=California/L=San Francisco/O=Databricks Test/OU=Testing/CN=Databricks Test Root CA"
56+
57+
# Generate Intermediate CA
58+
openssl genrsa -out intermediateCA.key 4096
59+
openssl req -new -key intermediateCA.key -out intermediateCA.csr \
60+
-subj "/C=US/ST=California/L=San Francisco/O=Databricks Test/OU=Testing/CN=Databricks Test Intermediate CA"
61+
62+
# Create extension file for intermediate CA
63+
cat > intermediate_ext.cnf << EOF
64+
[ v3_ca ]
65+
subjectKeyIdentifier = hash
66+
authorityKeyIdentifier = keyid:always,issuer
67+
basicConstraints = critical, CA:true, pathlen:0
68+
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
69+
EOF
70+
71+
# Sign Intermediate CA with Root CA
72+
openssl x509 -req -in intermediateCA.csr -CA rootCA.crt -CAkey rootCA.key \
73+
-CAcreateserial -out intermediateCA.crt -days 365 -sha256 \
74+
-extfile intermediate_ext.cnf -extensions v3_ca
75+
76+
# Generate Squid Proxy Certificate
77+
openssl genrsa -out squid.key 2048
78+
openssl req -new -key squid.key -out squid.csr \
79+
-subj "/C=US/ST=California/L=San Francisco/O=Databricks Test/OU=Testing/CN=localhost"
80+
81+
# Create extension file for Squid certificate
82+
cat > squid_ext.cnf << EOF
83+
[ v3_req ]
84+
basicConstraints = CA:FALSE
85+
keyUsage = digitalSignature, keyEncipherment
86+
extendedKeyUsage = serverAuth
87+
subjectAltName = @alt_names
88+
89+
[alt_names]
90+
DNS.1 = localhost
91+
IP.1 = 127.0.0.1
92+
EOF
93+
94+
# Sign Squid certificate with Intermediate CA
95+
openssl x509 -req -in squid.csr -CA intermediateCA.crt -CAkey intermediateCA.key \
96+
-CAcreateserial -out squid.crt -days 365 -sha256 \
97+
-extfile squid_ext.cnf -extensions v3_req
98+
99+
# Create PEM file for Squid
100+
cat squid.crt squid.key > squid.pem
101+
chmod 400 squid.pem
102+
103+
# Copy to appropriate locations
104+
sudo cp squid.pem /etc/squid/
105+
sudo chown proxy:proxy /etc/squid/squid.pem
106+
107+
# Create Java Keystore from Root CA - with proper trust anchors
108+
rm -f test-truststore.jks
109+
110+
# Create a truststore with the root CA as a trusted certificate entry
111+
keytool -importcert -noprompt -trustcacerts -alias rootca -file rootCA.crt \
112+
-keystore test-truststore.jks -storepass changeit
113+
114+
# Also add the intermediate CA to the trust store
115+
keytool -importcert -noprompt -trustcacerts -alias intermediateca -file intermediateCA.crt \
116+
-keystore test-truststore.jks -storepass changeit
117+
118+
chmod 644 test-truststore.jks
119+
120+
- name: Configure Squid with Standard SSL
121+
run: |
122+
sudo cp /etc/squid/squid.conf /etc/squid/squid.conf.orig
123+
124+
echo "
125+
# Basic Configuration
126+
http_port 3128
127+
128+
# Plain HTTPS port with certificate
129+
https_port 3129 tls-cert=/etc/squid/squid.pem
130+
131+
# Access Control - very permissive for testing
132+
http_access allow all
133+
always_direct allow all
134+
135+
# Avoid DNS issues in test environment
136+
dns_v4_first on
137+
138+
# Disable caching for testing
139+
cache deny all
140+
141+
# Logging
142+
debug_options ALL,1
143+
logfile_rotate 0
144+
cache_log /var/log/squid/cache.log
145+
access_log /var/log/squid/access.log squid
146+
" | sudo tee /etc/squid/squid.conf
147+
148+
sudo mkdir -p /var/log/squid
149+
sudo chown -R proxy:proxy /var/log/squid
150+
sudo chmod 755 /var/log/squid
151+
152+
sudo squid -k parse || echo "Configuration has issues but we'll try to run it anyway"
153+
154+
- name: Start Squid Proxy
155+
run: |
156+
sudo systemctl stop squid || true
157+
sudo pkill squid || true
158+
159+
sudo squid -N -d 3 -f /etc/squid/squid.conf &
160+
161+
sleep 5
162+
ps aux | grep squid
163+
164+
- name: Wait for Squid to be Ready
165+
run: |
166+
for i in {1..5}; do
167+
if curl -v -x http://localhost:3128 http://databricks.com -m 10 -o /dev/null; then
168+
echo "HTTP proxy on 3128 is working!"
169+
break
170+
fi
171+
172+
sleep 3
173+
done
174+
175+
if ps aux | grep -v grep | grep squid > /dev/null; then
176+
echo "Squid is running"
177+
else
178+
echo "Squid is not running! Attempting restart..."
179+
sudo squid -N -d 3 -f /etc/squid/squid.conf &
180+
sleep 5
181+
fi
182+
183+
- name: Install Root CA in System Trust Store
184+
run: |
185+
sudo cp /tmp/ssl-certs/rootCA.crt /usr/local/share/ca-certificates/databricks-test-rootca.crt
186+
sudo update-ca-certificates
187+
188+
- name: Maven Build
189+
run: |
190+
mvn clean package -DskipTests
191+
192+
- name: Set Environment Variables
193+
env:
194+
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
195+
DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
196+
DATABRICKS_HTTP_PATH: ${{ secrets.DATABRICKS_HTTP_PATH }}
197+
HTTP_PROXY_URL: "http://localhost:3128"
198+
HTTPS_PROXY_URL: "https://localhost:3129"
199+
TRUSTSTORE_PATH: "/tmp/ssl-certs/test-truststore.jks"
200+
TRUSTSTORE_PASSWORD: "changeit"
201+
run: |
202+
echo "DATABRICKS_TOKEN=${DATABRICKS_TOKEN}" >> $GITHUB_ENV
203+
echo "DATABRICKS_HOST=${DATABRICKS_HOST}" >> $GITHUB_ENV
204+
echo "DATABRICKS_HTTP_PATH=${DATABRICKS_HTTP_PATH}" >> $GITHUB_ENV
205+
echo "HTTP_PROXY_URL=${HTTP_PROXY_URL}" >> $GITHUB_ENV
206+
echo "HTTPS_PROXY_URL=${HTTPS_PROXY_URL}" >> $GITHUB_ENV
207+
echo "TRUSTSTORE_PATH=${TRUSTSTORE_PATH}" >> $GITHUB_ENV
208+
echo "TRUSTSTORE_PASSWORD=${TRUSTSTORE_PASSWORD}" >> $GITHUB_ENV
209+
210+
- name: Run SSL Tests
211+
run: |
212+
mvn test -Dtest=**/SSLTest.java
213+
214+
- name: Cleanup
215+
if: always()
216+
run: |
217+
sudo systemctl stop squid
218+
sudo systemctl disable squid
219+
sudo pkill squid
220+
sudo rm -f /usr/local/share/ca-certificates/databricks-test-rootca.crt
221+
sudo update-ca-certificates --fresh

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@
313313
<exclude>**/ErrorCodes.java</exclude>
314314
<exclude>**/ProxyTest.java</exclude>
315315
<exclude>**/LoggingTest.java</exclude>
316+
<exclude>**/SSLTest.java</exclude>
316317
</excludes>
317318
<argLine>
318319
@{argLine}

src/main/java/com/databricks/jdbc/api/impl/volume/DBFSVolumeClient.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.databricks.jdbc.dbclient.IDatabricksHttpClient;
1717
import com.databricks.jdbc.dbclient.impl.common.ClientConfigurator;
1818
import com.databricks.jdbc.dbclient.impl.http.DatabricksHttpClientFactory;
19+
import com.databricks.jdbc.exception.DatabricksHttpException;
1920
import com.databricks.jdbc.exception.DatabricksSQLException;
2021
import com.databricks.jdbc.exception.DatabricksVolumeOperationException;
2122
import com.databricks.jdbc.log.JdbcLogger;
@@ -58,7 +59,8 @@ public DBFSVolumeClient(WorkspaceClient workspaceClient) {
5859
this.allowedVolumeIngestionPaths = "";
5960
}
6061

61-
public DBFSVolumeClient(IDatabricksConnectionContext connectionContext) {
62+
public DBFSVolumeClient(IDatabricksConnectionContext connectionContext)
63+
throws DatabricksHttpException {
6264
this.connectionContext = connectionContext;
6365
this.workspaceClient = getWorkspaceClientFromConnectionContext(connectionContext);
6466
this.apiClient = workspaceClient.apiClient();
@@ -392,7 +394,7 @@ public boolean deleteObject(String catalog, String schema, String volume, String
392394
}
393395

394396
WorkspaceClient getWorkspaceClientFromConnectionContext(
395-
IDatabricksConnectionContext connectionContext) {
397+
IDatabricksConnectionContext connectionContext) throws DatabricksHttpException {
396398
ClientConfigurator clientConfigurator = new ClientConfigurator(connectionContext);
397399
DatabricksThreadContextHolder.setDatabricksConfig(clientConfigurator.getDatabricksConfig());
398400
return clientConfigurator.getWorkspaceClient();

src/main/java/com/databricks/jdbc/api/impl/volume/DatabricksVolumeClientFactory.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.databricks.jdbc.api.IDatabricksVolumeClient;
44
import com.databricks.jdbc.api.internal.IDatabricksConnectionContext;
55
import com.databricks.jdbc.common.util.DatabricksThreadContextHolder;
6+
import com.databricks.jdbc.exception.DatabricksHttpException;
67
import com.databricks.jdbc.log.JdbcLogger;
78
import com.databricks.jdbc.log.JdbcLoggerFactory;
89
import java.sql.Connection;
@@ -33,7 +34,7 @@ public static IDatabricksVolumeClient getVolumeClient(Connection con) {
3334
* @return an instance of {@link IDatabricksVolumeClient}
3435
*/
3536
public static IDatabricksVolumeClient getVolumeClient(
36-
IDatabricksConnectionContext connectionContext) {
37+
IDatabricksConnectionContext connectionContext) throws DatabricksHttpException {
3738
LOGGER.debug(
3839
String.format(
3940
"Entering public static IDatabricksVolumeClient getVolumeClient with IDatabricksConnectionContext connectionContext = {%s}",

src/main/java/com/databricks/jdbc/common/util/SocketFactoryUtil.java

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,75 +5,78 @@
55
import com.databricks.sdk.core.DatabricksException;
66
import java.security.SecureRandom;
77
import java.security.cert.X509Certificate;
8-
import javax.net.ssl.HostnameVerifier;
98
import javax.net.ssl.SSLContext;
109
import javax.net.ssl.TrustManager;
1110
import javax.net.ssl.X509TrustManager;
1211
import org.apache.http.config.Registry;
1312
import org.apache.http.config.RegistryBuilder;
1413
import org.apache.http.conn.socket.ConnectionSocketFactory;
1514
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
15+
import org.apache.http.conn.ssl.NoopHostnameVerifier;
1616
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
1717

1818
public class SocketFactoryUtil {
19-
2019
private static final JdbcLogger LOGGER = JdbcLoggerFactory.getLogger(SocketFactoryUtil.class);
2120

2221
/**
23-
* <b>NOTE: </b> Only for testing purposes and should never be used in production.
24-
*
25-
* <p>Builds a registry of connection socket factories that trusts all SSL certificates.
22+
* Builds a registry of connection socket factories that trusts all SSL certificates. This should
23+
* only be used in testing environments or when explicitly configured to allow self-signed
24+
* certificates.
2625
*
2726
* @return A registry of connection socket factories.
2827
*/
2928
public static Registry<ConnectionSocketFactory> getTrustAllSocketFactoryRegistry() {
3029
LOGGER.warn(
31-
"This driver is configured to trust all SSL certificates. This is insecure and should be never used in production.");
32-
LOGGER.debug("Entering the getTrustAllSocketFactoryRegistry method");
33-
30+
"This driver is configured to trust all SSL certificates. This is insecure and should never be used in production.");
3431
try {
3532
// Create a TrustManager that trusts all certificates
36-
TrustManager[] trustAllCerts =
37-
new TrustManager[] {
38-
new X509TrustManager() {
39-
@Override
40-
public X509Certificate[] getAcceptedIssuers() {
41-
return null; // Accept all issuers
42-
}
43-
44-
@Override
45-
public void checkClientTrusted(X509Certificate[] certs, String authType) {
46-
// No-op: Trust all client certificates
47-
}
48-
49-
@Override
50-
public void checkServerTrusted(X509Certificate[] certs, String authType) {
51-
// No-op: Trust all server certificates
52-
}
53-
}
54-
};
33+
TrustManager[] trustAllCerts = getTrustManagerThatTrustsAllCertificates();
5534

5635
// Initialize the SSLContext with trust-all settings
5736
SSLContext sslContext = SSLContext.getInstance("TLS");
5837
sslContext.init(null, trustAllCerts, new SecureRandom());
5938

60-
// Disable hostname verification
61-
HostnameVerifier allHostsValid = (hostname, session) -> true;
62-
63-
// Configure SSLConnectionSocketFactory with the trust-all SSLContext
39+
// Use the NoopHostnameVerifier to disable hostname verification
6440
SSLConnectionSocketFactory sslSocketFactory =
65-
new SSLConnectionSocketFactory(sslContext, allHostsValid);
41+
new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
6642

6743
// Build and return the registry
6844
return RegistryBuilder.<ConnectionSocketFactory>create()
6945
.register("https", sslSocketFactory)
7046
.register("http", new PlainConnectionSocketFactory())
7147
.build();
72-
7348
} catch (Exception e) {
7449
String errorMessage = "Error while setting up trust-all SSL context.";
7550
LOGGER.error(errorMessage, e);
7651
throw new DatabricksException(errorMessage, e);
7752
}
7853
}
54+
55+
/**
56+
* Creates a TrustManager array that accepts all certificates without validation. This should only
57+
* be used in testing environments or when explicitly configured to allow self-signed
58+
* certificates.
59+
*
60+
* @return An array containing a single TrustManager that trusts all certificates.
61+
*/
62+
public static TrustManager[] getTrustManagerThatTrustsAllCertificates() {
63+
return new TrustManager[] {
64+
new X509TrustManager() {
65+
@Override
66+
public X509Certificate[] getAcceptedIssuers() {
67+
return new X509Certificate[0]; // Empty array instead of null for better compatibility
68+
}
69+
70+
@Override
71+
public void checkClientTrusted(X509Certificate[] certs, String authType) {
72+
// No-op: Trust all client certificates
73+
}
74+
75+
@Override
76+
public void checkServerTrusted(X509Certificate[] certs, String authType) {
77+
// No-op: Trust all server certificates
78+
}
79+
}
80+
};
81+
}
7982
}

0 commit comments

Comments
 (0)