Search code examples
javassljedis

With Jedis in Java, how can it choose the correct alias for SSL if you have multiple keys in your keystore?


I'm trying to connect to Redis using TLS, and it works fine for a keystore that has only a single cert inside of it.

The problem is, if I have multiple certs imported to my keystore, how does it know to choose the correct alias to pull the correct key?

I implemented my own X509KeyManager to see how it works, and the chooseClientAlias(String[] strings, Principal[] prncpls, Socket socket) method appears to be passed an empty array for prncples, which I'd presume would be how it could tell what cert to use.

But since that is empty, it simply returns whatever the first alias is that matches the keytype specified in the strings input, aka RSA, and that first alias might not be the correct one (which then ends up with it picking the incorrect key, and the ssl connection fails).

Is there something I'm misunderstanding about how this should be working to choose the correct alias for the connection, like do I need to be creating a different SSL Socket Factory & KeyManager for every SSL application I interface with, and explicitly specify the alias to use? Sorry, I'm not super well versed in TLS with java. Thanks.


Commands I used to generate the certs (ran this twice to create the real test cert, and a random fake cert which I imported after the real one to test if it would pick the right alias):

Create CA:
===
"C:\Program Files\Git\mingw64\bin\openssl.exe" genrsa -out ca.key 2048
"C:\Program Files\Git\mingw64\bin\openssl.exe" req -new -x509 -sha256 -key ca.key -out ca.crt

Create Redis Server Cert:
===
"C:\Program Files\Git\mingw64\bin\openssl.exe" genrsa -out redis.key
"C:\Program Files\Git\mingw64\bin\openssl.exe" req -new -sha256 -key redis.key -out redis.csr
"C:\Program Files\Git\mingw64\bin\openssl.exe" x509 -req -in redis.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out redis.crt -days 1000 -sha256

Create Client:
===
"C:\Program Files\Git\mingw64\bin\openssl.exe" genrsa -out client1.key 2048
"C:\Program Files\Git\mingw64\bin\openssl.exe" req -new -sha256 -key client1.key -out client1.csr
"C:\Program Files\Git\mingw64\bin\openssl.exe" x509 -req -in client1.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client1.crt -days 1000 -sha256

Commands I used to import the certs to a keystore:

Add ca to truststore:
=====
keytool -import -alias redisCA -keystore keystore.jks -file ca.crt

generate pkcs12:
=====
openssl pkcs12 -export -in client1.crt -inkey client1.key -out keystore.p12 -name my_cert

Import pkcs12 cert/key to keystore:
=====
keytool -importkeystore -destkeystore keystore.jks -srckeystore keystore.p12 -srcstoretype PKCS12 -alias my_cert

Code I used to interface with Redis (taken basically straight off their websites example):

public void testWithTls() throws IOException, GeneralSecurityException {
        HostAndPort address = new HostAndPort("localhost", 6379);
        
        SSLSocketFactory sslFactory = createSslSocketFactory(
                "D:\\tmp\\keystore.jks",
                "123456",
                "D:\\tmp\\keystore.jks",
                "123456"
        );

        JedisClientConfig config = DefaultJedisClientConfig.builder()
                .ssl(true).sslSocketFactory(sslFactory)
                .build();

        JedisPooled jedis = new JedisPooled(address, config);
        jedis.set("foo", "bar");
        System.out.println(jedis.get("foo")); // prints bar
}
   
private static SSLSocketFactory createSslSocketFactory(
            String caCertPath, String caCertPassword, String userCertPath, String userCertPassword)
            throws IOException, GeneralSecurityException {

        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(new FileInputStream(userCertPath), userCertPassword.toCharArray());

        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(new FileInputStream(caCertPath), caCertPassword.toCharArray());

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
        trustManagerFactory.init(trustStore);

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
        keyManagerFactory.init(keyStore, userCertPassword.toCharArray());

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

        return sslContext.getSocketFactory();
}

Information:

Jedis version: 4.4.3
Redis Docker container version: redis:7.0.10
Redis Docker container run command: `redis-server --tls-port 6379 --port 0 --tls-cert-file /tls/redis.crt --tls-key-file /tls/redis.key --tls-ca-cert-file /tls/ca.crt --loglevel warning`
Why am I using a jks store and not the p12: Because thats what the company I work at uses

Solution

  • There is no build in solution in the JDK for this kind of use case. I would either suggest to create 2 SSLContext. For every keystore 1 and use that to call the Jedis server.

    If you are willing to use a library, then this option is possible. I have created this kind of option which will do the trick. See below for the code snippet:

    import nl.altindag.ssl.SSLFactory;
    import nl.altindag.ssl.util.CertificateUtils;
    
    import javax.net.ssl.SSLSocketFactory;
    import java.nio.file.Paths;
    import java.security.cert.Certificate;
    import java.util.List;
    
    public class App {
        public static void main(String[] args) {
            String caCertPath = "D:\\tmp\\ca-certs.crt";
            List<Certificate> certificates = CertificateUtils.loadCertificate(Paths.get(caCertPath));
    
            SSLFactory sslFactory = SSLFactory.builder()
                    .withIdentityMaterial(Paths.get("D:\\tmp\\keystore-one.jks"), "123456".toCharArray())
                    .withIdentityMaterial(Paths.get("D:\\tmp\\keystore-two.jks"), "123456".toCharArray())
                    .withIdentityRoute("client-alias-one", "https://localhost:6379/", "https://localhost:6380/")
                    .withIdentityRoute("client-alias-two", "https://localhost:6381/", "https://localhost:6382/")
                    .withTrustMaterial(certificates)
                    .build();
    
            SSLSocketFactory sslSocketFactory = sslFactory.getSslSocketFactory();
    
            JedisClientConfig config = DefaultJedisClientConfig.builder()
                    .ssl(true)
                    .sslSocketFactory(sslSocketFactory)
                    .build();
        }
        
    }
    

    In this case you need to specify the alias which is named in the keystore file for the key and map that to the specific host and port as an identity route aka key material route. In that way it will use a specific key for a list of target servers. The library can be found here: sslcontext-kickstart