Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,35 @@ jdbc:redis:cluster://[[<user>:]<password>@][<host1>[:<port1>],<host2>[:<port2>],
| clientName | String | null | |
| ssl | Boolean | false | Enable SSL. |
| verifyServerCertificate | Boolean | true | Configure a connection that uses SSL but does not verify the identity of the server. |
| truststorePath | String | null | Path to truststore file for server certificate validation. |
| truststorePassword | String | null | Password for truststore file. |
| truststoreType | String | JKS | Truststore type (JKS, PKCS12, etc.). |
| keystorePath | String | null | Path to keystore file for client certificate authentication. |
| keystorePassword | String | null | Password for keystore file. |
| keystoreType | String | JKS | Keystore type (JKS, PKCS12, etc.). |
| hostAndPortMapping | Map<String,String> | null | |
| verifyConnectionMode | Boolean | true | Verify that the mode specified for a connection in the URL prefix matches the server mode (standalone, cluster, sentinel). |

### SSL

Set the property `ssl` to `true`.

Pass arguments for your keystore and trust store:
Configure keystore and truststore using connection properties or system properties (as fallback):

```
jdbc:redis://[[<user>:]<password>@][<host>[:<port>]][/<database>]?ssl=true&truststorePath=/path/to/client.truststore&truststorePassword=password&<property1>=<value>&<property2>=<value>&...]
```

For client authentication (mutual TLS), also provide keystore:

```
jdbc:redis://[[<user>:]<password>@][<host>[:<port>]][/<database>]?ssl=true&keystorePath=/path/to/client.keystore&keystorePassword=password&<property1>=<value>&<property2>=<value>&...]
```

System properties are used as fallback if connection properties are not provided:
```
-Djavax.net.ssl.trustStore=/path/to/client.truststore
-Djavax.net.ssl.trustStorePassword=password123
# If you're using client authentication:
-Djavax.net.ssl.keyStore=/path/to/client.keystore
-Djavax.net.ssl.keyStorePassword=password123
```
Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ sourceSets {
java {
srcDirs = ['driver/src/test']
}
resources {
srcDirs = ['driver/src/test/resources']
}
}
}

Expand Down
56 changes: 43 additions & 13 deletions driver/src/main/java/jdbc/client/impl/RedisJedisURIBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import static jdbc.properties.RedisDefaultConfig.CONFIG;
import static jdbc.properties.RedisDriverPropertyInfoHelper.*;
import static jdbc.utils.SSLUtils.getTrustEverybodySSLContext;
import static jdbc.utils.SSLUtils.getValidatingSSLContext;
import static jdbc.utils.Utils.*;

public abstract class RedisJedisURIBase implements JedisClientConfig {
Expand Down Expand Up @@ -104,8 +105,20 @@ protected RedisJedisURIBase(String url, Properties info) throws SQLException {
return url.replaceFirst(prefix, "");
}

protected abstract @NotNull String getPrefix();
private String validateAndGetUrl(String path) {
String url = path;
if (!isNullOrEmpty(url)) {
try {
new URL(url);
}
catch (MalformedURLException e) {
url = "file:" + url;
}
}
return url;
}

protected abstract @NotNull String getPrefix();

private void setAuth(@NotNull String authBlock, Properties info) {
String user = CONFIG.getUser();
Expand Down Expand Up @@ -166,20 +179,37 @@ private void setSSLParameters(@NotNull Map<String, String> parameters, Propertie
ssl = getBoolean(parameters, info, SSL, CONFIG.isSsl());
if (ssl) {
boolean verifyServerCertificate = getBoolean(parameters, info, VERIFY_SERVER_CERTIFICATE, CONFIG.isVerifyServerCertificate());

// Read keystore properties for client certificate authentication (used in both branches)
String keystorePath = getString(parameters, info, KEYSTORE_PATH, System.getProperty("javax.net.ssl.keyStore", ""));
String keystorePassword
= getString(parameters, info, KEYSTORE_PASSWORD, System.getProperty("javax.net.ssl.keyStorePassword", ""));
String keystoreType = getString(parameters, info, KEYSTORE_TYPE,
System.getProperty("javax.net.ssl.keyStoreType", KeyStore.getDefaultType()));
String keystoreUrl = validateAndGetUrl(keystorePath);

if (!verifyServerCertificate) {
String keyStoreType = System.getProperty("javax.net.ssl.keyStoreType", KeyStore.getDefaultType());
String keyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword", "");
String keyStoreUrl = System.getProperty("javax.net.ssl.keyStore", "");
// check keyStoreUrl
if (!isNullOrEmpty(keyStoreUrl)) {
try {
new URL(keyStoreUrl);
} catch (MalformedURLException e) {
keyStoreUrl = "file:" + keyStoreUrl;
}
}
SSLContext context = getTrustEverybodySSLContext(keyStoreUrl, keyStoreType, keyStorePassword);
// For verifyServerCertificate=false, create custom SSLContext that trusts everything
// but still loads client certificate from keystore if provided
SSLContext context = getTrustEverybodySSLContext(keystoreUrl, keystoreType, keystorePassword);
sslSocketFactory = context.getSocketFactory();
} else {
// Read truststore properties (only needed for verifyServerCertificate=true)
String truststorePath = getString(parameters, info, TRUSTSTORE_PATH, System.getProperty("javax.net.ssl.trustStore", ""));
String truststorePassword
= getString(parameters, info, TRUSTSTORE_PASSWORD, System.getProperty("javax.net.ssl.trustStorePassword", ""));
String truststoreType = getString(parameters, info, TRUSTSTORE_TYPE,
System.getProperty("javax.net.ssl.trustStoreType", KeyStore.getDefaultType()));
String truststoreUrl = validateAndGetUrl(truststorePath);

if (!isNullOrEmpty(truststoreUrl) || !isNullOrEmpty(keystoreUrl)) {
// Custom truststore or keystore provided - create validating SSLContext
SSLContext context = getValidatingSSLContext(truststoreUrl, truststoreType, truststorePassword,
keystoreUrl, keystoreType, keystorePassword);
sslSocketFactory = context.getSocketFactory();
}
// else: No custom truststore/keystore - leave sslSocketFactory as null
// Jedis will use SSLContext.getDefault() with JVM's default truststore
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package jdbc.properties;

import java.security.KeyStore;
import java.sql.DriverPropertyInfo;
import java.util.ArrayList;

Expand All @@ -17,6 +18,12 @@ public class RedisDriverPropertyInfoHelper {
public static final String MAX_ATTEMPTS = "maxAttempts";
public static final String SSL = "ssl";
public static final String VERIFY_SERVER_CERTIFICATE = "verifyServerCertificate";
public static final String TRUSTSTORE_PATH = "truststorePath";
public static final String TRUSTSTORE_PASSWORD = "truststorePassword";
public static final String TRUSTSTORE_TYPE = "truststoreType";
public static final String KEYSTORE_PATH = "keystorePath";
public static final String KEYSTORE_PASSWORD = "keystorePassword";
public static final String KEYSTORE_TYPE = "keystoreType";

public static final String HOST_AND_PORT_MAPPING = "hostAndPortMapping";
public static final String HOST_AND_PORT_MAPPING_DEFAULT = null;
Expand All @@ -43,6 +50,12 @@ public static DriverPropertyInfo[] getPropertyInfo() {
addPropInfo(propInfos, SSL, String.valueOf(CONFIG.isSsl()), "Enable SSL.", booleanChoices);
addPropInfo(propInfos, VERIFY_SERVER_CERTIFICATE, String.valueOf(CONFIG.isVerifyServerCertificate()),
"Configure a connection that uses SSL but does not verify the identity of the server.", booleanChoices);
addPropInfo(propInfos, TRUSTSTORE_PATH, null, "Path to truststore file for server certificate validation.");
addPropInfo(propInfos, TRUSTSTORE_PASSWORD, null, "Password for truststore file.");
addPropInfo(propInfos, TRUSTSTORE_TYPE, KeyStore.getDefaultType(), "Truststore type (default: " + KeyStore.getDefaultType() + ").");
addPropInfo(propInfos, KEYSTORE_PATH, null, "Path to keystore file for client certificate authentication.");
addPropInfo(propInfos, KEYSTORE_PASSWORD, null, "Password for keystore file.");
addPropInfo(propInfos, KEYSTORE_TYPE, KeyStore.getDefaultType(), "Keystore type (default: " + KeyStore.getDefaultType() + ").");
addPropInfo(propInfos, HOST_AND_PORT_MAPPING, HOST_AND_PORT_MAPPING_DEFAULT, "Host and port mapping.");
addPropInfo(propInfos, VERIFY_CONNECTION_MODE, String.valueOf(VERIFY_CONNECTION_MODE_DEFAULT),
"Verify that the mode specified for a connection in the URL prefix matches the server mode (standalone, cluster, sentinel).", booleanChoices);
Expand Down
166 changes: 121 additions & 45 deletions driver/src/main/java/jdbc/utils/SSLUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,62 +15,50 @@
public class SSLUtils {

public static SSLContext getTrustEverybodySSLContext(String clientCertificateKeyStoreUrl, String clientCertificateKeyStoreType, String clientCertificateKeyStorePassword) throws SSLParamsException {
KeyManagerFactory kmf;
// Delegate to unified method with trust-all flag
TrustManager[] tms = new TrustManager[] { new MyTrustEverybodyManager() };
// Load KeyManagers (for client certificate authentication)
KeyManager[] kms = null;
if (!isNullOrEmpty(clientCertificateKeyStoreUrl)) {
kms = loadKeyManagers(clientCertificateKeyStoreUrl, clientCertificateKeyStoreType, clientCertificateKeyStorePassword);
}

// Create and initialize SSLContext
try {
kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kms, tms, null);
return sslContext;
}
catch (NoSuchAlgorithmException nsae) {
throw new SSLParamsException("Default algorithm definitions for TrustManager and/or KeyManager are invalid. Check java security properties file.", nsae);
throw new SSLParamsException("TLS is not a valid SSL protocol.", nsae);
}
catch (KeyManagementException kme) {
throw new SSLParamsException("KeyManagementException: " + kme.getMessage(), kme);
}
}

if (!isNullOrEmpty(clientCertificateKeyStoreUrl)) {
InputStream ksIS = null;
try {
if (!isNullOrEmpty(clientCertificateKeyStoreType)) {
KeyStore clientKeyStore = KeyStore.getInstance(clientCertificateKeyStoreType);
URL ksURL = new URL(clientCertificateKeyStoreUrl);
char[] password = (clientCertificateKeyStorePassword == null) ? new char[0] : clientCertificateKeyStorePassword.toCharArray();
ksIS = ksURL.openStream();
clientKeyStore.load(ksIS, password);
kmf.init(clientKeyStore, password);
kms = kmf.getKeyManagers();
}
}
catch (UnrecoverableKeyException uke) {
throw new SSLParamsException("Could not recover keys from client keystore. Check password?", uke);
}
catch (NoSuchAlgorithmException nsae) {
throw new SSLParamsException("Unsupported keystore algorithm [" + nsae.getMessage() + "]", nsae);
}
catch (KeyStoreException kse) {
throw new SSLParamsException("Could not create KeyStore instance [" + kse.getMessage() + "]", kse);
}
catch (CertificateException nsae) {
throw new SSLParamsException("Could not load client" + clientCertificateKeyStoreType + " keystore from " + clientCertificateKeyStoreUrl, nsae);
}
catch (MalformedURLException mue) {
throw new SSLParamsException(clientCertificateKeyStoreUrl + " does not appear to be a valid URL.", mue);
}
catch (IOException ioe) {
throw new SSLParamsException("Cannot open " + clientCertificateKeyStoreUrl + " [" + ioe.getMessage() + "]", ioe);
}
finally {
if (ksIS != null) {
try {
ksIS.close();
}
catch (IOException e) {
// can't close input stream, but keystore can be properly initialized so we shouldn't throw this exception
}
}
}
public static SSLContext getValidatingSSLContext(String truststoreUrl, String truststoreType, String truststorePassword,
String keystoreUrl, String keystoreType, String keystorePassword) throws SSLParamsException
{
// Delegate to unified method with validation enabled
TrustManager[] tms;
if (!isNullOrEmpty(truststoreUrl)) {
// SECURE: Load truststore for server certificate validation
tms = loadTrustManagers(truststoreUrl, truststoreType, truststorePassword);
} else {
// No truststore provided - use default
tms = null;
}
// Load KeyManagers (for client certificate authentication)
KeyManager[] kms = null;
if (!isNullOrEmpty(keystoreUrl)) {
kms = loadKeyManagers(keystoreUrl, keystoreType, keystorePassword);
}

// Create and initialize SSLContext
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kms, new TrustManager[]{new MyTrustEverybodyManager()}, null);
sslContext.init(kms, tms, null);
return sslContext;
}
catch (NoSuchAlgorithmException nsae) {
Expand All @@ -81,6 +69,94 @@ public static SSLContext getTrustEverybodySSLContext(String clientCertificateKey
}
}

private static TrustManager[] loadTrustManagers(String truststoreUrl, String truststoreType, String truststorePassword)
throws SSLParamsException
{
InputStream tsIS = null;
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
// Use provided type or default to JKS if not specified
String storeType = isNullOrEmpty(truststoreType) ? KeyStore.getDefaultType() : truststoreType;
KeyStore trustStore = KeyStore.getInstance(storeType);
URL tsURL = new URL(truststoreUrl);
char[] password = (truststorePassword == null) ? new char[0] : truststorePassword.toCharArray();
tsIS = tsURL.openStream();
trustStore.load(tsIS, password);
tmf.init(trustStore);
return tmf.getTrustManagers();
}
catch (NoSuchAlgorithmException nsae) {
throw new SSLParamsException("Unsupported truststore algorithm [" + nsae.getMessage() + "]", nsae);
}
catch (KeyStoreException kse) {
throw new SSLParamsException("Could not create TrustStore instance [" + kse.getMessage() + "]", kse);
}
catch (CertificateException ce) {
throw new SSLParamsException("Could not load truststore from " + truststoreUrl, ce);
}
catch (MalformedURLException mue) {
throw new SSLParamsException(truststoreUrl + " does not appear to be a valid URL.", mue);
}
catch (IOException ioe) {
throw new SSLParamsException("Cannot open " + truststoreUrl + " [" + ioe.getMessage() + "]", ioe);
}
finally {
if (tsIS != null) {
try {
tsIS.close();
}
catch (IOException e) {
// can't close input stream, but truststore can be properly initialized so we shouldn't throw this exception
}
}
}
}

private static KeyManager[] loadKeyManagers(String keystoreUrl, String keystoreType, String keystorePassword) throws SSLParamsException
{
InputStream ksIS = null;
try {
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
// Use provided type or default to JKS if not specified
String storeType = isNullOrEmpty(keystoreType) ? KeyStore.getDefaultType() : keystoreType;
KeyStore keyStore = KeyStore.getInstance(storeType);
URL ksURL = new URL(keystoreUrl);
char[] password = (keystorePassword == null) ? new char[0] : keystorePassword.toCharArray();
ksIS = ksURL.openStream();
keyStore.load(ksIS, password);
kmf.init(keyStore, password);
return kmf.getKeyManagers();
}
catch (UnrecoverableKeyException uke) {
throw new SSLParamsException("Could not recover keys from client keystore. Check password?", uke);
}
catch (NoSuchAlgorithmException nsae) {
throw new SSLParamsException("Unsupported keystore algorithm [" + nsae.getMessage() + "]", nsae);
}
catch (KeyStoreException kse) {
throw new SSLParamsException("Could not create KeyStore instance [" + kse.getMessage() + "]", kse);
}
catch (CertificateException ce) {
throw new SSLParamsException("Could not load keystore from " + keystoreUrl, ce);
}
catch (MalformedURLException mue) {
throw new SSLParamsException(keystoreUrl + " does not appear to be a valid URL.", mue);
}
catch (IOException ioe) {
throw new SSLParamsException("Cannot open " + keystoreUrl + " [" + ioe.getMessage() + "]", ioe);
}
finally {
if (ksIS != null) {
try {
ksIS.close();
}
catch (IOException e) {
// can't close input stream, but keystore can be properly initialized so we shouldn't throw this exception
}
}
}
}

private static class MyTrustEverybodyManager implements X509TrustManager {

public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
Expand Down
Loading