/*
* NOTE: This copyright does *not* cover user programs that use HQ
* 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) [2004-2011], VMWare, Inc.
* This file is part of HQ.
*
* HQ 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.hq.security.server.session;
import static org.hyperic.util.security.KeyStoreUtils.keyStoreToByteArray;
import static org.hyperic.util.security.KeyStoreUtils.loadKeyStore;
import static org.hyperic.util.security.KeyStoreUtils.persistKeyStore;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableEntryException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hyperic.hq.common.SystemException;
import org.hyperic.hq.security.ServerKeystoreConfig;
import org.hyperic.util.exec.ShutdownType;
import org.hyperic.util.security.DbKeyStoreSpi;
import org.hyperic.util.security.DbKeystoreManager;
import org.hyperic.util.security.KeystoreEntry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Transactional
@Service("dbKeystoreManager")
public class DbKeystoreManagerImpl implements DbKeystoreManager {
private final Log log = LogFactory.getLog(DbKeystoreManagerImpl.class);
@Autowired
private DbKeystoreDAO dbKeystoreDao;
@Autowired
private SessionFactory sessionFactory;
@Autowired
private ServerKeystoreConfig serverKeystoreConfig;
@Transactional(readOnly = true)
public Collection<? extends KeystoreEntry> getKeystore() {
return dbKeystoreDao.findAll();
}// EOM
/**
* This simply adds the certs from hyperic.keystore, saves them to the DB
* and deletes the certs from the file
* <p>
* <b>Pre-condition:
* </p>
* </b> There can be only one private key stored in the database.
*/
@PostConstruct
public void initDbKeystore() {
new HibernateTemplate(sessionFactory, true)
.execute(new HibernateCallback<Object>() {
public Object doInHibernate(Session session)
throws HibernateException, SQLException {
@SuppressWarnings("unchecked")
final Collection<KeystoreEntry> keys = session
.createCriteria(KeystoreEntryImpl.class).list();
final Map<String, KeystoreEntry> mapKeys = new HashMap<String, KeystoreEntry>();
for (KeystoreEntry entry : keys) {
mapKeys.put(entry.getAlias(), entry);
}
final Collection<KeystoreEntry> entries = new ArrayList<KeystoreEntry>();
final KeystoreContext ctx = new KeystoreContext();
try {
ctx.fileKeystore = loadKeyStore(
serverKeystoreConfig.getFilePath(),
serverKeystoreConfig.getFilePasswordCharArray()
) ;
final Enumeration<String> ksAliases = ctx.fileKeystore
.aliases();
while (ksAliases.hasMoreElements()) {
final String alias = ksAliases.nextElement();
final boolean isKey = ctx.fileKeystore
.isKeyEntry(alias);
final KeystoreEntryImpl entry = new KeystoreEntryImpl();
final String type = (isKey) ? DbKeyStoreSpi.PRIVATE_KEY_ENTRY
: DbKeyStoreSpi.TRUSTED_CERT_ENTRY;
entry.setType(type);
entry.setAlias(alias);
final Certificate cert = ctx.fileKeystore
.getCertificate(alias);
entry.setCertificate(cert);
final Certificate[] chain = ctx.fileKeystore
.getCertificateChain(alias);
entry.setCertificateChain(chain);
if (!mapKeys.containsKey(alias)) {
entries.add(entry);
}
if (!isKey) {
ctx.overrideKeystore = ctx.fileKeystore;
ctx.fileKeystore.deleteEntry(alias);
} else {
ctx.persistedPKEntry = mapKeys.get(alias);
ctx.newPKEntry = entry;
}// EO else if private key entry
}
// if private key entry, synchronize the
// file and persisted keystores
handlePK(ctx);
// if an override/updated keystore version was
// found, store it
if (ctx.overrideKeystore != null) {
persistKeyStore(ctx.overrideKeystore,
serverKeystoreConfig.getFilePath(),
serverKeystoreConfig.getFilePasswordCharArray()) ;
}
for (final KeystoreEntry entry : entries) {
if (!mapKeys.containsKey(entry.getAlias())) {
session.save(entry);
}
}
} catch (Throwable t) {
throw new SystemException(t);
} finally {
// if the system restart flag was set to true, log
// and restart
if (ctx.shouldRestartJVM) {
log.error("********** SYSTEM IS SHUTTING DOWN DUE TO PRIVATE KEY(S) "
+ "SYNCHRONIZATION. AUTOMATIC RESTART WOULD ONLY OCCUR IF "
+ "WRAPPER WATCHDOG IS INSTALLED ***************************");
ShutdownType.Restart.shutdown();
}// EO if JVM restart was requested
}// EO catch block
return null;
}
});
}
/**
* Processes a {@link DbKeyStoreSpi#PRIVATE_KEY_ENTRY} record.
*
* @param ctx
* DB kestore processing state containing the file keystore and
* persisted<BR>
* PrivateKey entries as well as the the file keystore instance.
*
* @throws KeyStoreException
* @throws NoSuchAlgorithmException
* @throws CertificateException
* @throws IOException
* @throws UnrecoverableEntryException
*/
private final void handlePK(final KeystoreContext ctx)
throws KeyStoreException, NoSuchAlgorithmException,
CertificateException, IOException, UnrecoverableEntryException {
// if the key is new, the store the fileKeystore as byte[]
// in the file member of the newPkEntry so that other server would be
// able to
// share this server's private key as a cluster singleton
if (ctx.persistedPKEntry == null) {
// load the keystore into byte[] and store it
final byte[] arrFileKeystoreContent = keyStoreToByteArray(ctx.fileKeystore,
serverKeystoreConfig.getFilePasswordCharArray()) ;
ctx.newPKEntry.setFile(arrFileKeystoreContent);
} else {
// extract the public key certificate from the persistentPKEntry
// instance
// and compare to that of the fileKeyStore's one.
// if the same (server already shares the private key), do nothing,
// else, load the keystore file into a keystore instance and replace
// the server's
// file keystore (requires JVM bounce)
final Certificate persistedCertificate = ctx.persistedPKEntry
.getCertificate();
if (!persistedCertificate.equals(ctx.newPKEntry.getCertificate())) {
final String sPKAlias = ctx.newPKEntry.getAlias();
final String sMsg = "Private key entry with alias " + sPKAlias
+ " differs from persisted version";
log.warn(sMsg + ", overriding local file keystore (REQUIRES SYSTEM RESTART).");
// load the byte[] into an in-memory keystore and store in the
// context's overrideKeystore so that it would replace the
// original one
ctx.overrideKeystore = loadKeyStore(ctx.persistedPKEntry.getFile(),
serverKeystoreConfig.getFilePasswordCharArray()
) ;
// set the restartJvm flag to true to indicate
// that the changes would not take hold without a restart
ctx.shouldRestartJVM = true;
}// EO if persisted certificate is different than the server's local
// file keystore's one
}// EO else if private key already exists in persistence store (not
// first server to boot)
}// EOM
/**
* Helper storing DB Keystore processing state
*
* @author guy
*/
private static final class KeystoreContext {
boolean shouldRestartJVM;
/**
* Instance corresponds to the {@link DbKeyStoreSpi#PRIVATE_KEY_ENTRY}
* record
*/
KeystoreEntry persistedPKEntry; // corresponding to the DB record
/**
* Instance corresponds to the file keystore private key entry
*/
KeystoreEntry newPKEntry;
KeyStore fileKeystore;
/**
* A new keystore to physically replace the server's keystore file
*/
KeyStore overrideKeystore;
}// EOM
/**
* reason for REQUIRES_NEW here is HHQ-4185, spring transaction manager
* doesn't upgrade session when it comes across a rw transaction from a ro
* transactional context
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void create(String alias, String type, Certificate cert,
Certificate[] chain) throws KeyStoreException {
final KeystoreEntryImpl keystoreEntry = new KeystoreEntryImpl();
keystoreEntry.setAlias(alias);
keystoreEntry.setType(type);
try {
keystoreEntry.setCertificate(cert);
keystoreEntry.setCertificateChain(chain);
} catch (IOException ioe) {
throw new KeyStoreException(ioe);
}// EO catch block
dbKeystoreDao.save(keystoreEntry);
}
}