All Downloads are FREE. Search and download functionalities are using the official Maven repository.

usn.net.ssl.util.InstallCert Maven / Gradle / Ivy

Go to download

A utility to add certs to the trust store from a connection attempt, originially written by Oracle

There is a newer version: 1.0.16
Show newest version
/*
 * 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.
 */
package usn.net.ssl.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.SocketException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;

import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;

import sun.security.provider.certpath.SunCertPathBuilderException;
import sun.security.validator.ValidatorException;
import static usn.net.ssl.util.InstallCert.certToString;

/**
 * 

* A program to obtain SSL certificate(s) from a host and save them to a * keystore and optionally install them in local JSSE storage; the program * collects SSL/TLS certificates from plain SSL/TLS hosts, and also from hosts * that operate with STARTTLS extension for LDAP, SMTP, POP3 and IMAP.

*

* Original article: http://blogs.sun.com/andreas/entry/no_more_unable_to_find
* Original source: http://blogs.sun.com/andreas/resource/InstallCert.java
* Author: Andreas Sterbenz, 2006

*

* Currently available at: https://java.net/projects/javamail/pages/InstallCert
* Current Google Code branch as web page: http://code.google.com/p/java-use-examples/source/browse/trunk/src/com/aw/ad/util/InstallCert.java
* Current Google Code branch as Java code: http://java-use-examples.googlecode.com/svn/trunk/src/com/aw/ad/util/InstallCert.java
* Source path in Google Code repository: svn/ trunk/ src/ com/ * aw/ ad/ util/ InstallCert.java

*

* Approach to STARTTLS with JavaMail: Eugen Kuleshov and Dmitry I. * Platonoff, JavaWorld.com, August 31, 2001
* Java Tip 115: * Secure JavaMail with JSSE

*

* Merged together by: Sergey Ushakov (usn), 2012–2013

*

* Use without STARTTLS extension for SMTP, POP3 and IMAP protocols:
* java -jar installcert-usn-....jar {@literal [:] * []}
* Default port is 443.
* Default truststore password is "changeit" as per JSSE convention.
* The program uses a keystore file named "extracerts" in the current directory * to store the new certificates, and also attempts to add them to the standard * system keystore jssecacerts, see http://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/JSSERefGuide.html#X509TrustManager.

*

* Example:
* java -jar installcert-usn-20140115.jar * ecc.fedora.redhat.com

*

* Use with STARTTLS extension for SMTP, POP3 and IMAP protocols:
* on Windows:
* java -cp installcert-usn-....jar;.../javax.mail.jar * usn.net.ssl.util.InstallCert {@literal [:] * []}
* on *ix:
* java -cp installcert-usn-....jar:.../javax.mail.jar * usn.net.ssl.util.InstallCert {@literal [:] * []}
* Be sure to provide the real path to your local copy of * javax.mail.jar :)

*

* See Oracle Notes for use of SSL with JavaMail.

*/ public class InstallCert { final static String PROGRAM_TERMINATED = "Program terminated."; final static String EXTRA_CERTS_FILE_NAME = "extracerts"; //this is also the only variable that prevents this class from being thread safe. // this one is needed here to allow being shared with embedded classes private static SSLContext context; private static void saveCerts(Set certsToSave, String host) throws Exception { for (X509Certificate cert : certsToSave) { String alias = host + " - " + getCommonName(cert); File file = null; file = new File(alias + ".crt"); int i = 0; while (file.exists()) { file = new File(alias + "-" + i + ".crt"); } FileWriter fw = new FileWriter(file); fw.write(certToString(cert)); fw.close(); System.out.println("Cert saved to: " + file.getAbsolutePath()); } } public static String certToString(X509Certificate cert) throws CertificateEncodingException { StringWriter sw = new StringWriter(); sw.write("-----BEGIN CERTIFICATE-----\n"); sw.write(DatatypeConverter.printBase64Binary(cert.getEncoded()).replaceAll("(.{64})", "$1\n")); sw.write("\n-----END CERTIFICATE-----\n"); return sw.toString(); } private KeyStore store; private File keyStoreLocation; private char[] keyStorePassword; private KeyStore store2; private File keyStoreLocation2; private char[] keyStorePassword2; public static SSLContext getContext() { return context; } private void setTrustStore(File file, char[] password) throws Exception { keyStoreLocation = file; keyStorePassword = password; store = getKeyStore(file, password); } private void setExtraTrustStore(File file, char[] password) throws Exception { keyStoreLocation2 = file; keyStorePassword2 = password; store2 = getKeyStore(file, password); } public static File getJreDefaultKeyStore() { char SEP = File.separatorChar; File systemSecurityDir = new File(System.getProperty("java.home") + SEP + "lib" + SEP + "security"); File file = new File(systemSecurityDir, "jssecacerts"); if (!file.exists()) { file = new File(systemSecurityDir, "cacerts"); } return file; } public static KeyStore getKeyStore(File file, char[] password) throws Exception { KeyStore ksKnown = KeyStore.getInstance(KeyStore.getDefaultType()); System.out.println("... loading system truststore from '" + file.getCanonicalPath() + "' ..."); InputStream in = new FileInputStream(file); ksKnown.load(in, password); in.close(); return ksKnown; } public void setTrustStore(final KeyStore store) { this.store = store; } public void setExtraTrustStore(final KeyStore store) { this.store2 = store; } /** * returns a potentially empty list of certificates that are not trusted * * @param host * @param port * @return */ public synchronized Set getCerts(String host, int port) throws Exception { // obtain an instance of a TLS SSLContext context = SSLContext.getInstance("TLS"); // obtain a TrustManagerFactory instance TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); // initialize it with known certificate data tmf.init(store); // obtain default TrustManager instance X509TrustManager defaultTrustManager = (X509TrustManager) tmf.getTrustManagers()[0]; SavingTrustManager tm = new SavingTrustManager(defaultTrustManager, store); context.init(null, new TrustManager[]{tm}, null); SSLSocketFactory factory = context.getSocketFactory(); /* * Set up a socket to do tunneling through the proxy. * Start it off as a regular socket, then layer SSL * over the top of it. */ String tunnelHost = System.getProperty("https.proxyHost"); String tunnelPortStr = System.getProperty("https.proxyPort"); int tunnelPort = 0; if ((tunnelPortStr != null) && (!tunnelPortStr.trim().isEmpty())) { // Integer tunnelPortInteger = Integer.getInteger(tunnelPortStr); // tunnelPort = (tunnelPortInteger != null) ? tunnelPortInteger.intValue() : 0; tunnelPort = Integer.parseInt(tunnelPortStr); } Socket tunnel = null; if ((tunnelHost != null) && (!tunnelHost.trim().isEmpty())) { System.out.println("Opening socket to proxy " + tunnelHost + ":" + tunnelPort + "..."); tunnel = new Socket(tunnelHost, tunnelPort); doTunnelHandshake(tunnel, host, port); } System.out.println("Opening connection to " + host + ":" + port + "..."); System.out.println("... opening connection to " + host + ":" + port + " ..."); SSLSocket sslSocket = null; try { if (tunnel != null) { System.out.println("Using proxy configuration. proxy: " + tunnelHost + ":" + tunnelPort); sslSocket = (SSLSocket) factory.createSocket(tunnel, host, port, true); } else { sslSocket = (SSLSocket) factory.createSocket(host, port); } sslSocket.setSoTimeout(10000); System.out.println("... starting SSL handshake ..."); sslSocket.startHandshake(); System.out.println("No errors, certificate is already trusted."); } // SMTP/STARTTLS and IMAP/STARTTLS servers seem tending to yield an // SSLException with // "Unrecognized SSL message, plaintext connection?" message. // LDAP/STARTTLS servers seem tending to yield an // SSLHandshakeException with nested EOFException or a // SocketException with "Connection reset" message. // Thus three distinct cases for considering a STARTTLS extension below catch (SSLHandshakeException e) { if (e.getCause().getClass().equals(ValidatorException.class) && e.getCause().getCause().getClass().equals(SunCertPathBuilderException.class)) { // this is the standard case: looks like we just got a // previously unknown certificate, so report it and go // ahead... System.out.println(e.toString()); } else if (e.getCause().getClass().getName().equals("java.io.EOFException")) // "Remote host closed connection during handshake" { // close the unsuccessful SSL socket sslSocket.close(); // consider trying STARTTLS extension over ordinary socket if (!Starttls.consider(host, port, tunnel)) { // Starttls.consider () is expected to have reported // everything except the final good-bye... e.printStackTrace(); } } else { e.printStackTrace(); } } catch (SSLException e) { if (e.getMessage().equals("Unrecognized SSL message, plaintext connection?")) { System.out.println("ERROR on SSL handshake: " + e.toString()); sslSocket.close(); // consider trying STARTTLS extension over ordinary socket if (!Starttls.consider(host, port, tunnel)) { // Starttls.consider () is expected to have reported // everything except the final good-bye... e.printStackTrace(); } } else { e.printStackTrace(); } } catch (SocketException e) { if (e.getMessage().equals("Connection reset")) { System.out.println("ERROR on SSL handshake: " + e.toString()); sslSocket.close(); // consider trying STARTTLS extension over ordinary socket if (!Starttls.consider(host, port, tunnel)) { // Starttls.consider () is expected to have reported // everything except the final good-bye... e.printStackTrace(); } } else { e.printStackTrace(); } } finally { if (!sslSocket.isClosed()) { sslSocket.close(); } } // get the full set of new accumulated certificates as an array X509Certificate[] chain = tm.newCerts.toArray(new X509Certificate[0]); // an empty set for certificates to be selected for saving Set certsToSave = new HashSet(); // display the list of obtained certificates, inspect them and // interrogate the user whether to save them if (chain.length > 0) { System.out.println(); System.out.println("Server sent " + chain.length + " certificate(s):"); for (int i = 0; i < chain.length; i++) { X509Certificate cert = chain[i]; if (store.getCertificateAlias(cert) != null) { System.out.println("Certificate already known to the" + " system truststore."); } else if (store2 != null && store2.getCertificateAlias(cert) != null) { System.out.println("Certificate already known to the" + " extra truststore."); } else { certsToSave.add(cert); } } } return certsToSave; } /** * clears all settings and nullifies are cached passwords. This should be * called when this object is no longer needed */ public void close() { if (this.keyStorePassword != null) { clearPassword(keyStorePassword); } if (this.keyStorePassword2 != null) { clearPassword(keyStorePassword2); } this.keyStorePassword = null; this.keyStorePassword2 = null; this.store = null; this.store2 = null; this.keyStoreLocation = null; this.keyStoreLocation2 = null; } /** * see https://github.com/escline/InstallCert/issues/9 * @author vpablos@github * @param tunnel * @param host * @param port * @throws IOException */ private void doTunnelHandshake(Socket tunnel, String host, int port) throws IOException { OutputStream out = tunnel.getOutputStream(); String msg = "CONNECT " + host + ":" + port + " HTTP/1.0\n" + "User-Agent: " + sun.net.www.protocol.http.HttpURLConnection.userAgent + "\r\n\r\n"; byte b[]; try { /* * We really do want ASCII7 -- the http protocol doesn't change * with locale. */ b = msg.getBytes("ASCII7"); } catch (UnsupportedEncodingException ignored) { /* * If ASCII7 isn't there, something serious is wrong, but * Paranoia Is Good (tm) */ b = msg.getBytes(); } out.write(b); out.flush(); /* * We need to store the reply so we can create a detailed * error message to the user. */ byte reply[] = new byte[200]; int replyLen = 0; int newlinesSeen = 0; boolean headerDone = false; /* Done on first newline */ InputStream in = tunnel.getInputStream(); boolean error = false; while (newlinesSeen < 2) { int i = in.read(); if (i < 0) { throw new IOException("Unexpected EOF from proxy"); } if (i == '\n') { headerDone = true; ++newlinesSeen; } else if (i != '\r') { newlinesSeen = 0; if (!headerDone && replyLen < reply.length) { reply[replyLen++] = (byte) i; } } } /* * Converting the byte array to a string is slightly wasteful * in the case where the connection was successful, but it's * insignificant compared to the network overhead. */ String replyStr; try { replyStr = new String(reply, 0, replyLen, "ASCII7"); } catch (UnsupportedEncodingException ignored) { replyStr = new String(reply, 0, replyLen); } /* We check for Connection Established because our proxy returns * HTTP/1.1 instead of 1.0 */ //if (!replyStr.startsWith("HTTP/1.0 200")) { if (replyStr.toLowerCase().indexOf( "200 connection established") == -1) { throw new IOException("Unable to tunnel through " + host + ":" + port + ". Proxy returns \"" + replyStr + "\""); } /* tunneling Handshake was successful! */ } /** * Run the program from command line. * * @param args command line arguments as: {@literal [:] * []} * @throws Exception */ public static void main(final String[] args) throws Exception { Options opts = new Options(); opts.addOption("host", true, "The host:port of the server to pull a ssl cert chain from. If not specified, 443 will be used."); opts.addOption("truststore", true, "if specified, this trust store will be used, otherwise JAVA_HOME/cacerts will be used"); opts.addOption("truststoreExtra", true, "if specified, this trust store will also be used"); opts.addOption("password", true, "if specified, your value will be used for the trust store password. if not specified the default jre password will be used"); opts.addOption("passwordExtra", true, "if specified, password for the extra trust store"); opts.addOption("noimport", false, "if specified, no changes will be made to trust stores"); opts.addOption("file", false, "if specified, untrusted certificates will be stored to individial .crt files"); CommandLineParser parser = new DefaultParser(); CommandLine inputs = parser.parse(opts, args); if (!inputs.hasOption("host")) { new HelpFormatter().printHelp("java -jar install-cert--jar-with-dependencies.jar", opts); return; } InstallCert ref = new InstallCert(); // handle command line arguments String host = null; int port = 0; char[] password = null; File storeLocation = getJreDefaultKeyStore(); char[] pwd2 = null; File storeLocation2 = null; // handle standard arguments String[] c = inputs.getOptionValue("host").split(":"); host = c[0]; port = (c.length < 2) ? 443 : Integer.parseInt(c[1]); if (inputs.hasOption("password")) { password = inputs.getOptionValue("password").toCharArray(); } else { password = "changeit".toCharArray(); } if (inputs.hasOption("passwordExtra")) { pwd2 = inputs.getOptionValue("passwordExtra").toCharArray(); } else { pwd2 = "changeit".toCharArray(); } if (inputs.hasOption("truststore")) { ref.setTrustStore(new File(inputs.getOptionValue("truststore")), password); } else { storeLocation = getJreDefaultKeyStore(); ref.setTrustStore(storeLocation, password); } if (inputs.hasOption("truststoreExtra")) { storeLocation2 = new File(inputs.getOptionValue("truststoreExtra")); ref.setExtraTrustStore(storeLocation2, pwd2); } Set untrustedCerts = ref.getCerts(host, port); Set certsToSave = new HashSet(); MessageDigest sha1 = MessageDigest.getInstance("SHA1"); MessageDigest md5 = MessageDigest.getInstance("MD5"); // save the new certificates approved by the user if (!untrustedCerts.isEmpty()) { // assign aliases using host name and certificate common name for (X509Certificate cert : untrustedCerts) { System.out.println(); System.out.println("Subject " + cert.getSubjectDN()); System.out.println(" Issuer " + cert.getIssuerDN()); System.out.println(" CN " + getCommonName(cert)); sha1.update(cert.getEncoded()); System.out.println(" sha1 " + toHexString(sha1.digest())); md5.update(cert.getEncoded()); System.out.println(" md5 " + toHexString(md5.digest())); System.out.print("Do you want to trust this certifcate (y/n)? > "); String answer = System.console().readLine(); if ("y".equalsIgnoreCase(answer)) { certsToSave.add(cert); } } } // save the new certificates approved by the user if (!certsToSave.isEmpty()) { if (inputs.hasOption("file")) { saveCerts(certsToSave, host); } if (inputs.hasOption("noimport")) { System.out.println("Skipping JKS import due to -noimport flag"); } else { ref.applyChanges(certsToSave, host); } } else { System.out.println(); System.out.println("No new certificates found to be added."); } } // main private static void clearPassword(char[] pwd) { if (pwd == null) { return; } for (int i = 0; i < pwd.length; i++) { pwd[i] = '\0'; } } protected static String joinStringArray(String[] array, String delimiter) { StringBuilder sb = new StringBuilder(); for (String s : array) { if (sb.length() > 0) { sb.append(delimiter); } sb.append(s); } return sb.toString(); } // joinStringArray protected static String toHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(bytes.length * 3); for (int b : bytes) { sb.append(String.format("%02x ", b & 0xff)); } return sb.toString(); } // toHexString protected static String ask(String prompt) throws IOException { System.out.print(prompt); BufferedReader stdinReader = new BufferedReader(new InputStreamReader(System.in)); String line = stdinReader.readLine().trim(); return line; } // ask public static String getCommonName(X509Certificate cert) throws InvalidNameException { // use LDAP API to parse the certifiate Subject :) // see http://stackoverflow.com/a/7634755/972463 LdapName ldapDN = new LdapName(cert.getSubjectX500Principal().getName()); String cn = ""; for (Rdn rdn : ldapDN.getRdns()) { if (rdn.getType().equals("CN")) { cn = rdn.getValue().toString(); } } return cn; } // getCommonName private void applyChanges(Set certsToSave, String host) throws Exception { System.out.println("Applying changes to " + this.keyStoreLocation.getAbsolutePath()); if (keyStoreLocation2!=null) System.out.println("Applying changes to " + this.keyStoreLocation2.getAbsolutePath()); // assign aliases using host name and certificate common name for (X509Certificate cert : certsToSave) { String alias = host + " - " + getCommonName(cert); store.setCertificateEntry(alias, cert); if (store2 != null) { store2.setCertificateEntry(alias, cert); } System.out.println(); System.out.println(cert); System.out.println(); System.out.println("Certificate will be added using alias '" + alias + "'."); } // save them to the extra truststore for certificates known just // locally System.out.println(); System.out.println("... adding certificate(s) to the truststore ..."); OutputStream out = new FileOutputStream(keyStoreLocation); store.store(out, keyStorePassword); out.close(); if (store2 != null) { System.out.println("... adding certificate(s) to the extra truststore ..."); out = new FileOutputStream(keyStoreLocation2); store2.store(out, keyStorePassword2); out.close(); } } // -- class SavingTrustManager --------------------------------------------- /** * An {@link X509TrustManager} subclass that accumulates unknown * certificates in order to allow saving them afterwards. */ protected static class SavingTrustManager implements X509TrustManager { protected X509TrustManager parentTm; protected Set allAccumulatedCerts = new HashSet(); protected Set newCerts = new HashSet(); /** * The constructor. * * @param parentTm an {@link X509TrustManager} instance to do the * standard part of certificates validation job * @param ksExtra a {@link KeyStore} instance that contains previously * accumulated certificates * @throws KeyStoreException */ SavingTrustManager(X509TrustManager parentTm, KeyStore ksExtra) throws KeyStoreException { if (parentTm == null) { throw new IllegalArgumentException("Parent trust manager cannot be null."); } else { this.parentTm = parentTm; } if (ksExtra != null) { Enumeration ksAliases = ksExtra.aliases(); while (ksAliases.hasMoreElements()) { String alias = ksAliases.nextElement(); this.allAccumulatedCerts.add((X509Certificate) ksExtra.getCertificate(alias)); } } } // SavingTrustManager // .. javax.net.ssl.X509TrustManager methods ........................... @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { throw new UnsupportedOperationException(); } // checkClientTrusted @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { CertificateException exceptionToRethrow = null; // check the certificate chain against the system truststore try { parentTm.checkServerTrusted(chain, authType); } catch (CertificateException e) // the certificate chain was found not trusted { // check if the first certificate in the chain is not known yet // to the local certificate storage if (!this.allAccumulatedCerts.contains(chain[0])) { // save the exception to be re-thrown later if not known exceptionToRethrow = e; // save the full chain to both local accumulators for (X509Certificate cert : chain) { this.allAccumulatedCerts.add(cert); this.newCerts.add(cert); } } } // check and re-throw the exception if any if (exceptionToRethrow != null) { throw exceptionToRethrow; } } // checkServerTrusted @Override public X509Certificate[] getAcceptedIssuers() { return this.parentTm.getAcceptedIssuers(); } // getAcceptedIssuers } // class SavingTrustManager } // class InstallCert




© 2015 - 2024 Weber Informatics LLC | Privacy Policy