package org.browsermob.proxy.selenium; import org.apache.commons.logging.Log; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.browsermob.proxy.jetty.log.LogFactory; import java.io.*; import java.security.*; import java.security.cert.*; import java.util.HashMap; /** * 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. * * In a special exception, Selenium/OpenQA is allowed to use this code under the Apache License 2.0. * * @author Brad Hill * */ public class KeyStoreManager { static Log log = LogFactory.getLog(KeyStoreManager.class); private final String CERTMAP_SER_FILE = "certmap.ser"; private final String SUBJMAP_SER_FILE = "subjmap.ser"; private final String EXPORTED_CERT_NAME = "cybervillainsCA.cer"; private final char[] _keypassword = "password".toCharArray(); private final char[] _keystorepass = "password".toCharArray(); private final String _caPrivateKeystore = "cybervillainsCA.jks"; private final String _caCertAlias = "signingCert"; public static final String _caPrivKeyAlias = "signingCertPrivKey"; X509Certificate _caCert; PrivateKey _caPrivKey; KeyStore _ks; private HashMap<PublicKey, PrivateKey> _rememberedPrivateKeys; private HashMap<PublicKey, PublicKey> _mappedPublicKeys; private HashMap<String, String> _certMap; private HashMap<String, String> _subjectMap; private final String KEYMAP_SER_FILE = "keymap.ser"; private final String PUB_KEYMAP_SER_FILE = "pubkeymap.ser"; public final String RSA_KEYGEN_ALGO = "RSA"; public final String DSA_KEYGEN_ALGO = "DSA"; public final KeyPairGenerator _rsaKpg; public final KeyPairGenerator _dsaKpg; private SecureRandom _sr; private boolean persistImmediately = true; private File root; @SuppressWarnings("unchecked") public KeyStoreManager(File root) { this.root = root; Security.insertProviderAt(new BouncyCastleProvider(), 2); _sr = new SecureRandom(); 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(root, 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(root, 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); 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); } try { File file = new File(root, 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(root, 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 void reloadKeystore() throws FileNotFoundException, IOException, NoSuchAlgorithmException, CertificateException, KeyStoreException, UnrecoverableKeyException { InputStream is = new FileInputStream(new File(root, _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 void createKeystore() { java.security.cert.Certificate signingCert = null; PrivateKey caPrivKey = null; if(_caCert == null || _caPrivKey == null) { try { log.debug("Keystore or signing cert & keypair not found. Generating..."); KeyPair caKeypair = getRSAKeyPair(); caPrivKey = caKeypair.getPrivate(); signingCert = CertificateCreator.createTypicalMasterCert(caKeypair); log.debug("Done generating signing cert"); log.debug(signingCert); _ks.load(null, _keystorepass); _ks.setCertificateEntry(_caCertAlias, signingCert); _ks.setKeyEntry(_caPrivKeyAlias, caPrivKey, _keypassword, new java.security.cert.Certificate[] {signingCert}); File caKsFile = new File(root, _caPrivateKeystore); OutputStream os = new FileOutputStream(caKsFile); _ks.store(os, _keystorepass); log.debug("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(root, EXPORTED_CERT_NAME); FileOutputStream cerOut = new FileOutputStream(signingCertFile); byte[] buf = signingCert.getEncoded(); log.debug("Wrote signing cert to: " + signingCertFile.getAbsolutePath()); cerOut.write(buf); cerOut.flush(); cerOut.close(); _caCert = (X509Certificate)signingCert; _caPrivKey = caPrivKey; } catch(Exception e) { log.error("Fatal error creating/storing keystore or signing cert.", e); throw new Error(e); } } else { log.debug("Successfully loaded keystore."); log.debug(_caCert); } } /** * Stores a new certificate and its associated private key in the keystore. * @param hostname *@param cert * @param privKey @throws KeyStoreException * @throws CertificateException * @throws NoSuchAlgorithmException */ public synchronized void addCertAndPrivateKey(String hostname, final X509Certificate cert, final PrivateKey privKey) throws KeyStoreException, CertificateException, NoSuchAlgorithmException { // String alias = ThumbprintUtil.getThumbprint(cert); _ks.deleteEntry(hostname); _ks.setCertificateEntry(hostname, cert); _ks.setKeyEntry(hostname, privKey, _keypassword, new java.security.cert.Certificate[] {cert}); if(persistImmediately) { persist(); } } /** * Writes the keystore and certificate/keypair mappings to disk. * @throws KeyStoreException * @throws NoSuchAlgorithmException * @throws CertificateException */ public synchronized void persist() throws KeyStoreException, NoSuchAlgorithmException, CertificateException { try { FileOutputStream kso = new FileOutputStream(new File(root, _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 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 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); } return getMappedCertificateForHostname(hostname); } /** * Gets the authority root signing cert. * @return * @throws KeyStoreException */ @SuppressWarnings("unused") public synchronized X509Certificate getSigningCert() throws KeyStoreException { return _caCert; } /** * Gets the authority private signing key. * @return * @throws KeyStoreException * @throws NoSuchAlgorithmException * @throws UnrecoverableKeyException */ @SuppressWarnings("unused") public synchronized PrivateKey getSigningPrivateKey() throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { return _caPrivKey; } /** * Whether updates are immediately written to disk. * @return */ public boolean getPersistImmediately() { return persistImmediately; } /** * Whether updates are immediately written to disk. * @param persistImmediately */ public void setPersistImmediately(final boolean persistImmediately) { this.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 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 = getRSAKeyPair(); } else if(algo.equals("DSA")) { kp = 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(null, 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; } 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 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 = getRSAKeyPair(); X509Certificate newCert = CertificateCreator.generateStdSSLServerCertificate(kp.getPublic(), getSigningCert(), getSigningPrivateKey(), subject); addCertAndPrivateKey(hostname, newCert, kp.getPrivate()); thumbprint = ThumbprintUtil.getThumbprint(newCert); _subjectMap.put(subject, thumbprint); if(persistImmediately) { persist(); } return newCert; } return getCertificateByAlias(thumbprint); } private 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 void persistCertMap() { try { ObjectOutput out = new ObjectOutputStream(new FileOutputStream(new File(root, 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 void persistSubjectMap() { try { ObjectOutput out = new ObjectOutputStream(new FileOutputStream(new File(root, 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 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 KeyPair getRSAKeyPair() { KeyPair kp = _rsaKpg.generateKeyPair(); rememberKeyPair(kp); return kp; } /** * Generate a DSA Key Pair * @return */ public KeyPair getDSAKeyPair() { KeyPair kp = _dsaKpg.generateKeyPair(); rememberKeyPair(kp); return kp; } private synchronized void persistPublicKeyMap() { try { ObjectOutput out = new ObjectOutputStream(new FileOutputStream(new File(root, 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 void persistKeyPairMap() { try { ObjectOutput out = new ObjectOutputStream(new FileOutputStream(new File(root, 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 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 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 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 PrivateKey getPrivateKey(final PublicKey pk) { return _rememberedPrivateKeys.get(pk); } public KeyStore getKeyStore() { return _ks; } }