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.
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
.