Remove `nonApplicationWrite` from `SSLDriver` (#42954)

Currently, when the SSLEngine needs to produce handshake or close data,
we must manually call the nonApplicationWrite method. However, this data
is only required when something triggers the need (starting handshake,
reading from the wire, initiating close, etc). As we have a dedicated
outbound buffer, this data can be produced automatically. Additionally,
with this refactoring, we combine handshake and application mode into a
single mode. This is necessary as there are non-application messages that
are sent post handshake in TLS 1.3. Finally, this commit modifies the
SSLDriver tests to test against TLS 1.3.
This commit is contained in:
Tim Brooks 2019-06-06 17:44:40 -04:00 committed by GitHub
parent 61b62125b8
commit 667c613d9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 255 additions and 423 deletions

View File

@ -95,7 +95,7 @@ public abstract class ChannelContext<S extends SelectableChannel & NetworkChanne
return closeContext.isDone() == false;
}
void handleException(Exception e) {
protected void handleException(Exception e) {
exceptionHandler.accept(e);
}

View File

@ -16,6 +16,7 @@ import org.elasticsearch.nio.SocketChannelContext;
import org.elasticsearch.nio.WriteOperation;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.util.LinkedList;
@ -33,7 +34,8 @@ import java.util.function.Predicate;
public final class SSLChannelContext extends SocketChannelContext {
private static final long CLOSE_TIMEOUT_NANOS = new TimeValue(10, TimeUnit.SECONDS).nanos();
private static final Runnable DEFAULT_TIMEOUT_CANCELLER = () -> {};
private static final Runnable DEFAULT_TIMEOUT_CANCELLER = () -> {
};
private final SSLDriver sslDriver;
private final InboundChannelBuffer networkReadBuffer;
@ -68,9 +70,17 @@ public final class SSLChannelContext extends SocketChannelContext {
public void queueWriteOperation(WriteOperation writeOperation) {
getSelector().assertOnSelectorThread();
if (writeOperation instanceof CloseNotifyOperation) {
sslDriver.initiateClose();
long relativeNanos = CLOSE_TIMEOUT_NANOS + System.nanoTime();
closeTimeoutCanceller = getSelector().getTaskScheduler().scheduleAtRelativeTime(this::channelCloseTimeout, relativeNanos);
try {
sslDriver.initiateClose();
SSLOutboundBuffer outboundBuffer = sslDriver.getOutboundBuffer();
if (outboundBuffer.hasEncryptedBytesToFlush()) {
encryptedFlushes.addLast(outboundBuffer.buildNetworkFlushOperation());
}
long relativeNanos = CLOSE_TIMEOUT_NANOS + System.nanoTime();
closeTimeoutCanceller = getSelector().getTaskScheduler().scheduleAtRelativeTime(this::channelCloseTimeout, relativeNanos);
} catch (SSLException e) {
handleException(e);
}
} else {
super.queueWriteOperation(writeOperation);
}
@ -92,39 +102,25 @@ public final class SSLChannelContext extends SocketChannelContext {
}
// If the driver is ready for application writes, we can attempt to proceed with any queued writes.
if (sslDriver.readyForApplicationWrites()) {
FlushOperation unencryptedFlush;
while (pendingChannelFlush() == false && (unencryptedFlush = getPendingFlush()) != null) {
if (unencryptedFlush.isFullyFlushed()) {
currentFlushOperationComplete();
} else {
try {
// Attempt to encrypt application write data. The encrypted data ends up in the
// outbound write buffer.
sslDriver.write(unencryptedFlush);
SSLOutboundBuffer outboundBuffer = sslDriver.getOutboundBuffer();
if (outboundBuffer.hasEncryptedBytesToFlush() == false) {
break;
}
encryptedFlushes.addLast(outboundBuffer.buildNetworkFlushOperation());
// Flush the write buffer to the channel
flushEncryptedOperation();
} catch (IOException e) {
currentFlushOperationFailed(e);
throw e;
FlushOperation unencryptedFlush;
while (pendingChannelFlush() == false && (unencryptedFlush = getPendingFlush()) != null) {
if (unencryptedFlush.isFullyFlushed()) {
currentFlushOperationComplete();
} else {
try {
// Attempt to encrypt application write data. The encrypted data ends up in the
// outbound write buffer.
sslDriver.write(unencryptedFlush);
SSLOutboundBuffer outboundBuffer = sslDriver.getOutboundBuffer();
if (outboundBuffer.hasEncryptedBytesToFlush() == false) {
break;
}
}
}
} else {
// We are not ready for application writes, check if the driver has non-application writes. We
// only want to continue producing new writes if the outbound write buffer is fully flushed.
while (pendingChannelFlush() == false && sslDriver.needsNonApplicationWrite()) {
sslDriver.nonApplicationWrite();
// If non-application writes were produced, flush the outbound write buffer.
SSLOutboundBuffer outboundBuffer = sslDriver.getOutboundBuffer();
if (outboundBuffer.hasEncryptedBytesToFlush()) {
encryptedFlushes.addFirst(outboundBuffer.buildNetworkFlushOperation());
encryptedFlushes.addLast(outboundBuffer.buildNetworkFlushOperation());
// Flush the write buffer to the channel
flushEncryptedOperation();
} catch (IOException e) {
currentFlushOperationFailed(e);
throw e;
}
}
}
@ -147,10 +143,10 @@ public final class SSLChannelContext extends SocketChannelContext {
@Override
public boolean readyForFlush() {
getSelector().assertOnSelectorThread();
if (sslDriver.readyForApplicationWrites()) {
if (sslDriver.readyForApplicationData()) {
return pendingChannelFlush() || super.readyForFlush();
} else {
return pendingChannelFlush() || sslDriver.needsNonApplicationWrite();
return pendingChannelFlush();
}
}

View File

@ -33,17 +33,14 @@ import java.util.function.IntFunction;
* decrypted and placed into the application buffer passed as an argument. Otherwise, it will be consumed
* internally and advance the SSL/TLS close or handshake process.
*
* Producing writes for a channel is more complicated. The method {@link #needsNonApplicationWrite()} can be
* called to determine if this driver needs to produce more data to advance the handshake or close process.
* If that method returns true, {@link #nonApplicationWrite()} should be called (and the
* data produced then flushed to the channel) until no further non-application writes are needed.
* When the handshake begins, handshake data is read from the wire, or the channel close initiated, internal
* bytes that need to be written will be produced. The bytes will be placed in the outbound buffer for
* flushing to a channel.
*
* If no non-application writes are needed, {@link #readyForApplicationWrites()} can be called to determine
* if the driver is ready to consume application data. (Note: It is possible that
* {@link #readyForApplicationWrites()} and {@link #needsNonApplicationWrite()} can both return false if the
* driver is waiting on non-application data from the peer.) If the driver indicates it is ready for
* application writes, {@link #write(FlushOperation)} can be called. This method will
* encrypt flush operation application data and place it in the outbound buffer for flushing to a channel.
* The method {@link #readyForApplicationData()} can be called to determine if the driver is ready to consume
* application data. If the driver indicates it is ready for application writes,
* {@link #write(FlushOperation)} can be called. This method will encrypt flush operation application data
* and place it in the outbound buffer for flushing to a channel.
*
* If you are ready to close the channel {@link #initiateClose()} should be called. After that is called, the
* driver will start producing non-application writes related to notifying the peer connection that this
@ -62,7 +59,7 @@ public class SSLDriver implements AutoCloseable {
private final boolean isClientMode;
// This should only be accessed by the network thread associated with this channel, so nothing needs to
// be volatile.
private Mode currentMode = new HandshakeMode();
private Mode currentMode = new RegularMode();
private int packetSize;
public SSLDriver(SSLEngine engine, IntFunction<Page> pageAllocator, boolean isClientMode) {
@ -76,26 +73,31 @@ public class SSLDriver implements AutoCloseable {
public void init() throws SSLException {
engine.setUseClientMode(isClientMode);
if (currentMode.isHandshake()) {
engine.beginHandshake();
((HandshakeMode) currentMode).startHandshake();
} else {
if (currentMode.isClose()) {
throw new AssertionError("Attempted to init outside from non-handshaking mode: " + currentMode.modeName());
} else {
engine.beginHandshake();
((RegularMode) currentMode).startHandshake();
try {
((RegularMode) currentMode).handshake();
} catch (SSLException e) {
currentMode = new CloseMode(((RegularMode) currentMode).isHandshaking, false);
throw e;
}
}
}
/**
* Requests a TLS renegotiation. This means the we will request that the peer performs another handshake
* prior to the continued exchange of application data. This can only be requested if we are currently in
* APPLICATION mode.
* prior to the continued exchange of application data. This can only be requested if we are currently
* not closing.
*
* @throws SSLException if the handshake cannot be initiated
*/
public void renegotiate() throws SSLException {
if (currentMode.isApplication()) {
currentMode = new HandshakeMode();
if (currentMode.isClose() == false) {
engine.beginHandshake();
((HandshakeMode) currentMode).startHandshake();
((RegularMode) currentMode).startHandshake();
} else {
throw new IllegalStateException("Attempted to renegotiate while in invalid mode: " + currentMode.modeName());
}
@ -105,10 +107,6 @@ public class SSLDriver implements AutoCloseable {
return engine;
}
public boolean isHandshaking() {
return currentMode.isHandshake();
}
public SSLOutboundBuffer getOutboundBuffer() {
return outboundBuffer;
}
@ -116,43 +114,34 @@ public class SSLDriver implements AutoCloseable {
public void read(InboundChannelBuffer encryptedBuffer, InboundChannelBuffer applicationBuffer) throws SSLException {
networkReadPage = pageAllocator.apply(packetSize);
try {
Mode modePriorToRead;
do {
modePriorToRead = currentMode;
currentMode.read(encryptedBuffer, applicationBuffer);
// It is possible that we received multiple SSL packets from the network since the last read.
// If one of those packets causes us to change modes (such as finished handshaking), we need
// to call read in the new mode to handle the remaining packets.
} while (modePriorToRead != currentMode);
boolean continueUnwrap = true;
while (continueUnwrap && encryptedBuffer.getIndex() > 0) {
int bytesConsumed = currentMode.read(encryptedBuffer, applicationBuffer);
continueUnwrap = bytesConsumed > 0;
}
} finally {
networkReadPage.close();
networkReadPage = null;
}
}
public boolean readyForApplicationWrites() {
return currentMode.isApplication();
}
public boolean needsNonApplicationWrite() {
return currentMode.needsNonApplicationWrite();
public boolean readyForApplicationData() {
return currentMode.readyForApplicationData();
}
public int write(FlushOperation applicationBytes) throws SSLException {
return currentMode.write(applicationBytes);
}
public void nonApplicationWrite() throws SSLException {
assert currentMode.isApplication() == false : "Should not be called if driver is in application mode";
if (currentMode.isApplication() == false) {
currentMode.write(EMPTY_FLUSH_OPERATION);
} else {
throw new AssertionError("Attempted to non-application write from invalid mode: " + currentMode.modeName());
int totalBytesProduced = 0;
boolean continueWrap = true;
while (continueWrap && applicationBytes.isFullyFlushed() == false) {
int bytesProduced = currentMode.write(applicationBytes);
totalBytesProduced += bytesProduced;
continueWrap = bytesProduced > 0;
}
return totalBytesProduced;
}
public void initiateClose() {
closingInternal();
public void initiateClose() throws SSLException {
internalClose();
}
public boolean isClosed() {
@ -163,7 +152,9 @@ public class SSLDriver implements AutoCloseable {
public void close() throws SSLException {
outboundBuffer.close();
ArrayList<SSLException> closingExceptions = new ArrayList<>(2);
closingInternal();
if (currentMode.isClose() == false) {
currentMode = new CloseMode(((RegularMode) currentMode).isHandshaking, false);
}
CloseMode closeMode = (CloseMode) this.currentMode;
if (closeMode.needToSendClose) {
closingExceptions.add(new SSLException("Closed engine without completely sending the close alert message."));
@ -172,8 +163,8 @@ public class SSLDriver implements AutoCloseable {
if (closeMode.needToReceiveClose) {
closingExceptions.add(new SSLException("Closed engine without receiving the close alert message."));
closeMode.closeInboundAndSwallowPeerDidNotCloseException();
}
closeMode.closeInboundAndSwallowPeerDidNotCloseException();
ExceptionsHelper.rethrowAndSuppress(closingExceptions);
}
@ -208,7 +199,7 @@ public class SSLDriver implements AutoCloseable {
break;
case CLOSED:
assert engine.isInboundDone() : "We received close_notify so read should be done";
closingInternal();
internalClose();
return result;
default:
throw new IllegalStateException("Unexpected UNWRAP result: " + result.getStatus());
@ -236,6 +227,7 @@ public class SSLDriver implements AutoCloseable {
applicationBytes.incrementIndex(result.bytesConsumed());
switch (result.getStatus()) {
case OK:
case CLOSED:
return result;
case BUFFER_UNDERFLOW:
throw new IllegalStateException("Should not receive BUFFER_UNDERFLOW on WRAP");
@ -244,19 +236,16 @@ public class SSLDriver implements AutoCloseable {
// There is not enough space in the network buffer for an entire SSL packet. We will
// allocate a buffer with the correct packet size the next time through the loop.
break;
case CLOSED:
assert result.bytesProduced() > 0 : "WRAP during close processing should produce close message.";
return result;
default:
throw new IllegalStateException("Unexpected WRAP result: " + result.getStatus());
}
}
}
private void closingInternal() {
private void internalClose() throws SSLException {
// This check prevents us from attempting to send close_notify twice
if (currentMode.isClose() == false) {
currentMode = new CloseMode(currentMode.isHandshake());
currentMode = new CloseMode(((RegularMode) currentMode).isHandshaking);
}
}
@ -267,55 +256,42 @@ public class SSLDriver implements AutoCloseable {
}
}
// There are three potential modes for the driver to be in - HANDSHAKE, APPLICATION, or CLOSE. HANDSHAKE
// is the initial mode. During this mode data that is read and written will be related to the TLS
// handshake process. Application related data cannot be encrypted until the handshake is complete. From
// HANDSHAKE mode the driver can transition to APPLICATION (if the handshake is successful) or CLOSE (if
// an error occurs or we initiate a close). In APPLICATION mode data read from the channel will be
// decrypted and placed into the buffer passed as an argument to the read call. Additionally, application
// writes will be accepted and encrypted into the outbound write buffer. APPLICATION mode will proceed
// until we receive a request for renegotiation (currently unsupported) or the CLOSE mode begins. CLOSE
// mode can begin if we receive a CLOSE_NOTIFY message from the peer or if initiateClose is called. In
// CLOSE mode we attempt to both send and receive an SSL CLOSE_NOTIFY message. The exception to this is
// when we enter CLOSE mode from HANDSHAKE mode. In this scenario we only need to send the alert to the
// peer and then close the channel. Some SSL/TLS implementations do not properly adhere to the full
// two-direction close_notify process. Additionally, in newer TLS specifications it is not required to
// wait to receive close_notify. However, we will make our best attempt to both send and receive as it is
// expected by the java SSLEngine (it throws an exception if close_notify has not been received when
// inbound is closed).
// There are two potential modes for the driver to be in - REGULAR or CLOSE. REGULAR is the initial mode.
// During this mode the initial data that is read and written will be related to the TLS handshake
// process. Application related data cannot be encrypted until the handshake is complete. Once the
// handshake is complete data read from the channel will be decrypted and placed into the buffer passed
// as an argument to the read call. Additionally, application writes will be accepted and encrypted into
// the outbound write buffer. REGULAR mode will proceed until CLOSE mode begins. CLOSE mode can begin if
// we receive a CLOSE_NOTIFY message from the peer or if initiateClose is called. In CLOSE mode we attempt
// to both send and receive an SSL CLOSE_NOTIFY message. The exception to this is when we enter CLOSE mode
// during a handshake. In this scenario we only need to send the alert to the peer and then close the
// channel. Some SSL/TLS implementations do not properly adhere to the full two-direction close_notify
// process. Additionally, in newer TLS specifications it is not required to wait to receive close_notify.
// However, we will make our best attempt to both send and receive as it is expected by the java SSLEngine
// (it throws an exception if close_notify has not been received when inbound is closed).
private interface Mode {
void read(InboundChannelBuffer encryptedBuffer, InboundChannelBuffer applicationBuffer) throws SSLException;
int read(InboundChannelBuffer encryptedBuffer, InboundChannelBuffer applicationBuffer) throws SSLException;
int write(FlushOperation applicationBytes) throws SSLException;
boolean needsNonApplicationWrite();
boolean isHandshake();
boolean isApplication();
boolean isClose();
boolean readyForApplicationData();
String modeName();
}
private class HandshakeMode implements Mode {
private class RegularMode implements Mode {
private SSLEngineResult.HandshakeStatus handshakeStatus;
private boolean isHandshaking = false;
private void startHandshake() throws SSLException {
private void startHandshake() {
handshakeStatus = engine.getHandshakeStatus();
if (handshakeStatus != SSLEngineResult.HandshakeStatus.NEED_UNWRAP) {
try {
handshake();
} catch (SSLException e) {
closingInternal();
throw e;
}
}
isHandshaking = true;
}
private void handshake() throws SSLException {
@ -323,22 +299,22 @@ public class SSLDriver implements AutoCloseable {
while (continueHandshaking) {
switch (handshakeStatus) {
case NEED_UNWRAP:
isHandshaking = true;
// We UNWRAP as much as possible immediately after a read. Do not need to do it here.
continueHandshaking = false;
break;
case NEED_WRAP:
isHandshaking = true;
handshakeStatus = wrap(outboundBuffer).getHandshakeStatus();
break;
case NEED_TASK:
runTasks();
isHandshaking = true;
handshakeStatus = engine.getHandshakeStatus();
break;
case NOT_HANDSHAKING:
maybeFinishHandshake();
continueHandshaking = false;
break;
case FINISHED:
maybeFinishHandshake();
isHandshaking = false;
continueHandshaking = false;
break;
}
@ -346,48 +322,30 @@ public class SSLDriver implements AutoCloseable {
}
@Override
public void read(InboundChannelBuffer encryptedBuffer, InboundChannelBuffer applicationBuffer) throws SSLException {
boolean continueUnwrap = true;
while (continueUnwrap && encryptedBuffer.getIndex() > 0) {
try {
SSLEngineResult result = unwrap(encryptedBuffer, applicationBuffer);
handshakeStatus = result.getHandshakeStatus();
public int read(InboundChannelBuffer encryptedBuffer, InboundChannelBuffer applicationBuffer) throws SSLException {
try {
SSLEngineResult result = unwrap(encryptedBuffer, applicationBuffer);
handshakeStatus = result.getHandshakeStatus();
if (result.getStatus() != SSLEngineResult.Status.CLOSED) {
handshake();
// If we are done handshaking we should exit the handshake read
continueUnwrap = result.bytesConsumed() > 0 && currentMode.isHandshake();
} catch (SSLException e) {
closingInternal();
throw e;
}
return result.bytesConsumed();
} catch (SSLException e) {
handshakeStatus = engine.getHandshakeStatus();
try {
internalClose();
} catch (SSLException closeException) {
e.addSuppressed(closeException);
}
throw e;
}
}
@Override
public int write(FlushOperation applicationBytes) throws SSLException {
try {
handshake();
} catch (SSLException e) {
closingInternal();
throw e;
}
return 0;
}
@Override
public boolean needsNonApplicationWrite() {
return handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP
|| handshakeStatus == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING
|| handshakeStatus == SSLEngineResult.HandshakeStatus.FINISHED;
}
@Override
public boolean isHandshake() {
return true;
}
@Override
public boolean isApplication() {
return false;
SSLEngineResult result = wrap(outboundBuffer, applicationBytes);
handshakeStatus = result.getHandshakeStatus();
return result.bytesProduced();
}
@Override
@ -395,9 +353,14 @@ public class SSLDriver implements AutoCloseable {
return false;
}
@Override
public boolean readyForApplicationData() {
return isHandshaking == false;
}
@Override
public String modeName() {
return "HANDSHAKE";
return "REGULAR";
}
private void runTasks() {
@ -406,89 +369,6 @@ public class SSLDriver implements AutoCloseable {
delegatedTask.run();
}
}
private void maybeFinishHandshake() {
if (engine.isOutboundDone() || engine.isInboundDone()) {
// If the engine is partially closed, immediate transition to close mode.
if (currentMode.isHandshake()) {
currentMode = new CloseMode(true);
} else if (currentMode.isApplication()) {
// It is possible to be in CLOSED mode if the prior UNWRAP call returned CLOSE_NOTIFY.
// However we should not be in application mode at this point.
String message = "Expected to be in handshaking/closed mode. Instead in application mode.";
throw new AssertionError(message);
}
} else {
if (currentMode.isHandshake()) {
currentMode = new ApplicationMode();
} else {
String message = "Attempted to transition to application mode from non-handshaking mode: " + currentMode;
throw new AssertionError(message);
}
}
}
}
private class ApplicationMode implements Mode {
@Override
public void read(InboundChannelBuffer encryptedBuffer, InboundChannelBuffer applicationBuffer) throws SSLException {
boolean continueUnwrap = true;
while (continueUnwrap && encryptedBuffer.getIndex() > 0) {
SSLEngineResult result = unwrap(encryptedBuffer, applicationBuffer);
boolean renegotiationRequested = result.getStatus() != SSLEngineResult.Status.CLOSED
&& maybeRenegotiation(result.getHandshakeStatus());
continueUnwrap = result.bytesConsumed() > 0 && renegotiationRequested == false;
}
}
@Override
public int write(FlushOperation applicationBytes) throws SSLException {
boolean continueWrap = true;
int totalBytesProduced = 0;
while (continueWrap && applicationBytes.isFullyFlushed() == false) {
SSLEngineResult result = wrap(outboundBuffer, applicationBytes);
int bytesProduced = result.bytesProduced();
totalBytesProduced += bytesProduced;
boolean renegotiationRequested = maybeRenegotiation(result.getHandshakeStatus());
continueWrap = bytesProduced > 0 && renegotiationRequested == false;
}
return totalBytesProduced;
}
private boolean maybeRenegotiation(SSLEngineResult.HandshakeStatus newStatus) throws SSLException {
if (newStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING && newStatus != SSLEngineResult.HandshakeStatus.FINISHED) {
renegotiate();
return true;
} else {
return false;
}
}
@Override
public boolean needsNonApplicationWrite() {
return false;
}
@Override
public boolean isHandshake() {
return false;
}
@Override
public boolean isApplication() {
return true;
}
@Override
public boolean isClose() {
return false;
}
@Override
public String modeName() {
return "APPLICATION";
}
}
private class CloseMode implements Mode {
@ -496,7 +376,11 @@ public class SSLDriver implements AutoCloseable {
private boolean needToSendClose = true;
private boolean needToReceiveClose = true;
private CloseMode(boolean isHandshaking) {
private CloseMode(boolean isHandshaking) throws SSLException {
this(isHandshaking, true);
}
private CloseMode(boolean isHandshaking, boolean tryToWrap) throws SSLException {
if (isHandshaking && engine.isInboundDone() == false) {
// If we attempt to close during a handshake either we are sending an alert and inbound
// should already be closed or we are sending a close_notify. If we send a close_notify
@ -511,60 +395,40 @@ public class SSLDriver implements AutoCloseable {
needToSendClose = false;
} else {
engine.closeOutbound();
if (tryToWrap) {
try {
boolean continueWrap = true;
while (continueWrap) {
continueWrap = wrap(outboundBuffer).getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP;
}
} finally {
needToSendClose = false;
}
}
}
}
@Override
public void read(InboundChannelBuffer encryptedBuffer, InboundChannelBuffer applicationBuffer) throws SSLException {
public int read(InboundChannelBuffer encryptedBuffer, InboundChannelBuffer applicationBuffer) throws SSLException {
if (needToReceiveClose == false) {
// There is an issue where receiving handshake messages after initiating the close process
// can place the SSLEngine back into handshaking mode. In order to handle this, if we
// initiate close during a handshake we do not wait to receive close. As we do not need to
// receive close, we will not handle reads.
return;
return 0;
}
boolean continueUnwrap = true;
while (continueUnwrap && encryptedBuffer.getIndex() > 0) {
SSLEngineResult result = unwrap(encryptedBuffer, applicationBuffer);
continueUnwrap = result.bytesProduced() > 0 || result.bytesConsumed() > 0;
}
SSLEngineResult result = unwrap(encryptedBuffer, applicationBuffer);
if (engine.isInboundDone()) {
needToReceiveClose = false;
}
return result.bytesConsumed();
}
@Override
public int write(FlushOperation applicationBytes) throws SSLException {
int bytesProduced = 0;
if (engine.isOutboundDone() == false) {
bytesProduced += wrap(outboundBuffer).bytesProduced();
if (engine.isOutboundDone()) {
needToSendClose = false;
// Close inbound if it is still open and we have decided not to wait for response.
if (needToReceiveClose == false && engine.isInboundDone() == false) {
closeInboundAndSwallowPeerDidNotCloseException();
}
}
} else {
needToSendClose = false;
}
return bytesProduced;
}
@Override
public boolean needsNonApplicationWrite() {
return needToSendClose;
}
@Override
public boolean isHandshake() {
return false;
}
@Override
public boolean isApplication() {
return false;
public int write(FlushOperation applicationBytes) {
return 0;
}
@Override
@ -572,13 +436,20 @@ public class SSLDriver implements AutoCloseable {
return true;
}
@Override
public boolean readyForApplicationData() {
return false;
}
@Override
public String modeName() {
return "CLOSE";
}
private boolean isCloseDone() {
return needToSendClose == false && needToReceiveClose == false;
// We do as much as possible to generate the outbound messages in the ctor. At this point, we are
// only interested in interrogating if we need to wait to receive the close message.
return needToReceiveClose == false;
}
private void closeInboundAndSwallowPeerDidNotCloseException() throws SSLException {

View File

@ -11,6 +11,7 @@ import org.elasticsearch.nio.Page;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.function.BiConsumer;
import java.util.function.IntFunction;
public class SSLOutboundBuffer implements AutoCloseable {
@ -29,6 +30,8 @@ public class SSLOutboundBuffer implements AutoCloseable {
if (encryptedBytesProduced != 0) {
currentPage.byteBuffer().limit(encryptedBytesProduced);
pages.addLast(currentPage);
} else if (currentPage != null) {
currentPage.close();
}
currentPage = null;
}
@ -45,6 +48,10 @@ public class SSLOutboundBuffer implements AutoCloseable {
}
FlushOperation buildNetworkFlushOperation() {
return buildNetworkFlushOperation((r, e) -> {});
}
FlushOperation buildNetworkFlushOperation(BiConsumer<Void, Exception> listener) {
int pageCount = pages.size();
ByteBuffer[] byteBuffers = new ByteBuffer[pageCount];
Page[] pagesToClose = new Page[pageCount];
@ -54,7 +61,10 @@ public class SSLOutboundBuffer implements AutoCloseable {
byteBuffers[i] = page.byteBuffer();
}
return new FlushOperation(byteBuffers, (r, e) -> IOUtils.closeWhileHandlingException(pagesToClose));
return new FlushOperation(byteBuffers, (r, e) -> {
IOUtils.closeWhileHandlingException(pagesToClose);
listener.accept(r, e);
});
}
boolean hasEncryptedBytesToFlush() {
@ -63,6 +73,7 @@ public class SSLOutboundBuffer implements AutoCloseable {
@Override
public void close() {
IOUtils.closeWhileHandlingException(currentPage);
IOUtils.closeWhileHandlingException(pages);
}
}

View File

@ -21,6 +21,7 @@ import org.junit.Before;
import org.mockito.ArgumentCaptor;
import org.mockito.stubbing.Answer;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
@ -186,8 +187,7 @@ public class SSLChannelContextTests extends ESTestCase {
}
public void testQueuedWritesAreIgnoredWhenNotReadyForAppWrites() {
when(sslDriver.readyForApplicationWrites()).thenReturn(false);
when(sslDriver.needsNonApplicationWrite()).thenReturn(false);
when(sslDriver.readyForApplicationData()).thenReturn(false);
context.queueWriteOperation(mock(FlushReadyWrite.class));
@ -195,34 +195,20 @@ public class SSLChannelContextTests extends ESTestCase {
}
public void testPendingEncryptedFlushMeansWriteInterested() throws Exception {
when(sslDriver.readyForApplicationWrites()).thenReturn(false);
when(sslDriver.needsNonApplicationWrite()).thenReturn(true, false);
doAnswer(getWriteAnswer(1, false)).when(sslDriver).nonApplicationWrite();
context.queueWriteOperation(mock(FlushReadyWrite.class));
when(sslDriver.readyForApplicationData()).thenReturn(true);
doAnswer(getWriteAnswer(1, false)).when(sslDriver).write(any(FlushOperation.class));
// Call will put bytes in buffer to flush
context.flushChannel();
assertTrue(context.readyForFlush());
}
public void testNeedsNonAppWritesMeansWriteInterested() {
when(sslDriver.readyForApplicationWrites()).thenReturn(false);
when(sslDriver.needsNonApplicationWrite()).thenReturn(true);
assertTrue(context.readyForFlush());
}
public void testNoNonAppWriteInterestInAppMode() {
when(sslDriver.readyForApplicationWrites()).thenReturn(true);
assertFalse(context.readyForFlush());
verify(sslDriver, times(0)).needsNonApplicationWrite();
}
public void testFirstFlushMustFinishForWriteToContinue() throws Exception {
when(sslDriver.readyForApplicationWrites()).thenReturn(false);
when(sslDriver.needsNonApplicationWrite()).thenReturn(true);
doAnswer(getWriteAnswer(1, false)).when(sslDriver).nonApplicationWrite();
context.queueWriteOperation(mock(FlushReadyWrite.class));
context.queueWriteOperation(mock(FlushReadyWrite.class));
when(sslDriver.readyForApplicationData()).thenReturn(true);
doAnswer(getWriteAnswer(1, false)).when(sslDriver).write(any(FlushOperation.class));
// First call will put bytes in buffer to flush
context.flushChannel();
@ -231,31 +217,7 @@ public class SSLChannelContextTests extends ESTestCase {
context.flushChannel();
assertTrue(context.readyForFlush());
verify(sslDriver, times(1)).nonApplicationWrite();
}
public void testNonAppWrites() throws Exception {
when(sslDriver.needsNonApplicationWrite()).thenReturn(true, true, false);
when(sslDriver.readyForApplicationWrites()).thenReturn(false);
doAnswer(getWriteAnswer(1, false)).when(sslDriver).nonApplicationWrite();
when(rawChannel.write(same(selector.getIoBuffer()))).thenReturn(1);
context.flushChannel();
verify(sslDriver, times(2)).nonApplicationWrite();
verify(rawChannel, times(2)).write(same(selector.getIoBuffer()));
}
public void testNonAppWritesStopIfBufferNotFullyFlushed() throws Exception {
when(sslDriver.needsNonApplicationWrite()).thenReturn(true);
when(sslDriver.readyForApplicationWrites()).thenReturn(false);
doAnswer(getWriteAnswer(1, false)).when(sslDriver).nonApplicationWrite();
when(rawChannel.write(same(selector.getIoBuffer()))).thenReturn(0);
context.flushChannel();
verify(sslDriver, times(1)).nonApplicationWrite();
verify(rawChannel, times(1)).write(same(selector.getIoBuffer()));
verify(sslDriver, times(1)).write(any(FlushOperation.class));
}
public void testQueuedWriteIsFlushedInFlushCall() throws Exception {
@ -263,7 +225,7 @@ public class SSLChannelContextTests extends ESTestCase {
FlushReadyWrite flushOperation = new FlushReadyWrite(context, buffers, listener);
context.queueWriteOperation(flushOperation);
when(sslDriver.readyForApplicationWrites()).thenReturn(true);
when(sslDriver.readyForApplicationData()).thenReturn(true);
doAnswer(getWriteAnswer(10, true)).when(sslDriver).write(eq(flushOperation));
when(rawChannel.write(same(selector.getIoBuffer()))).thenReturn(10);
@ -279,7 +241,7 @@ public class SSLChannelContextTests extends ESTestCase {
FlushReadyWrite flushOperation = new FlushReadyWrite(context, buffers, listener);
context.queueWriteOperation(flushOperation);
when(sslDriver.readyForApplicationWrites()).thenReturn(true);
when(sslDriver.readyForApplicationData()).thenReturn(true);
doAnswer(getWriteAnswer(5, true)).when(sslDriver).write(eq(flushOperation));
when(rawChannel.write(same(selector.getIoBuffer()))).thenReturn(4);
context.flushChannel();
@ -299,7 +261,7 @@ public class SSLChannelContextTests extends ESTestCase {
context.queueWriteOperation(flushOperation1);
context.queueWriteOperation(flushOperation2);
when(sslDriver.readyForApplicationWrites()).thenReturn(true);
when(sslDriver.readyForApplicationData()).thenReturn(true);
doAnswer(getWriteAnswer(5, true)).when(sslDriver).write(any(FlushOperation.class));
when(rawChannel.write(same(selector.getIoBuffer()))).thenReturn(5, 5, 2);
context.flushChannel();
@ -316,7 +278,7 @@ public class SSLChannelContextTests extends ESTestCase {
context.queueWriteOperation(flushOperation);
IOException exception = new IOException();
when(sslDriver.readyForApplicationWrites()).thenReturn(true);
when(sslDriver.readyForApplicationData()).thenReturn(true);
doAnswer(getWriteAnswer(5, true)).when(sslDriver).write(eq(flushOperation));
when(rawChannel.write(any(ByteBuffer.class))).thenThrow(exception);
expectThrows(IOException.class, () -> context.flushChannel());
@ -326,9 +288,9 @@ public class SSLChannelContextTests extends ESTestCase {
}
public void testWriteIOExceptionMeansChannelReadyToClose() throws Exception {
when(sslDriver.readyForApplicationWrites()).thenReturn(false);
when(sslDriver.needsNonApplicationWrite()).thenReturn(true);
doAnswer(getWriteAnswer(1, false)).when(sslDriver).nonApplicationWrite();
context.queueWriteOperation(mock(FlushReadyWrite.class));
when(sslDriver.readyForApplicationData()).thenReturn(true);
doAnswer(getWriteAnswer(1, false)).when(sslDriver).write(any(FlushOperation.class));
context.flushChannel();
@ -383,7 +345,7 @@ public class SSLChannelContextTests extends ESTestCase {
}
}
public void testInitiateCloseFromDifferentThreadSchedulesCloseNotify() {
public void testInitiateCloseFromDifferentThreadSchedulesCloseNotify() throws SSLException {
when(selector.isOnCurrentThread()).thenReturn(false, true);
context.closeChannel();
@ -394,7 +356,7 @@ public class SSLChannelContextTests extends ESTestCase {
verify(sslDriver).initiateClose();
}
public void testInitiateCloseFromSameThreadSchedulesCloseNotify() {
public void testInitiateCloseFromSameThreadSchedulesCloseNotify() throws SSLException {
context.closeChannel();
ArgumentCaptor<WriteOperation> captor = ArgumentCaptor.forClass(WriteOperation.class);

View File

@ -57,7 +57,6 @@ public class SSLDriverTests extends ESTestCase {
assertEquals(ByteBuffer.wrap("pong".getBytes(StandardCharsets.UTF_8)), applicationBuffer.sliceBuffersTo(4)[0]);
applicationBuffer.release(4);
assertFalse(clientDriver.needsNonApplicationWrite());
normalClose(clientDriver, serverDriver);
}
@ -82,6 +81,7 @@ public class SSLDriverTests extends ESTestCase {
SSLEngine serverEngine = sslContext.createSSLEngine();
SSLEngine clientEngine = sslContext.createSSLEngine();
// Lock the protocol to 1.2 as 1.3 does not support renegotiation
String[] serverProtocols = {"TLSv1.2"};
serverEngine.setEnabledProtocols(serverProtocols);
String[] clientProtocols = {"TLSv1.2"};
@ -98,8 +98,7 @@ public class SSLDriverTests extends ESTestCase {
applicationBuffer.release(4);
clientDriver.renegotiate();
assertTrue(clientDriver.isHandshaking());
assertFalse(clientDriver.readyForApplicationWrites());
assertFalse(clientDriver.readyForApplicationData());
// This tests that the client driver can still receive data based on the prior handshake
ByteBuffer[] buffers2 = {ByteBuffer.wrap("pong".getBytes(StandardCharsets.UTF_8))};
@ -148,7 +147,6 @@ public class SSLDriverTests extends ESTestCase {
assertEquals(ByteBuffer.wrap("pong".getBytes(StandardCharsets.UTF_8)), applicationBuffer.sliceBuffersTo(4)[0]);
applicationBuffer.release(4);
assertFalse(clientDriver.needsNonApplicationWrite());
normalClose(clientDriver, serverDriver);
}
@ -156,6 +154,7 @@ public class SSLDriverTests extends ESTestCase {
SSLContext sslContext = getSSLContext();
SSLEngine clientEngine = sslContext.createSSLEngine();
SSLEngine serverEngine = sslContext.createSSLEngine();
String[] serverProtocols = {"TLSv1.2"};
serverEngine.setEnabledProtocols(serverProtocols);
String[] clientProtocols = {"TLSv1.1"};
@ -209,26 +208,31 @@ public class SSLDriverTests extends ESTestCase {
serverDriver.init();
assertTrue(clientDriver.getOutboundBuffer().hasEncryptedBytesToFlush());
assertFalse(serverDriver.needsNonApplicationWrite());
sendHandshakeMessages(clientDriver, serverDriver);
sendHandshakeMessages(serverDriver, clientDriver);
assertTrue(clientDriver.isHandshaking());
assertTrue(serverDriver.isHandshaking());
// Sometimes send server messages before closing
if (randomBoolean()) {
sendHandshakeMessages(serverDriver, clientDriver);
if ("TLSv1.3".equals(clientDriver.getSSLEngine().getEnabledProtocols()[0])) {
assertTrue(clientDriver.readyForApplicationData());
} else {
assertFalse(clientDriver.readyForApplicationData());
}
}
assertFalse(serverDriver.readyForApplicationData());
assertFalse(serverDriver.needsNonApplicationWrite());
serverDriver.initiateClose();
assertTrue(serverDriver.needsNonApplicationWrite());
assertFalse(serverDriver.isClosed());
sendNonApplicationWrites(serverDriver);
assertTrue(serverDriver.getOutboundBuffer().hasEncryptedBytesToFlush());
// We are immediately fully closed due to SSLEngine inconsistency
assertTrue(serverDriver.isClosed());
SSLException sslException = expectThrows(SSLException.class, () -> clientDriver.read(networkReadBuffer, applicationBuffer));
assertEquals("Received close_notify during handshake", sslException.getMessage());
sendNonApplicationWrites(clientDriver);
sendNonApplicationWrites(serverDriver);
clientDriver.read(networkReadBuffer, applicationBuffer);
assertTrue(clientDriver.isClosed());
sendNonApplicationWrites(clientDriver);
serverDriver.read(networkReadBuffer, applicationBuffer);
}
@ -242,32 +246,27 @@ public class SSLDriverTests extends ESTestCase {
serverDriver.init();
assertTrue(clientDriver.getOutboundBuffer().hasEncryptedBytesToFlush());
assertFalse(serverDriver.needsNonApplicationWrite());
sendHandshakeMessages(clientDriver, serverDriver);
sendHandshakeMessages(serverDriver, clientDriver);
assertTrue(clientDriver.isHandshaking());
assertTrue(serverDriver.isHandshaking());
assertFalse(clientDriver.readyForApplicationData());
assertFalse(serverDriver.readyForApplicationData());
assertFalse(serverDriver.needsNonApplicationWrite());
serverDriver.initiateClose();
assertTrue(serverDriver.needsNonApplicationWrite());
assertFalse(serverDriver.isClosed());
assertTrue(serverDriver.getOutboundBuffer().hasEncryptedBytesToFlush());
sendNonApplicationWrites(serverDriver);
// We are immediately fully closed due to SSLEngine inconsistency
assertTrue(serverDriver.isClosed());
// This should not throw exception yet as the SSLEngine will not UNWRAP data while attempting to WRAP
// This should not throw exception yet as the SSLEngine will not UNWRAP data while attempting to WRAP
SSLException sslException = expectThrows(SSLException.class, () -> clientDriver.read(networkReadBuffer, applicationBuffer));
assertEquals("Received close_notify during handshake", sslException.getMessage());
assertTrue(clientDriver.getOutboundBuffer().hasEncryptedBytesToFlush());
sendNonApplicationWrites(clientDriver);
assertTrue(clientDriver.isClosed());
serverDriver.read(networkReadBuffer, applicationBuffer);
assertTrue(clientDriver.isClosed());
}
private void failedCloseAlert(SSLDriver sendDriver, SSLDriver receiveDriver, List<String> messages) throws SSLException {
assertTrue(sendDriver.needsNonApplicationWrite());
assertTrue(sendDriver.getOutboundBuffer().hasEncryptedBytesToFlush());
assertFalse(sendDriver.isClosed());
sendNonApplicationWrites(sendDriver);
@ -277,13 +276,8 @@ public class SSLDriverTests extends ESTestCase {
SSLException sslException = expectThrows(SSLException.class, () -> receiveDriver.read(networkReadBuffer, applicationBuffer));
assertTrue("Expected one of the following exception messages: " + messages + ". Found: " + sslException.getMessage(),
messages.stream().anyMatch(m -> sslException.getMessage().equals(m)));
if (receiveDriver.needsNonApplicationWrite() == false) {
assertTrue(receiveDriver.isClosed());
receiveDriver.close();
} else {
assertFalse(receiveDriver.isClosed());
expectThrows(SSLException.class, receiveDriver::close);
}
assertTrue(receiveDriver.isClosed());
receiveDriver.close();
}
private SSLContext getSSLContext() throws Exception {
@ -294,26 +288,28 @@ public class SSLDriverTests extends ESTestCase {
(certPath))));
KeyManager km = CertParsingUtils.keyManager(CertParsingUtils.readCertificates(Collections.singletonList(getDataPath
(certPath))), PemUtils.readPrivateKey(getDataPath(keyPath), "testclient"::toCharArray), "testclient".toCharArray());
sslContext = SSLContext.getInstance("TLSv1.2");
if (JavaVersion.current().compareTo(JavaVersion.parse("11")) >= 0) {
sslContext = SSLContext.getInstance(randomFrom("TLSv1.2", "TLSv1.3"));
} else {
sslContext = SSLContext.getInstance(randomFrom("TLSv1.2"));
}
sslContext.init(new KeyManager[] { km }, new TrustManager[] { tm }, new SecureRandom());
return sslContext;
}
private void normalClose(SSLDriver sendDriver, SSLDriver receiveDriver) throws IOException {
sendDriver.initiateClose();
assertFalse(sendDriver.readyForApplicationWrites());
assertTrue(sendDriver.needsNonApplicationWrite());
assertFalse(sendDriver.readyForApplicationData());
assertTrue(sendDriver.getOutboundBuffer().hasEncryptedBytesToFlush());
sendNonApplicationWrites(sendDriver);
assertFalse(sendDriver.isClosed());
receiveDriver.read(networkReadBuffer, applicationBuffer);
assertFalse(receiveDriver.isClosed());
assertFalse(receiveDriver.readyForApplicationWrites());
assertTrue(receiveDriver.needsNonApplicationWrite());
sendNonApplicationWrites(receiveDriver);
assertTrue(receiveDriver.getOutboundBuffer().hasEncryptedBytesToFlush());
assertTrue(receiveDriver.isClosed());
sendNonApplicationWrites(receiveDriver);
sendDriver.read(networkReadBuffer, applicationBuffer);
assertTrue(sendDriver.isClosed());
@ -322,15 +318,9 @@ public class SSLDriverTests extends ESTestCase {
assertEquals(0, openPages.get());
}
private void sendNonApplicationWrites(SSLDriver sendDriver) throws SSLException {
private void sendNonApplicationWrites(SSLDriver sendDriver) {
SSLOutboundBuffer outboundBuffer = sendDriver.getOutboundBuffer();
while (sendDriver.needsNonApplicationWrite() || outboundBuffer.hasEncryptedBytesToFlush()) {
if (outboundBuffer.hasEncryptedBytesToFlush()) {
sendData(outboundBuffer.buildNetworkFlushOperation());
} else {
sendDriver.nonApplicationWrite();
}
}
sendData(outboundBuffer.buildNetworkFlushOperation());
}
private void handshake(SSLDriver clientDriver, SSLDriver serverDriver) throws IOException {
@ -344,48 +334,50 @@ public class SSLDriverTests extends ESTestCase {
}
assertTrue(clientDriver.getOutboundBuffer().hasEncryptedBytesToFlush());
assertFalse(serverDriver.needsNonApplicationWrite());
sendHandshakeMessages(clientDriver, serverDriver);
assertTrue(clientDriver.isHandshaking());
assertTrue(serverDriver.isHandshaking());
assertFalse(clientDriver.readyForApplicationData());
assertFalse(serverDriver.readyForApplicationData());
sendHandshakeMessages(serverDriver, clientDriver);
assertTrue(clientDriver.isHandshaking());
assertTrue(serverDriver.isHandshaking());
if ("TLSv1.3".equals(clientDriver.getSSLEngine().getEnabledProtocols()[0])) {
assertTrue(clientDriver.readyForApplicationData());
assertFalse(serverDriver.readyForApplicationData());
sendHandshakeMessages(clientDriver, serverDriver);
sendHandshakeMessages(clientDriver, serverDriver);
assertTrue(clientDriver.isHandshaking());
assertTrue(clientDriver.readyForApplicationData());
assertTrue(serverDriver.readyForApplicationData());
} else {
assertFalse(clientDriver.readyForApplicationData());
assertFalse(serverDriver.readyForApplicationData());
sendHandshakeMessages(clientDriver, serverDriver);
assertFalse(clientDriver.readyForApplicationData());
assertTrue(serverDriver.readyForApplicationData());
sendHandshakeMessages(serverDriver, clientDriver);
assertTrue(clientDriver.readyForApplicationData());
assertTrue(serverDriver.readyForApplicationData());
}
sendHandshakeMessages(serverDriver, clientDriver);
assertFalse(clientDriver.isHandshaking());
assertFalse(serverDriver.isHandshaking());
}
private void sendHandshakeMessages(SSLDriver sendDriver, SSLDriver receiveDriver) throws IOException {
assertTrue(sendDriver.needsNonApplicationWrite() || sendDriver.getOutboundBuffer().hasEncryptedBytesToFlush());
assertTrue(sendDriver.getOutboundBuffer().hasEncryptedBytesToFlush());
SSLOutboundBuffer outboundBuffer = sendDriver.getOutboundBuffer();
while (sendDriver.needsNonApplicationWrite() || outboundBuffer.hasEncryptedBytesToFlush()) {
if (outboundBuffer.hasEncryptedBytesToFlush()) {
sendData(outboundBuffer.buildNetworkFlushOperation());
receiveDriver.read(networkReadBuffer, applicationBuffer);
} else {
sendDriver.nonApplicationWrite();
}
}
if (receiveDriver.isHandshaking()) {
assertTrue(receiveDriver.needsNonApplicationWrite() || receiveDriver.getOutboundBuffer().hasEncryptedBytesToFlush());
sendData(sendDriver.getOutboundBuffer().buildNetworkFlushOperation());
receiveDriver.read(networkReadBuffer, applicationBuffer);
if (receiveDriver.readyForApplicationData() == false) {
assertTrue(receiveDriver.getOutboundBuffer().hasEncryptedBytesToFlush());
}
}
private void sendAppData(SSLDriver sendDriver, ByteBuffer[] message) throws IOException {
assertFalse(sendDriver.needsNonApplicationWrite());
FlushOperation flushOperation = new FlushOperation(message, (r, l) -> {});
while (flushOperation.isFullyFlushed() == false) {