package cybervillains.ca;
import com.isecpartners.gizmo.GizmoView;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
/**
* This is the main entry point into the Cybervillains CA.
*
* This class handles generation, storage and the persistent
* mapping of input to duplicated certificates and mapped public
* keys.
*
* Default setting is to immediately persist changes to the store
* by writing out the keystore and mapping file every time a new
* certificate is added. This behavior can be disabled if desired,
* to enhance performance or allow temporary testing without modifying
* the certificate store.
*
***************************************************************************************
* Copyright (c) 2007, Information Security Partners, LLC
* All rights reserved.
*
* This software licensed under the GPLv2, available in LICENSE.txt and at
* http://www.gnu.org/licenses/gpl.txt
*
* @author Brad Hill
*
*/
public class KeyStoreManager {
private static final String CERTMAP_SER_FILE = "certmap.ser";
private static final String SUBJMAP_SER_FILE = "subjmap.ser";
private static final String EXPORTED_CERT_NAME = "cybervillainsCA.cer";
private static final char[] _keypassword = "password".toCharArray();
private static final char[] _keystorepass = "password".toCharArray();
private static final String _caPrivateKeystore = "cybervillainsCA.jks";
private static final String _caCertAlias = "signingCert";
private static final String _caPrivKeyAlias = "signingCertPrivKey";
static X509Certificate _caCert;
static PrivateKey _caPrivKey;
static KeyStore _ks;
private static HashMap<PublicKey, PrivateKey> _rememberedPrivateKeys;
private static HashMap<PublicKey, PublicKey> _mappedPublicKeys;
private static HashMap<String, String> _certMap;
private static HashMap<String, String> _subjectMap;
private static final String KEYMAP_SER_FILE = "keymap.ser";
private static final String PUB_KEYMAP_SER_FILE = "pubkeymap.ser";
public static final String RSA_KEYGEN_ALGO = "RSA";
public static final String DSA_KEYGEN_ALGO = "DSA";
public static final KeyPairGenerator _rsaKpg;
public static final KeyPairGenerator _dsaKpg;
private static SecureRandom _sr;
private static boolean persistImmediately = true;
static
{
Security.insertProviderAt(new BouncyCastleProvider(), 2);
_sr = new SecureRandom();
GizmoView.log("hunh");
try
{
_rsaKpg = KeyPairGenerator.getInstance(RSA_KEYGEN_ALGO);
_dsaKpg = KeyPairGenerator.getInstance(DSA_KEYGEN_ALGO);
}
catch(Throwable t)
{
throw new Error(t);
}
try {
File privKeys = new File(KEYMAP_SER_FILE);
if(!privKeys.exists())
{
_rememberedPrivateKeys = new HashMap<PublicKey,PrivateKey>();
}
else
{
ObjectInputStream in = new ObjectInputStream(new FileInputStream(privKeys));
// Deserialize the object
_rememberedPrivateKeys = (HashMap<PublicKey,PrivateKey>)in.readObject();
in.close();
}
File pubKeys = new File(PUB_KEYMAP_SER_FILE);
if(!pubKeys.exists())
{
_mappedPublicKeys = new HashMap<PublicKey,PublicKey>();
}
else
{
ObjectInputStream in = new ObjectInputStream(new FileInputStream(pubKeys));
// Deserialize the object
_mappedPublicKeys = (HashMap<PublicKey,PublicKey>)in.readObject();
in.close();
}
} catch (FileNotFoundException e) {
// check for file exists, won't happen.
e.printStackTrace();
} catch (IOException e) {
// we could correct, but this probably indicates a corruption
// of the serialized file that we want to know about; likely
// synchronization problems during serialization.
e.printStackTrace();
throw new Error(e);
} catch (ClassNotFoundException e) {
// serious problem.
e.printStackTrace();
throw new Error(e);
}
_rsaKpg.initialize(1024, _sr);
_dsaKpg.initialize(1024, _sr);
initKeystore();
try {
File file = new File(CERTMAP_SER_FILE);
if(!file.exists())
{
_certMap = new HashMap<String,String>();
}
else
{
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
// Deserialize the object
_certMap = (HashMap<String,String>)in.readObject();
in.close();
}
} catch (FileNotFoundException e) {
// won't happen, check file.exists()
e.printStackTrace();
} catch (IOException e) {
// corrupted file, we want to know.
e.printStackTrace();
throw new Error(e);
} catch (ClassNotFoundException e) {
// something very wrong, exit
e.printStackTrace();
throw new Error(e);
}
try {
File file = new File(SUBJMAP_SER_FILE);
if(!file.exists())
{
_subjectMap = new HashMap<String,String>();
}
else
{
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
// Deserialize the object
_subjectMap = (HashMap<String,String>)in.readObject();
in.close();
}
} catch (FileNotFoundException e) {
// won't happen, check file.exists()
e.printStackTrace();
} catch (IOException e) {
// corrupted file, we want to know.
e.printStackTrace();
throw new Error(e);
} catch (ClassNotFoundException e) {
// something very wrong, exit
e.printStackTrace();
throw new Error(e);
}
}
private static void reloadKeystore() throws FileNotFoundException, IOException, NoSuchAlgorithmException, CertificateException, KeyStoreException, UnrecoverableKeyException {
InputStream is = new FileInputStream(_caPrivateKeystore);
if (is != null) {
_ks.load(is, _keystorepass);
_caCert = (X509Certificate)_ks.getCertificate(_caCertAlias);
_caPrivKey = (PrivateKey)_ks.getKey(_caPrivKeyAlias, _keypassword);
}
}
/**
* Creates, writes and loads a new keystore and CA root certificate.
*/
protected static void createKeystore() {
Certificate signingCert = null;
PrivateKey caPrivKey = null;
if(_caCert == null || _caPrivKey == null)
{
try
{
GizmoView.log("and whatnot");
System.out.println("Keystore or signing cert & keypair not found. Generating...");
KeyPair caKeypair = KeyStoreManager.getRSAKeyPair();
caPrivKey = caKeypair.getPrivate();
signingCert = CertificateCreator.createTypicalMasterCert(caKeypair);
System.out.println("Done generating signing cert");
System.out.println(signingCert);
_ks.load(null, _keystorepass);
_ks.setCertificateEntry(_caCertAlias, signingCert);
_ks.setKeyEntry(_caPrivKeyAlias, caPrivKey, _keypassword, new Certificate[] {signingCert});
File caKsFile = new File(_caPrivateKeystore);
GizmoView.log(new String(_keystorepass));
OutputStream os = new FileOutputStream(caKsFile);
_ks.store(os, _keystorepass);
System.out.println("Wrote JKS keystore to: " +
caKsFile.getAbsolutePath());
// also export a .cer that can be imported as a trusted root
// to disable all warning dialogs for interception
File signingCertFile = new File(EXPORTED_CERT_NAME);
GizmoView.log(EXPORTED_CERT_NAME);
FileOutputStream cerOut = new FileOutputStream(signingCertFile);
byte[] buf = signingCert.getEncoded();
System.out.println("Wrote signing cert to: " + signingCertFile.getAbsolutePath());
cerOut.write(buf);
cerOut.flush();
cerOut.close();
GizmoView.log("length: " + signingCertFile.length());
_caCert = (X509Certificate)signingCert;
_caPrivKey = caPrivKey;
}
catch(Exception e)
{
GizmoView.log(e.toString());
System.out.println("Fatal error creating/storing keystore or signing cert.");
e.printStackTrace();
throw new Error(e);
}
}
else
{
System.out.println("Successfully loaded keystore.");
System.out.println(_caCert);
}
}
/**
* Stores a new certificate and its associated private key in the keystore.
* @param cert
* @param privKey
* @throws KeyStoreException
* @throws CertificateException
* @throws NoSuchAlgorithmException
*/
public static synchronized void addCertAndPrivateKey(final X509Certificate cert, final PrivateKey privKey)
throws KeyStoreException, CertificateException, NoSuchAlgorithmException
{
String alias = ThumbprintUtil.getThumbprint(cert);
_ks.deleteEntry(alias);
_ks.setCertificateEntry(alias, cert);
_ks.setKeyEntry(alias, privKey, _keypassword, new Certificate[] {cert});
if(persistImmediately)
{
persist();
}
}
/**
* Writes the keystore and certificate/keypair mappings to disk.
* @throws KeyStoreException
* @throws NoSuchAlgorithmException
* @throws CertificateException
*/
public synchronized static void persist() throws KeyStoreException, NoSuchAlgorithmException, CertificateException {
try
{
FileOutputStream kso = new FileOutputStream(_caPrivateKeystore);
_ks.store(kso, _keystorepass);
kso.flush();
kso.close();
persistCertMap();
persistSubjectMap();
persistKeyPairMap();
persistPublicKeyMap();
}
catch(IOException ioe)
{
ioe.printStackTrace();
}
}
/**
* Returns the aliased certificate. Certificates are aliased by their SHA1 digest.
* @see ThumbprintUtil
* @param alias
* @return
* @throws KeyStoreException
*/
public static synchronized X509Certificate getCertificateByAlias(final String alias) throws KeyStoreException{
return (X509Certificate)_ks.getCertificate(alias);
}
/**
* Returns the aliased certificate. Certificates are aliased by their hostname.
* @see ThumbprintUtil
* @param alias
* @return
* @throws KeyStoreException
* @throws UnrecoverableKeyException
* @throws NoSuchProviderException
* @throws NoSuchAlgorithmException
* @throws CertificateException
* @throws SignatureException
* @throws CertificateNotYetValidException
* @throws CertificateExpiredException
* @throws InvalidKeyException
* @throws CertificateParsingException
*/
public static synchronized X509Certificate getCertificateByHostname(final String hostname) throws KeyStoreException, CertificateParsingException, InvalidKeyException, CertificateExpiredException, CertificateNotYetValidException, SignatureException, CertificateException, NoSuchAlgorithmException, NoSuchProviderException, UnrecoverableKeyException{
String alias = _subjectMap.get(getSubjectForHostname(hostname));
if(alias != null) {
return (X509Certificate)_ks.getCertificate(alias);
}
else {
return getMappedCertificateForHostname(hostname);
}
}
/**
* Gets the authority root signing cert.
* @return
* @throws KeyStoreException
*/
public static synchronized X509Certificate getSigningCert() throws KeyStoreException {
return _caCert;
}
/**
* Gets the authority private signing key.
* @return
* @throws KeyStoreException
* @throws NoSuchAlgorithmException
* @throws UnrecoverableKeyException
*/
public static synchronized PrivateKey getSigningPrivateKey() throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
return _caPrivKey;
}
/**
* Whether updates are immediately written to disk.
* @return
*/
public static boolean getPersistImmediately() {
return persistImmediately;
}
/**
* Whether updates are immediately written to disk.
* @param persistImmediately
*/
public static void setPersistImmediately(final boolean persistImmediately) {
KeyStoreManager.persistImmediately = persistImmediately;
}
/**
* This method returns the duplicated certificate mapped to the passed in cert, or
* creates and returns one if no mapping has yet been performed. If a naked public
* key has already been mapped that matches the key in the cert, the already mapped
* keypair will be reused for the mapped cert.
* @param cert
* @return
* @throws CertificateEncodingException
* @throws InvalidKeyException
* @throws CertificateException
* @throws CertificateNotYetValidException
* @throws NoSuchAlgorithmException
* @throws NoSuchProviderException
* @throws SignatureException
* @throws KeyStoreException
* @throws UnrecoverableKeyException
*/
public static synchronized X509Certificate getMappedCertificate(final X509Certificate cert)
throws CertificateEncodingException,
InvalidKeyException,
CertificateException,
CertificateNotYetValidException,
NoSuchAlgorithmException,
NoSuchProviderException,
SignatureException,
KeyStoreException,
UnrecoverableKeyException
{
String thumbprint = ThumbprintUtil.getThumbprint(cert);
String mappedCertThumbprint = _certMap.get(thumbprint);
if(mappedCertThumbprint == null)
{
// Check if we've already mapped this public key from a KeyValue
PublicKey mappedPk = getMappedPublicKey(cert.getPublicKey());
PrivateKey privKey;
if(mappedPk == null)
{
PublicKey pk = cert.getPublicKey();
String algo = pk.getAlgorithm();
KeyPair kp;
if(algo.equals("RSA")) {
kp = KeyStoreManager.getRSAKeyPair();
}
else if(algo.equals("DSA")) {
kp = KeyStoreManager.getDSAKeyPair();
}
else
{
throw new InvalidKeyException("Key algorithm " + algo + " not supported.");
}
mappedPk = kp.getPublic();
privKey = kp.getPrivate();
mapPublicKeys(cert.getPublicKey(), mappedPk);
}
else
{
privKey = getPrivateKey(mappedPk);
}
X509Certificate replacementCert =
CertificateCreator.mitmDuplicateCertificate(
cert,
mappedPk,
getSigningCert(),
getSigningPrivateKey());
addCertAndPrivateKey(replacementCert, privKey);
mappedCertThumbprint = ThumbprintUtil.getThumbprint(replacementCert);
_certMap.put(thumbprint, mappedCertThumbprint);
_certMap.put(mappedCertThumbprint, thumbprint);
_subjectMap.put(replacementCert.getSubjectX500Principal().getName(), thumbprint);
if(persistImmediately) {
persist();
}
return replacementCert;
}
else
{
return getCertificateByAlias(mappedCertThumbprint);
}
}
/**
* This method returns the mapped certificate for a hostname, or generates a "standard"
* SSL server certificate issued by the CA to the supplied subject if no mapping has been
* created. This is not a true duplication, just a shortcut method
* that is adequate for web browsers.
*
* @param hostname
* @return
* @throws CertificateParsingException
* @throws InvalidKeyException
* @throws CertificateExpiredException
* @throws CertificateNotYetValidException
* @throws SignatureException
* @throws CertificateException
* @throws NoSuchAlgorithmException
* @throws NoSuchProviderException
* @throws KeyStoreException
* @throws UnrecoverableKeyException
*/
public static X509Certificate getMappedCertificateForHostname(String hostname) throws CertificateParsingException, InvalidKeyException, CertificateExpiredException, CertificateNotYetValidException, SignatureException, CertificateException, NoSuchAlgorithmException, NoSuchProviderException, KeyStoreException, UnrecoverableKeyException
{
String subject = getSubjectForHostname(hostname);
String thumbprint = _subjectMap.get(subject);
if(thumbprint == null) {
KeyPair kp = KeyStoreManager.getRSAKeyPair();
X509Certificate newCert = CertificateCreator.generateStdSSLServerCertificate(kp.getPublic(),
getSigningCert(),
getSigningPrivateKey(),
subject);
addCertAndPrivateKey(newCert, kp.getPrivate());
thumbprint = ThumbprintUtil.getThumbprint(newCert);
_subjectMap.put(subject, thumbprint);
if(persistImmediately) {
persist();
}
return newCert;
}
else {
return getCertificateByAlias(thumbprint);
}
}
private static String getSubjectForHostname(String hostname) {
//String subject = "C=USA, ST=WA, L=Seattle, O=Cybervillains, OU=CertificationAutority, CN=" + hostname + ", EmailAddress=evilRoot@cybervillains.com";
String subject = "CN=" + hostname + ", OU=Test, O=CyberVillainsCA, L=Seattle, S=Washington, C=US";
return subject;
}
private synchronized static void persistCertMap() {
try {
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(CERTMAP_SER_FILE));
out.writeObject(_certMap);
out.flush();
out.close();
} catch (FileNotFoundException e) {
// writing, this shouldn't happen...
e.printStackTrace();
} catch (IOException e) {
// big problem!
e.printStackTrace();
throw new Error(e);
}
}
private synchronized static void persistSubjectMap() {
try {
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(SUBJMAP_SER_FILE));
out.writeObject(_subjectMap);
out.flush();
out.close();
} catch (FileNotFoundException e) {
// writing, this shouldn't happen...
e.printStackTrace();
} catch (IOException e) {
// big problem!
e.printStackTrace();
throw new Error(e);
}
}
/**
* For a cert we have generated, return the private key.
* @param cert
* @return
* @throws CertificateEncodingException
* @throws KeyStoreException
* @throws UnrecoverableKeyException
* @throws NoSuchAlgorithmException
*/
public synchronized static PrivateKey getPrivateKeyForLocalCert(final X509Certificate cert)
throws CertificateEncodingException, KeyStoreException, UnrecoverableKeyException,
NoSuchAlgorithmException
{
String thumbprint = ThumbprintUtil.getThumbprint(cert);
return (PrivateKey)_ks.getKey(thumbprint, _keypassword);
}
/**
* Generate an RSA Key Pair
* @return
*/
public static KeyPair getRSAKeyPair()
{
KeyPair kp = _rsaKpg.generateKeyPair();
rememberKeyPair(kp);
return kp;
}
/**
* Generate a DSA Key Pair
* @return
*/
public static KeyPair getDSAKeyPair()
{
KeyPair kp = _dsaKpg.generateKeyPair();
rememberKeyPair(kp);
return kp;
}
private synchronized static void persistPublicKeyMap() {
try {
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(PUB_KEYMAP_SER_FILE));
out.writeObject(_mappedPublicKeys);
out.flush();
out.close();
} catch (FileNotFoundException e) {
// writing, won't happen
e.printStackTrace();
} catch (IOException e) {
// very bad
e.printStackTrace();
throw new Error(e);
}
}
private synchronized static void persistKeyPairMap() {
try {
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(KEYMAP_SER_FILE));
out.writeObject(_rememberedPrivateKeys);
out.flush();
out.close();
} catch (FileNotFoundException e) {
// writing, won't happen.
e.printStackTrace();
} catch (IOException e) {
// very bad
e.printStackTrace();
throw new Error(e);
}
}
private synchronized static void rememberKeyPair(final KeyPair kp)
{
_rememberedPrivateKeys.put(kp.getPublic(), kp.getPrivate());
if(persistImmediately) { persistKeyPairMap(); }
}
/**
* Stores a public key mapping.
* @param original
* @param substitute
*/
public synchronized static void mapPublicKeys(final PublicKey original, final PublicKey substitute)
{
_mappedPublicKeys.put(original, substitute);
if(persistImmediately) { persistPublicKeyMap(); }
}
/**
* If we get a KeyValue with a given public key, then
* later see an X509Data with the same public key, we shouldn't split this
* in our MITM impl. So when creating a new cert, we should check if we've already
* assigned a substitute key and re-use it, and vice-versa.
* @param pk
* @return
*/
public synchronized static PublicKey getMappedPublicKey(final PublicKey original)
{
return _mappedPublicKeys.get(original);
}
/**
* Returns the private key for a public key we have generated.
* @param pk
* @return
*/
public synchronized static PrivateKey getPrivateKey(final PublicKey pk)
{
return _rememberedPrivateKeys.get(pk);
}
public static void initKeystore() throws Error {
try {
_ks = KeyStore.getInstance("JKS");
reloadKeystore();
} catch (FileNotFoundException fnfe) {
try {
createKeystore();
} catch (Exception e) {
throw new Error(e);
}
} catch (Exception e) {
throw new Error(e);
}
}
}