20

I was getting an SSL Handshake Exception error: PKIX "path does not chain" (described here). I fixed it by importing a certificate chain using openssl:

openssl s_client -host www.envmgr.com -port 443 -showcerts > cert_chain.crt

and installed it into my JDK's keystore:

keytool -import -alias envmgrchain -file cert_chain.crt -keystore cacerts -storepass changeit

Well this works. Hooray. The problem is we'll be putting our application up on a cloud server like rackspace or AWS and I think there is a good chance that we won't have access to modify the keystore of the JVM to add this chain.

I thought, "no problem, I'll just add this certificate chain to the keystore programmatically" so I removed it from my JVM:

keytool -delete -alias envmgrchain -keystore cacerts -storepass changeit

and added this code:

    KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    //Create an empty keystore that we can load certificate into
    trustStore.load(null);
    InputStream fis = new FileInputStream("cert_chain.crt");
    BufferedInputStream bis = new BufferedInputStream(fis);

    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    while(bis.available()>0) {
        Collection<? extends Certificate> certs = cf.generateCertificates(bis);
        Iterator<? extends Certificate> iter = certs.iterator();
        //Add each cert in the chain one at a time
        for(int i=0; i<certs.size(); i++) {
            Certificate cert = iter.next();
            String alias = "chaincert"+((i>0)?i:"");
            trustStore.setCertificateEntry(alias, cert);
        }
    }
    bis.close();
    fis.close();
//Add custom keystore to TrustManager
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(trustStore);
    SSLContext ctx = SSLContext.getInstance("TLSv1");
    ctx.init(null, tmf.getTrustManagers(), null);

But when I run it, the PKIX error reappears. Is the above code not equivalent to keytool -import? I feel like I'm either adding certificates to the KeyStore incorrectly, or I'm not installing the Keystore into the TrustManager in the right way.

FYI: I am also attempting to address this issue by implementing an X509TrustManager.

Cœur
  • 37,241
  • 25
  • 195
  • 267
IcedDante
  • 6,145
  • 12
  • 57
  • 100
  • What about just shipping the truststore you want to use with your application and reference it via vm arguments? – hooknc Jun 04 '14 at 17:55
  • Also, here is a famous example by Andreas Sterbenz called InstallCert that does what you're trying to do, but you will have to parse though the code to get what you want exactly: http://code.google.com/p/java-use-examples/source/browse/trunk/src/com/aw/ad/util/InstallCert.java – hooknc Jun 04 '14 at 18:01

2 Answers2

31

Here's code you can use for clients to programatically add your CA at runtime. You don't need to put it in any store - just carry around the PEM encoded file. You can even hard code it into your program so there's no separate file to manage.

static String CA_FILE = "ca-cert.pem";
...

FileInputStream fis = new FileInputStream(CA_FILE);
X509Certificate ca = (X509Certificate) CertificateFactory.getInstance("X.509")
                        .generateCertificate(new BufferedInputStream(fis));

KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry(Integer.toString(1), ca);

TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);

SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);
...

You will need a trusted distribution channel to ensure your program is not modified while sitting on the server waiting to be picked or while traveling down the wire while being installed.


openssl s_client -host www.envmgr.com -port 443 -showcerts > cert_chain.crt

You should only need to trust the root certificate, and not the entire chain. The server is responsible for sending all intermediate certificates required to build the chain. If the server is not sending all intermediate certificates required to build the chain, then the server is misconfigured.

The problem you are experiencing is called the "Which Directory" problem. Its a well known problem in PKI. Essentially, it means a client does not know where to go to fetch a missing intermediate certificate. You solve it by having the server send all required intermediates along with the server's certifcate. See OWASP's TLS Cheatsheet and Rule - Always Provide All Needed Certificates.


Just bike shedding, but there's a whole nother can of worms here with Java (especially Java 7 and lower):

SSLContext ctx = SSLContext.getInstance("TLSv1");
ctx.init(null, tmf.getTrustManagers(), null);

You can improve upon it, if desired. See SSLSocketFactoryEx at Which Cipher Suites to enable for SSL Socket?. It closes some gaps in protocol versions, cipher suites, etc provided by default in Java SSLSocketFactory.

jww
  • 97,681
  • 90
  • 411
  • 885
  • 2
    So much good info here- thanks! What I do currently is supply the certificate myself. I created a custom X509TrustManager that, upon catching a CertificateException from the default manager, cycles through a bunch of certificates I store in a directory called "trustedCerts". I convert these to X509 certificates and do a comparison check on the chain using X509Certificate.equals()... if there's a match, I accept the certificate. Do you think using equals is a valid comparison here? (btw if this question is too big for a comment let me know and I'll start a separate question for it). – IcedDante Jun 09 '14 at 15:49
  • 1
    "What I do currently is supply the certificate myself... using a custom `X509TrustManager` ..." - yes, its fine to carry around the server's expected certificate. That's called pinning, and its a great security enhancement. Its beats the snot out of the Web Security Model. See OWASP's [Certificate and Public Key Pinning](https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning). – jww Jun 09 '14 at 19:22
  • 1
    Just a tip. When hardcoding the CA as a String in one line, add \n in place of line breaks. Not that I would recommend this approach. I needed it for testing though – v0rin Feb 28 '18 at 13:30
0

Don't. You shouldn't be modifying files that come with the JRE. Next upgrade, your updates are gone. You should ship your own truststore, built from the one that comes with the JRE plus whatever extra certificates you want to trust. This is part of your application build.

If you then want to modify your own truststore at runtime, go ahead, but then you need to be aware that the JVM won't necessarily see the changes until it's restarted: it certainly won't see them within the same SSLContext that you use to obtain the certificates you want to add.

user207421
  • 305,947
  • 44
  • 307
  • 483
  • "If you then want to modify your own truststore at runtime, go ahead, but then you need to be aware that the JVM won't necessarily see the changes until it's restarted: it certainly won't see them within the same SSLContext that you use to obtain the certificates you want to add." – IcedDante Jun 08 '14 at 20:08
  • It's a logical consequence of the fact that a KeyStore is loaded from an input stream rather than provided with a File. – user207421 Jun 09 '14 at 01:33