Merge bang-tls: full-session TLS + narya 1.20
Merges the TLS transport work (PR #1). Conflict resolution note: master carried an abandoned June-14 experiment pointing deps at local-stack SNAPSHOTs (ooo-util 1.6-SNAPSHOT, depot 1.9-SNAPSHOT, jmustache 1.17-SNAPSHOT) plus the upstream post-release 1.20-SNAPSHOT bump; the merge resolves all three poms to the bang-tls state — released deps (ooo-util 1.5, depot 1.8, jmustache 1.16) and version 1.20, which is exactly what was built, verified against Bang, and published to GitHub Packages.
This commit is contained in:
+11
-3
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>com.threerings</groupId>
|
<groupId>com.threerings</groupId>
|
||||||
<artifactId>narya-parent</artifactId>
|
<artifactId>narya-parent</artifactId>
|
||||||
<version>1.20-SNAPSHOT</version>
|
<version>1.20</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>narya</artifactId>
|
<artifactId>narya</artifactId>
|
||||||
@@ -16,19 +16,27 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.threerings</groupId>
|
<groupId>com.threerings</groupId>
|
||||||
<artifactId>ooo-util</artifactId>
|
<artifactId>ooo-util</artifactId>
|
||||||
<version>1.6-SNAPSHOT</version>
|
<version>1.5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.inject</groupId>
|
<groupId>com.google.inject</groupId>
|
||||||
<artifactId>guice</artifactId>
|
<artifactId>guice</artifactId>
|
||||||
<version>5.1.0</version>
|
<version>5.1.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<!-- thin ByteChannel-over-SSLEngine wrapper used to give the Presents NIO transport
|
||||||
|
full-session TLS (see com.threerings.presents.net.TlsContextFactory); MIT, no
|
||||||
|
transitive deps, targets Java 8 -->
|
||||||
|
<groupId>com.github.marianobarrios</groupId>
|
||||||
|
<artifactId>tls-channel</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- optional dependencies -->
|
<!-- optional dependencies -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.samskivert</groupId>
|
<groupId>com.samskivert</groupId>
|
||||||
<artifactId>depot</artifactId>
|
<artifactId>depot</artifactId>
|
||||||
<version>1.9-SNAPSHOT</version>
|
<version>1.8</version>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import static com.threerings.NaryaLog.log;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
|
import java.nio.channels.ByteChannel;
|
||||||
import java.nio.channels.SelectionKey;
|
import java.nio.channels.SelectionKey;
|
||||||
import java.nio.channels.SocketChannel;
|
import java.nio.channels.SocketChannel;
|
||||||
|
|
||||||
@@ -33,6 +34,9 @@ public abstract class Connection implements NetEventHandler
|
|||||||
{
|
{
|
||||||
_cmgr = cmgr;
|
_cmgr = cmgr;
|
||||||
_channel = channel;
|
_channel = channel;
|
||||||
|
// by default we do plaintext I/O directly on the raw socket channel; if TLS is enabled
|
||||||
|
// a TLS-wrapping ByteChannel will be installed via setIoChannel() before any I/O happens
|
||||||
|
_ioChannel = channel;
|
||||||
_lastEvent = createStamp;
|
_lastEvent = createStamp;
|
||||||
_connectionId = ++_lastConnectionId;
|
_connectionId = ++_lastConnectionId;
|
||||||
}
|
}
|
||||||
@@ -53,6 +57,26 @@ public abstract class Connection implements NetEventHandler
|
|||||||
return _channel;
|
return _channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the byte channel through which application data should be read and written. This is
|
||||||
|
* the raw socket channel for plaintext connections, or a TLS-wrapping channel when TLS is
|
||||||
|
* enabled. Selector registration, connection-pending checks and close still operate on the raw
|
||||||
|
* {@link #getChannel} socket channel.
|
||||||
|
*/
|
||||||
|
public ByteChannel getIoChannel ()
|
||||||
|
{
|
||||||
|
return _ioChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs the byte channel through which application data is read and written. Used to wrap
|
||||||
|
* the raw socket channel with a TLS channel when TLS is enabled.
|
||||||
|
*/
|
||||||
|
public void setIoChannel (ByteChannel ioChannel)
|
||||||
|
{
|
||||||
|
_ioChannel = ioChannel;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the address associated with this connection or null if it has no underlying socket
|
* Returns the address associated with this connection or null if it has no underlying socket
|
||||||
* channel.
|
* channel.
|
||||||
@@ -163,6 +187,16 @@ public abstract class Connection implements NetEventHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.debug("Closing channel " + this + ".");
|
log.debug("Closing channel " + this + ".");
|
||||||
|
// if we wrapped the raw socket in a TLS channel, close it first (best effort) so it can
|
||||||
|
// emit its close_notify; closing the raw channel afterward is idempotent
|
||||||
|
if (_ioChannel != null && _ioChannel != _channel) {
|
||||||
|
try {
|
||||||
|
_ioChannel.close();
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
// best effort; we're tearing down anyway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ioChannel = null;
|
||||||
try {
|
try {
|
||||||
_channel.close();
|
_channel.close();
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
@@ -176,6 +210,10 @@ public abstract class Connection implements NetEventHandler
|
|||||||
protected ConnectionManager _cmgr;
|
protected ConnectionManager _cmgr;
|
||||||
protected SocketChannel _channel;
|
protected SocketChannel _channel;
|
||||||
|
|
||||||
|
/** The channel through which we do application I/O. Defaults to {@link #_channel} (plaintext);
|
||||||
|
* replaced with a TLS-wrapping channel when TLS is enabled. */
|
||||||
|
protected ByteChannel _ioChannel;
|
||||||
|
|
||||||
protected long _lastEvent;
|
protected long _lastEvent;
|
||||||
|
|
||||||
protected int _connectionId;
|
protected int _connectionId;
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ import com.samskivert.util.Tuple;
|
|||||||
|
|
||||||
import com.threerings.nio.SelectorIterable;
|
import com.threerings.nio.SelectorIterable;
|
||||||
|
|
||||||
|
import tlschannel.NeedsReadException;
|
||||||
|
import tlschannel.NeedsWriteException;
|
||||||
|
|
||||||
import static com.threerings.NaryaLog.log;
|
import static com.threerings.NaryaLog.log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -378,8 +381,10 @@ public abstract class ConnectionManager extends LoopingThread
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// then write the data to the socket
|
// then write the data to the io channel (the raw socket for plaintext connections, or
|
||||||
int wrote = sochan.write(_outbuf);
|
// the TLS-wrapping channel when TLS is enabled); the connection-pending guard above
|
||||||
|
// still uses the raw socket channel
|
||||||
|
int wrote = conn.getIoChannel().write(_outbuf);
|
||||||
noteWrite(1, wrote);
|
noteWrite(1, wrote);
|
||||||
|
|
||||||
// if we didn't write our entire message, deal with the leftover bytes
|
// if we didn't write our entire message, deal with the leftover bytes
|
||||||
@@ -388,6 +393,18 @@ public abstract class ConnectionManager extends LoopingThread
|
|||||||
pwh.handlePartialWrite(conn, _outbuf);
|
pwh.handlePartialWrite(conn, _outbuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (NeedsWriteException nwe) {
|
||||||
|
// the TLS layer couldn't flush all its bytes to the socket right now; treat this just
|
||||||
|
// like a partial write and stash the remainder in the overflow queue for the next tick
|
||||||
|
fully = false;
|
||||||
|
pwh.handlePartialWrite(conn, _outbuf);
|
||||||
|
|
||||||
|
} catch (NeedsReadException nre) {
|
||||||
|
// the TLS layer needs inbound bytes (handshake) before it can write; treat as a partial
|
||||||
|
// write. OP_READ is always set so the inbound data will arrive and the next tick retries
|
||||||
|
fully = false;
|
||||||
|
pwh.handlePartialWrite(conn, _outbuf);
|
||||||
|
|
||||||
} catch (NotYetConnectedException nyce) {
|
} catch (NotYetConnectedException nyce) {
|
||||||
// this should be caught by isConnectionPending() but awesomely it's not
|
// this should be caught by isConnectionPending() but awesomely it's not
|
||||||
pwh.handlePartialWrite(conn, _outbuf);
|
pwh.handlePartialWrite(conn, _outbuf);
|
||||||
@@ -537,8 +554,18 @@ public abstract class ConnectionManager extends LoopingThread
|
|||||||
return false; // not ready to write to this connection yet
|
return false; // not ready to write to this connection yet
|
||||||
}
|
}
|
||||||
|
|
||||||
// write all we can of our partial buffer
|
// write all we can of our partial buffer through the io channel (raw socket for
|
||||||
int wrote = sochan.write(_partial);
|
// plaintext, TLS-wrapping channel when TLS is enabled)
|
||||||
|
int wrote;
|
||||||
|
try {
|
||||||
|
wrote = conn.getIoChannel().write(_partial);
|
||||||
|
} catch (NeedsWriteException nwe) {
|
||||||
|
// TLS can't flush right now; leave the partial for the next tick
|
||||||
|
return false;
|
||||||
|
} catch (NeedsReadException nre) {
|
||||||
|
// TLS needs inbound handshake bytes first; leave the partial for the next tick
|
||||||
|
return false;
|
||||||
|
}
|
||||||
noteWrite(0, wrote);
|
noteWrite(0, wrote);
|
||||||
|
|
||||||
if (_partial.remaining() == 0) {
|
if (_partial.remaining() == 0) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import java.net.InetAddress;
|
|||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.channels.AsynchronousCloseException;
|
import java.nio.channels.AsynchronousCloseException;
|
||||||
|
import java.nio.channels.ByteChannel;
|
||||||
import java.nio.channels.DatagramChannel;
|
import java.nio.channels.DatagramChannel;
|
||||||
import java.nio.channels.SelectionKey;
|
import java.nio.channels.SelectionKey;
|
||||||
import java.nio.channels.Selector;
|
import java.nio.channels.Selector;
|
||||||
@@ -22,6 +23,8 @@ import java.security.MessageDigest;
|
|||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
|
||||||
import com.samskivert.util.LoopingThread;
|
import com.samskivert.util.LoopingThread;
|
||||||
import com.samskivert.util.Queue;
|
import com.samskivert.util.Queue;
|
||||||
import com.samskivert.util.StringUtil;
|
import com.samskivert.util.StringUtil;
|
||||||
@@ -289,6 +292,17 @@ public class BlockingCommunicator extends Communicator
|
|||||||
if (_channel != null) {
|
if (_channel != null) {
|
||||||
log.debug("Closing socket channel.");
|
log.debug("Closing socket channel.");
|
||||||
|
|
||||||
|
// if we wrapped the socket in a TLS channel, close it first (best effort) so it can
|
||||||
|
// emit its close_notify; closing the raw channel afterward is idempotent
|
||||||
|
if (_ioChannel != null && _ioChannel != _channel) {
|
||||||
|
try {
|
||||||
|
_ioChannel.close();
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
// best effort; we're tearing down anyway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ioChannel = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_channel.close();
|
_channel.close();
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
@@ -407,7 +421,9 @@ public class BlockingCommunicator extends Communicator
|
|||||||
protected int writeMessage (ByteBuffer buf)
|
protected int writeMessage (ByteBuffer buf)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
return _channel.write(buf);
|
// write through the io channel: the raw socket for plaintext, or the TLS channel when TLS
|
||||||
|
// is enabled. In blocking mode the TLS channel drives the handshake and blocks transparently
|
||||||
|
return _ioChannel.write(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -502,7 +518,9 @@ public class BlockingCommunicator extends Communicator
|
|||||||
protected boolean readFrame ()
|
protected boolean readFrame ()
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
return _fin.readFrame(_channel);
|
// read through the io channel: the raw socket for plaintext, or the TLS channel when TLS is
|
||||||
|
// enabled (which decrypts and drives the handshake transparently in blocking mode)
|
||||||
|
return _fin.readFrame(_ioChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -664,6 +682,18 @@ public class BlockingCommunicator extends Communicator
|
|||||||
_channel.configureBlocking(true);
|
_channel.configureBlocking(true);
|
||||||
_channel.socket().setKeepAlive(true);
|
_channel.socket().setKeepAlive(true);
|
||||||
|
|
||||||
|
// if a TLS context was injected, wrap the connected (blocking) socket in a client TLS
|
||||||
|
// channel; the handshake is driven transparently on the first read/write. Otherwise we
|
||||||
|
// do plaintext I/O directly on the raw socket, exactly as before. Both the Reader and
|
||||||
|
// Writer threads use this same instance: the reader only reads and the writer only
|
||||||
|
// writes, which is the one-unwrap + one-wrap concurrency that tls-channel allows.
|
||||||
|
SSLContext sslContext = _client.getSSLContext();
|
||||||
|
if (sslContext != null) {
|
||||||
|
_ioChannel = tlschannel.ClientTlsChannel.newBuilder(_channel, sslContext).build();
|
||||||
|
} else {
|
||||||
|
_ioChannel = _channel;
|
||||||
|
}
|
||||||
|
|
||||||
// our messages are framed (preceded by their length), so we use these helper streams
|
// our messages are framed (preceded by their length), so we use these helper streams
|
||||||
// to manage the framing
|
// to manage the framing
|
||||||
_fin = new FramedInputStream();
|
_fin = new FramedInputStream();
|
||||||
@@ -1008,6 +1038,11 @@ public class BlockingCommunicator extends Communicator
|
|||||||
protected DatagramWriter _datagramWriter;
|
protected DatagramWriter _datagramWriter;
|
||||||
|
|
||||||
protected SocketChannel _channel;
|
protected SocketChannel _channel;
|
||||||
|
|
||||||
|
/** The channel through which we do application I/O: the raw {@link #_channel} for plaintext, or
|
||||||
|
* a TLS-wrapping channel when TLS is enabled. Shared by the reader and writer threads. */
|
||||||
|
protected ByteChannel _ioChannel;
|
||||||
|
|
||||||
protected Queue<UpstreamMessage> _msgq = new Queue<UpstreamMessage>();
|
protected Queue<UpstreamMessage> _msgq = new Queue<UpstreamMessage>();
|
||||||
|
|
||||||
protected Selector _selector;
|
protected Selector _selector;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ package com.threerings.presents.client;
|
|||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
|
|
||||||
import com.samskivert.util.Interval;
|
import com.samskivert.util.Interval;
|
||||||
@@ -220,6 +222,26 @@ public class Client
|
|||||||
return key == null ? false : setPublicKey(SecureUtil.stringToRSAPublicKey(key));
|
return key == null ? false : setPublicKey(SecureUtil.stringToRSAPublicKey(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link SSLContext} used to wrap the connection to the server in full-session TLS,
|
||||||
|
* or null if TLS is not enabled.
|
||||||
|
*/
|
||||||
|
public SSLContext getSSLContext ()
|
||||||
|
{
|
||||||
|
return _sslContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link SSLContext} used to wrap the entire connection to the server in TLS. When set
|
||||||
|
* (non-null), the communicator wraps its socket in a client TLS channel before authenticating;
|
||||||
|
* when null (the default) the connection remains plaintext and behaves exactly as before. TLS
|
||||||
|
* is strictly opt-in. This must be set before any call to <code>logon</code>.
|
||||||
|
*/
|
||||||
|
public void setSSLContext (SSLContext ctx)
|
||||||
|
{
|
||||||
|
_sslContext = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets if we require a secure authentication.
|
* Sets if we require a secure authentication.
|
||||||
*/
|
*/
|
||||||
@@ -1061,6 +1083,9 @@ public class Client
|
|||||||
/** Our public key. */
|
/** Our public key. */
|
||||||
protected PublicKey _publicKey;
|
protected PublicKey _publicKey;
|
||||||
|
|
||||||
|
/** The TLS context used to wrap our connection to the server, or null if TLS is disabled. */
|
||||||
|
protected SSLContext _sslContext;
|
||||||
|
|
||||||
/** Our session secret key. */
|
/** Our session secret key. */
|
||||||
protected byte[] _secret;
|
protected byte[] _secret;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
//
|
||||||
|
// Narya library - tools for developing networked games
|
||||||
|
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
|
||||||
|
// https://github.com/threerings/narya/blob/master/LICENSE
|
||||||
|
|
||||||
|
package com.threerings.presents.net;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
|
||||||
|
import javax.net.ssl.KeyManagerFactory;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
|
||||||
|
import static com.threerings.presents.Log.log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the {@link SSLContext}s used to give the Presents NIO transport full-session TLS.
|
||||||
|
*
|
||||||
|
* <p>The server side loads a keystore holding its certificate + private key. The client side
|
||||||
|
* loads a truststore holding the certificate(s) it is willing to trust (for a self-hosted /
|
||||||
|
* community game with no public CA, this is the pinned server certificate). Both default to
|
||||||
|
* TLS 1.3 only.
|
||||||
|
*
|
||||||
|
* <p>This is deliberately a thin wrapper over standard JSSE: the actual TLS is driven by an
|
||||||
|
* {@code SSLEngine} (wrapped by the tls-channel library) in the connection manager and the
|
||||||
|
* client communicator. Nothing here rolls its own crypto.
|
||||||
|
*/
|
||||||
|
public class TlsContextFactory
|
||||||
|
{
|
||||||
|
/** The TLS protocol we negotiate. TLS 1.3 gives us forward secrecy and authenticated
|
||||||
|
* encryption with none of the legacy-cipher / IV footguns. */
|
||||||
|
public static final String PROTOCOL = "TLSv1.3";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a server {@link SSLContext} from a keystore containing the server certificate and its
|
||||||
|
* private key.
|
||||||
|
*
|
||||||
|
* @param keystorePath path to a PKCS12 (or JKS) keystore.
|
||||||
|
* @param storePassword password protecting the keystore (and assumed to also protect the key
|
||||||
|
* entry; standard for a single-entry server keystore).
|
||||||
|
*/
|
||||||
|
public static SSLContext serverContext (String keystorePath, char[] storePassword)
|
||||||
|
throws GeneralSecurityException, IOException
|
||||||
|
{
|
||||||
|
KeyStore ks = loadStore(keystorePath, storePassword);
|
||||||
|
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
|
||||||
|
KeyManagerFactory.getDefaultAlgorithm());
|
||||||
|
kmf.init(ks, storePassword);
|
||||||
|
|
||||||
|
SSLContext ctx = SSLContext.getInstance(PROTOCOL);
|
||||||
|
ctx.init(kmf.getKeyManagers(), null, null);
|
||||||
|
log.info("Initialized server TLS context", "protocol", PROTOCOL, "keystore", keystorePath);
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a client {@link SSLContext} that trusts exactly the certificate(s) in the supplied
|
||||||
|
* truststore. For our self-signed deployment this is effectively certificate pinning: the
|
||||||
|
* client trusts only the server cert we shipped it, not the public CA roots.
|
||||||
|
*
|
||||||
|
* @param truststorePath path to a PKCS12 (or JKS) truststore.
|
||||||
|
* @param storePassword password protecting the truststore.
|
||||||
|
*/
|
||||||
|
public static SSLContext clientContext (String truststorePath, char[] storePassword)
|
||||||
|
throws GeneralSecurityException, IOException
|
||||||
|
{
|
||||||
|
KeyStore ts = loadStore(truststorePath, storePassword);
|
||||||
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
|
||||||
|
TrustManagerFactory.getDefaultAlgorithm());
|
||||||
|
tmf.init(ts);
|
||||||
|
|
||||||
|
SSLContext ctx = SSLContext.getInstance(PROTOCOL);
|
||||||
|
ctx.init(null, tmf.getTrustManagers(), null);
|
||||||
|
log.info("Initialized client TLS context", "protocol", PROTOCOL, "truststore",
|
||||||
|
truststorePath);
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a keystore/truststore, inferring the type (PKCS12 vs JKS) from the file extension and
|
||||||
|
* falling back to the platform default.
|
||||||
|
*/
|
||||||
|
protected static KeyStore loadStore (String path, char[] password)
|
||||||
|
throws GeneralSecurityException, IOException
|
||||||
|
{
|
||||||
|
Path p = Paths.get(path);
|
||||||
|
if (!Files.isReadable(p)) {
|
||||||
|
throw new IOException("TLS keystore not found or unreadable: " + path);
|
||||||
|
}
|
||||||
|
String type = path.endsWith(".jks") ? "JKS" :
|
||||||
|
(path.endsWith(".p12") || path.endsWith(".pfx") || path.endsWith(".pkcs12")) ?
|
||||||
|
"PKCS12" : KeyStore.getDefaultType();
|
||||||
|
KeyStore ks = KeyStore.getInstance(type);
|
||||||
|
try (InputStream in = Files.newInputStream(p)) {
|
||||||
|
ks.load(in, password);
|
||||||
|
}
|
||||||
|
return ks;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1319,6 +1319,27 @@ public abstract class PeerManager
|
|||||||
return PeerNode.class;
|
return PeerNode.class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link SSLContext} that {@link PeerNode}s use to wrap their connections to other
|
||||||
|
* nodes in TLS. When the cluster's client port is TLS-enabled, peer connections (which connect
|
||||||
|
* to that same port) must also speak TLS; this is the client-side (pinned-truststore) context
|
||||||
|
* the peer node installs on its client before logon. Null (the default) leaves peer connections
|
||||||
|
* plaintext. Set this before peers are discovered/connected.
|
||||||
|
*/
|
||||||
|
public void setPeerClientContext (javax.net.ssl.SSLContext ctx)
|
||||||
|
{
|
||||||
|
_peerClientContext = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link SSLContext} peer nodes use to wrap connections to other nodes, or null if
|
||||||
|
* peer connections are plaintext.
|
||||||
|
*/
|
||||||
|
public javax.net.ssl.SSLContext getPeerClientContext ()
|
||||||
|
{
|
||||||
|
return _peerClientContext;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates credentials that a {@link PeerNode} can use to authenticate with another node.
|
* Creates credentials that a {@link PeerNode} can use to authenticate with another node.
|
||||||
*/
|
*/
|
||||||
@@ -1812,6 +1833,9 @@ public abstract class PeerManager
|
|||||||
protected String _nodeName;
|
protected String _nodeName;
|
||||||
protected String _sharedSecret;
|
protected String _sharedSecret;
|
||||||
protected NodeRecord _self;
|
protected NodeRecord _self;
|
||||||
|
|
||||||
|
/** TLS context peer nodes use when connecting to other nodes, or null for plaintext peering. */
|
||||||
|
protected javax.net.ssl.SSLContext _peerClientContext;
|
||||||
protected NodeObject _nodeobj;
|
protected NodeObject _nodeobj;
|
||||||
protected String _nodeNamespace;
|
protected String _nodeNamespace;
|
||||||
protected Map<String,PeerNode> _peers = Maps.newHashMap();
|
protected Map<String,PeerNode> _peers = Maps.newHashMap();
|
||||||
|
|||||||
@@ -140,6 +140,9 @@ public class PeerNode
|
|||||||
|
|
||||||
// otherwise configure our client with the right bits and logon
|
// otherwise configure our client with the right bits and logon
|
||||||
_client.setCredentials(_peermgr.createCreds());
|
_client.setCredentials(_peermgr.createCreds());
|
||||||
|
// if the cluster's client port is TLS-enabled, peer connections must speak TLS too;
|
||||||
|
// null (plaintext peering) is a no-op (see PeerManager.setPeerClientContext)
|
||||||
|
_client.setSSLContext(_peermgr.getPeerClientContext());
|
||||||
_client.setServer(hostName, new int[] { _record.port });
|
_client.setServer(hostName, new int[] { _record.port });
|
||||||
_client.logon();
|
_client.logon();
|
||||||
_lastConnectStamp = System.currentTimeMillis();
|
_lastConnectStamp = System.currentTimeMillis();
|
||||||
|
|||||||
@@ -42,6 +42,17 @@ public class DatagramChannelReader
|
|||||||
|
|
||||||
public void bind()
|
public void bind()
|
||||||
{
|
{
|
||||||
|
// fail closed: we have no DTLS, so binding datagram ports while the TCP session is
|
||||||
|
// TLS-encrypted would silently send these datagrams in plaintext, defeating the point of
|
||||||
|
// TLS. Refuse to bind rather than create that footgun. (Bang passes zero datagram ports, so
|
||||||
|
// this guard never triggers today; it prevents a future regression.)
|
||||||
|
if (_datagramPorts.length > 0 && _conMan.isTlsEnabled()) {
|
||||||
|
log.error("Refusing to bind datagram ports while TLS is enabled: UDP datagrams are " +
|
||||||
|
"not encrypted (no DTLS) and must not run alongside the encrypted TLS " +
|
||||||
|
"session.", "ports", _datagramPorts.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (int port : _datagramPorts) {
|
for (int port : _datagramPorts) {
|
||||||
try {
|
try {
|
||||||
acceptDatagrams(port);
|
acceptDatagrams(port);
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.ByteChannel;
|
||||||
import java.nio.channels.DatagramChannel;
|
import java.nio.channels.DatagramChannel;
|
||||||
|
import java.nio.channels.SelectionKey;
|
||||||
import java.nio.channels.SocketChannel;
|
import java.nio.channels.SocketChannel;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
@@ -30,6 +32,9 @@ import com.threerings.presents.util.DatagramSequencer;
|
|||||||
import com.threerings.nio.conman.Connection;
|
import com.threerings.nio.conman.Connection;
|
||||||
import com.threerings.nio.conman.ConnectionManager;
|
import com.threerings.nio.conman.ConnectionManager;
|
||||||
|
|
||||||
|
import tlschannel.NeedsReadException;
|
||||||
|
import tlschannel.NeedsWriteException;
|
||||||
|
|
||||||
import static com.threerings.presents.Log.log;
|
import static com.threerings.presents.Log.log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -207,9 +212,14 @@ public class PresentsConnection extends Connection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// read frames through the io channel: the raw socket for plaintext connections, or the
|
||||||
|
// TLS-wrapping channel when TLS is enabled (which decrypts and drives the handshake)
|
||||||
|
ByteChannel ioChannel = getIoChannel();
|
||||||
|
boolean tls = (ioChannel != _channel);
|
||||||
|
|
||||||
// there may be more than one frame in the buffer, so we keep reading them until we run
|
// there may be more than one frame in the buffer, so we keep reading them until we run
|
||||||
// out of data
|
// out of data
|
||||||
while (_fin.readFrame(_channel)) {
|
while (_fin.readFrame(ioChannel)) {
|
||||||
// make a note of how many bytes are in this frame (including the frame length
|
// make a note of how many bytes are in this frame (including the frame length
|
||||||
// bytes which aren't reported in available())
|
// bytes which aren't reported in available())
|
||||||
bytesIn = _fin.available() + 4;
|
bytesIn = _fin.available() + 4;
|
||||||
@@ -220,6 +230,28 @@ public class PresentsConnection extends Connection
|
|||||||
_handler.handleMessage(msg);
|
_handler.handleMessage(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we read all we could without the TLS layer asking us to write; if we previously set
|
||||||
|
// OP_WRITE to flush a pending handshake write, clear it again so we don't spin (OP_READ
|
||||||
|
// always stays set). No-op for plaintext connections, which never touch OP_WRITE.
|
||||||
|
if (tls && selkey != null && (selkey.interestOps() & SelectionKey.OP_WRITE) != 0) {
|
||||||
|
selkey.interestOps(selkey.interestOps() & ~SelectionKey.OP_WRITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (NeedsReadException nre) {
|
||||||
|
// the TLS layer has no complete frame for us yet (or the handshake needs more inbound
|
||||||
|
// bytes); stop reading this turn. The key stays OP_READ so the next readable event
|
||||||
|
// resumes us. Return whatever whole frames we managed to read above.
|
||||||
|
return bytesIn;
|
||||||
|
|
||||||
|
} catch (NeedsWriteException nwe) {
|
||||||
|
// the TLS layer needs to write (handshake) but the socket isn't writable right now; add
|
||||||
|
// OP_WRITE interest so handleEvent is re-invoked when writable (processIncomingEvents
|
||||||
|
// dispatches on any ready key), at which point our read pump flushes the handshake write
|
||||||
|
if (selkey != null) {
|
||||||
|
selkey.interestOps(selkey.interestOps() | SelectionKey.OP_WRITE);
|
||||||
|
}
|
||||||
|
return bytesIn;
|
||||||
|
|
||||||
} catch (EOFException eofe) {
|
} catch (EOFException eofe) {
|
||||||
// close down the socket gracefully
|
// close down the socket gracefully
|
||||||
close();
|
close();
|
||||||
|
|||||||
+58
-1
@@ -17,6 +17,8 @@ import java.nio.channels.SocketChannel;
|
|||||||
|
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
@@ -193,6 +195,34 @@ public class PresentsConnectionManager extends ConnectionManager
|
|||||||
return _privateKey;
|
return _privateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link SSLContext} used to wrap accepted client connections in full-session TLS. If
|
||||||
|
* this is null (the default) connections remain plaintext and behave exactly as before; TLS is
|
||||||
|
* strictly opt-in. This must be set before the connection manager starts accepting sockets.
|
||||||
|
*/
|
||||||
|
public void setSSLContext (SSLContext ctx)
|
||||||
|
{
|
||||||
|
_sslContext = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link SSLContext} used to wrap accepted connections in TLS, or null if TLS is
|
||||||
|
* not enabled.
|
||||||
|
*/
|
||||||
|
public SSLContext getSSLContext ()
|
||||||
|
{
|
||||||
|
return _sslContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if full-session TLS is enabled on this connection manager (i.e. an
|
||||||
|
* {@link SSLContext} has been injected).
|
||||||
|
*/
|
||||||
|
public boolean isTlsEnabled ()
|
||||||
|
{
|
||||||
|
return _sslContext != null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a datagram message is ready to be read off its channel.
|
* Called when a datagram message is ready to be read off its channel.
|
||||||
*/
|
*/
|
||||||
@@ -475,7 +505,25 @@ public class PresentsConnectionManager extends ConnectionManager
|
|||||||
@Override
|
@Override
|
||||||
protected void handleAcceptedSocket (SocketChannel channel)
|
protected void handleAcceptedSocket (SocketChannel channel)
|
||||||
{
|
{
|
||||||
handleAcceptedSocket(channel, new AuthingConnection());
|
AuthingConnection conn = new AuthingConnection();
|
||||||
|
// the super implementation configures the channel non-blocking, inits the connection and
|
||||||
|
// registers it for OP_READ; afterwards the connection's io channel is the raw socket
|
||||||
|
handleAcceptedSocket(channel, conn);
|
||||||
|
|
||||||
|
// if TLS is enabled, wrap the (now non-blocking) raw channel in a server-side TLS channel
|
||||||
|
// and install it as the connection's io channel before any read or write happens. This
|
||||||
|
// must happen here, after init+registration, so the AuthingConnection's very first frame is
|
||||||
|
// read through the TLS channel and the handshake is driven transparently
|
||||||
|
if (_sslContext != null && !conn.isClosed()) {
|
||||||
|
try {
|
||||||
|
tlschannel.ServerTlsChannel tlsChannel =
|
||||||
|
tlschannel.ServerTlsChannel.newBuilder(channel, _sslContext).build();
|
||||||
|
conn.setIoChannel(tlsChannel);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warning("Failed to wrap accepted socket in TLS", "channel", channel, e);
|
||||||
|
conn.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -493,6 +541,12 @@ public class PresentsConnectionManager extends ConnectionManager
|
|||||||
rconn.init(this, conn.getChannel(), iterStamp);
|
rconn.init(this, conn.getChannel(), iterStamp);
|
||||||
rconn.selkey = conn.selkey;
|
rconn.selkey = conn.selkey;
|
||||||
|
|
||||||
|
// carry over the io channel from the authing connection: if TLS is enabled this is
|
||||||
|
// the TLS channel with an established session, and the running connection must keep
|
||||||
|
// using it rather than reverting to the raw (plaintext) socket that init() defaults
|
||||||
|
// to. For plaintext connections this is just the raw socket channel (a no-op).
|
||||||
|
rconn.setIoChannel(conn.getIoChannel());
|
||||||
|
|
||||||
// we need to keep using the same object input and output streams from the
|
// we need to keep using the same object input and output streams from the
|
||||||
// beginning of the session because they have context that needs to be preserved
|
// beginning of the session because they have context that needs to be preserved
|
||||||
rconn.inheritStreams(conn);
|
rconn.inheritStreams(conn);
|
||||||
@@ -607,6 +661,9 @@ public class PresentsConnectionManager extends ConnectionManager
|
|||||||
protected List<ChainedAuthenticator> _authors = Lists.newArrayList();
|
protected List<ChainedAuthenticator> _authors = Lists.newArrayList();
|
||||||
protected PrivateKey _privateKey;
|
protected PrivateKey _privateKey;
|
||||||
|
|
||||||
|
/** The TLS context used to wrap accepted connections, or null if TLS is disabled. */
|
||||||
|
protected SSLContext _sslContext;
|
||||||
|
|
||||||
protected Queue<AuthingConnection> _authq = Queue.newQueue();
|
protected Queue<AuthingConnection> _authq = Queue.newQueue();
|
||||||
protected Queue<Tuple<Connection, InetSocketAddress>> _connectq = Queue.newQueue();
|
protected Queue<Tuple<Connection, InetSocketAddress>> _connectq = Queue.newQueue();
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<groupId>com.threerings</groupId>
|
<groupId>com.threerings</groupId>
|
||||||
<artifactId>narya-parent</artifactId>
|
<artifactId>narya-parent</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<version>1.20-SNAPSHOT</version>
|
<version>1.20</version>
|
||||||
|
|
||||||
<name>Narya Parent</name>
|
<name>Narya Parent</name>
|
||||||
<description>Facilities for making networked multiplayer games.</description>
|
<description>Facilities for making networked multiplayer games.</description>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<connection>scm:git:git://github.com/threerings/narya.git</connection>
|
<connection>scm:git:git://github.com/threerings/narya.git</connection>
|
||||||
<developerConnection>scm:git:git@github.com:threerings/narya.git</developerConnection>
|
<developerConnection>scm:git:git@github.com:threerings/narya.git</developerConnection>
|
||||||
<url>https://github.com/threerings/narya/</url>
|
<url>https://github.com/threerings/narya/</url>
|
||||||
<tag>HEAD</tag>
|
<tag>narya-1.19</tag>
|
||||||
</scm>
|
</scm>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>com.threerings</groupId>
|
<groupId>com.threerings</groupId>
|
||||||
<artifactId>narya-parent</artifactId>
|
<artifactId>narya-parent</artifactId>
|
||||||
<version>1.20-SNAPSHOT</version>
|
<version>1.20</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>narya-tools</artifactId>
|
<artifactId>narya-tools</artifactId>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.samskivert</groupId>
|
<groupId>com.samskivert</groupId>
|
||||||
<artifactId>jmustache</artifactId>
|
<artifactId>jmustache</artifactId>
|
||||||
<version>1.17-SNAPSHOT</version>
|
<version>1.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.javassist</groupId>
|
<groupId>org.javassist</groupId>
|
||||||
|
|||||||
Reference in New Issue
Block a user