From 945e7b75fd842104d02f64c8930b7c93468933cd Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 12 Sep 2013 14:27:16 +0000 Subject: [PATCH] Crypto - prep for using certificates in SU3File: Consolidate KeyStore code from SSLEepGet, I2CPSSLSocketFactory, SSLClientListenerRunner, and RouterConsoleRunner into new KeyStoreUtil and CertUtil classes in net.i2p.crypto (ticket #744) --- .../i2p/router/web/RouterConsoleRunner.java | 42 +- .../net/i2p/client/I2CPSSLSocketFactory.java | 86 +--- core/java/src/net/i2p/crypto/CertUtil.java | 80 ++++ .../java/src/net/i2p/crypto/KeyStoreUtil.java | 399 ++++++++++++++++++ core/java/src/net/i2p/util/SSLEepGet.java | 205 +-------- .../client/SSLClientListenerRunner.java | 93 +--- 6 files changed, 512 insertions(+), 393 deletions(-) create mode 100644 core/java/src/net/i2p/crypto/CertUtil.java create mode 100644 core/java/src/net/i2p/crypto/KeyStoreUtil.java diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java index 627254c89..b2be30a8f 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java @@ -27,6 +27,7 @@ import net.i2p.app.ClientAppManager; import net.i2p.app.ClientAppState; import static net.i2p.app.ClientAppState.*; import net.i2p.apps.systray.SysTray; +import net.i2p.crypto.KeyStoreUtil; import net.i2p.data.Base32; import net.i2p.data.DataHelper; import net.i2p.jetty.I2PLogger; @@ -38,8 +39,6 @@ import net.i2p.util.FileUtil; import net.i2p.util.I2PAppThread; import net.i2p.util.PortMapper; import net.i2p.util.SecureDirectory; -import net.i2p.util.SecureFileOutputStream; -import net.i2p.util.ShellCommand; import net.i2p.util.SystemVersion; import net.i2p.util.VersionComparator; @@ -675,12 +674,6 @@ public class RouterConsoleRunner implements RouterApp { System.err.println("Console SSL error, must set " + PROP_KEY_PASSWORD + " in " + (new File(_context.getConfigDir(), "router.config")).getAbsolutePath()); return rv; } - File dir = ks.getParentFile(); - if (!dir.exists()) { - File sdir = new SecureDirectory(dir.getAbsolutePath()); - if (!sdir.mkdir()) - return false; - } return createKeyStore(ks); } @@ -695,31 +688,13 @@ public class RouterConsoleRunner implements RouterApp { */ private boolean createKeyStore(File ks) { // make a random 48 character password (30 * 8 / 5) - byte[] rand = new byte[30]; - _context.random().nextBytes(rand); - String keyPassword = Base32.encode(rand); + String keyPassword = KeyStoreUtil.randomString(); // and one for the cname - _context.random().nextBytes(rand); - String cname = Base32.encode(rand) + ".console.i2p.net"; - - String keytool = (new File(System.getProperty("java.home"), "bin/keytool")).getAbsolutePath(); - String[] args = new String[] { - keytool, - "-genkey", // -genkeypair preferred in newer keytools, but this works with more - "-storetype", KeyStore.getDefaultType(), - "-keystore", ks.getAbsolutePath(), - "-storepass", DEFAULT_KEYSTORE_PASSWORD, - "-alias", "console", - "-dname", "CN=" + cname + ",OU=Console,O=I2P Anonymous Network,L=XX,ST=XX,C=XX", - "-validity", "3652", // 10 years - "-keyalg", "DSA", - "-keysize", "1024", - "-keypass", keyPassword}; - boolean success = (new ShellCommand()).executeSilentAndWaitTimed(args, 30); // 30 secs + String cname = KeyStoreUtil.randomString() + ".console.i2p.net"; + boolean success = KeyStoreUtil.createKeys(ks, "console", cname, "Console", keyPassword); if (success) { success = ks.exists(); if (success) { - SecureFileOutputStream.setPerms(ks); try { Map changes = new HashMap(); changes.put(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); @@ -733,13 +708,8 @@ public class RouterConsoleRunner implements RouterApp { "The certificate name was generated randomly, and is not associated with your " + "IP address, host name, router identity, or destination keys."); } else { - System.err.println("Failed to create console SSL keystore using command line:"); - StringBuilder buf = new StringBuilder(256); - for (int i = 0; i < args.length; i++) { - buf.append('"').append(args[i]).append("\" "); - } - System.err.println(buf.toString()); - System.err.println("This is for the Sun/Oracle keytool, others may be incompatible.\n" + + System.err.println("Failed to create console SSL keystore.\n" + + "This is for the Sun/Oracle keytool, others may be incompatible.\n" + "If you create the keystore manually, you must add " + PROP_KEYSTORE_PASSWORD + " and " + PROP_KEY_PASSWORD + " to " + (new File(_context.getConfigDir(), "router.config")).getAbsolutePath()); } diff --git a/core/java/src/net/i2p/client/I2CPSSLSocketFactory.java b/core/java/src/net/i2p/client/I2CPSSLSocketFactory.java index 8eb8422b9..d6bba72a5 100644 --- a/core/java/src/net/i2p/client/I2CPSSLSocketFactory.java +++ b/core/java/src/net/i2p/client/I2CPSSLSocketFactory.java @@ -7,17 +7,13 @@ import java.io.InputStream; import java.net.Socket; import java.security.KeyStore; import java.security.GeneralSecurityException; -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Locale; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManagerFactory; import net.i2p.I2PAppContext; +import net.i2p.crypto.KeyStoreUtil; import net.i2p.util.Log; /** @@ -71,14 +67,14 @@ class I2CPSSLSocketFactory { } File dir = new File(context.getConfigDir(), CERT_DIR); - int adds = addCerts(dir, ks); + int adds = KeyStoreUtil.addCerts(dir, ks); int totalAdds = adds; if (adds > 0) info(context, "Loaded " + adds + " trusted certificates from " + dir.getAbsolutePath()); File dir2 = new File(System.getProperty("user.dir"), CERT_DIR); if (!dir.getAbsolutePath().equals(dir2.getAbsolutePath())) { - adds = addCerts(dir2, ks); + adds = KeyStoreUtil.addCerts(dir2, ks); totalAdds += adds; if (adds > 0) info(context, "Loaded " + adds + " trusted certificates from " + dir.getAbsolutePath()); @@ -105,82 +101,6 @@ class I2CPSSLSocketFactory { } } - /** - * Load all X509 Certs from a directory and add them to the - * trusted set of certificates in the key store - * - * @return number successfully added - */ - private static int addCerts(File dir, KeyStore ks) { - info("Looking for X509 Certificates in " + dir.getAbsolutePath()); - int added = 0; - if (dir.exists() && dir.isDirectory()) { - File[] files = dir.listFiles(); - if (files != null) { - for (int i = 0; i < files.length; i++) { - File f = files[i]; - if (!f.isFile()) - continue; - // use file name as alias - String alias = f.getName().toLowerCase(Locale.US); - boolean success = addCert(f, alias, ks); - if (success) - added++; - } - } - } - return added; - } - - /** - * Load an X509 Cert from a file and add it to the - * trusted set of certificates in the key store - * - * @return success - */ - private static boolean addCert(File file, String alias, KeyStore ks) { - InputStream fis = null; - try { - fis = new FileInputStream(file); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - X509Certificate cert = (X509Certificate)cf.generateCertificate(fis); - info("Read X509 Certificate from " + file.getAbsolutePath() + - " Issuer: " + cert.getIssuerX500Principal() + - "; Valid From: " + cert.getNotBefore() + - " To: " + cert.getNotAfter()); - try { - cert.checkValidity(); - } catch (CertificateExpiredException cee) { - error("Rejecting expired X509 Certificate: " + file.getAbsolutePath(), cee); - return false; - } catch (CertificateNotYetValidException cnyve) { - error("Rejecting X509 Certificate not yet valid: " + file.getAbsolutePath(), cnyve); - return false; - } - ks.setCertificateEntry(alias, cert); - info("Now trusting X509 Certificate, Issuer: " + cert.getIssuerX500Principal()); - } catch (GeneralSecurityException gse) { - error("Error reading X509 Certificate: " + file.getAbsolutePath(), gse); - return false; - } catch (IOException ioe) { - error("Error reading X509 Certificate: " + file.getAbsolutePath(), ioe); - return false; - } finally { - try { if (fis != null) fis.close(); } catch (IOException foo) {} - } - return true; - } - - /** @since 0.9.8 */ - private static void info(String msg) { - log(I2PAppContext.getGlobalContext(), Log.INFO, msg, null); - } - - /** @since 0.9.8 */ - private static void error(String msg, Throwable t) { - log(I2PAppContext.getGlobalContext(), Log.ERROR, msg, t); - } - /** @since 0.9.8 */ private static void info(I2PAppContext ctx, String msg) { log(ctx, Log.INFO, msg, null); diff --git a/core/java/src/net/i2p/crypto/CertUtil.java b/core/java/src/net/i2p/crypto/CertUtil.java new file mode 100644 index 000000000..464ec6d1d --- /dev/null +++ b/core/java/src/net/i2p/crypto/CertUtil.java @@ -0,0 +1,80 @@ +package net.i2p.crypto; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import net.i2p.I2PAppContext; +import net.i2p.data.Base64; +import net.i2p.util.Log; +import net.i2p.util.SecureFileOutputStream; + +/** + * Java X.509 certificate utilities, consolidated from various places. + * + * @since 0.9.9 + */ +public class CertUtil { + + private static final int LINE_LENGTH = 64; + + /** + * Modified from: + * http://www.exampledepot.com/egs/java.security.cert/ExportCert.html + * + * This method writes a certificate to a file in base64 format. + * + * @return success + * @since 0.8.2, moved from SSLEepGet in 0.9.9 + */ + public static boolean saveCert(Certificate cert, File file) { + OutputStream os = null; + try { + // Get the encoded form which is suitable for exporting + byte[] buf = cert.getEncoded(); + os = new SecureFileOutputStream(file); + PrintWriter wr = new PrintWriter(os); + wr.println("-----BEGIN CERTIFICATE-----"); + String b64 = Base64.encode(buf, true); // true = use standard alphabet + for (int i = 0; i < b64.length(); i += LINE_LENGTH) { + wr.println(b64.substring(i, Math.min(i + LINE_LENGTH, b64.length()))); + } + wr.println("-----END CERTIFICATE-----"); + wr.flush(); + return true; + } catch (CertificateEncodingException cee) { + error("Error writing X509 Certificate " + file.getAbsolutePath(), cee); + return false; + } catch (IOException ioe) { + error("Error writing X509 Certificate " + file.getAbsolutePath(), ioe); + return false; + } finally { + try { if (os != null) os.close(); } catch (IOException foo) {} + } + } + + private static void error(String msg, Throwable t) { + log(I2PAppContext.getGlobalContext(), Log.ERROR, msg, t); + } + + private static void error(I2PAppContext ctx, String msg, Throwable t) { + log(ctx, Log.ERROR, msg, t); + } + + private static void log(I2PAppContext ctx, int level, String msg, Throwable t) { + Log l = ctx.logManager().getLog(CertUtil.class); + l.log(level, msg, t); + } +} diff --git a/core/java/src/net/i2p/crypto/KeyStoreUtil.java b/core/java/src/net/i2p/crypto/KeyStoreUtil.java new file mode 100644 index 000000000..5f8771f47 --- /dev/null +++ b/core/java/src/net/i2p/crypto/KeyStoreUtil.java @@ -0,0 +1,399 @@ +package net.i2p.crypto; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.Locale; + +import net.i2p.I2PAppContext; +import net.i2p.data.Base32; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; +import net.i2p.util.SecureDirectory; +import net.i2p.util.SecureFileOutputStream; +import net.i2p.util.ShellCommand; +import net.i2p.util.SystemVersion; + +/** + * Keystore utilities, consolidated from various places. + * + * @since 0.9.9 + */ +public class KeyStoreUtil { + + public static final String DEFAULT_KEYSTORE_PASSWORD = "changeit"; + private static final String DEFAULT_KEY_ALGORITHM = "DSA"; + private static final int DEFAULT_KEY_SIZE = 1024; + private static final int DEFAULT_KEY_VALID_DAYS = 3562; + + /** + * Create a new KeyStore object, and load it from ksFile if it is + * non-null and it exists. + * If ksFile is non-null and it does not exist, create a new empty + * keystore file. + * + * @param ksFile may be null + * @param password may be null + * @return success + */ + public static KeyStore createKeyStore(File ksFile, String password) + throws GeneralSecurityException, IOException { + boolean exists = ksFile != null && ksFile.exists(); + char[] pwchars = password != null ? password.toCharArray() : null; + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + if (exists) { + InputStream fis = new FileInputStream(ksFile); + ks.load(fis, pwchars); + } + if (ksFile != null && !exists) + ks.store(new SecureFileOutputStream(ksFile), pwchars); + return ks; + } + + /** + * Loads certs from location of javax.net.ssl.keyStore property, + * else from $JAVA_HOME/lib/security/jssacacerts, + * else from $JAVA_HOME/lib/security/cacerts. + * + * @return null on catastrophic failure, returns empty KeyStore if can't load system file + * @since 0.8.2, moved from SSLEepGet.initSSLContext() in 0.9.9 + */ + public static KeyStore loadSystemKeyStore() { + KeyStore ks; + try { + ks = KeyStore.getInstance(KeyStore.getDefaultType()); + } catch (GeneralSecurityException gse) { + error("Key Store init error", gse); + return null; + } + boolean success = false; + String override = System.getProperty("javax.net.ssl.keyStore"); + if (override != null) + success = loadCerts(new File(override), ks); + if (!success) { + if (SystemVersion.isAndroid()) { + // thru API 13. As of API 14 (ICS), the file is gone, but + // ks.load(null, pw) will bring in the default certs? + success = loadCerts(new File(System.getProperty("java.home"), "etc/security/cacerts.bks"), ks); + } else { + success = loadCerts(new File(System.getProperty("java.home"), "lib/security/jssecacerts"), ks); + if (!success) + success = loadCerts(new File(System.getProperty("java.home"), "lib/security/cacerts"), ks); + } + } + + if (!success) { + try { + // must be initted + ks.load(null, DEFAULT_KEYSTORE_PASSWORD.toCharArray()); + } catch (Exception e) {} + error("All key store loads failed, will only load local certificates", null); + } + return ks; + } + + /** + * Load all X509 Certs from a key store File into a KeyStore + * Note that each call reinitializes the KeyStore + * + * @return success + * @since 0.8.2, moved from SSLEepGet in 0.9.9 + */ + private static boolean loadCerts(File file, KeyStore ks) { + if (!file.exists()) + return false; + InputStream fis = null; + try { + fis = new FileInputStream(file); + // "changeit" is the default password + ks.load(fis, DEFAULT_KEYSTORE_PASSWORD.toCharArray()); + info("Certs loaded from " + file); + } catch (GeneralSecurityException gse) { + error("KeyStore load error, no default keys: " + file.getAbsolutePath(), gse); + try { + // not clear if null is allowed for password + ks.load(null, DEFAULT_KEYSTORE_PASSWORD.toCharArray()); + } catch (Exception foo) {} + return false; + } catch (IOException ioe) { + error("KeyStore load error, no default keys: " + file.getAbsolutePath(), ioe); + try { + ks.load(null, DEFAULT_KEYSTORE_PASSWORD.toCharArray()); + } catch (Exception foo) {} + return false; + } finally { + try { if (fis != null) fis.close(); } catch (IOException foo) {} + } + return true; + } + + + /** + * Count all X509 Certs in a key store + * + * @return number successfully added + * @since 0.8.2, moved from SSLEepGet in 0.9.9 + */ + public static int countCerts(KeyStore ks) { + int count = 0; + try { + for(Enumeration e = ks.aliases(); e.hasMoreElements();) { + String alias = e.nextElement(); + if (ks.isCertificateEntry(alias)) { + info("Found cert " + alias); + count++; + } + } + } catch (Exception foo) {} + return count; + } + + /** + * Load all X509 Certs from a directory and add them to the + * trusted set of certificates in the key store + * + * @return number successfully added + * @since 0.8.2, moved from SSLEepGet in 0.9.9 + */ + public static int addCerts(File dir, KeyStore ks) { + info("Looking for X509 Certificates in " + dir.getAbsolutePath()); + int added = 0; + if (dir.exists() && dir.isDirectory()) { + File[] files = dir.listFiles(); + if (files != null) { + for (int i = 0; i < files.length; i++) { + File f = files[i]; + if (!f.isFile()) + continue; + // use file name as alias + // https://www.sslshopper.com/ssl-converter.html + // No idea if all these formats can actually be read by CertificateFactory + String alias = f.getName().toLowerCase(Locale.US); + if (alias.endsWith(".crt") || alias.endsWith(".pem") || alias.endsWith(".key") || + alias.endsWith(".der") || alias.endsWith(".key") || alias.endsWith(".p7b") || + alias.endsWith(".p7c") || alias.endsWith(".pfx") || alias.endsWith(".p12")) + alias = alias.substring(0, alias.length() - 4); + boolean success = addCert(f, alias, ks); + if (success) + added++; + } + } + } + return added; + } + + /** + * Load an X509 Cert from a file and add it to the + * trusted set of certificates in the key store + * + * @return success + * @since 0.8.2, moved from SSLEepGet in 0.9.9 + */ + public static boolean addCert(File file, String alias, KeyStore ks) { + InputStream fis = null; + try { + fis = new FileInputStream(file); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate)cf.generateCertificate(fis); + info("Read X509 Certificate from " + file.getAbsolutePath() + + " Issuer: " + cert.getIssuerX500Principal() + + "; Valid From: " + cert.getNotBefore() + + " To: " + cert.getNotAfter()); + try { + cert.checkValidity(); + } catch (CertificateExpiredException cee) { + error("Rejecting expired X509 Certificate: " + file.getAbsolutePath(), cee); + return false; + } catch (CertificateNotYetValidException cnyve) { + error("Rejecting X509 Certificate not yet valid: " + file.getAbsolutePath(), cnyve); + return false; + } + ks.setCertificateEntry(alias, cert); + info("Now trusting X509 Certificate, Issuer: " + cert.getIssuerX500Principal()); + } catch (GeneralSecurityException gse) { + error("Error reading X509 Certificate: " + file.getAbsolutePath(), gse); + return false; + } catch (IOException ioe) { + error("Error reading X509 Certificate: " + file.getAbsolutePath(), ioe); + return false; + } finally { + try { if (fis != null) fis.close(); } catch (IOException foo) {} + } + return true; + } + + /** 48 char b32 string (30 bytes of entropy) */ + public static String randomString() { + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + // make a random 48 character password (30 * 8 / 5) + byte[] rand = new byte[30]; + ctx.random().nextBytes(rand); + return Base32.encode(rand); + } + + /** + * Create a keypair and store it in the keystore at ks, creating it if necessary. + * Use default keystore password, valid days, algorithm, and key size. + * + * Warning, may take a long time. + * + * @param ks path to the keystore + * @param alias the name of the key + * @param cname e.g. randomstuff.console.i2p.net + * @param ou e.g. console + * @param keqPW the key password + * + * @return success + * @since 0.8.3, consolidated from RouterConsoleRUnner and SSLClientListenerRunner in 0.9.9 + */ + public static boolean createKeys(File ks, String alias, String cname, String ou, + String keyPW) { + return createKeys(ks, DEFAULT_KEYSTORE_PASSWORD, alias, cname, ou, + DEFAULT_KEY_VALID_DAYS, DEFAULT_KEY_ALGORITHM, DEFAULT_KEY_SIZE, keyPW); + } + + /** + * Create a keypair and store it in the keystore at ks, creating it if necessary. + * + * Warning, may take a long time. + * + * @param ks path to the keystore + * @param ksPW the keystore password + * @param alias the name of the key + * @param cname e.g. randomstuff.console.i2p.net + * @param ou e.g. console + * @param validDays e.g. 3652 (10 years) + * @param keyAlg e.g. DSA , RSA, EC + * @param keySize e.g. 1024 + * @param keqPW the key password + * + * @return success + * @since 0.8.3, consolidated from RouterConsoleRUnner and SSLClientListenerRunner in 0.9.9 + */ + public static boolean createKeys(File ks, String ksPW, String alias, String cname, String ou, + int validDays, String keyAlg, int keySize, String keyPW) { + if (!ks.exists()) { + File dir = ks.getParentFile(); + if (!dir.exists()) { + File sdir = new SecureDirectory(dir.getAbsolutePath()); + if (!sdir.mkdir()) { + error("Can't create directory " + dir, null); + return false; + } + } + } + String keytool = (new File(System.getProperty("java.home"), "bin/keytool")).getAbsolutePath(); + String[] args = new String[] { + keytool, + "-genkey", // -genkeypair preferred in newer keytools, but this works with more + "-storetype", KeyStore.getDefaultType(), + "-keystore", ks.getAbsolutePath(), + "-storepass", ksPW, + "-alias", alias, + "-dname", "CN=" + cname + ",OU=" + ou + ",O=I2P Anonymous Network,L=XX,ST=XX,C=XX", + "-validity", Integer.toString(validDays), // 10 years + "-keyalg", keyAlg, + "-keysize", "1024", + "-keypass", keyPW + }; + boolean success = (new ShellCommand()).executeSilentAndWaitTimed(args, 30); // 30 secs + if (success) { + success = ks.exists(); + } + if (success) { + SecureFileOutputStream.setPerms(ks); + info("Created self-signed certificate for " + cname + " in keystore: " + ks.getAbsolutePath()); + } else { + StringBuilder buf = new StringBuilder(256); + for (int i = 0; i < args.length; i++) { + buf.append('"').append(args[i]).append("\" "); + } + error("Failed to create SSL keystore using command line:" + buf, null); + } + return success; + } + + /** + * Pull the cert back OUT of the keystore and save it as ascii + * so the clients can get to it. + * + * @param ks path to the keystore + * @param ksPW the keystore password, may be null + * @param alias the name of the key + * @param certFile output + * @return success + * @since 0.8.3 moved from SSLClientListenerRunner in 0.9.9 + */ + public static boolean exportCert(File ks, String ksPW, String alias, File certFile) { + InputStream fis = null; + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + fis = new FileInputStream(ks); + char[] pwchars = ksPW != null ? ksPW.toCharArray() : null; + keyStore.load(fis, pwchars); + Certificate cert = keyStore.getCertificate(alias); + if (cert != null) + return CertUtil.saveCert(cert, certFile); + } catch (GeneralSecurityException gse) { + error("Error saving ASCII SSL keys", gse); + } catch (IOException ioe) { + error("Error saving ASCII SSL keys", ioe); + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + } + return false; + } + + private static void info(String msg) { + log(I2PAppContext.getGlobalContext(), Log.INFO, msg, null); + } + + private static void error(String msg, Throwable t) { + log(I2PAppContext.getGlobalContext(), Log.ERROR, msg, t); + } + + private static void info(I2PAppContext ctx, String msg) { + log(ctx, Log.INFO, msg, null); + } + + private static void error(I2PAppContext ctx, String msg, Throwable t) { + log(ctx, Log.ERROR, msg, t); + } + + private static void log(I2PAppContext ctx, int level, String msg, Throwable t) { + Log l = ctx.logManager().getLog(KeyStoreUtil.class); + l.log(level, msg, t); + } + + public static void main(String[] args) { + try { + if (args.length > 0) { + File ksf = new File(args[0]); + createKeyStore(ksf, DEFAULT_KEYSTORE_PASSWORD); + System.out.println("Created empty keystore " + ksf); + } else { + KeyStore ks = loadSystemKeyStore(); + if (ks != null) { + System.out.println("Loaded system keystore"); + int count = countCerts(ks); + System.out.println("Found " + count + " certs"); + } else { + System.out.println("FAIL"); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/core/java/src/net/i2p/util/SSLEepGet.java b/core/java/src/net/i2p/util/SSLEepGet.java index 8958a466e..839b31989 100644 --- a/core/java/src/net/i2p/util/SSLEepGet.java +++ b/core/java/src/net/i2p/util/SSLEepGet.java @@ -46,18 +46,13 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; -import java.io.PrintWriter; import java.net.URL; import java.security.KeyStore; import java.security.GeneralSecurityException; import java.security.cert.Certificate; -import java.security.cert.CertificateException; import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; -import java.security.cert.CertificateFactory; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.util.Enumeration; import java.util.Locale; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; @@ -67,7 +62,8 @@ import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import net.i2p.I2PAppContext; -import net.i2p.data.Base64; +import net.i2p.crypto.CertUtil; +import net.i2p.crypto.KeyStoreUtil; import net.i2p.data.DataHelper; /** @@ -91,8 +87,6 @@ public class SSLEepGet extends EepGet { /** may be null if init failed */ private SavingTrustManager _stm; - private static final boolean _isAndroid = SystemVersion.isAndroid(); - /** * A new SSLEepGet with a new SSLState */ @@ -184,55 +178,24 @@ public class SSLEepGet extends EepGet { * @since 0.8.2 */ private SSLContext initSSLContext() { - KeyStore ks; - try { - ks = KeyStore.getInstance(KeyStore.getDefaultType()); - } catch (GeneralSecurityException gse) { - _log.error("Key Store init error", gse); + KeyStore ks = KeyStoreUtil.loadSystemKeyStore(); + if (ks == null) { + _log.error("Key Store init error"); return null; } - boolean success = false; - String override = System.getProperty("javax.net.ssl.keyStore"); - if (override != null) - success = loadCerts(new File(override), ks); - if (!success) { - if (_isAndroid) { - // thru API 13. As of API 14 (ICS), the file is gone, but - // ks.load(null, pw) will bring in the default certs? - success = loadCerts(new File(System.getProperty("java.home"), "etc/security/cacerts.bks"), ks); - } else { - success = loadCerts(new File(System.getProperty("java.home"), "lib/security/jssecacerts"), ks); - if (!success) - success = loadCerts(new File(System.getProperty("java.home"), "lib/security/cacerts"), ks); - } - } - - if (!success) { - try { - // must be initted - ks.load(null, "changeit".toCharArray()); - } catch (Exception e) {} - _log.error("All key store loads failed, will only load local certificates"); - } else if (_log.shouldLog(Log.INFO)) { - int count = 0; - try { - for(Enumeration e = ks.aliases(); e.hasMoreElements();) { - String alias = e.nextElement(); - if (ks.isCertificateEntry(alias)) - count++; - } - } catch (Exception foo) {} + if (_log.shouldLog(Log.INFO)) { + int count = KeyStoreUtil.countCerts(ks); _log.info("Loaded " + count + " default trusted certificates"); } File dir = new File(_context.getBaseDir(), "certificates"); - int adds = addCerts(dir, ks); + int adds = KeyStoreUtil.addCerts(dir, ks); int totalAdds = adds; if (adds > 0 && _log.shouldLog(Log.INFO)) _log.info("Loaded " + adds + " trusted certificates from " + dir.getAbsolutePath()); if (!_context.getBaseDir().getAbsolutePath().equals(_context.getConfigDir().getAbsolutePath())) { dir = new File(_context.getConfigDir(), "certificates"); - adds = addCerts(dir, ks); + adds = KeyStoreUtil.addCerts(dir, ks); totalAdds += adds; if (adds > 0 && _log.shouldLog(Log.INFO)) _log.info("Loaded " + adds + " trusted certificates from " + dir.getAbsolutePath()); @@ -240,7 +203,7 @@ public class SSLEepGet extends EepGet { dir = new File(System.getProperty("user.dir")); if (!_context.getBaseDir().getAbsolutePath().equals(dir.getAbsolutePath())) { dir = new File(_context.getConfigDir(), "certificates"); - adds = addCerts(dir, ks); + adds = KeyStoreUtil.addCerts(dir, ks); totalAdds += adds; if (adds > 0 && _log.shouldLog(Log.INFO)) _log.info("Loaded " + adds + " trusted certificates from " + dir.getAbsolutePath()); @@ -261,118 +224,6 @@ public class SSLEepGet extends EepGet { } return null; } - - /** - * Load all X509 Certs from a key store File into a KeyStore - * Note that each call reinitializes the KeyStore - * - * @return success - * @since 0.8.2 - */ - private boolean loadCerts(File file, KeyStore ks) { - if (!file.exists()) - return false; - InputStream fis = null; - try { - fis = new FileInputStream(file); - // "changeit" is the default password - ks.load(fis, "changeit".toCharArray()); - } catch (GeneralSecurityException gse) { - _log.error("KeyStore load error, no default keys: " + file.getAbsolutePath(), gse); - try { - // not clear if null is allowed for password - ks.load(null, "changeit".toCharArray()); - } catch (Exception foo) {} - return false; - } catch (IOException ioe) { - _log.error("KeyStore load error, no default keys: " + file.getAbsolutePath(), ioe); - try { - ks.load(null, "changeit".toCharArray()); - } catch (Exception foo) {} - return false; - } finally { - try { if (fis != null) fis.close(); } catch (IOException foo) {} - } - return true; - } - - /** - * Load all X509 Certs from a directory and add them to the - * trusted set of certificates in the key store - * - * @return number successfully added - * @since 0.8.2 - */ - private int addCerts(File dir, KeyStore ks) { - if (_log.shouldLog(Log.INFO)) - _log.info("Looking for X509 Certificates in " + dir.getAbsolutePath()); - int added = 0; - if (dir.exists() && dir.isDirectory()) { - File[] files = dir.listFiles(); - if (files != null) { - for (int i = 0; i < files.length; i++) { - File f = files[i]; - if (!f.isFile()) - continue; - // use file name as alias - // https://www.sslshopper.com/ssl-converter.html - // No idea if all these formats can actually be read by CertificateFactory - String alias = f.getName().toLowerCase(Locale.US); - if (alias.endsWith(".crt") || alias.endsWith(".pem") || alias.endsWith(".key") || - alias.endsWith(".der") || alias.endsWith(".key") || alias.endsWith(".p7b") || - alias.endsWith(".p7c") || alias.endsWith(".pfx") || alias.endsWith(".p12")) - alias = alias.substring(0, alias.length() - 4); - boolean success = addCert(f, alias, ks); - if (success) - added++; - } - } - } - return added; - } - - /** - * Load an X509 Cert from a file and add it to the - * trusted set of certificates in the key store - * - * @return success - * @since 0.8.2 - */ - private boolean addCert(File file, String alias, KeyStore ks) { - InputStream fis = null; - try { - fis = new FileInputStream(file); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - X509Certificate cert = (X509Certificate)cf.generateCertificate(fis); - if (_log.shouldLog(Log.INFO)) { - _log.info("Read X509 Certificate from " + file.getAbsolutePath() + - " Issuer: " + cert.getIssuerX500Principal() + - "; Valid From: " + cert.getNotBefore() + - " To: " + cert.getNotAfter()); - } - try { - cert.checkValidity(); - } catch (CertificateExpiredException cee) { - _log.error("Rejecting expired X509 Certificate: " + file.getAbsolutePath(), cee); - return false; - } catch (CertificateNotYetValidException cnyve) { - _log.error("Rejecting X509 Certificate not yet valid: " + file.getAbsolutePath(), cnyve); - return false; - } - ks.setCertificateEntry(alias, cert); - if (_log.shouldLog(Log.INFO)) - _log.info("Now trusting X509 Certificate, Issuer: " + cert.getIssuerX500Principal()); - } catch (GeneralSecurityException gse) { - _log.error("Error reading X509 Certificate: " + file.getAbsolutePath(), gse); - return false; - } catch (IOException ioe) { - _log.error("Error reading X509 Certificate: " + file.getAbsolutePath(), ioe); - return false; - } finally { - try { if (fis != null) fis.close(); } catch (IOException foo) {} - } - return true; - } /** * From http://blogs.sun.com/andreas/resource/InstallCert.java @@ -425,44 +276,12 @@ public class SSLEepGet extends EepGet { } catch (Exception e) { System.out.println(" WARNING: Certificate is not currently valid, it cannot be used"); } - saveCert(cert, new File(name)); + CertUtil.saveCert(cert, new File(name)); } System.out.println("NOTE: To trust them, copy the certificate file(s) to the certificates directory and rerun without the -s option"); System.out.println("NOTE: EepGet failed, certificate error follows:"); } - private static final int LINE_LENGTH = 64; - - /** - * Modified from: - * http://www.exampledepot.com/egs/java.security.cert/ExportCert.html - * - * This method writes a certificate to a file in base64 format. - * @since 0.8.2 - */ - private static void saveCert(Certificate cert, File file) { - OutputStream os = null; - try { - // Get the encoded form which is suitable for exporting - byte[] buf = cert.getEncoded(); - os = new FileOutputStream(file); - PrintWriter wr = new PrintWriter(os); - wr.println("-----BEGIN CERTIFICATE-----"); - String b64 = Base64.encode(buf, true); // true = use standard alphabet - for (int i = 0; i < b64.length(); i += LINE_LENGTH) { - wr.println(b64.substring(i, Math.min(i + LINE_LENGTH, b64.length()))); - } - wr.println("-----END CERTIFICATE-----"); - wr.flush(); - } catch (CertificateEncodingException cee) { - System.out.println("Error writing X509 Certificate " + file.getAbsolutePath() + ' ' + cee); - } catch (IOException ioe) { - System.out.println("Error writing X509 Certificate " + file.getAbsolutePath() + ' ' + ioe); - } finally { - try { if (os != null) os.close(); } catch (IOException foo) {} - } - } - /** * An opaque class for the caller to pass to repeated instantiations of SSLEepGet. * @since 0.8.2 diff --git a/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java b/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java index b43d48bec..9e8ba08ce 100644 --- a/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java +++ b/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java @@ -21,13 +21,12 @@ import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLContext; import net.i2p.client.I2PClient; +import net.i2p.crypto.CertUtil; +import net.i2p.crypto.KeyStoreUtil; import net.i2p.data.Base32; -import net.i2p.data.Base64; import net.i2p.router.RouterContext; import net.i2p.util.Log; import net.i2p.util.SecureDirectory; -import net.i2p.util.SecureFileOutputStream; -import net.i2p.util.ShellCommand; /** * SSL version of ClientListenerRunner @@ -87,31 +86,14 @@ class SSLClientListenerRunner extends ClientListenerRunner { */ private boolean createKeyStore(File ks) { // make a random 48 character password (30 * 8 / 5) - byte[] rand = new byte[30]; - _context.random().nextBytes(rand); - String keyPassword = Base32.encode(rand); + String keyPassword = KeyStoreUtil.randomString(); // and one for the cname - _context.random().nextBytes(rand); - String cname = Base32.encode(rand) + ".i2cp.i2p.net"; + String cname = KeyStoreUtil.randomString() + ".i2cp.i2p.net"; - String keytool = (new File(System.getProperty("java.home"), "bin/keytool")).getAbsolutePath(); - String[] args = new String[] { - keytool, - "-genkey", // -genkeypair preferred in newer keytools, but this works with more - "-storetype", KeyStore.getDefaultType(), - "-keystore", ks.getAbsolutePath(), - "-storepass", DEFAULT_KEYSTORE_PASSWORD, - "-alias", KEY_ALIAS, - "-dname", "CN=" + cname + ",OU=I2CP,O=I2P Anonymous Network,L=XX,ST=XX,C=XX", - "-validity", "3652", // 10 years - "-keyalg", "DSA", - "-keysize", "1024", - "-keypass", keyPassword}; - boolean success = (new ShellCommand()).executeSilentAndWaitTimed(args, 30); // 30 secs + boolean success = KeyStoreUtil.createKeys(ks, KEY_ALIAS, cname, "I2CP", keyPassword); if (success) { success = ks.exists(); if (success) { - SecureFileOutputStream.setPerms(ks); Map changes = new HashMap(); changes.put(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); changes.put(PROP_KEY_PASSWORD, keyPassword); @@ -123,13 +105,8 @@ class SSLClientListenerRunner extends ClientListenerRunner { "The certificate name was generated randomly, and is not associated with your " + "IP address, host name, router identity, or destination keys."); } else { - _log.error("Failed to create I2CP SSL keystore using command line:"); - StringBuilder buf = new StringBuilder(256); - for (int i = 0; i < args.length; i++) { - buf.append('"').append(args[i]).append("\" "); - } - _log.error(buf.toString()); - _log.error("This is for the Sun/Oracle keytool, others may be incompatible.\n" + + _log.error("Failed to create I2CP SSL keystore.\n" + + "This is for the Sun/Oracle keytool, others may be incompatible.\n" + "If you create the keystore manually, you must add " + PROP_KEYSTORE_PASSWORD + " and " + PROP_KEY_PASSWORD + " to " + (new File(_context.getConfigDir(), "router.config")).getAbsolutePath()); } @@ -143,62 +120,16 @@ class SSLClientListenerRunner extends ClientListenerRunner { private void exportCert(File ks) { File sdir = new SecureDirectory(_context.getConfigDir(), "certificates"); if (sdir.exists() || sdir.mkdir()) { - InputStream fis = null; - try { - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - fis = new FileInputStream(ks); - String ksPass = _context.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); - keyStore.load(fis, ksPass.toCharArray()); - Certificate cert = keyStore.getCertificate(KEY_ALIAS); - if (cert != null) { - File certFile = new File(sdir, ASCII_KEYFILE); - saveCert(cert, certFile); - } else { - _log.error("Error getting SSL cert to save as ASCII"); - } - } catch (GeneralSecurityException gse) { - _log.error("Error saving ASCII SSL keys", gse); - } catch (IOException ioe) { - _log.error("Error saving ASCII SSL keys", ioe); - } finally { - if (fis != null) try { fis.close(); } catch (IOException ioe) {} - } + String ksPass = _context.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); + File out = new File(sdir, ASCII_KEYFILE); + boolean success = KeyStoreUtil.exportCert(ks, ksPass, KEY_ALIAS, out); + if (!success) + _log.error("Error getting SSL cert to save as ASCII"); } else { _log.error("Error saving ASCII SSL keys"); } } - private static final int LINE_LENGTH = 64; - - /** - * Modified from: - * http://www.exampledepot.com/egs/java.security.cert/ExportCert.html - * - * Write a certificate to a file in base64 format. - */ - private void saveCert(Certificate cert, File file) { - OutputStream os = null; - try { - // Get the encoded form which is suitable for exporting - byte[] buf = cert.getEncoded(); - os = new SecureFileOutputStream(file); - PrintWriter wr = new PrintWriter(os); - wr.println("-----BEGIN CERTIFICATE-----"); - String b64 = Base64.encode(buf, true); // true = use standard alphabet - for (int i = 0; i < b64.length(); i += LINE_LENGTH) { - wr.println(b64.substring(i, Math.min(i + LINE_LENGTH, b64.length()))); - } - wr.println("-----END CERTIFICATE-----"); - wr.flush(); - } catch (CertificateEncodingException cee) { - _log.error("Error writing X509 Certificate " + file.getAbsolutePath(), cee); - } catch (IOException ioe) { - _log.error("Error writing X509 Certificate " + file.getAbsolutePath(), ioe); - } finally { - try { if (os != null) os.close(); } catch (IOException foo) {} - } - } - /** * Sets up the SSLContext and sets the socket factory. * @return success