/** * NOTE: This copyright does *not* cover user programs that use Hyperic * program services by normal system calls through the application * program interfaces provided as part of the Hyperic Plug-in Development * Kit or the Hyperic Client Development Kit - this is merely considered * normal use of the program, and does *not* fall under the heading of * "derived work". * * Copyright (C) [2010], VMware, Inc. * This file is part of Hyperic. * * Hyperic is free software; you can redistribute it and/or modify * it under the terms version 2 of the GNU General Public License as * published by the Free Software Foundation. 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 org.hyperic.util.security; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; import java.security.cert.X509Certificate; import java.util.concurrent.atomic.AtomicBoolean; import javax.net.ssl.X509TrustManager; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.conn.ssl.AbstractVerifier; import org.hyperic.util.exec.Execute; import org.hyperic.util.exec.ExecuteWatchdog; import org.hyperic.util.exec.PumpStreamHandler; import org.hyperic.util.file.FileUtil; import org.hyperic.util.timer.StopWatch; import org.springframework.util.StringUtils; public class KeystoreManager { private final Log log; private final AtomicBoolean isDB = new AtomicBoolean(false); private static KeystoreManager keystoreManager= new KeystoreManager(); private KeystoreManager() { this.log = LogFactory.getLog(KeystoreManager.class); } public static KeystoreManager getKeystoreManager() { return keystoreManager; } private String getDName(KeystoreConfig keystoreConfig) { return "CN=" + keystoreConfig.getKeyCN()+ " (HQ Self-Signed Cert), OU=HQ, O=hyperic.net, L=Unknown, ST=Unknown, C=US"; } public KeyStore getKeyStore(KeystoreConfig keystoreConfig) throws KeyStoreException, IOException { FileInputStream keyStoreFileInputStream = null; String filePath = keystoreConfig.getFilePath(); String filePassword = keystoreConfig.getFilePassword(); //check if keystoreConfig valid (block if it's null or "") String errorMsg=""; if(keystoreConfig.getAlias()==null){ errorMsg+=" alias is null. "; } if(keystoreConfig.getFilePath()==null){ errorMsg+=" filePath is null. "; } if(keystoreConfig.getFilePassword()==null){ errorMsg+=" password is null. "; } if(!"".equals(errorMsg)){ throw new KeyStoreException(errorMsg); } try { KeyStore keystore = DbKeyStore.getInstance(KeyStore.getDefaultType(), isDB); File file = new File(filePath); char[] password = null; if (!file.exists()) { // ...if file doesn't exist, and path was user specified throw IOException... if (StringUtils.hasText(filePath) && !keystoreConfig.isHqDefault()) { throw new IOException("User specified keystore [" + filePath + "] does not exist."); } password = filePassword.toCharArray(); createInternalKeystore(keystoreConfig); FileUtil.setReadWriteOnlyByOwner(file); } // ...keystore exist, so init the file input stream... keyStoreFileInputStream = new FileInputStream(file); keystore.load(keyStoreFileInputStream, password); return keystore; } catch (NoSuchAlgorithmException e) { // can't check integrity of keystore, if this happens we're kind of screwed // is there anything we can do to self heal this problem? errorMsg = "The algorithm used to check the integrity of the keystore cannot be found."; throw new KeyStoreException(errorMsg,e); } catch (CertificateException e) { // there are some corrupted certificates in the keystore, a bad thing // is there anything we can do to self heal this problem? errorMsg = "Keystore cannot be loaded. One possibility is that the password is incorrect."; throw new KeyStoreException(errorMsg, e); } finally { if (keyStoreFileInputStream != null) { keyStoreFileInputStream.close(); keyStoreFileInputStream = null; } } } private void createInternalKeystore(KeystoreConfig keystoreConfig) throws KeyStoreException{ ByteArrayOutputStream output = new ByteArrayOutputStream(); String javaHome = System.getProperty("java.home"); String keytool = javaHome + File.separator + "bin" + File.separator + "keytool"; String[] args = { keytool, "-genkey", "-dname", getDName(keystoreConfig), "-alias", keystoreConfig.getAlias(), "-keystore", keystoreConfig.getFilePath(), "-storepass", keystoreConfig.getFilePassword(), "-keypass", keystoreConfig.getFilePassword(), "-keyalg", "RSA", "-validity", "3650" //10 years }; int timeout = 5 * 60 * 1000; //5min ExecuteWatchdog wdog = new ExecuteWatchdog(timeout); Execute exec = new Execute(new PumpStreamHandler(output), wdog); exec.setCommandline(args); //TODO shouldn't have password in log log.debug("Generating keystore: " + keystoreConfig.getFilePath()); int rc; try { rc = exec.execute(); } catch (Exception e) { rc = -1; log.error(e); } if (rc != 0) { String msg = output.toString().trim(); if (msg.length() == 0) { msg = "timeout after " + timeout + "ms"; } // TODO This is super fugly but considering how we're creating the keystore file, there isn't a clean way of accomplishing this // Basically, there is a small window of opportunity where two agent processes could discover no keystore file and try to // generate one using the ExceuteWatchdog. One will succeed, the other will fail, if that happens we shouldn't kill the process. // For any other exception throw it... if (!msg.toLowerCase().contains("key pair not generated, alias <" + keystoreConfig.getAlias().toLowerCase() + "> already exists")) { //can't have password in log throw new KeyStoreException("Failed to create keystore:"+keystoreConfig.getAlias()+", "+msg); } } } public X509TrustManager getCustomTrustManager(X509TrustManager defaultTrustManager, KeystoreConfig keystoreConfig, boolean acceptUnverifiedCertificates, KeyStore trustStore) { return new CustomTrustManager(defaultTrustManager, keystoreConfig, acceptUnverifiedCertificates, trustStore, isDB.get()); } private class CustomTrustManager implements X509TrustManager { private final Log log = LogFactory.getLog(X509TrustManager.class); private final X509TrustManager defaultTrustManager; private final KeystoreConfig keystoreConfig; private final boolean acceptUnverifiedCertificates; private final KeyStore trustStore; private final boolean isDB; private CustomTrustManager(X509TrustManager defaultTrustManager, KeystoreConfig keystoreConfig, boolean acceptUnverifiedCertificates, KeyStore trustStore, boolean isDB) { this.defaultTrustManager = defaultTrustManager; this.keystoreConfig = keystoreConfig; this.acceptUnverifiedCertificates = acceptUnverifiedCertificates; this.trustStore = trustStore; this.isDB = isDB; } public X509Certificate[] getAcceptedIssuers() { return defaultTrustManager.getAcceptedIssuers(); } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { try { defaultTrustManager.checkServerTrusted(chain, authType); } catch (CertificateException e){ CertificateExpiredException expiredCertException = getCertExpiredException(e); if (expiredCertException!=null){ log.error("Fail the connection because received certificate is expired. " + "Please update the certificate.",expiredCertException); throw new CertificateException(e); } if (acceptUnverifiedCertificates) { log.info("Import the certification. (Received certificate is not trusted by keystore)"); importCertificate(chain); } else { log.warn("Fail the connection because received certificate is not trusted by " + "keystore: alias=" + keystoreConfig.getAlias()); log.debug("Fail the connection because received certificate is not trusted by " + "keystore: alias=" + keystoreConfig.getAlias() + ", acceptUnverifiedCertificates="+acceptUnverifiedCertificates,e); throw new CertificateException(e); } } } private CertificateExpiredException getCertExpiredException(Exception e){ while (e !=null){ if (e instanceof CertificateExpiredException){ return (CertificateExpiredException)e; } e = (Exception) e.getCause(); } return null; } public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { defaultTrustManager.checkClientTrusted(chain, authType); } private void importCertificate(X509Certificate[] chain) throws CertificateException { FileOutputStream ksFileOutputStream = null; final boolean debug = log.isDebugEnabled(); final StopWatch watch = new StopWatch(); try { for (X509Certificate cert : chain) { String[] cnValues = AbstractVerifier.getCNs(cert); String alias = (cnValues != null && cnValues.length > 0) ? cnValues[0] : "UnknownCN"; alias += "-ts=" + System.currentTimeMillis(); trustStore.setCertificateEntry(alias, cert); } if (!isDB) { ksFileOutputStream = new FileOutputStream(keystoreConfig.getFilePath()); trustStore.store(ksFileOutputStream, keystoreConfig.getFilePassword().toCharArray()); } } catch (FileNotFoundException e) { // Can't find the keystore in the path log.error("Can't find the keystore in " + keystoreConfig.getFilePath() + ". Error message: " + e, e); } catch (NoSuchAlgorithmException e) { log.error("The algorithm is not supported. Error message: " + e, e); } catch (Exception e) { // expect KeyStoreException, IOException log.error("Exception when trying to import certificate: " + e, e); } finally { close(ksFileOutputStream); ksFileOutputStream = null; if (debug) log.debug("importCert: " + watch); } } private void close(FileOutputStream fos) { if (fos == null) { return; } try { fos.close(); } catch (IOException e) {} } } }