3

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
NPC
  • 31
  • 1
  • 3
  • 1
    The TLS protocols provide for the server, when requesting client authentication, to specify the CA(s) it wants the client cert to use; that info is passed to X509[Extended]KeyManager as 'principals' for it to use in choosing the cert-and-key. I don't know redis but according to doc at redis.io it uses OpenSSL, and OpenSSL by default does NOT implement this 'desired CA' information. You could confirm this by (temporarily) setting sysprop `javax.net.debug=ssl,handshake` in your client, or by getting an external trace with wireshark or similar. – dave_thompson_085 Jul 15 '23 at 17:29
  • 1
    ... If so, yes you will need a different KeyManager (and thus context and socketfactory) for each server where you want to use a different cert. These could be instances of a custom KeyManager that selects the correct entry from a common keystore, or it might be easier to create multiple in-memory keystores each with only one cert, and instantiate the standard KeyManager for each of those keystores. – dave_thompson_085 Jul 15 '23 at 17:31
  • @dave_thompson_085 Thanks man! Stackoverflow is always a 50/50 on whether people will provide helpful responses or just close out the question, and your response was super solid in helping me understand what's going on. – NPC Jul 15 '23 at 21:19
  • 1
    @NPC This might work: https://github.com/Hakky54/sslcontext-kickstart#routing-identity-material-to-specific-host It is possible to specify which alias it should use for a specific hosts. So there is no need to recreate a new sslcontext for each clean targetting a different server. I have used this setup with a traditional client/servver setup and it worked, so I assume it will also work for Jedis, can you maybe give it a try? – Hakan54 Jul 16 '23 at 22:37
  • @Hakan54 Yeah that would also work, I see what that's doing for matching the hosts to aliases and it's a pretty nifty idea. I'm surprised Java doesn't have built-in functions for that already, it seems like that should be a core feature. – NPC Jul 16 '23 at 23:20
  • 1
    Java normally just picks the first alias which matches the authentication type. If you happen to have more keys which only works for some target servers it just does not work out of the box. I didn't liked the idea of recreating a sslcontext for every connection having a different key, so I thought this would be a good idea. It is just routing the alias to the correct url. – Hakan54 Jul 17 '23 at 06:52

0 Answers0