/* ================================================================== * ConfigurableSSLService.java - 2/04/2017 10:19:45 AM * * Copyright 2007-2017 SolarNetwork.net Dev Team * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA * ================================================================== */ package net.solarnetwork.support; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.GeneralSecurityException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.util.Enumeration; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Basic implementation of {@link SSLService} that allows configuring a keystore * and truststore to use for the {@code SSLSocketFactory} returned by * {@link #getSSLSocketFactory()}. * * @author matt * @version 1.0 */ public class ConfigurableSSLService implements SSLService { /** The default value for the {@code keyStorePath} property. */ public static final String DEFAULT_KEY_STORE_PATH = "conf/tls/keystore.jks"; /** The default value for the {@code trustStorePath} property. */ public static final String DEFAULT_TRUST_STORE_PATH = "conf/tls/trust.jks"; /** The default password used for all configurable password properties. */ public static final String DEFAULT_PASSWORD = "changeit"; private String keyStorePath = DEFAULT_KEY_STORE_PATH; private String keyStorePassword = DEFAULT_PASSWORD; private String trustStorePath = DEFAULT_TRUST_STORE_PATH; private String trustStorePassword = DEFAULT_PASSWORD; private String jreTrustStorePassword = DEFAULT_PASSWORD; private SSLSocketFactory socketFactory; /** A class-level logger to use. */ protected final Logger log = LoggerFactory.getLogger(getClass()); /** * Load a keystore from an {@code InputStream}. * * @param type * The keystore type, e.g. {@code KeyStore.getDefaultType()}. * @param in * The stream to load from. * @param password * The keystore password to use. * @return The keystore. * @throws CertificateException * if any error occurs */ public static final KeyStore loadKeyStore(String type, InputStream in, String password) { if ( password == null ) { password = ""; } KeyStore keyStore = null; try { keyStore = KeyStore.getInstance(type); keyStore.load(in, (password != null ? password.toCharArray() : null)); return keyStore; } catch ( GeneralSecurityException e ) { throw new CertificateException("Error loading certificate key store", e); } catch ( IOException e ) { String msg; if ( e.getCause() instanceof UnrecoverableKeyException ) { msg = "Invalid password loading key store"; } else { msg = "Error loading certificate key store"; } throw new CertificateException(msg, e); } finally { if ( in != null ) { try { in.close(); } catch ( IOException e ) { // ignore this one } } } } /** * Serialize a {@code KeyStore} to an output stream. * * @param keyStore * The keystore to serialize. * @param password * The password to use. * @param out * The stream to write to. * @throws CertificateException * if any error occurs */ public static final void saveKeyStore(KeyStore keyStore, String password, OutputStream out) { if ( password == null ) { password = ""; } try { keyStore.store(out, password.toCharArray()); } catch ( KeyStoreException e ) { throw new CertificateException("Error saving certificate key store", e); } catch ( NoSuchAlgorithmException e ) { throw new CertificateException("Error saving certificate key store", e); } catch ( java.security.cert.CertificateException e ) { throw new CertificateException("Error saving certificate key store", e); } catch ( IOException e ) { throw new CertificateException("Error saving certificate key store", e); } finally { if ( out != null ) { try { out.flush(); out.close(); } catch ( IOException e ) { throw new CertificateException("Error closing KeyStore stream", e); } } } } protected synchronized KeyStore loadKeyStore() { File ksFile = new File(keyStorePath); InputStream in = null; String passwd = getKeyStorePassword(); try { if ( ksFile.isFile() ) { in = new BufferedInputStream(new FileInputStream(ksFile)); } return loadKeyStore(KeyStore.getDefaultType(), in, passwd); } catch ( IOException e ) { throw new CertificateException("Error opening file " + keyStorePath, e); } } protected synchronized KeyStore loadTrustStore() { // first load in JDK trust store File jdkTrustStoreFile = new File(System.getProperty("java.home"), "lib/security/cacerts"); KeyStore ks = null; InputStream in = null; if ( jdkTrustStoreFile.canRead() ) { try { in = new BufferedInputStream(new FileInputStream(jdkTrustStoreFile)); } catch ( FileNotFoundException e ) { // shouldn't really get here after canRead() } } ks = loadKeyStore(KeyStore.getDefaultType(), in, jreTrustStorePassword); // now custom trust store File snTrustStoreFile = new File(trustStorePath); if ( snTrustStoreFile.canRead() ) { KeyStore snTrustStore = null; try { in = new BufferedInputStream(new FileInputStream(snTrustStoreFile)); snTrustStore = loadKeyStore(KeyStore.getDefaultType(), in, trustStorePassword); Enumeration<String> aliases = snTrustStore.aliases(); while ( aliases.hasMoreElements() ) { String alias = aliases.nextElement(); Certificate cert = snTrustStore.getCertificate(alias); if ( cert != null ) { ks.setCertificateEntry(alias, cert); } } } catch ( FileNotFoundException e ) { // shouldn't really get here after canRead() } catch ( KeyStoreException e ) { log.warn("Error processing trusted certs in {}: {}", snTrustStoreFile, e.getMessage()); } } return ks; } /** * Clear any cached {@code SSLSocketFactory} so that a subsequent call to * {@link #getSSLSocketFactory()} returns a new instance. */ protected synchronized void resetSocketFactory() { socketFactory = null; } @Override public synchronized SSLSocketFactory getSSLSocketFactory() { if ( socketFactory == null ) { try { KeyStore trustStore = loadTrustStore(); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("PKIX"); trustManagerFactory.init(trustStore); X509TrustManager x509TrustManager = null; for ( TrustManager trustManager : trustManagerFactory.getTrustManagers() ) { if ( trustManager instanceof X509TrustManager ) { x509TrustManager = (X509TrustManager) trustManager; break; } } if ( x509TrustManager == null ) { throw new CertificateException("No X509 TrustManager available"); } KeyManager[] keyManagers = null; File ksFile = new File(keyStorePath); if ( ksFile.isFile() ) { KeyStore keyStore = loadKeyStore(); KeyManagerFactory keyManagerFactory = KeyManagerFactory .getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, getKeyStorePassword().toCharArray()); for ( KeyManager keyManager : keyManagerFactory.getKeyManagers() ) { if ( keyManager instanceof X509KeyManager ) { keyManagers = new KeyManager[] { keyManager }; } } } SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagers, new TrustManager[] { x509TrustManager }, null); socketFactory = sslContext.getSocketFactory(); } catch ( NoSuchAlgorithmException e ) { throw new CertificateException("Error creating SSLContext", e); } catch ( KeyStoreException e ) { throw new CertificateException("Error creating SSLContext", e); } catch ( UnrecoverableKeyException e ) { throw new CertificateException("Error creating SSLContext", e); } catch ( KeyManagementException e ) { throw new CertificateException("Error creating SSLContext", e); } } return socketFactory; } /** * Get the path to the keystore. * * @return the keyStorePath */ public String getKeyStorePath() { return keyStorePath; } /** * Set the path to the keystore. * * @param keyStorePath * the keyStorePath to set */ public void setKeyStorePath(String keyStorePath) { this.keyStorePath = keyStorePath; } /** * Get the path to the truststore. * * @return the trustStorePath */ public String getTrustStorePath() { return trustStorePath; } /** * Set the path to the truststore. * * @param trustStorePath * the trustStorePath to set */ public void setTrustStorePath(String trustStorePath) { this.trustStorePath = trustStorePath; } /** * Get the truststore password. * * @return the trustStorePassword */ protected String getTrustStorePassword() { return trustStorePassword; } /** * Set the truststore password. * * @param trustStorePassword * the trustStorePassword to set */ public void setTrustStorePassword(String trustStorePassword) { this.trustStorePassword = trustStorePassword; } /** * Get the JRE truststore password. * * @return the jreTrustStorePassword */ protected String getJreTrustStorePassword() { return jreTrustStorePassword; } /** * Set the JRE truststore password. * * @param jreTrustStorePassword * the jreTrustStorePassword to set */ public void setJreTrustStorePassword(String jreTrustStorePassword) { this.jreTrustStorePassword = jreTrustStorePassword; } /** * Get the keystore password. * * @return The keystore password. */ protected String getKeyStorePassword() { String password = keyStorePassword; if ( password != null && password.length() > 0 ) { return password; } return ""; } /** * Set the keystore password. * * @param keyStorePassword * the keyStorePassword to set */ public void setKeyStorePassword(String keyStorePassword) { this.keyStorePassword = keyStorePassword; } }