1
votes

I have a java application that opens a SSLServerSocket. To create that SSLServerSocket, it uses a KeyStore which it loads from the DB. Every so often we will roll over the certificates in the DB.

The way the application is set up right now, it keeps the SSLServerSocket forever and will never start using the new certificates unless it it manually restarted.

Does java have a way to hot replace the certificate an SSLServerSocket uses? If not, what is the accepted best practice in this situation?

1

1 Answers

1
votes

It is possible but not supported out of the box. I had the same challenge for one of my projects. I resolved it by wrapping the KeyManager and TrustManager instance into a delegating one and I added an additional method to set the internal KeyManager and TrustManager whenever I want to replace to old one. So you have two option in my opinion see below for the details:

Option 1

Use only plain java code and copy the below KeyManager and TrustManager.

import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedKeyManager;
import java.net.Socket;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Objects;

public final class HotSwappableX509ExtendedKeyManager extends X509ExtendedKeyManager {

    private X509ExtendedKeyManager keyManager;

    public HotSwappableX509ExtendedKeyManager(X509ExtendedKeyManager keyManager) {
        this.keyManager = keyManager;
    }

    @Override
    public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
        return keyManager.chooseClientAlias(keyType, issuers, socket);
    }

    @Override
    public String[] getClientAliases(String keyType, Principal[] issuers) {
        return keyManager.getClientAliases(keyType, issuers);
    }

    @Override
    public String chooseEngineClientAlias(String[] keyTypes, Principal[] issuers, SSLEngine sslEngine) {
        return keyManager.chooseEngineClientAlias(keyTypes, issuers, sslEngine);
    }

    @Override
    public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
        return keyManager.chooseServerAlias(keyType, issuers, socket);
    }

    @Override
    public String[] getServerAliases(String keyType, Principal[] issuers) {
        return keyManager.getServerAliases(keyType, issuers);
    }

    @Override
    public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine sslEngine) {
        return keyManager.chooseEngineServerAlias(keyType, issuers, sslEngine);
    }

    @Override
    public PrivateKey getPrivateKey(String alias) {
        return keyManager.getPrivateKey(alias);
    }

    @Override
    public X509Certificate[] getCertificateChain(String alias) {
        return keyManager.getCertificateChain(alias);
    }

    public void setKeyManager(X509ExtendedKeyManager keyManager) {
        this.keyManager = Objects.requireNonNull(keyManager);
    }

}

import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedTrustManager;
import java.net.Socket;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Objects;

public class HotSwappableX509ExtendedTrustManager extends X509ExtendedTrustManager {

    private X509ExtendedTrustManager trustManager;

    public HotSwappableX509ExtendedTrustManager(X509ExtendedTrustManager trustManager) {
        this.trustManager = trustManager;
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
        trustManager.checkClientTrusted(chain, authType, socket);
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        trustManager.checkClientTrusted(chain, authType);
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException {
        trustManager.checkClientTrusted(chain, authType, sslEngine);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
        trustManager.checkServerTrusted(chain, authType, socket);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        trustManager.checkServerTrusted(chain, authType);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException {
        trustManager.checkServerTrusted(chain, authType, sslEngine);
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        X509Certificate[] acceptedIssuers = trustManager.getAcceptedIssuers();
        return Arrays.copyOf(acceptedIssuers, acceptedIssuers.length);
    }

    public void setTrustManager(X509ExtendedTrustManager trustManager) {
        this.trustManager = Objects.requireNonNull(trustManager);
    }

}

And use the above wrappers like the below snippet:

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.Objects;
import java.security.KeyStore;

public class App {
    
    public static void main(String[] args) throws Exception {
        Path keyStorePath = Paths.get("/path/to/keystore.jks");
        InputStream keyStoreInputStream = Files.newInputStream(keyStorePath, StandardOpenOption.READ);
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(keyStoreInputStream, "secret".toCharArray());

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, "secret".toCharArray());
        KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();

        Path trustStorePath = Paths.get("/path/to/truststore.jks");
        InputStream trustStoreInputStream = Files.newInputStream(trustStorePath, StandardOpenOption.READ);
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(trustStoreInputStream, "secret".toCharArray());

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

        HotSwappableX509ExtendedKeyManager hotSwappableX509ExtendedKeyManager = new HotSwappableX509ExtendedKeyManager((X509ExtendedKeyManager) keyManagers[0]);
        HotSwappableX509ExtendedTrustManager hotSwappableX509ExtendedTrustManager = new HotSwappableX509ExtendedTrustManager((X509ExtendedTrustManager) trustManagers[0]);

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(new KeyManager[]{hotSwappableX509ExtendedKeyManager}, new TrustManager[]{hotSwappableX509ExtendedTrustManager}, null);
        SSLServerSocketFactory sslServerSocketFactory = sslContext.getServerSocketFactory();

        // use the sslServerSocketFactory
        // after some time update the key and trust material with the following snippet
        X509ExtendedKeyManager myNewKeyManager = ...;
        X509ExtendedTrustManager myNewTrustManager = ...;
        hotSwappableX509ExtendedKeyManager.setKeyManager(myNewKeyManager);
        hotSwappableX509ExtendedTrustManager.setTrustManager(myNewTrustManager);
        SSLSessionContext sslSessionContext = sslContext.getServerSessionContext()
        Collections.list(sslSessionContext.getIds()).stream()
                .map(sslSessionContext::getSession)
                .filter(Objects::nonNull)
                .forEach(SSLSession::invalidate);
    }
    
}

Option 2

I have made option 1 already available within my own library, see here for the details: GitHub - SSLContext Kickstart.

Add the following dependency to your project:

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>sslcontext-kickstart</artifactId>
    <version>7.4.4</version>
</dependency>

And use the following snippet:

import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.util.SSLFactoryUtils;

import javax.net.ssl.SSLServerSocketFactory;
import java.nio.file.Paths;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class App {

    public static void main(String[] args) throws Exception {
        SSLFactory baseSslFactory = SSLFactory.builder()
                .withDummyIdentityMaterial()
                .withDummyTrustMaterial()
                .withSwappableIdentityMaterial()
                .withSwappableTrustMaterial()
                .build();

        SSLServerSocketFactory sslServerSocketFactory = baseSslFactory.getSslServerSocketFactory();

        Runnable sslUpdater = () -> {
            SSLFactory updatedSslFactory = SSLFactory.builder()
                    .withIdentityMaterial(Paths.get("/path/to/your/identity.jks"), "password".toCharArray())
                    .withTrustMaterial(Paths.get("/path/to/your/truststore.jks"), "password".toCharArray())
                    .build();

            SSLFactoryUtils.reload(baseSslFactory, updatedSslFactory);
        };

        // initial update of ssl material to replace the dummies
        sslUpdater.run();

        // update ssl material every hour    
        Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(sslUpdater, 1, 1, TimeUnit.HOURS);
    }
}