* i2ptunnel:

- Add local SSL support for std. and IRC client tunnels (ticket #1107)
    Keystore goes in ~/.i2p/keystore; pubkey cert goes in ~/.i2p/certificates/i2ptunnel
  - Escape messages to index page
  - Show message for uncaught exception
This commit is contained in:
zzz
2014-08-21 12:21:29 +00:00
parent 915e003355
commit 975378b224
7 changed files with 276 additions and 8 deletions

View File

@ -24,6 +24,8 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicLong;
import javax.net.ssl.SSLServerSocketFactory;
import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.client.I2PSession;
@ -86,6 +88,8 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
private static int _executorThreadCount;
private static final Object _executorLock = new Object();
public static final String PROP_USE_SSL = I2PTunnelServer.PROP_USE_SSL;
/**
* This constructor always starts the tunnel (ignoring the i2cp.delayOpen option).
* It is used to add a client to an existing socket manager.
@ -599,7 +603,24 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
}
return;
}
ss = new ServerSocket(localPort, 0, addr);
Properties opts = getTunnel().getClientOptions();
boolean useSSL = Boolean.parseBoolean(opts.getProperty(PROP_USE_SSL));
if (useSSL) {
// was already done in web/IndexBean.java when saving the config
boolean wasCreated = SSLClientUtil.verifyKeyStore(opts);
if (wasCreated) {
// From here, we can't save the config.
// We shouldn't get here, as SSL isn't the default, so it would
// be enabled via the GUI only.
// If it was done manually, the keys will be regenerated at every startup,
// which is bad.
_log.logAlways(Log.WARN, "Created new i2ptunnel SSL keys but can't save the config, disable and enable via i2ptunnel GUI");
}
SSLServerSocketFactory fact = SSLClientUtil.initializeFactory(opts);
ss = fact.createServerSocket(localPort, 0, addr);
} else {
ss = new ServerSocket(localPort, 0, addr);
}
// If a free port was requested, find out what we got
if (localPort == 0) {

View File

@ -0,0 +1,203 @@
package net.i2p.i2ptunnel;
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 I2PTunnel client SSL server sockets.
*
* @since 0.9.15 adopted from net.i2p.router.client.SSLClientListenerRunner
*/
public class SSLClientUtil {
private static final String PROP_KEYSTORE_PASSWORD = "keystorePassword";
private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit";
private static final String PROP_KEY_PASSWORD = "keyPassword";
private static final String PROP_KEY_ALIAS = "keyAlias";
private static final String ASCII_KEYFILE_SUFFIX = ".local.crt";
private static final String PROP_KS_NAME = "keystoreFile";
private static final String KS_DIR = "keystore";
private static final String PREFIX = "i2ptunnel-";
private static final String KS_SUFFIX = ".ks";
private static final String CERT_DIR = "certificates/i2ptunnel";
/**
* 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 {
return verifyKeyStore(opts, "");
}
/**
* 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
* @param optPfx add this prefix when getting/setting options
* @return false if it already exists; if true, caller must save opts
* @throws IOException on creation fail
*/
public static boolean verifyKeyStore(Properties opts, String optPfx) throws IOException {
String name = opts.getProperty(optPfx + PROP_KEY_ALIAS);
if (name == null) {
name = KeyStoreUtil.randomString();
opts.setProperty(optPfx + PROP_KEY_ALIAS, name);
}
String ksname = opts.getProperty(optPfx + PROP_KS_NAME);
if (ksname == null) {
ksname = PREFIX + name + KS_SUFFIX;
opts.setProperty(optPfx + 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, optPfx);
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, optPfx);
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
* @param optPfx add this prefix when getting/setting options
* @return success, if true, opts will have password properties added to be saved
*/
private static boolean createKeyStore(File ks, String name, Properties opts, String optPfx) {
// make a random 48 character password (30 * 8 / 5)
String keyPassword = KeyStoreUtil.randomString();
// and one for the cname
String cname = name + ".i2ptunnel.i2p.net";
String keyName = opts.getProperty(optPfx + PROP_KEY_ALIAS);
boolean success = KeyStoreUtil.createKeys(ks, keyName, cname, "I2PTUNNEL", keyPassword);
if (success) {
success = ks.exists();
if (success) {
opts.setProperty(optPfx + PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
opts.setProperty(optPfx + 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 I2PTunnel SSL keystore.\n" +
"If you create the keystore manually, you must add " + optPfx + PROP_KEYSTORE_PASSWORD + " and " + optPfx + PROP_KEY_PASSWORD +
" to " + (new File(I2PAppContext.getGlobalContext().getConfigDir(), "i2ptunnel.config")).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 optPfx + PROP_KEY_ALIAS
* @param optPfx add this prefix when getting options
*/
private static void exportCert(File ks, String name, Properties opts, String optPfx) {
File sdir = new SecureDirectory(I2PAppContext.getGlobalContext().getConfigDir(), CERT_DIR);
if (sdir.exists() || sdir.mkdirs()) {
String keyAlias = opts.getProperty(optPfx + PROP_KEY_ALIAS);
String ksPass = opts.getProperty(optPfx + 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(), "i2ptunnel.config")).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(), "i2ptunnel.config")).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(SSLClientUtil.class).error(s);
}
private static void logAlways(String s) {
I2PAppContext.getGlobalContext().logManager().getLog(SSLClientUtil.class).logAlways(Log.INFO, s);
}
}

View File

@ -26,15 +26,18 @@ import net.i2p.app.ClientAppManager;
import net.i2p.app.Outproxy;
import net.i2p.client.I2PClient;
import net.i2p.data.Certificate;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.PrivateKeyFile;
import net.i2p.data.SessionKey;
import net.i2p.i2ptunnel.I2PTunnelClientBase;
import net.i2p.i2ptunnel.I2PTunnelConnectClient;
import net.i2p.i2ptunnel.I2PTunnelHTTPClient;
import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase;
import net.i2p.i2ptunnel.I2PTunnelHTTPServer;
import net.i2p.i2ptunnel.I2PTunnelIRCClient;
import net.i2p.i2ptunnel.I2PTunnelServer;
import net.i2p.i2ptunnel.SSLClientUtil;
import net.i2p.i2ptunnel.TunnelController;
import net.i2p.i2ptunnel.TunnelControllerGroup;
import net.i2p.util.Addresses;
@ -255,7 +258,7 @@ public class IndexBean {
// give the messages a chance to make it to the window
try { Thread.sleep(1000); } catch (InterruptedException ie) {}
// and give them something to look at in any case
return _("Starting tunnel") + ' ' + getTunnelName(_tunnel) + "…";
return _("Starting tunnel") + ' ' + getTunnelName(_tunnel) + "...";
}
private String stop() {
@ -268,7 +271,7 @@ public class IndexBean {
// give the messages a chance to make it to the window
try { Thread.sleep(1000); } catch (InterruptedException ie) {}
// and give them something to look at in any case
return _("Stopping tunnel") + ' ' + getTunnelName(_tunnel) + "…";
return _("Stopping tunnel") + ' ' + getTunnelName(_tunnel) + "...";
}
private String saveChanges() {
@ -276,7 +279,27 @@ public class IndexBean {
TunnelController cur = getController(_tunnel);
Properties config = getConfig();
String ksMsg = null;
String type = config.getProperty(TunnelController.PROP_TYPE);
if (TunnelController.TYPE_STD_CLIENT.equals(type) || TunnelController.TYPE_IRC_CLIENT.equals(type)) {
//
// If we switch to SSL, create the keystore here, so we can store the new properties.
// Down in I2PTunnelClientBase it's very hard to save the config.
//
if (Boolean.parseBoolean(config.getProperty(OPT + I2PTunnelClientBase.PROP_USE_SSL))) {
try {
boolean created = SSLClientUtil.verifyKeyStore(config, OPT);
if (created) {
// config now contains new keystore props
ksMsg = "Created new self-signed certificate for tunnel " + getTunnelName(_tunnel);
}
} catch (IOException ioe) {
ksMsg = "Failed to create new self-signed certificate for tunnel " +
getTunnelName(_tunnel) + ", check logs: " + ioe;
}
}
}
if (cur == null) {
// creating new
cur = new TunnelController(config, "", true);
@ -327,6 +350,8 @@ public class IndexBean {
}
List<String> msgs = doSave();
if (ksMsg != null)
msgs.add(ksMsg);
return getMessages(msgs);
}
@ -397,6 +422,7 @@ public class IndexBean {
* Executes any action requested (start/stop/etc) and dump out the
* messages.
*
* @return HTML escaped
*/
public String getMessages() {
if (_group == null)
@ -405,13 +431,14 @@ public class IndexBean {
StringBuilder buf = new StringBuilder(512);
if (_action != null) {
try {
buf.append(processAction()).append("\n");
buf.append(processAction()).append('\n');
} catch (Exception e) {
_log.log(Log.CRIT, "Error processing " + _action, e);
buf.append("Error: ").append(e.toString()).append('\n');
}
}
getMessages(_group.clearAllMessages(), buf);
return buf.toString();
return DataHelper.escapeHTML(buf.toString());
}
////
@ -1293,7 +1320,8 @@ public class IndexBean {
I2PTunnelIRCClient.PROP_DCC
};
private static final String _booleanClientOpts[] = {
"i2cp.reduceOnIdle", "i2cp.closeOnIdle", "i2cp.newDestOnResume", "persistentClientKey", "i2cp.delayOpen"
"i2cp.reduceOnIdle", "i2cp.closeOnIdle", "i2cp.newDestOnResume", "persistentClientKey", "i2cp.delayOpen",
I2PTunnelClientBase.PROP_USE_SSL,
};
private static final String _booleanProxyOpts[] = {
I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH,

View File

@ -140,6 +140,14 @@ input.default { width: 1px; height: 1px; visibility: hidden; }
</select>
<% } /* streamrclient */ %>
</div>
<% if ("client".equals(tunnelType) || "ircclient".equals(tunnelType)) {
%><div id="portField" class="rowItem">
<label>
<%=intl._("Use SSL?")%>
</label>
<input value="1" type="checkbox" id="startOnLoad" name="useSSL" title="Clients use SSL to connect" <%=(editBean.isSSLEnabled(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" />
</div>
<% } /* tunnel types */ %>
<div class="subdivider">
<hr />

View File

@ -244,6 +244,8 @@
<%
String cPort= indexBean.getClientPort2(curClient);
out.write(cPort);
if (indexBean.isSSLEnabled(curClient))
out.write(" SSL");
%>
</span>
</div>

View File

@ -1,3 +1,9 @@
2014-08-21 zzz
* i2psnark:
- Escape control chars in encodePath()
- Increase max piece size to 8 MB (ticket #1347)
* i2ptunnel: Add local SSL support for std. and IRC client tunnels (ticket #1107)
2014-08-19 zzz
* i2psnark:
- Don't filter create torrent form, and

View File

@ -18,7 +18,7 @@ public class RouterVersion {
/** deprecated */
public final static String ID = "Monotone";
public final static String VERSION = CoreVersion.VERSION;
public final static long BUILD = 7;
public final static long BUILD = 8;
/** for example "-test" */
public final static String EXTRA = "";