package net.i2p.sam; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; import java.security.GeneralSecurityException; import java.util.Properties; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLContext; import net.i2p.I2PAppContext; import net.i2p.crypto.KeyStoreUtil; import net.i2p.util.Log; import net.i2p.util.SecureDirectory; /** * Utilities for SAM SSL server sockets. * * @since 0.9.24 adopted from net.i2p.i2ptunnel.SSLClientUtil */ class SSLUtil { private static final String PROP_KEYSTORE_PASSWORD = "sam.keystorePassword"; private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit"; private static final String PROP_KEY_PASSWORD = "sam.keyPassword"; private static final String PROP_KEY_ALIAS = "sam.keyAlias"; private static final String ASCII_KEYFILE_SUFFIX = ".local.crt"; private static final String PROP_KS_NAME = "sam.keystoreFile"; private static final String KS_DIR = "keystore"; private static final String PREFIX = "sam-"; private static final String KS_SUFFIX = ".ks"; private static final String CERT_DIR = "certificates/sam"; /** * Create a new selfsigned cert and keystore and pubkey cert if they don't exist. * May take a while. * * @param opts in/out, updated if rv is true * @return false if it already exists; if true, caller must save opts * @throws IOException on creation fail */ public static boolean verifyKeyStore(Properties opts) throws IOException { String name = opts.getProperty(PROP_KEY_ALIAS); if (name == null) { name = KeyStoreUtil.randomString(); opts.setProperty(PROP_KEY_ALIAS, name); } String ksname = opts.getProperty(PROP_KS_NAME); if (ksname == null) { ksname = PREFIX + name + KS_SUFFIX; opts.setProperty(PROP_KS_NAME, ksname); } File ks = new File(ksname); if (!ks.isAbsolute()) { ks = new File(I2PAppContext.getGlobalContext().getConfigDir(), KS_DIR); ks = new File(ks, ksname); } if (ks.exists()) return false; File dir = ks.getParentFile(); if (!dir.exists()) { File sdir = new SecureDirectory(dir.getAbsolutePath()); if (!sdir.mkdirs()) throw new IOException("Unable to create keystore " + ks); } boolean rv = createKeyStore(ks, name, opts); if (!rv) throw new IOException("Unable to create keystore " + ks); // Now read it back out of the new keystore and save it in ascii form // where the clients can get to it. // Failure of this part is not fatal. exportCert(ks, name, opts); return true; } /** * Call out to keytool to create a new keystore with a keypair in it. * * @param name used in CNAME * @param opts in/out, updated if rv is true, must contain PROP_KEY_ALIAS * @return success, if true, opts will have password properties added to be saved */ private static boolean createKeyStore(File ks, String name, Properties opts) { // make a random 48 character password (30 * 8 / 5) String keyPassword = KeyStoreUtil.randomString(); // and one for the cname String cname = name + ".sam.i2p.net"; String keyName = opts.getProperty(PROP_KEY_ALIAS); boolean success = KeyStoreUtil.createKeys(ks, keyName, cname, "SAM", keyPassword); if (success) { success = ks.exists(); if (success) { opts.setProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); opts.setProperty(PROP_KEY_PASSWORD, keyPassword); } } if (success) { logAlways("Created self-signed certificate for " + cname + " in keystore: " + ks.getAbsolutePath() + "\n" + "The certificate name was generated randomly, and is not associated with your " + "IP address, host name, router identity, or destination keys."); } else { error("Failed to create SAM SSL keystore.\n" + "If you create the keystore manually, you must add " + PROP_KEYSTORE_PASSWORD + " and " + PROP_KEY_PASSWORD + " to " + (new File(I2PAppContext.getGlobalContext().getConfigDir(), SAMBridge.DEFAULT_SAM_CONFIGFILE)).getAbsolutePath()); } return success; } /** * Pull the cert back OUT of the keystore and save it as ascii * so the clients can get to it. * * @param name used to generate output file name * @param opts must contain PROP_KEY_ALIAS */ private static void exportCert(File ks, String name, Properties opts) { File sdir = new SecureDirectory(I2PAppContext.getGlobalContext().getConfigDir(), CERT_DIR); if (sdir.exists() || sdir.mkdirs()) { String keyAlias = opts.getProperty(PROP_KEY_ALIAS); String ksPass = opts.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); File out = new File(sdir, PREFIX + name + ASCII_KEYFILE_SUFFIX); boolean success = KeyStoreUtil.exportCert(ks, ksPass, keyAlias, out); if (!success) error("Error getting SSL cert to save as ASCII"); } else { error("Error saving ASCII SSL keys"); } } /** * Sets up the SSLContext and sets the socket factory. * No option prefix allowed. * * @throws IOException GeneralSecurityExceptions are wrapped in IOE for convenience * @return factory, throws on all errors */ public static SSLServerSocketFactory initializeFactory(Properties opts) throws IOException { String ksPass = opts.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); String keyPass = opts.getProperty(PROP_KEY_PASSWORD); if (keyPass == null) { throw new IOException("No key password, set " + PROP_KEY_PASSWORD + " in " + (new File(I2PAppContext.getGlobalContext().getConfigDir(), SAMBridge.DEFAULT_SAM_CONFIGFILE)).getAbsolutePath()); } String ksname = opts.getProperty(PROP_KS_NAME); if (ksname == null) { throw new IOException("No keystore, set " + PROP_KS_NAME + " in " + (new File(I2PAppContext.getGlobalContext().getConfigDir(), SAMBridge.DEFAULT_SAM_CONFIGFILE)).getAbsolutePath()); } File ks = new File(ksname); if (!ks.isAbsolute()) { ks = new File(I2PAppContext.getGlobalContext().getConfigDir(), KS_DIR); ks = new File(ks, ksname); } InputStream fis = null; try { SSLContext sslc = SSLContext.getInstance("TLS"); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); fis = new FileInputStream(ks); keyStore.load(fis, ksPass.toCharArray()); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, keyPass.toCharArray()); sslc.init(kmf.getKeyManagers(), null, I2PAppContext.getGlobalContext().random()); return sslc.getServerSocketFactory(); } catch (GeneralSecurityException gse) { IOException ioe = new IOException("keystore error"); ioe.initCause(gse); throw ioe; } finally { if (fis != null) try { fis.close(); } catch (IOException ioe) {} } } private static void error(String s) { I2PAppContext.getGlobalContext().logManager().getLog(SSLUtil.class).error(s); } private static void logAlways(String s) { I2PAppContext.getGlobalContext().logManager().getLog(SSLUtil.class).logAlways(Log.INFO, s); } }