Thursday, October 30, 2008

Unable to find valid certification path to requested target? How to Download and Install a Cert in Java

If you get the error: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target It likely means that the cert that is coming back for the host you specified is no good or Java doesn't recognize the cert. If the issue is that the cert is good, but Java just doesn't recognize it (because it is self-signed, etc.), then you can use one of the following options:

Option 1: Export Cert from Browser and Import Into Keystore

This method only works sometimes for me. Why it doesn't always work, I don't know yet. Download the x.509 cert and install the x.509 cert into the java keystore (usually /jre/lib/security/cacerts) that your Java application is using. * Using using existing tools in your browser, a browser plugin like Cert Viewer Plus for Firefox, or some other tool, download/export the cert for the host(:port) you are trying to get at. Export the cert as pem or der format. Then scp that cert to the /tmp directory on whatever server you need to install it on. * You will likely need to change the rights on cacerts temporarily to give your user write access to it, so that you can add the cert:
cd ${JAVA_HOME}/jre/lib/security/
ls -al cacerts
(note those flags just in case)
chmod +w cacerts
* Import the cert using keytool.
${JAVA_HOME}/bin/keytool -import -alias ${HOSTNAME_OR_NAME_FOR_CERT_TO_BE_LISTED_IN_KEYSTORE}-1 -file /tmp/${CERT_FILE_NAME} -keystore ${JAVA_HOME}/jre/lib/security/cacerts
* Enter keystore password: (If you don't know it, it is likely: changeit)

Note: if the cert already exists, you can stop here or remove the old cert using "${JAVA_HOME}/bin/keytool -delete -alias ${HOSTNAME_OR_NAME_FOR_CERT_TO_BE_LISTED_IN_KEYSTORE} -keystore ${JAVA_HOME}/jre/lib/security/cacerts"

* Trust this certificate? (Enter: yes) * It should respond "Certificate was added to keystore" (otherwise maybe you forget to change permissions on the cacerts file) * You will likely want to change the rights on cacerts back to the way it was before:
cd ${JAVA_HOME}/jre/lib/security/
chmod -w cacerts
* Restart the application server, if needed.

Option 2a: Use Andreas Sterbenz's InstallCert.java and Swap the Cacerts files Around

This has always worked for me: * http://blogs.sun.com/andreas/entry/no_more_unable_to_find Basically you do this: * Download the InstallCert.java file. * scp that file to /tmp on the server(s) you want to install it on. * Repeat the following for each server: 1. ${JAVA_HOME} should be the java directory that your application is using. Installing a cert elsewhere won't help unless you plan to copy around cacerts files, which is just asking for losing one or more certs. 2. Do the following commands as a user that has rights to create and change permissions on files in the ${JAVA_HOME}/jre/lib/security directory. In Linux: Note: Replace YYYY-MM-DD with today's date.
sudo su (user that has rights)
cd ${JAVA_HOME}/jre/lib/security/
chmod +w jssecacerts
chmod +w cacerts
cp jssecacerts jssecacerts.bak.YYYY-MM-DD
cp cacerts jssecacerts
cp /tmp/InstallCert.java .
${JAVA_HOME}/bin/javac InstallCert.java
${JAVA_HOME}/bin/java InstallCert host:port
(press enter)
cp cacerts cacerts.bak.YYYY-MM-DD
cp jssecacerts cacerts
mv jssecacerts.bak.YYYY-MM-DD jssecacerts
chmod -w jssecacerts*
chmod -w cacerts*
rm InstallCert*
In OS X: Note: Replace YYYY-MM-DD with today's date.
sudo chmod +w /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home/lib/security/cacerts
cp /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home/lib/security/cacerts jssecerts
/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Commands/java InstallCert host:port
sudo cp /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home/lib/security/cacerts /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home/lib/security/cacerts-backup-YYYY-MM-DD
sudo cp jssecacerts /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home/lib/security/cacerts
sudo chmod -w /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home/lib/security/cacerts
(note: if your CurrentJDK is different than your Current (JVM aka "A") then this is only affecting java/JVM at CurrentJDK, and running java at command line uses Current which could be a different java/JVM.) 3. Restart the application server(s) that use(s) that cacerts file.

Option 2b: Use Andreas Sterbenz's InstallCert.java and Export Certs

Saw this on: * http://docs.ofbiz.org/display/OFBIZ/Google+Checkout+Integration Use method above but instead of swapping our jssecacerts, just export cert from it after using InstallCert and import into keystore. Just to make sure that InstallCert.java is never lost, here it is (the original is here):
/*
 * Copyright 2006 Sun Microsystems, Inc.  All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - Neither the name of Sun Microsystems nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.io.*;
import java.net.URL;

import java.security.*;
import java.security.cert.*;

import javax.net.ssl.*;

public class InstallCert {

    public static void main(String[] args) throws Exception {
 String host;
 int port;
 char[] passphrase;
 if ((args.length == 1) || (args.length == 2)) {
     String[] c = args[0].split(":");
     host = c[0];
     port = (c.length == 1) ? 443 : Integer.parseInt(c[1]);
     String p = (args.length == 1) ? "changeit" : args[1];
     passphrase = p.toCharArray();
 } else {
     System.out.println("Usage: java InstallCert <host>[:port] [passphrase]");
     return;
 }

 File file = new File("jssecacerts");
 if (file.isFile() == false) {
     char SEP = File.separatorChar;
     File dir = new File(System.getProperty("java.home") + SEP
      + "lib" + SEP + "security");
     file = new File(dir, "jssecacerts");
     if (file.isFile() == false) {
  file = new File(dir, "cacerts");
     }
 }
 System.out.println("Loading KeyStore " + file + "...");
 InputStream in = new FileInputStream(file);
 KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
 ks.load(in, passphrase);
 in.close();

 SSLContext context = SSLContext.getInstance("TLS");
 TrustManagerFactory tmf =
     TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
 tmf.init(ks);
 X509TrustManager defaultTrustManager = (X509TrustManager)tmf.getTrustManagers()[0];
 SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);
 context.init(null, new TrustManager[] {tm}, null);
 SSLSocketFactory factory = context.getSocketFactory();

 System.out.println("Opening connection to " + host + ":" + port + "...");
 SSLSocket socket = (SSLSocket)factory.createSocket(host, port);
 socket.setSoTimeout(10000);
 try {
     System.out.println("Starting SSL handshake...");
     socket.startHandshake();
     socket.close();
     System.out.println();
     System.out.println("No errors, certificate is already trusted");
 } catch (SSLException e) {
     System.out.println();
     e.printStackTrace(System.out);
 }

 X509Certificate[] chain = tm.chain;
 if (chain == null) {
     System.out.println("Could not obtain server certificate chain");
     return;
 }

 BufferedReader reader =
  new BufferedReader(new InputStreamReader(System.in));

 System.out.println();
 System.out.println("Server sent " + chain.length + " certificate(s):");
 System.out.println();
 MessageDigest sha1 = MessageDigest.getInstance("SHA1");
 MessageDigest md5 = MessageDigest.getInstance("MD5");
 for (int i = 0; i < chain.length; i++) {
     X509Certificate cert = chain[i];
     System.out.println
      (" " + (i + 1) + " Subject " + cert.getSubjectDN());
     System.out.println("   Issuer  " + cert.getIssuerDN());
     sha1.update(cert.getEncoded());
     System.out.println("   sha1    " + toHexString(sha1.digest()));
     md5.update(cert.getEncoded());
     System.out.println("   md5     " + toHexString(md5.digest()));
     System.out.println();
 }

 System.out.println("Enter certificate to add to trusted keystore or 'q' to quit: [1]");
 String line = reader.readLine().trim();
 int k;
 try {
     k = (line.length() == 0) ? 0 : Integer.parseInt(line) - 1;
 } catch (NumberFormatException e) {
     System.out.println("KeyStore not changed");
     return;
 }

 X509Certificate cert = chain[k];
 String alias = host + "-" + (k + 1);
 ks.setCertificateEntry(alias, cert);

 OutputStream out = new FileOutputStream("jssecacerts");
 ks.store(out, passphrase);
 out.close();

 System.out.println();
 System.out.println(cert);
 System.out.println();
 System.out.println
  ("Added certificate to keystore 'jssecacerts' using alias '"
  + alias + "'");
    }

    private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();

    private static String toHexString(byte[] bytes) {
 StringBuilder sb = new StringBuilder(bytes.length * 3);
 for (int b : bytes) {
     b &= 0xff;
     sb.append(HEXDIGITS[b >> 4]);
     sb.append(HEXDIGITS[b & 15]);
     sb.append(' ');
 }
 return sb.toString();
    }

    private static class SavingTrustManager implements X509TrustManager {

 private final X509TrustManager tm;
 private X509Certificate[] chain;

 SavingTrustManager(X509TrustManager tm) {
     this.tm = tm;
 }

 public X509Certificate[] getAcceptedIssuers() {
     throw new UnsupportedOperationException();
 }

 public void checkClientTrusted(X509Certificate[] chain, String authType)
  throws CertificateException {
     throw new UnsupportedOperationException();
 }

 public void checkServerTrusted(X509Certificate[] chain, String authType)
  throws CertificateException {
     this.chain = chain;
     tm.checkServerTrusted(chain, authType);
 }
    }

}

Troubleshooting

If you install either Java for Mac OS X 10.6 Update 1 - Java for Mac OS X 10.6 Update 1 or Java for Mac OS X 10.5 Update 6 - Java for Mac OS X 10.5 Update 6, you might see:
Exception in thread "main" java.io.IOException: Keystore was tampered with, or password was incorrect
 at sun.security.provider.JavaKeyStore.engineLoad(JavaKeyStore.java:771)
 at sun.security.provider.JavaKeyStore$JKS.engineLoad(JavaKeyStore.java:38)
 at java.security.KeyStore.load(KeyStore.java:1185)
 at InstallCert.main(InstallCert.java:46)
Caused by: java.security.UnrecoverableKeyException: Password verification failed
 at sun.security.provider.JavaKeyStore.engineLoad(JavaKeyStore.java:769)
 ... 3 more
The reason this happens, as Matt Fleming describes in his post, is that Apple changed the default keystore password from changeit to changeme. To change it back, Matt provides a sample command:
sudo keytool -storepasswd -new changeit -keystore /Library/Java/Home/lib/security/cacerts -storepass changeme
However in OS X 10.6.2 with Java 6, the command I had to use was:
sudo keytool -storepasswd -new changeit -keystore /System/Library/Frameworks/JavaVM.framework/Resources/Deploy.bundle/Contents/Home/lib/security/cacerts -storepass changeme

1 comment:

Gary S. Weaver said...

Note: to get this working in the rpm version of Java 6, I compiled it in that version (via javac InstallCert.java). I was getting 'Exception in thread "main" java.lang.NoClassDefFoundError: InstallCert$SavingTrustManager' caused by 'java.lang.ClassNotFoundException: InstallCert$SavingTrustManager'. I also copied cacerts to jssecacerts. Remember to use java InstallCert host:port rather than java InstallCert host port, or you will get 'Exception in thread "main" java.io.IOException: Keystore was tampered with, or password was incorrect' caused by 'java.security.UnrecoverableKeyException: Password verification failed'.