1

I've read quite a lot about this topic and it seems impossible to do, but just to be sure I'd like one more opinion.

Use case: Web application that connects programmatically with one or more https services, the services are dynamic and the certificates get frequent updates.

What the application should do it's to update the TrustStore with the new certificates and use them without restarting the application. It's important that no new code should be implemented to do the https connections (hence, it should integrate seamlessly).

I've tried (with no luck) to override the default Java TrustManager, any help will be appreciated.

EDIT: I've tryied some of the solution proposed in the comments/answers, but I still need to restart my tomcat afterwards

Fabio
  • 415
  • 1
  • 5
  • 14
  • It is technically possible, some Java EE servers offer this feature. Now they maybe override a lot of the native Java processes to achieve this... – Aaron Nov 02 '16 at 14:48
  • Your use case could also be a security flaw, blindly accepting new certificates isn't a good idea. – Aaron Nov 02 '16 at 14:50
  • @Aaron is right, this would be a security issue, as you are accepting certificate without knowing if it's a good one or not. This is the same as not verifying the certificates at all – Cédric O. Nov 02 '16 at 14:54
  • I could use Tomcat `trustManagerClassName`, but that would solve only half of the problem, unfortunately. We wouldn't accept certificates blindly, we would delegate administrative users to dynamically import them from the application GUI. – Fabio Nov 02 '16 at 14:54
  • Im not familar with oracle java's interactions with https servers, however I would like to know how the new certificates are getting there? Also I found this to import certificate on oracle site `keytool -import -noprompt -trustcacerts -alias ${cert.alias} -file ${cert.file} -keystore ${keystore.file} -storepass ${keystore.pass}` – marshal craft Nov 02 '16 at 14:58
  • You can create your own trust manager to verify another truststore which would be dynamic, so you can add the certificate in this truststore when the admin checks it, and then use the trust manager to verify the next webservice calls – Cédric O. Nov 02 '16 at 14:59
  • @marshalcraft , with progamatically I mean with java, I'm well aware of the keytool feature. – Fabio Nov 02 '16 at 15:00
  • @CédricO. Even if I create my own trustmanager (and I did), how can I force Java to use it instead of his own? – Fabio Nov 02 '16 at 15:01
  • @Fabio, have a look here https://www.mkyong.com/webservices/jax-ws/how-to-bypass-certificate-checking-in-a-java-web-service-client/ this is not exactly the same subject, but the functionality used will be the same for you – Cédric O. Nov 02 '16 at 15:03
  • As for trust issues with the certificates. They would have to be sent via a secure tls 1.2 connection which further utilizes both the client and server certificates for that connection. You trust the server which is distributing new certificates, and have a permanent certificate for that server, then there is no security flaw with using the dynamic certificates for the application server. – marshal craft Nov 02 '16 at 15:04
  • @Fabio http://www.java2s.com/Tutorial/Java/0490__Security/Createsa1024bitRSAkeypairandstoresittothefilesystemastwofiles.htm using the java.security class. It only shows generation of key's, however you should be able to store the keystore if I understand correctly that it is simply a directory and outputing the key to text file. At the very least. Or this http://www.java2s.com/Tutorial/Java/0490__Security/KeyStoreExample.htm – marshal craft Nov 02 '16 at 15:31
  • Also `java.security.keystore.setEntry()` api. So it seems it is completely possible. – marshal craft Nov 02 '16 at 15:37
  • Possible duplicate of [Programmatically Import CA trust cert into existing keystore file without using keytool](http://stackoverflow.com/questions/18889058/programmatically-import-ca-trust-cert-into-existing-keystore-file-without-using) – marshal craft Nov 02 '16 at 15:58
  • Do not try to override the JVM's default trust/key store. Create your own keystores, instanciate you own Tust/KeyManagers with them, and feed them to init your own SSLContext and instanciate your SSLSocketFactory from this SSLContext. Then make sure this socket factory is used by your HTTP Layer (e.g. Apache HTTP Client or HTTPURLConnection). See http://stackoverflow.com/questions/28883632/setting-a-client-certificate-as-a-request-property-in-a-java-http-conneciton/28883926#28883926 – GPI Nov 02 '16 at 16:07
  • @Fabio, are you able to solve this problem ? I am also searching a solution for a similar problem. I want my server process to dynamically reload its trust store whenever a new client certificate gets added to the trust store. new client certificate addition to the trust store will be an offline step which will happen in background. I don't want my server process to be restarted in order to reload the updated trust store. I am even open to sending a trigger event through some hook to my server process for initiating reload. But nothing like automatic reload. Any pointers ? – nirmalsingh Jun 15 '17 at 09:44

3 Answers3

2

While it is posted in the comment of this other SO post, I want to mention this method as a potential answer because it helped me solve this problem too.

This article tells us about creating a new SSLContext that contains a wrapper (ReloadableX509TrustManager) around the standard X509TrustManager: https://jcalcote.wordpress.com/2010/06/22/managing-a-dynamic-java-trust-store/

Whenever a client/server is being authenticated (with checkClientTrusted/checkServerTrusted), the X509ReloadableTrustManager will call the related method of the X509TrustManager within. If it fails (CertificateException is thrown), then it will reload the TrustStore before making another attempt. Each "reload" actually replaces the X509TrustManager with a new instance, since we cannot touch the certificates within.

Personally, I varied from this article slightly. Within checkClientTrusted/checkServerTrusted of ReloadableX509TrustManager:

  1. Check if the TrustStore was modified, by the file's modified timestamp. If modified, then create a new TrustManager with the new TrustStore.
  2. Call checkClientTrusted/checkServerTrusted of the embedded X509TrustManager.

To reduce the number of file I/O requests, I kept track of the timestamp of the last check on the TrustStore, to restrict the polling interval on the TrustStore to a minimum of 15s.

I believe my method is slightly better because it would allow authentication to take place with the current TrustStore, which could also have certificates removed from it. The original method would still allow the application to continuously trust a client/server, even if the related certificate(s) was/were removed.

EDIT: In retrospect, I think the reloading process should be made thread-safe because I cannot find anything that indicates that the checkClientTrusted() and checkServerTrusted() methods within X509TrustManager may be designed without thread-safety in mind. In fact, the checkTrustedInit() method of the default X509TrustManagerImpl class has some synchronized blocks - which might hint that these functions must be made thread-safe.

EDIT 2021/04/10: Here is a sample implementation:

package com.test.certificate;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchProviderException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.List;

import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ReloadableX509TrustManager implements X509TrustManager {

    private static final Logger logger = LoggerFactory.getLogger(ReloadableX509TrustManager.class);

    private X509TrustManager trustManager;

    private static final String KEYSTORE_RUNTIME_FORMAT = "JKS";
    private static final String CERTIFICATE_ENTRY_FORMAT = "X.509";

    public ReloadableX509TrustManager() throws Exception {
        reload();
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        try {
            reload();
        } catch (Exception ex) {
            logger.warn("Failed to reload TrustStore due to " + ex, ex);
        }
        trustManager.checkClientTrusted(chain, authType);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        try {
            reload();
        } catch (Exception ex) {
            logger.warn("Failed to reload TrustStore due to " + ex, ex);
        }
        trustManager.checkServerTrusted(chain, authType);
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return trustManager.getAcceptedIssuers();
    }

    /**
     * Reloads the inner TrustStore.
     * For performance, reloading of the TrustStore will only be done if there is a change.
     * @throws Exception
     */
    public synchronized void reload() throws Exception {
        if (!isUpdated())
            return;

        KeyStore trustStore = KeyStore.getInstance(KEYSTORE_RUNTIME_FORMAT);
        trustStore.load(null, null);

        List<TrustedCertificate> certs = getCertificates();
        CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_ENTRY_FORMAT);

        for (TrustedCertificate cert : certs) {
            InputStream is = new ByteArrayInputStream(cert.getCertificate());
            Certificate certEntry;
            try {
                certEntry = cf.generateCertificate(is);
            } catch (CertificateException e) {
                logger.error("Failed to generate certificate " + cert.getAliasForKeystore() + " due to: " + e);
                continue;
            } finally {
                is.close();
            }

            try {
                trustStore.setCertificateEntry(cert.getAliasForKeystore(), certEntry);
            } catch (KeyStoreException e) {
                logger.error("Failed to insert certificate " + cert.getAliasForKeystore() + " due to: " + e);
                continue;
            }
        }

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(trustStore);

        // Locate the X509TrustManager and get a reference to it.
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
        for (TrustManager tm : trustManagers) {
            if (tm instanceof X509TrustManager) {
                trustManager = (X509TrustManager)tm;
                return;
            }
        }

        throw new NoSuchProviderException("X509TrustManager not available from TrustManagerFactory.");
    }

    /**
     * Indicates whether the TrustStore was updated.
     * @return Whether the TrustStore was updated.
     */
    private boolean isUpdated() {
        // TODO Write your logic to check whether the TrustStore was updated.
        // If disk I/O is used, it may be good to limit how often the file is accessed for performance.
        return false;
    }

    /**
     * Returns a list of certificates from the TrustStore.
     * @return A list of certificates from the TrustStore.
     * @throws Exception
     */
    private List<TrustedCertificate> getCertificates() throws Exception {
        // TODO Write your logic to retrieve all certificates from the TrustStore.
        return ;
    }

}

And then to generate a new SSLContext that uses the new TrustManager:

    private SSLContext initContext() throws Exception {
        TrustManager[] trustManagers = { getTrustManager() };

        //Initialize a new SSLContext, with our custom TrustManager.
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagers, null);
        return sslContext;
    }

Something similar can be done if you needed a reloadable KeyStore. But instead of implementing X509TrustManager, the class shall implement X509KeyManager instead. This custom KeyManager is passed as an array to the first argument of sslContext.init().

SP193
  • 156
  • 2
  • 5
  • 1
    Can you please post your implementation? – Sapnesh Naik Jul 22 '19 at 10:27
  • I know it might be really late, but anyway. Nice example of implementation can be found in `apache-hadoop` project, see [ReloadingX509TrustManager](https://github.com/apache/hadoop/blob/d4fd675a95c16f6ecd8d8514cc3c0bef34bc9eff/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/ReloadingX509TrustManager.java) – Yurii Melnychuk Mar 03 '21 at 09:22
  • I have added an example. – SP193 Apr 10 '21 at 03:54
0

I had posted a similar answer here: Reloading a java.net.http.HttpClient's SSLContext

Basically what you need is a custom trust manager which wraps around the actual trustmanager which is able to swap the actual trustmanager whenever needed, for example when the truststore gets updated.

You can find a full answer on the link here, below is a small snippet which should do the trick for you and it uses the wrapper trustmanager named hot swappable trustmanager under the covers

SSLFactory sslFactory = SSLFactory.builder()
          .withSwappableTrustMaterial()
          .withTrustMaterial("truststore.jks", "password".toCharArray())
          .build();
          
HttpClient httpClient = HttpClient.newBuilder()
          .sslParameters(sslFactory.getSslParameters())
          .sslContext(sslFactory.getSslContext())
          .build()

// execute https request
HttpResponse<String> response = httpClient.send(aRequest, HttpResponse.BodyHandlers.ofString());

// swap trust materials and reuse existing http client
TrustManagerUtils.swapTrustManager(sslFactory.getTrustManager().get(), anotherTrustManager);

// Cleanup old ssl sessions by invalidating them all. Forces to use new ssl sessions which will be created by the swapped TrustManager
SSLSessionUtils.invalidateCaches(sslFactory.getSslContext());

HttpResponse<String> response = httpClient.send(aRequest, HttpResponse.BodyHandlers.ofString());
Hakan54
  • 3,121
  • 1
  • 23
  • 37
-3

Seems this question has been answered here Programmatically Import CA trust cert into existing keystore file without using keytool .

I think the issue is that really a truststore and keystore are the same things but they are used with a keymanager for private keys (client authentication (typically not used and no real signing authority)) and trustmanager for server authentication (always done for full tls connection).

Programatically you still use keystore for a truststore in this aspect. Hope I am correct on this.

Community
  • 1
  • 1
marshal craft
  • 439
  • 5
  • 18