/* ==================================================================
* DefaultKeystoreService.java - Dec 5, 2012 9:10:53 AM
*
* Copyright 2007-2012 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.node.setup.impl;
import static net.solarnetwork.node.SetupSettings.SETUP_TYPE_KEY;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import javax.annotation.Resource;
import javax.net.ssl.SSLSocketFactory;
import javax.security.auth.x500.X500Principal;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Base64OutputStream;
import org.springframework.context.MessageSource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.util.FileCopyUtils;
import net.solarnetwork.node.SSLService;
import net.solarnetwork.node.backup.BackupResource;
import net.solarnetwork.node.backup.BackupResourceInfo;
import net.solarnetwork.node.backup.BackupResourceProvider;
import net.solarnetwork.node.backup.BackupResourceProviderInfo;
import net.solarnetwork.node.backup.ResourceBackupResource;
import net.solarnetwork.node.backup.SimpleBackupResourceInfo;
import net.solarnetwork.node.backup.SimpleBackupResourceProviderInfo;
import net.solarnetwork.node.dao.SettingDao;
import net.solarnetwork.node.setup.PKIService;
import net.solarnetwork.support.CertificateException;
import net.solarnetwork.support.CertificateService;
import net.solarnetwork.support.ConfigurableSSLService;
/**
* Service for managing a {@link KeyStore}.
*
* <p>
* This implementation maintains a key store with two primary aliases:
* {@code ca} and {@code node}. The key store is created as needed, and a random
* password is generated and assigned to the key store. The password is stored
* in the Settings database, using the {@link #KEY_PASSWORD} key. This key store
* is then used to implement {@link SSLService} and is used as both the key and
* trust store for SSL connections returned by that API.
* </p>
*
* @author matt
* @version 1.3
*/
public class DefaultKeystoreService extends ConfigurableSSLService
implements PKIService, SSLService, BackupResourceProvider {
private static final String BACKUP_RESOURCE_NAME_KEYSTORE = "node.jks";
/** The default value for the {@code keyStorePath} property. */
public static final String DEFAULT_KEY_STORE_PATH = "conf/tls/node.jks";
/** The settings key for the key store password. */
public static final String KEY_PASSWORD = "solarnode.keystore.pw";
private static final String PKCS12_KEYSTORE_TYPE = "pkcs12";
private static final int PASSWORD_LENGTH = 20;
private String nodeAlias = "node";
private String caAlias = "ca";
private int keySize = 2048;
private MessageSource messageSource;
@Resource
private CertificateService certificateService;
@Resource
private SettingDao settingDao;
/**
* Default constructor.
*/
public DefaultKeystoreService() {
super();
setKeyStorePath(DefaultKeystoreService.DEFAULT_KEY_STORE_PATH);
setTrustStorePassword("solarnode");
setKeyStorePassword(null);
}
@Override
public String getKey() {
return DefaultKeystoreService.class.getName();
}
@Override
public Iterable<BackupResource> getBackupResources() {
File ksFile = new File(getKeyStorePath());
if ( !(ksFile.isFile() && ksFile.canRead()) ) {
return Collections.emptyList();
}
List<BackupResource> result = new ArrayList<BackupResource>(1);
result.add(new ResourceBackupResource(new FileSystemResource(ksFile),
BACKUP_RESOURCE_NAME_KEYSTORE, getKey()));
return result;
}
@Override
public boolean restoreBackupResource(BackupResource resource) {
if ( resource != null
&& BACKUP_RESOURCE_NAME_KEYSTORE.equalsIgnoreCase(resource.getBackupPath()) ) {
final File ksFile = new File(getKeyStorePath());
final File ksDir = ksFile.getParentFile();
if ( !ksDir.isDirectory() ) {
if ( !ksDir.mkdirs() ) {
log.warn("Error creating keystore directory {}", ksDir.getAbsolutePath());
return false;
}
}
synchronized ( this ) {
try {
FileCopyUtils.copy(resource.getInputStream(), new FileOutputStream(ksFile));
ksFile.setLastModified(resource.getModificationDate());
return true;
} catch ( IOException e ) {
log.error("IO error restoring keystore resource {}: {}", ksFile.getAbsolutePath(),
e.getMessage());
return false;
}
}
}
return false;
}
@Override
public BackupResourceProviderInfo providerInfo(Locale locale) {
String name = "Certificate Backup Provider";
String desc = "Backs up the SolarNode certificates.";
MessageSource ms = messageSource;
if ( ms != null ) {
name = ms.getMessage("title", null, name, locale);
desc = ms.getMessage("desc", null, desc, locale);
}
return new SimpleBackupResourceProviderInfo(getKey(), name, desc);
}
@Override
public BackupResourceInfo resourceInfo(BackupResource resource, Locale locale) {
return new SimpleBackupResourceInfo(resource.getProviderKey(), resource.getBackupPath(), null);
}
/**
* Get the keystore password.
*
* If a password has been configured via
* {@link #setKeyStorePassword(String)} this method will return that.
* Otherwise, the {@link SettingDao} is used to query the
* {@link #KEY_PASSWORD} setting value. If that value is available, that
* value is returned. Othersise, a new random password will be generated and
* persisted into the {@code SettingDao} on {@link #KEY_PASSWORD}, and the
* generated password will be returned.
*/
@Override
protected String getKeyStorePassword() {
String manualKeyStorePassword = super.getKeyStorePassword();
if ( manualKeyStorePassword != null && manualKeyStorePassword.length() > 0 ) {
return manualKeyStorePassword;
}
String result = getSetting(KEY_PASSWORD);
if ( result == null ) {
// generate new random password
try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
final int start = 32;
final int end = 126;
final int range = end - start;
char[] passwd = new char[PASSWORD_LENGTH];
for ( int i = 0; i < PASSWORD_LENGTH; i++ ) {
passwd[i] = (char) (random.nextInt(range) + start);
}
result = new String(passwd);
saveSetting(KEY_PASSWORD, result);
} catch ( NoSuchAlgorithmException e ) {
throw new CertificateException("Error creating random key store password", e);
}
}
return result;
}
@Override
public boolean isNodeCertificateValid(String issuerDN) throws CertificateException {
KeyStore keyStore = loadKeyStore();
X509Certificate x509 = null;
try {
if ( keyStore == null || !keyStore.containsAlias(nodeAlias) ) {
return false;
}
Certificate cert = keyStore.getCertificate(nodeAlias);
if ( !(cert instanceof X509Certificate) ) {
return false;
}
x509 = (X509Certificate) cert;
x509.checkValidity();
X500Principal issuer = new X500Principal(issuerDN);
if ( !x509.getIssuerX500Principal().equals(issuer) ) {
log.debug("Certificate issuer {} not same as expected {}",
x509.getIssuerX500Principal().getName(), issuer.getName());
return false;
}
return true;
} catch ( KeyStoreException e ) {
throw new CertificateException("Error checking for node certificate", e);
} catch ( CertificateExpiredException e ) {
log.debug("Certificate {} has expired", x509.getSubjectDN().getName());
} catch ( CertificateNotYetValidException e ) {
log.debug("Certificate {} not valid yet", x509.getSubjectDN().getName());
}
return false;
}
@Override
public X509Certificate generateNodeSelfSignedCertificate(String dn) throws CertificateException {
KeyStore keyStore = null;
try {
keyStore = loadKeyStore();
} catch ( CertificateException e ) {
Throwable root = e;
while ( root.getCause() != null ) {
root = root.getCause();
}
if ( root instanceof UnrecoverableKeyException ) {
// bad password... we shall assume here that a new node association is underway,
// so delete the existing key store and re-create
File ksFile = new File(getKeyStorePath());
if ( ksFile.isFile() ) {
log.info(
"Deleting existing certificate store due to invalid password, will create new store");
if ( ksFile.delete() ) {
// clear out old key store password, so we generate a new one
deleteSetting(KEY_PASSWORD);
keyStore = loadKeyStore();
}
}
}
if ( keyStore == null ) {
// re-throw, we didn't handle it
throw e;
}
}
return createSelfSignedCertificate(keyStore, dn, nodeAlias);
}
private X509Certificate createSelfSignedCertificate(KeyStore keyStore, String dn, String alias) {
try {
// create new key pair for the node
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(keySize, new SecureRandom());
KeyPair keypair = keyGen.generateKeyPair();
PublicKey publicKey = keypair.getPublic();
PrivateKey privateKey = keypair.getPrivate();
Certificate cert = certificateService.generateCertificate(dn, publicKey, privateKey);
keyStore.setKeyEntry(alias, privateKey, getKeyStorePassword().toCharArray(),
new Certificate[] { cert });
saveKeyStore(keyStore);
return (X509Certificate) cert;
} catch ( NoSuchAlgorithmException e ) {
throw new CertificateException("Error setting up node key pair", e);
} catch ( KeyStoreException e ) {
throw new CertificateException("Error setting up node key pair", e);
}
}
private void saveTrustedCertificate(X509Certificate cert, String alias) {
KeyStore keyStore = loadKeyStore();
try {
log.info("Installing trusted CA certificate {}", cert.getSubjectDN());
keyStore.setCertificateEntry(alias, cert);
saveKeyStore(keyStore);
} catch ( KeyStoreException e ) {
throw new CertificateException("Error saving trusted certificate", e);
}
}
@Override
public void saveCACertificate(X509Certificate cert) throws CertificateException {
saveTrustedCertificate(cert, caAlias);
}
@Override
public String generateNodePKCS10CertificateRequestString() throws CertificateException {
KeyStore keyStore = loadKeyStore();
Key key;
try {
key = keyStore.getKey(nodeAlias, getKeyStorePassword().toCharArray());
} catch ( UnrecoverableKeyException e ) {
throw new CertificateException("Error opening node private key", e);
} catch ( KeyStoreException e ) {
throw new CertificateException("Error opening node private key", e);
} catch ( NoSuchAlgorithmException e ) {
throw new CertificateException("Error opening node private key", e);
}
assert key instanceof PrivateKey;
Certificate cert;
try {
cert = keyStore.getCertificate(nodeAlias);
} catch ( KeyStoreException e ) {
throw new CertificateException("Error opening node certificate", e);
}
assert cert instanceof X509Certificate;
return certificateService.generatePKCS10CertificateRequestString((X509Certificate) cert,
(PrivateKey) key);
}
@Override
public String generateNodePKCS7CertificateString() throws CertificateException {
KeyStore keyStore = loadKeyStore();
Key key;
try {
key = keyStore.getKey(nodeAlias, getKeyStorePassword().toCharArray());
} catch ( UnrecoverableKeyException e ) {
throw new CertificateException("Error opening node private key", e);
} catch ( KeyStoreException e ) {
throw new CertificateException("Error opening node private key", e);
} catch ( NoSuchAlgorithmException e ) {
throw new CertificateException("Error opening node private key", e);
}
assert key instanceof PrivateKey;
Certificate cert;
try {
cert = keyStore.getCertificate(nodeAlias);
} catch ( KeyStoreException e ) {
throw new CertificateException("Error opening node certificate", e);
}
assert cert instanceof X509Certificate;
return certificateService
.generatePKCS7CertificateChainString(new X509Certificate[] { (X509Certificate) cert });
}
@Override
public String generateNodePKCS7CertificateChainString() throws CertificateException {
KeyStore keyStore = loadKeyStore();
Key key;
try {
key = keyStore.getKey(nodeAlias, getKeyStorePassword().toCharArray());
} catch ( UnrecoverableKeyException e ) {
throw new CertificateException("Error opening node private key", e);
} catch ( KeyStoreException e ) {
throw new CertificateException("Error opening node private key", e);
} catch ( NoSuchAlgorithmException e ) {
throw new CertificateException("Error opening node private key", e);
}
assert key instanceof PrivateKey;
Certificate[] chain;
try {
chain = keyStore.getCertificateChain(nodeAlias);
} catch ( KeyStoreException e ) {
throw new CertificateException("Error opening node certificate", e);
}
X509Certificate[] x509Chain = new X509Certificate[chain.length];
for ( int i = 0; i < chain.length; i++ ) {
assert chain[i] instanceof X509Certificate;
x509Chain[i] = (X509Certificate) chain[i];
}
return certificateService.generatePKCS7CertificateChainString(x509Chain);
}
@Override
public X509Certificate getNodeCertificate() throws CertificateException {
return getNodeCertificate(loadKeyStore());
}
private X509Certificate getNodeCertificate(KeyStore keyStore) {
X509Certificate nodeCert;
try {
nodeCert = (X509Certificate) keyStore.getCertificate(nodeAlias);
} catch ( KeyStoreException e ) {
throw new CertificateException("Error opening node certificate", e);
}
return nodeCert;
}
@Override
public X509Certificate getCACertificate() throws CertificateException {
return getCACertificate(loadKeyStore());
}
private X509Certificate getCACertificate(KeyStore keyStore) {
X509Certificate nodeCert;
try {
nodeCert = (X509Certificate) keyStore.getCertificate(caAlias);
} catch ( KeyStoreException e ) {
throw new CertificateException("Error opening node certificate", e);
}
return nodeCert;
}
@Override
public void savePKCS12Keystore(String keystore, String password) throws CertificateException {
KeyStore keyStore = loadKeyStore(PKCS12_KEYSTORE_TYPE,
new ByteArrayInputStream(Base64.decodeBase64(keystore)), password);
deleteSetting(KEY_PASSWORD);
final String newPassword = getKeyStorePassword();
KeyStore newKeyStore = loadKeyStore(KeyStore.getDefaultType(), null, newPassword);
// change the password to our local random one
copyNodeChain(keyStore, password, newKeyStore, newPassword);
File ksFile = new File(getKeyStorePath());
if ( ksFile.isFile() ) {
ksFile.delete();
}
saveKeyStore(newKeyStore);
}
private void copyNodeChain(KeyStore keyStore, String password, KeyStore newKeyStore,
String newPassword) {
try {
// change the password to our local random one
Key key = keyStore.getKey(nodeAlias, password.toCharArray());
Certificate[] chain = keyStore.getCertificateChain(nodeAlias);
X509Certificate[] x509Chain = new X509Certificate[chain.length];
for ( int i = 0; i < chain.length; i += 1 ) {
x509Chain[i] = (X509Certificate) chain[i];
}
saveNodeCertificateChain(newKeyStore, key, newPassword, x509Chain[0], x509Chain);
} catch ( GeneralSecurityException e ) {
throw new CertificateException(e);
}
}
@Override
public String generatePKCS12KeystoreString(String password) throws CertificateException {
KeyStore keyStore = loadKeyStore();
KeyStore newKeyStore = loadKeyStore(PKCS12_KEYSTORE_TYPE, null, password);
copyNodeChain(keyStore, getKeyStorePassword(), newKeyStore, password);
ByteArrayOutputStream byos = new ByteArrayOutputStream();
saveKeyStore(newKeyStore, password, new Base64OutputStream(byos));
try {
return byos.toString("US-ASCII");
} catch ( UnsupportedEncodingException e ) {
// should never get here
throw new RuntimeException(e);
}
}
@Override
public void saveNodeSignedCertificate(String pem) throws CertificateException {
KeyStore keyStore = loadKeyStore();
Key key;
try {
key = keyStore.getKey(nodeAlias, getKeyStorePassword().toCharArray());
} catch ( UnrecoverableKeyException e ) {
throw new CertificateException("Error opening node private key", e);
} catch ( KeyStoreException e ) {
throw new CertificateException("Error opening node private key", e);
} catch ( NoSuchAlgorithmException e ) {
throw new CertificateException("Error opening node private key", e);
}
X509Certificate nodeCert = getNodeCertificate(keyStore);
if ( nodeCert == null ) {
throw new CertificateException(
"The node does not have a private key, start the association process over.");
}
X509Certificate[] chain = certificateService.parsePKCS7CertificateChainString(pem);
saveNodeCertificateChain(keyStore, key, getKeyStorePassword(), nodeCert, chain);
saveKeyStore(keyStore);
}
private void saveNodeCertificateChain(KeyStore keyStore, Key key, String keyPassword,
X509Certificate nodeCert, X509Certificate[] chain) {
if ( keyPassword == null ) {
keyPassword = "";
}
X509Certificate caCert = getCACertificate(keyStore);
if ( chain.length < 1 ) {
throw new CertificateException("No certificates avaialble");
}
if ( chain.length > 1 ) {
// we have to trust the parents... the end of the chain must be our CA
try {
final int caIdx = chain.length - 1;
if ( caCert == null ) {
// if we don't have a CA cert yet, install that now
log.info("Installing trusted CA certificate {}", chain[caIdx].getSubjectDN());
keyStore.setCertificateEntry(caAlias, chain[caIdx]);
caCert = chain[caIdx];
} else {
// verify CA is the same... maybe we shouldn't do this?
if ( !chain[caIdx].getSubjectDN().equals(caCert.getSubjectDN()) ) {
throw new CertificateException(
"Chain CA " + chain[caIdx].getSubjectDN().getName()
+ " does not match expected " + caCert.getSubjectDN().getName());
}
if ( !chain[caIdx].getIssuerDN().equals(caCert.getIssuerDN()) ) {
throw new CertificateException("Chain CA " + chain[caIdx].getIssuerDN().getName()
+ " does not match expected " + caCert.getIssuerDN().getName());
}
}
// install intermediate certs...
for ( int i = caIdx - 1, j = 1; i > 0; i--, j++ ) {
String alias = caAlias + "sub" + j;
log.info("Installing trusted intermediate certificate {}", chain[i].getSubjectDN());
keyStore.setCertificateEntry(alias, chain[i]);
}
} catch ( KeyStoreException e ) {
throw new CertificateException("Error storing CA chain", e);
}
} else {
// put CA at end of chain
if ( caCert == null ) {
throw new CertificateException("No CA certificate available");
}
chain = new X509Certificate[] { chain[0], caCert };
}
// the issuer must be our CA cert subject...
if ( !chain[0].getIssuerDN().equals(chain[1].getSubjectDN()) ) {
throw new CertificateException("Issuer " + chain[0].getIssuerDN().getName()
+ " does not match expected " + chain[1].getSubjectDN().getName());
}
// the subject must be our node's existing subject...
if ( !chain[0].getSubjectDN().equals(nodeCert.getSubjectDN()) ) {
throw new CertificateException("Subject " + chain[0].getIssuerDN().getName()
+ " does not match expected " + nodeCert.getSubjectDN().getName());
}
log.info("Installing node certificate {} reply {} issued by {}", chain[0].getSerialNumber(),
chain[0].getSubjectDN().getName(), chain[0].getIssuerDN().getName());
try {
keyStore.setKeyEntry(nodeAlias, key, keyPassword.toCharArray(), chain);
} catch ( KeyStoreException e ) {
throw new CertificateException("Error opening node certificate", e);
}
}
@Override
public synchronized SSLSocketFactory getSolarInSocketFactory() {
return getSSLSocketFactory();
}
private synchronized void saveKeyStore(KeyStore keyStore) {
if ( keyStore == null ) {
return;
}
File ksFile = new File(getKeyStorePath());
File ksDir = ksFile.getParentFile();
if ( !ksDir.isDirectory() && !ksDir.mkdirs() ) {
throw new RuntimeException("Unable to create KeyStore directory: " + ksFile.getParent());
}
String passwd = getKeyStorePassword();
try {
saveKeyStore(keyStore, passwd, new BufferedOutputStream(new FileOutputStream(ksFile)));
resetSocketFactory();
} catch ( IOException e ) {
throw new CertificateException("Error saving certificate key store to " + ksFile.getPath(),
e);
}
}
private String getSetting(String key) {
return settingDao.getSetting(key, SETUP_TYPE_KEY);
}
private void saveSetting(String key, String value) {
settingDao.storeSetting(key, SETUP_TYPE_KEY, value);
}
private void deleteSetting(String key) {
settingDao.deleteSetting(key, SETUP_TYPE_KEY);
}
public void setSettingDao(SettingDao settingDao) {
this.settingDao = settingDao;
}
public void setNodeAlias(String nodeAlias) {
this.nodeAlias = nodeAlias;
}
public void setCaAlias(String caAlias) {
this.caAlias = caAlias;
}
public void setKeySize(int keySize) {
this.keySize = keySize;
}
public void setCertificateService(CertificateService certificateService) {
this.certificateService = certificateService;
}
/**
* Set the manual keystore password to use.
*
* @param manualKeyStorePassword
* the password to use
* @deprecated use {@link #setKeyStorePassword(String)}
*/
@Deprecated
public void setManualKeyStorePassword(String manualKeyStorePassword) {
setKeyStorePassword(manualKeyStorePassword);
}
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
}