package redis.clients.jedis.tests; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.net.URI; import java.security.InvalidAlgorithmParameterException; import java.security.KeyStore; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisShardInfo; import redis.clients.jedis.exceptions.JedisConnectionException; import redis.clients.jedis.tests.commands.JedisCommandTestBase; public class SSLJedisTest extends JedisCommandTestBase { @BeforeClass public static void setupTrustStore() { setJvmTrustStore("src/test/resources/truststore.jceks", "jceks"); } private static void setJvmTrustStore(String trustStoreFilePath, String trustStoreType) { Assert.assertTrue(String.format("Could not find trust store at '%s'.", trustStoreFilePath), new File(trustStoreFilePath).exists()); System.setProperty("javax.net.ssl.trustStore", trustStoreFilePath); System.setProperty("javax.net.ssl.trustStoreType", trustStoreType); } /** * Tests opening a default SSL/TLS connection to redis. */ @Test public void connectWithoutShardInfo() { // The "rediss" scheme instructs jedis to open a SSL/TLS connection. Jedis jedis = new Jedis(URI.create("rediss://localhost:6390")); jedis.auth("foobared"); jedis.get("foo"); jedis.close(); } /** * Tests opening an SSL/TLS connection to redis. * NOTE: This test relies on a feature that is only available as of Java 7 and later. * It is commented out but not removed in case support for Java 6 is dropped or * we find a way to have the CI run a specific set of tests on Java 7 and above. */ @Test public void connectWithShardInfo() throws Exception { final URI uri = URI.create("rediss://localhost:6390"); final SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); // These SSL parameters ensure that we use the same hostname verifier used // for HTTPS. // Note: this options is only available in Java 7. final SSLParameters sslParameters = new SSLParameters(); sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); JedisShardInfo shardInfo = new JedisShardInfo(uri, sslSocketFactory, sslParameters, null); shardInfo.setPassword("foobared"); Jedis jedis = new Jedis(shardInfo); jedis.get("foo"); jedis.disconnect(); jedis.close(); } /** * Tests opening an SSL/TLS connection to redis using the loopback address of * 127.0.0.1. This test should fail because "127.0.0.1" does not match the * certificate subject common name and there are no subject alternative names * in the certificate. * * NOTE: This test relies on a feature that is only available as of Java 7 and later. * It is commented out but not removed in case support for Java 6 is dropped or * we find a way to have the CI run a specific set of tests on Java 7 and above. */ @Test public void connectWithShardInfoByIpAddress() throws Exception { final URI uri = URI.create("rediss://127.0.0.1:6390"); final SSLSocketFactory sslSocketFactory = createTrustStoreSslSocketFactory(); // These SSL parameters ensure that we use the same hostname verifier used // for HTTPS. // Note: this options is only available in Java 7. final SSLParameters sslParameters = new SSLParameters(); sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); JedisShardInfo shardInfo = new JedisShardInfo(uri, sslSocketFactory, sslParameters, null); shardInfo.setPassword("foobared"); Jedis jedis = new Jedis(shardInfo); try { jedis.get("foo"); Assert.fail("The code did not throw the expected JedisConnectionException."); } catch (JedisConnectionException e) { Assert.assertEquals("Unexpected first inner exception.", SSLHandshakeException.class, e.getCause().getClass()); Assert.assertEquals("Unexpected second inner exception.", CertificateException.class, e.getCause().getCause().getClass()); } try { jedis.close(); } catch (Throwable e1) { // Expected. } } /** * Tests opening an SSL/TLS connection to redis with a custom hostname * verifier. */ @Test public void connectWithShardInfoAndCustomHostnameVerifier() { final URI uri = URI.create("rediss://localhost:6390"); final SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); final SSLParameters sslParameters = new SSLParameters(); HostnameVerifier hostnameVerifier = new BasicHostnameVerifier(); JedisShardInfo shardInfo = new JedisShardInfo(uri, sslSocketFactory, sslParameters, hostnameVerifier); shardInfo.setPassword("foobared"); Jedis jedis = new Jedis(shardInfo); jedis.get("foo"); jedis.disconnect(); jedis.close(); } /** * Tests opening an SSL/TLS connection to redis with a custom socket factory. */ @Test public void connectWithShardInfoAndCustomSocketFactory() throws Exception { final URI uri = URI.create("rediss://localhost:6390"); final SSLSocketFactory sslSocketFactory = createTrustStoreSslSocketFactory(); final SSLParameters sslParameters = new SSLParameters(); HostnameVerifier hostnameVerifier = new BasicHostnameVerifier(); JedisShardInfo shardInfo = new JedisShardInfo(uri, sslSocketFactory, sslParameters, hostnameVerifier); shardInfo.setPassword("foobared"); Jedis jedis = new Jedis(shardInfo); jedis.get("foo"); jedis.disconnect(); jedis.close(); } /** * Tests opening an SSL/TLS connection to redis with a custom hostname * verifier. This test should fail because "127.0.0.1" does not match the * certificate subject common name and there are no subject alternative names * in the certificate. */ @Test public void connectWithShardInfoAndCustomHostnameVerifierByIpAddress() { final URI uri = URI.create("rediss://127.0.0.1:6390"); final SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); final SSLParameters sslParameters = new SSLParameters(); HostnameVerifier hostnameVerifier = new BasicHostnameVerifier(); JedisShardInfo shardInfo = new JedisShardInfo(uri, sslSocketFactory, sslParameters, hostnameVerifier); shardInfo.setPassword("foobared"); Jedis jedis = new Jedis(shardInfo); try { jedis.get("foo"); Assert.fail("The code did not throw the expected JedisConnectionException."); } catch (JedisConnectionException e) { Assert.assertEquals("The JedisConnectionException does not contain the expected message.", "The connection to '127.0.0.1' failed ssl/tls hostname verification.", e.getMessage()); } try { jedis.close(); } catch (Throwable e1) { // Expected. } } /** * Tests opening an SSL/TLS connection to redis with an empty certificate * trust store. This test should fail because there is no trust anchor for the * redis server certificate. * * @throws Exception */ @Test public void connectWithShardInfoAndEmptyTrustStore() throws Exception { final URI uri = URI.create("rediss://localhost:6390"); final SSLSocketFactory sslSocketFactory = createTrustNoOneSslSocketFactory(); JedisShardInfo shardInfo = new JedisShardInfo(uri, sslSocketFactory, null, null); shardInfo.setPassword("foobared"); Jedis jedis = new Jedis(shardInfo); try { jedis.get("foo"); Assert.fail("The code did not throw the expected JedisConnectionException."); } catch (JedisConnectionException e) { Assert.assertEquals("Unexpected first inner exception.", SSLException.class, e.getCause().getClass()); Assert.assertEquals("Unexpected second inner exception.", RuntimeException.class, e.getCause().getCause().getClass()); Assert.assertEquals("Unexpected third inner exception.", InvalidAlgorithmParameterException.class, e.getCause().getCause().getCause().getClass()); } try { jedis.close(); } catch (Throwable e1) { // Expected. } } /** * Creates an SSLSocketFactory that trusts all certificates in * truststore.jceks. */ private static SSLSocketFactory createTrustStoreSslSocketFactory() throws Exception { KeyStore trustStore = KeyStore.getInstance("jceks"); InputStream inputStream = null; try { inputStream = new FileInputStream("src/test/resources/truststore.jceks"); trustStore.load(inputStream, null); } finally { inputStream.close(); } TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("PKIX"); trustManagerFactory.init(trustStore); TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustManagers, new SecureRandom()); return sslContext.getSocketFactory(); } /** * Creates an SSLSocketFactory with a trust manager that does not trust any * certificates. */ private static SSLSocketFactory createTrustNoOneSslSocketFactory() throws Exception { TrustManager[] unTrustManagers = new TrustManager[] { new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } public void checkClientTrusted(X509Certificate[] chain, String authType) { throw new RuntimeException(new InvalidAlgorithmParameterException()); } public void checkServerTrusted(X509Certificate[] chain, String authType) { throw new RuntimeException(new InvalidAlgorithmParameterException()); } } }; SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, unTrustManagers, new SecureRandom()); return sslContext.getSocketFactory(); } /** * Very basic hostname verifier implementation for testing. NOT recommended * for production. * */ private static class BasicHostnameVerifier implements HostnameVerifier { private static final String COMMON_NAME_RDN_PREFIX = "CN="; @Override public boolean verify(String hostname, SSLSession session) { X509Certificate peerCertificate; try { peerCertificate = (X509Certificate) session.getPeerCertificates()[0]; } catch (SSLPeerUnverifiedException e) { throw new IllegalStateException("The session does not contain a peer X.509 certificate."); } String peerCertificateCN = getCommonName(peerCertificate); return hostname.equals(peerCertificateCN); } private String getCommonName(X509Certificate peerCertificate) { String subjectDN = peerCertificate.getSubjectDN().getName(); String[] dnComponents = subjectDN.split(","); for (String dnComponent : dnComponents) { if (dnComponent.startsWith(COMMON_NAME_RDN_PREFIX)) { return dnComponent.substring(COMMON_NAME_RDN_PREFIX.length()); } } throw new IllegalArgumentException("The certificate has no common name."); } } }