* I2CP over SSL, enable with i2cp.SSL=true
This commit is contained in:
183
core/java/src/net/i2p/client/I2CPSSLSocketFactory.java
Normal file
183
core/java/src/net/i2p/client/I2CPSSLSocketFactory.java
Normal file
@ -0,0 +1,183 @@
|
||||
package net.i2p.client;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
import java.security.KeyStore;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Loads trusted ASCII certs from ~/.i2p/certificates/ and $CWD/certificates/.
|
||||
* Keeps a single static SSLContext for the whole JVM.
|
||||
*
|
||||
* @author zzz
|
||||
* @since 0.8.3
|
||||
*/
|
||||
class I2CPSSLSocketFactory {
|
||||
|
||||
private static final Object _initLock = new Object();
|
||||
private static SSLSocketFactory _factory;
|
||||
private static Log _log;
|
||||
|
||||
private static final String CERT_DIR = "certificates";
|
||||
|
||||
/**
|
||||
* Initializes the static SSL Context if required, then returns a socket
|
||||
* to the host.
|
||||
*
|
||||
* @param ctx just for logging
|
||||
* @throws IOException on init error or usual socket errors
|
||||
*/
|
||||
public static Socket createSocket(I2PAppContext ctx, String host, int port) throws IOException {
|
||||
synchronized(_initLock) {
|
||||
if (_factory == null) {
|
||||
_log = ctx.logManager().getLog(I2CPSSLSocketFactory.class);
|
||||
initSSLContext(ctx);
|
||||
if (_factory == null)
|
||||
throw new IOException("Unable to create SSL Context for I2CP Client");
|
||||
_log.info("I2CP Client-side SSL Context initialized");
|
||||
}
|
||||
}
|
||||
return _factory.createSocket(host, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads certs from
|
||||
* the ~/.i2p/certificates/ and $CWD/certificates/ directories.
|
||||
*/
|
||||
private static void initSSLContext(I2PAppContext context) {
|
||||
KeyStore ks;
|
||||
try {
|
||||
ks = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
ks.load(null, "".toCharArray());
|
||||
} catch (GeneralSecurityException gse) {
|
||||
_log.error("Key Store init error", gse);
|
||||
return;
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Key Store init error", ioe);
|
||||
return;
|
||||
}
|
||||
|
||||
File dir = new File(context.getConfigDir(), CERT_DIR);
|
||||
int adds = addCerts(dir, ks);
|
||||
int totalAdds = adds;
|
||||
if (adds > 0 && _log.shouldLog(Log.INFO))
|
||||
_log.info("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);
|
||||
totalAdds += adds;
|
||||
if (adds > 0 && _log.shouldLog(Log.INFO))
|
||||
_log.info("Loaded " + adds + " trusted certificates from " + dir.getAbsolutePath());
|
||||
}
|
||||
if (totalAdds > 0) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Loaded total of " + totalAdds + " new trusted certificates");
|
||||
} else {
|
||||
_log.error("No trusted certificates loaded (looked in " +
|
||||
dir.getAbsolutePath() + (dir.getAbsolutePath().equals(dir2.getAbsolutePath()) ? "" : (" and " + dir2.getAbsolutePath())) +
|
||||
", I2CP SSL client connections will fail. " +
|
||||
"Copy the file certificates/i2cp.local.crt from the router to the directory.");
|
||||
// don't continue, since we didn't load the system keystore, we have nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SSLContext sslc = SSLContext.getInstance("TLS");
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(ks);
|
||||
sslc.init(null, tmf.getTrustManagers(), context.random());
|
||||
_factory = sslc.getSocketFactory();
|
||||
} catch (GeneralSecurityException gse) {
|
||||
_log.error("SSL context init error", gse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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
|
||||
String alias = f.getName().toLowerCase();
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
@ -131,6 +131,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
|
||||
private long _lastActivity;
|
||||
private boolean _isReduced;
|
||||
|
||||
/** SSL interface (only) @since 0.8.3 */
|
||||
protected static final String PROP_ENABLE_SSL = "i2cp.SSL";
|
||||
|
||||
void dateUpdated() {
|
||||
_dateReceived = true;
|
||||
synchronized (_dateReceivedLock) {
|
||||
@ -181,7 +184,10 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
|
||||
protected void loadConfig(Properties options) {
|
||||
_options = new Properties();
|
||||
_options.putAll(filter(options));
|
||||
if (!_context.isRouterContext()) {
|
||||
if (_context.isRouterContext()) {
|
||||
// just for logging
|
||||
_hostname = "[internal connection]";
|
||||
} else {
|
||||
_hostname = _options.getProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1");
|
||||
String portNum = _options.getProperty(I2PClient.PROP_TCP_PORT, LISTEN_PORT + "");
|
||||
try {
|
||||
@ -195,6 +201,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
|
||||
}
|
||||
|
||||
// auto-add auth if required, not set in the options, and we are in the same JVM
|
||||
// TODO bypass this on router side for internal connections
|
||||
if (_context.isRouterContext() &&
|
||||
Boolean.valueOf(_context.getProperty("i2cp.auth")).booleanValue() &&
|
||||
((!options.containsKey("i2cp.username")) || (!options.containsKey("i2cp.password")))) {
|
||||
@ -302,7 +309,10 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
|
||||
_queue = mgr.connect();
|
||||
_reader = new QueuedI2CPMessageReader(_queue, this);
|
||||
} else {
|
||||
_socket = new Socket(_hostname, _portNum);
|
||||
if (Boolean.valueOf(_options.getProperty(PROP_ENABLE_SSL)).booleanValue())
|
||||
_socket = I2CPSSLSocketFactory.createSocket(_context, _hostname, _portNum);
|
||||
else
|
||||
_socket = new Socket(_hostname, _portNum);
|
||||
// _socket.setSoTimeout(1000000); // Uhmmm we could really-really use a real timeout, and handle it.
|
||||
_out = _socket.getOutputStream();
|
||||
synchronized (_out) {
|
||||
|
@ -23,7 +23,6 @@ import net.i2p.internal.I2CPMessageQueue;
|
||||
import net.i2p.internal.InternalClientManager;
|
||||
import net.i2p.internal.QueuedI2CPMessageReader;
|
||||
import net.i2p.util.I2PAppThread;
|
||||
import net.i2p.util.InternalSocket;
|
||||
|
||||
/**
|
||||
* Create a new session for doing naming and bandwidth queries only. Do not create a Destination.
|
||||
@ -80,7 +79,10 @@ class I2PSimpleSession extends I2PSessionImpl2 {
|
||||
_queue = mgr.connect();
|
||||
_reader = new QueuedI2CPMessageReader(_queue, this);
|
||||
} else {
|
||||
_socket = new Socket(_hostname, _portNum);
|
||||
if (Boolean.valueOf(getOptions().getProperty(PROP_ENABLE_SSL)).booleanValue())
|
||||
_socket = I2CPSSLSocketFactory.createSocket(_context, _hostname, _portNum);
|
||||
else
|
||||
_socket = new Socket(_hostname, _portNum);
|
||||
_out = _socket.getOutputStream();
|
||||
synchronized (_out) {
|
||||
_out.write(I2PClient.PROTOCOL_BYTE);
|
||||
|
@ -25,12 +25,12 @@ import net.i2p.util.Log;
|
||||
* @author jrandom
|
||||
*/
|
||||
class ClientListenerRunner implements Runnable {
|
||||
protected Log _log;
|
||||
protected RouterContext _context;
|
||||
protected ClientManager _manager;
|
||||
protected final Log _log;
|
||||
protected final RouterContext _context;
|
||||
protected final ClientManager _manager;
|
||||
protected ServerSocket _socket;
|
||||
protected int _port;
|
||||
private boolean _bindAllInterfaces;
|
||||
protected final int _port;
|
||||
protected final boolean _bindAllInterfaces;
|
||||
protected boolean _running;
|
||||
protected boolean _listening;
|
||||
|
||||
@ -38,18 +38,33 @@ class ClientListenerRunner implements Runnable {
|
||||
|
||||
public ClientListenerRunner(RouterContext context, ClientManager manager, int port) {
|
||||
_context = context;
|
||||
_log = _context.logManager().getLog(ClientListenerRunner.class);
|
||||
_log = _context.logManager().getLog(getClass());
|
||||
_manager = manager;
|
||||
_port = port;
|
||||
|
||||
String val = context.getProperty(BIND_ALL_INTERFACES);
|
||||
_bindAllInterfaces = Boolean.valueOf(val).booleanValue();
|
||||
_bindAllInterfaces = context.getBooleanProperty(BIND_ALL_INTERFACES);
|
||||
}
|
||||
|
||||
public void setPort(int port) { _port = port; }
|
||||
public int getPort() { return _port; }
|
||||
public boolean isListening() { return _running && _listening; }
|
||||
|
||||
/**
|
||||
* Get a ServerSocket.
|
||||
* Split out so it can be overridden for SSL.
|
||||
* @since 0.8.3
|
||||
*/
|
||||
protected ServerSocket getServerSocket() throws IOException {
|
||||
if (_bindAllInterfaces) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Listening on port " + _port + " on all interfaces");
|
||||
return new ServerSocket(_port);
|
||||
} else {
|
||||
String listenInterface = _context.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_HOST,
|
||||
ClientManagerFacadeImpl.DEFAULT_HOST);
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Listening on port " + _port + " of the specific interface: " + listenInterface);
|
||||
return new ServerSocket(_port, 0, InetAddress.getByName(listenInterface));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start up the socket listener, listens for connections, and
|
||||
* fires those connections off via {@link #runConnection runConnection}.
|
||||
@ -62,18 +77,7 @@ class ClientListenerRunner implements Runnable {
|
||||
int curDelay = 1000;
|
||||
while (_running) {
|
||||
try {
|
||||
if (_bindAllInterfaces) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Listening on port " + _port + " on all interfaces");
|
||||
_socket = new ServerSocket(_port);
|
||||
} else {
|
||||
String listenInterface = _context.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_HOST,
|
||||
ClientManagerFacadeImpl.DEFAULT_HOST);
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Listening on port " + _port + " of the specific interface: " + listenInterface);
|
||||
_socket = new ServerSocket(_port, 0, InetAddress.getByName(listenInterface));
|
||||
}
|
||||
|
||||
_socket = getServerSocket();
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("ServerSocket created, before accept: " + _socket);
|
||||
@ -131,7 +135,8 @@ class ClientListenerRunner implements Runnable {
|
||||
}
|
||||
|
||||
/** give the i2cp client 5 seconds to show that they're really i2cp clients */
|
||||
private final static int CONNECT_TIMEOUT = 5*1000;
|
||||
protected final static int CONNECT_TIMEOUT = 5*1000;
|
||||
private final static int LOOP_DELAY = 250;
|
||||
|
||||
/**
|
||||
* Verify the first byte.
|
||||
@ -141,16 +146,17 @@ class ClientListenerRunner implements Runnable {
|
||||
protected boolean validate(Socket socket) {
|
||||
try {
|
||||
InputStream is = socket.getInputStream();
|
||||
for (int i = 0; i < 20; i++) {
|
||||
for (int i = 0; i < CONNECT_TIMEOUT / LOOP_DELAY; i++) {
|
||||
if (is.available() > 0)
|
||||
return is.read() == I2PClient.PROTOCOL_BYTE;
|
||||
try { Thread.sleep(250); } catch (InterruptedException ie) {}
|
||||
try { Thread.sleep(LOOP_DELAY); } catch (InterruptedException ie) {}
|
||||
}
|
||||
} catch (IOException ioe) {}
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Peer did not authenticate themselves as I2CP quickly enough, dropping");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the connection by passing it off to a {@link ClientConnectionRunner ClientConnectionRunner}
|
||||
*
|
||||
|
@ -53,6 +53,8 @@ class ClientManager {
|
||||
|
||||
/** Disable external interface, allow internal clients only @since 0.8.3 */
|
||||
private static final String PROP_DISABLE_EXTERNAL = "i2cp.disableInterface";
|
||||
/** SSL interface (only) @since 0.8.3 */
|
||||
private static final String PROP_ENABLE_SSL = "i2cp.SSL";
|
||||
|
||||
/** ms to wait before rechecking for inbound messages to deliver to clients */
|
||||
private final static int INBOUND_POLL_INTERVAL = 300;
|
||||
@ -60,10 +62,10 @@ class ClientManager {
|
||||
public ClientManager(RouterContext context, int port) {
|
||||
_ctx = context;
|
||||
_log = context.logManager().getLog(ClientManager.class);
|
||||
_ctx.statManager().createRateStat("client.receiveMessageSize",
|
||||
"How large are messages received by the client?",
|
||||
"ClientMessages",
|
||||
new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
|
||||
//_ctx.statManager().createRateStat("client.receiveMessageSize",
|
||||
// "How large are messages received by the client?",
|
||||
// "ClientMessages",
|
||||
// new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
|
||||
_runners = new HashMap();
|
||||
_pendingRunners = new HashSet();
|
||||
startListeners(port);
|
||||
@ -72,7 +74,11 @@ class ClientManager {
|
||||
/** Todo: Start a 3rd listener for IPV6? */
|
||||
private void startListeners(int port) {
|
||||
if (!_ctx.getBooleanProperty(PROP_DISABLE_EXTERNAL)) {
|
||||
_listener = new ClientListenerRunner(_ctx, this, port);
|
||||
// there's no option to start both an SSL and non-SSL listener
|
||||
if (_ctx.getBooleanProperty(PROP_ENABLE_SSL))
|
||||
_listener = new SSLClientListenerRunner(_ctx, this, port);
|
||||
else
|
||||
_listener = new ClientListenerRunner(_ctx, this, port);
|
||||
Thread t = new I2PThread(_listener, "ClientListener:" + port, true);
|
||||
t.start();
|
||||
}
|
||||
@ -494,8 +500,8 @@ class ClientManager {
|
||||
runner = getRunner(_msg.getDestinationHash());
|
||||
|
||||
if (runner != null) {
|
||||
_ctx.statManager().addRateData("client.receiveMessageSize",
|
||||
_msg.getPayload().getSize(), 0);
|
||||
//_ctx.statManager().addRateData("client.receiveMessageSize",
|
||||
// _msg.getPayload().getSize(), 0);
|
||||
runner.receiveMessage(_msg.getDestination(), null, _msg.getPayload());
|
||||
} else {
|
||||
// no client connection...
|
||||
|
@ -0,0 +1,282 @@
|
||||
package net.i2p.router.client;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.ServerSocket;
|
||||
import java.security.KeyStore;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLServerSocketFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import net.i2p.client.I2PClient;
|
||||
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
|
||||
*
|
||||
* @since 0.8.3
|
||||
* @author zzz
|
||||
*/
|
||||
class SSLClientListenerRunner extends ClientListenerRunner {
|
||||
|
||||
private SSLServerSocketFactory _factory;
|
||||
|
||||
private static final String PROP_KEYSTORE_PASSWORD = "i2cp.keystorePassword";
|
||||
private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit";
|
||||
private static final String PROP_KEY_PASSWORD = "i2cp.keyPassword";
|
||||
private static final String KEY_ALIAS = "i2cp";
|
||||
private static final String ASCII_KEYFILE = "i2cp.local.crt";
|
||||
|
||||
public SSLClientListenerRunner(RouterContext context, ClientManager manager, int port) {
|
||||
super(context, manager, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return success if it exists and we have a password, or it was created successfully.
|
||||
*/
|
||||
private boolean verifyKeyStore(File ks) {
|
||||
if (ks.exists()) {
|
||||
boolean rv = _context.getProperty(PROP_KEY_PASSWORD) != null;
|
||||
if (!rv)
|
||||
_log.error("I2CP 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;
|
||||
}
|
||||
boolean rv = createKeyStore(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.
|
||||
if (rv)
|
||||
exportCert(ks);
|
||||
return rv;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Call out to keytool to create a new keystore with a keypair in it.
|
||||
* Trying to do this programatically is a nightmare, requiring either BouncyCastle
|
||||
* libs or using proprietary Sun libs, and it's a huge mess.
|
||||
* If successful, stores the keystore password and key password in router.config.
|
||||
*
|
||||
* @return success
|
||||
*/
|
||||
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);
|
||||
// and one for the cname
|
||||
_context.random().nextBytes(rand);
|
||||
String cname = Base32.encode(rand) + ".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
|
||||
if (success) {
|
||||
success = ks.exists();
|
||||
if (success) {
|
||||
SecureFileOutputStream.setPerms(ks);
|
||||
_context.router().setConfigSetting(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
|
||||
_context.router().setConfigSetting(PROP_KEY_PASSWORD, keyPassword);
|
||||
_context.router().saveConfig();
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
_log.logAlways(Log.INFO, "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 {
|
||||
_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" +
|
||||
"If you create the keystore manually, you must add " + PROP_KEYSTORE_PASSWORD + " and " + PROP_KEY_PASSWORD +
|
||||
" to " + (new File(_context.getConfigDir(), "router.config")).getAbsolutePath());
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull the cert back OUT of the keystore and save it as ascii
|
||||
* so the clients can get to it.
|
||||
*/
|
||||
private void exportCert(File ks) {
|
||||
File sdir = new SecureDirectory(_context.getConfigDir(), "certificates");
|
||||
if (sdir.exists() || sdir.mkdir()) {
|
||||
try {
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
InputStream fis = new FileInputStream(ks);
|
||||
String ksPass = _context.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
|
||||
keyStore.load(fis, ksPass.toCharArray());
|
||||
fis.close();
|
||||
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);
|
||||
}
|
||||
} 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
|
||||
*/
|
||||
private boolean initializeFactory(File ks) {
|
||||
String ksPass = _context.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
|
||||
String keyPass = _context.getProperty(PROP_KEY_PASSWORD);
|
||||
if (keyPass == null) {
|
||||
_log.error("No key password, set " + PROP_KEY_PASSWORD +
|
||||
" in " + (new File(_context.getConfigDir(), "router.config")).getAbsolutePath());
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
SSLContext sslc = SSLContext.getInstance("TLS");
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
InputStream fis = new FileInputStream(ks);
|
||||
keyStore.load(fis, ksPass.toCharArray());
|
||||
fis.close();
|
||||
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
kmf.init(keyStore, keyPass.toCharArray());
|
||||
sslc.init(kmf.getKeyManagers(), null, _context.random());
|
||||
_factory = sslc.getServerSocketFactory();
|
||||
return true;
|
||||
} catch (GeneralSecurityException gse) {
|
||||
_log.error("Error loading SSL keys", gse);
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Error loading SSL keys", ioe);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a SSLServerSocket.
|
||||
*/
|
||||
@Override
|
||||
protected ServerSocket getServerSocket() throws IOException {
|
||||
ServerSocket rv;
|
||||
if (_bindAllInterfaces) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Listening on port " + _port + " on all interfaces");
|
||||
rv = _factory.createServerSocket(_port);
|
||||
} else {
|
||||
String listenInterface = _context.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_HOST,
|
||||
ClientManagerFacadeImpl.DEFAULT_HOST);
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Listening on port " + _port + " of the specific interface: " + listenInterface);
|
||||
rv = _factory.createServerSocket(_port, 0, InetAddress.getByName(listenInterface));
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create (if necessary) and load the key store, then run.
|
||||
*/
|
||||
@Override
|
||||
public void runServer() {
|
||||
File keyStore = new File(_context.getConfigDir(), "keystore/i2cp.ks");
|
||||
if (verifyKeyStore(keyStore) && initializeFactory(keyStore)) {
|
||||
super.runServer();
|
||||
} else {
|
||||
_log.error("SSL I2CP server error - Failed to create or open key store");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden because SSL handshake may need more time,
|
||||
* and available() in super doesn't work.
|
||||
* The handshake doesn't start until a read().
|
||||
*/
|
||||
@Override
|
||||
protected boolean validate(Socket socket) {
|
||||
try {
|
||||
InputStream is = socket.getInputStream();
|
||||
int oldTimeout = socket.getSoTimeout();
|
||||
socket.setSoTimeout(4 * CONNECT_TIMEOUT);
|
||||
boolean rv = is.read() == I2PClient.PROTOCOL_BYTE;
|
||||
socket.setSoTimeout(oldTimeout);
|
||||
return rv;
|
||||
} catch (IOException ioe) {}
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Peer did not authenticate themselves as I2CP quickly enough, dropping");
|
||||
return false;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user