Jetty 9.4.x 2711 tls 13 compliance (#2857)

Issue #2711 - TLS 1.3 compliance.

Disabled few tests that are TLS 1.2 specific.
Renegotiation in SslConnection is now skipped for TLS 1.3.
Replaced SNI keystore DSA certificate with RSA certificate.
First full build achieved with JDK 11+28.
Small changes after review.
Modified the test case to pass in JDK 8, where the implementation
throws SSLException, while in later JDKs throws SSLHandshakeException.
Minor cleanup

Signed-off-by: Greg Wilkins <gregw@webtide.com>
Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2018-08-28 13:43:45 +02:00 committed by Greg Wilkins
parent 9f6747d741
commit 7cf027b98f
9 changed files with 173 additions and 101 deletions

View File

@ -23,6 +23,7 @@ import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
@ -43,12 +44,14 @@ import org.eclipse.jetty.io.ssl.SslHandshakeListener;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.JavaVersion;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ExecutorThreadPool;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
public class HttpClientTLSTest
@ -245,6 +248,10 @@ public class HttpClientTLSTest
@Test
public void testMismatchBetweenTLSProtocolAndTLSCiphersOnClient() throws Exception
{
// In JDK 11, a mismatch on the client does not generate any bytes towards
// the server, while in TLS 1.2 the client sends to the server the close_notify.
Assume.assumeThat(JavaVersion.VERSION.getPlatform(), Matchers.lessThan(11));
SslContextFactory serverTLSFactory = createSslContextFactory();
startServer(serverTLSFactory, new EmptyServerHandler());
@ -330,6 +337,9 @@ public class HttpClientTLSTest
@Test
public void testHandshakeSucceededWithSessionResumption() throws Exception
{
// Excluded because of a bug in JDK 11+27 where session resumption does not work.
Assume.assumeThat(JavaVersion.VERSION.getPlatform(), Matchers.lessThan(11));
SslContextFactory serverTLSFactory = createSslContextFactory();
startServer(serverTLSFactory, new EmptyServerHandler());
@ -407,6 +417,9 @@ public class HttpClientTLSTest
@Test
public void testClientRawCloseDoesNotInvalidateSession() throws Exception
{
// Excluded because of a bug in JDK 11+27 where session resumption does not work.
Assume.assumeThat(JavaVersion.VERSION.getPlatform(), Matchers.lessThan(11));
SslContextFactory serverTLSFactory = createSslContextFactory();
startServer(serverTLSFactory, new EmptyServerHandler());
@ -427,6 +440,17 @@ public class HttpClientTLSTest
sslSocket.startHandshake();
Assert.assertTrue(handshakeLatch1.await(5, TimeUnit.SECONDS));
// In TLS 1.3 the server sends a NewSessionTicket post-handshake message
// to enable session resumption and without a read, the message is not processed.
try
{
sslSocket.setSoTimeout(1000);
sslSocket.getInputStream().read();
}
catch (SocketTimeoutException expected)
{
}
// The client closes abruptly.
socket.close();

View File

@ -22,6 +22,7 @@ import java.security.cert.Certificate;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;
@ -96,9 +97,7 @@ public class NeedWantClientAuthTest
@Test
public void testWantClientAuthWithoutAuth() throws Exception
{
SslContextFactory serverSSL = new SslContextFactory();
serverSSL.setKeyStorePath("src/test/resources/keystore.jks");
serverSSL.setKeyStorePassword("storepwd");
SslContextFactory serverSSL = createSslContextFactory();
serverSSL.setWantClientAuth(true);
startServer(serverSSL, new EmptyServerHandler());
@ -115,9 +114,7 @@ public class NeedWantClientAuthTest
@Test
public void testWantClientAuthWithAuth() throws Exception
{
SslContextFactory serverSSL = new SslContextFactory();
serverSSL.setKeyStorePath("src/test/resources/keystore.jks");
serverSSL.setKeyStorePassword("storepwd");
SslContextFactory serverSSL = createSslContextFactory();
serverSSL.setWantClientAuth(true);
startServer(serverSSL, new EmptyServerHandler());
CountDownLatch handshakeLatch = new CountDownLatch(1);
@ -157,9 +154,14 @@ public class NeedWantClientAuthTest
@Test
public void testNeedClientAuthWithoutAuth() throws Exception
{
SslContextFactory serverSSL = new SslContextFactory();
serverSSL.setKeyStorePath("src/test/resources/keystore.jks");
serverSSL.setKeyStorePassword("storepwd");
// In TLS 1.2, the TLS handshake on the client finishes after the TLS handshake on the server.
// The server detects the lack of the client certificate, fails its TLS handshake and sends
// bad_certificate to the client, which then fails its own TLS handshake.
// In TLS 1.3, the TLS handshake on the client finishes before the TLS handshake on the server.
// The server still sends bad_certificate to the client, but the client handshake has already
// completed successfully its TLS handshake.
SslContextFactory serverSSL = createSslContextFactory();
serverSSL.setNeedClientAuth(true);
startServer(serverSSL, new EmptyServerHandler());
@ -168,6 +170,13 @@ public class NeedWantClientAuthTest
CountDownLatch handshakeLatch = new CountDownLatch(1);
client.addBean(new SslHandshakeListener()
{
@Override
public void handshakeSucceeded(Event event)
{
if ("TLSv1.3".equals(event.getSSLEngine().getSession().getProtocol()))
handshakeLatch.countDown();
}
@Override
public void handshakeFailed(Event event, Throwable failure)
{
@ -182,7 +191,11 @@ public class NeedWantClientAuthTest
.send(result ->
{
if (result.isFailed())
latch.countDown();
{
Throwable failure = result.getFailure();
if (failure instanceof SSLException)
latch.countDown();
}
});
Assert.assertTrue(handshakeLatch.await(5, TimeUnit.SECONDS));
@ -192,9 +205,7 @@ public class NeedWantClientAuthTest
@Test
public void testNeedClientAuthWithAuth() throws Exception
{
SslContextFactory serverSSL = new SslContextFactory();
serverSSL.setKeyStorePath("src/test/resources/keystore.jks");
serverSSL.setKeyStorePassword("storepwd");
SslContextFactory serverSSL = createSslContextFactory();
serverSSL.setNeedClientAuth(true);
startServer(serverSSL, new EmptyServerHandler());
CountDownLatch handshakeLatch = new CountDownLatch(1);

View File

@ -42,9 +42,12 @@ import org.eclipse.jetty.client.util.FutureResponseListener;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.JavaVersion;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
@ -59,17 +62,20 @@ public class SslBytesClientTest extends SslBytesTest
@Before
public void init() throws Exception
{
// This whole test is very specific to how TLS < 1.3 works.
Assume.assumeThat(JavaVersion.VERSION.getPlatform(), Matchers.lessThan(11));
threadPool = Executors.newCachedThreadPool();
client = new HttpClient(new SslContextFactory(true));
sslContextFactory = new SslContextFactory(true);
client = new HttpClient(sslContextFactory);
client.setMaxConnectionsPerDestination(1);
File keyStore = MavenTestingUtils.getTestResourceFile("keystore.jks");
sslContextFactory = client.getSslContextFactory();
sslContextFactory.setKeyStorePath(keyStore.getAbsolutePath());
sslContextFactory.setKeyStorePassword("storepwd");
client.start();
SSLContext sslContext = sslContextFactory.getSslContext();
SSLContext sslContext = this.sslContextFactory.getSslContext();
acceptor = (SSLServerSocket)sslContext.getServerSocketFactory().createServerSocket(0);
int serverPort = acceptor.getLocalPort();

View File

@ -67,6 +67,7 @@ import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.OS;
import org.eclipse.jetty.util.JavaVersion;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.hamcrest.Matchers;
@ -94,6 +95,9 @@ public class SslBytesServerTest extends SslBytesTest
@Before
public void init() throws Exception
{
// This whole test is very specific to how TLS < 1.3 works.
Assume.assumeThat(JavaVersion.VERSION.getPlatform(), Matchers.lessThan(11));
threadPool = Executors.newCachedThreadPool();
server = new Server();

View File

@ -79,8 +79,7 @@ import org.eclipse.jetty.util.thread.Invocable;
public class SslConnection extends AbstractConnection
{
private static final Logger LOG = Log.getLogger(SslConnection.class);
// TODO reduce the about of debug
private static final String TLS_1_3 = "TLSv1.3";
private enum Handshake
{
@ -597,9 +596,8 @@ public class SslConnection extends AbstractConnection
{
if (unwrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED)
handshakeSucceeded();
// Check whether re-negotiation is allowed
if (!allowRenegotiate(_sslEngine.getHandshakeStatus()))
if (isRenegotiating() && !allowRenegotiate())
return filled = -1;
// If bytes were produced, don't bother with the handshake status;
@ -911,7 +909,7 @@ public class SslConnection extends AbstractConnection
if (wrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED)
handshakeSucceeded();
if (!allowRenegotiate(_sslEngine.getHandshakeStatus()))
if (isRenegotiating() && !allowRenegotiate())
{
getEndPoint().shutdownOutput();
if (allConsumed && BufferUtil.isEmpty(_encryptedOutput))
@ -1053,28 +1051,25 @@ public class SslConnection extends AbstractConnection
{
try
{
boolean close;
boolean flush = false;
boolean close = false;
synchronized(_decryptedEndPoint)
{
boolean ishut = isInputShutdown();
boolean oshut = isOutputShutdown();
boolean ishut = getEndPoint().isInputShutdown();
boolean oshut = getEndPoint().isOutputShutdown();
if (LOG.isDebugEnabled())
LOG.debug("shutdownOutput: {} oshut={}, ishut={} {}", SslConnection.this, oshut, ishut);
if (oshut)
return;
closeOutbound();
if (!_closedOutbound)
{
_closedOutbound=true; // Only attempt this once
closeOutbound();
flush = true;
_closedOutbound = true;
// Flush only once.
flush = !oshut;
}
// TODO review close logic here
if (ishut)
close = true;
close = ishut;
}
if (flush)
@ -1199,17 +1194,19 @@ public class SslConnection extends AbstractConnection
}
}
@Override
public String toString()
private boolean isRenegotiating()
{
return super.toEndPointString();
if (_handshake.get() == Handshake.INITIAL)
return false;
if (isTLS13())
return false;
if (_sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING)
return false;
return true;
}
private boolean allowRenegotiate(HandshakeStatus handshakeStatus)
{
if (_handshake.get() == Handshake.INITIAL || handshakeStatus == HandshakeStatus.NOT_HANDSHAKING)
return true;
private boolean allowRenegotiate()
{
if (!isRenegotiationAllowed())
{
if (LOG.isDebugEnabled())
@ -1217,7 +1214,7 @@ public class SslConnection extends AbstractConnection
terminateInput();
return false;
}
if (getRenegotiationLimit()==0)
{
if (LOG.isDebugEnabled())
@ -1225,10 +1222,22 @@ public class SslConnection extends AbstractConnection
terminateInput();
return false;
}
return true;
}
private boolean isTLS13()
{
String protocol = _sslEngine.getSession().getProtocol();
return TLS_1_3.equals(protocol);
}
@Override
public String toString()
{
return super.toEndPointString();
}
private final class IncompleteWriteCallback implements Callback, Invocable
{
@Override

View File

@ -47,19 +47,17 @@ import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.util.thread.TimerScheduler;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
public class SslConnectionTest
{
private final static int TIMEOUT = 1000000;
private static SslContextFactory __sslCtxFactory=new SslContextFactory();
private static final int TIMEOUT = 1000000;
private static ByteBufferPool __byteBufferPool = new LeakTrackingByteBufferPool(new MappedByteBufferPool.Tagged());
private final SslContextFactory _sslCtxFactory =new SslContextFactory();
protected volatile EndPoint _lastEndp;
private volatile boolean _testFill=true;
private volatile FutureCallback _writeCallback;
@ -81,11 +79,11 @@ public class SslConnectionTest
@Override
public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment)
{
SSLEngine engine = __sslCtxFactory.newSSLEngine();
SSLEngine engine = _sslCtxFactory.newSSLEngine();
engine.setUseClientMode(false);
SslConnection sslConnection = new SslConnection(__byteBufferPool, getExecutor(), endpoint, engine);
sslConnection.setRenegotiationAllowed(__sslCtxFactory.isRenegotiationAllowed());
sslConnection.setRenegotiationLimit(__sslCtxFactory.getRenegotiationLimit());
sslConnection.setRenegotiationAllowed(_sslCtxFactory.isRenegotiationAllowed());
sslConnection.setRenegotiationLimit(_sslCtxFactory.getRenegotiationLimit());
Connection appConnection = new TestConnection(sslConnection.getDecryptedEndPoint());
sslConnection.getDecryptedEndPoint().setConnection(appConnection);
return sslConnection;
@ -131,26 +129,20 @@ public class SslConnectionTest
return super.flush(buffers);
}
}
@BeforeClass
public static void initSslEngine() throws Exception
{
File keystore = MavenTestingUtils.getTestResourceFile("keystore");
__sslCtxFactory.setKeyStorePath(keystore.getAbsolutePath());
__sslCtxFactory.setKeyStorePassword("storepwd");
__sslCtxFactory.setKeyManagerPassword("keypwd");
__sslCtxFactory.start();
}
@AfterClass
public static void stopSsl() throws Exception
{
__sslCtxFactory.stop();
}
@Before
public void startManager() throws Exception
public void initSSL() throws Exception
{
File keystore = MavenTestingUtils.getTestResourceFile("keystore");
_sslCtxFactory.setKeyStorePath(keystore.getAbsolutePath());
_sslCtxFactory.setKeyStorePassword("storepwd");
_sslCtxFactory.setKeyManagerPassword("keypwd");
_sslCtxFactory.setRenegotiationAllowed(true);
_sslCtxFactory.setRenegotiationLimit(-1);
startManager();
}
private void startManager() throws Exception
{
_testFill=true;
_writeCallback=null;
@ -160,15 +152,23 @@ public class SslConnectionTest
_threadPool.start();
_scheduler.start();
_manager.start();
__sslCtxFactory.setRenegotiationAllowed(true);
__sslCtxFactory.setRenegotiationLimit(-1);
}
private void startSSL() throws Exception
{
_sslCtxFactory.start();
}
@After
public void stopManager() throws Exception
public void stopSSL() throws Exception
{
if (_lastEndp.isOpen())
stopManager();
_sslCtxFactory.stop();
}
private void stopManager() throws Exception
{
if (_lastEndp != null && _lastEndp.isOpen())
_lastEndp.close();
_manager.stop();
_scheduler.stop();
@ -253,9 +253,10 @@ public class SslConnectionTest
}
}
}
protected SSLSocket newClient() throws IOException
{
SSLSocket socket = __sslCtxFactory.newSslSocket();
SSLSocket socket = _sslCtxFactory.newSslSocket();
socket.connect(_connector.socket().getLocalSocketAddress());
return socket;
}
@ -263,6 +264,7 @@ public class SslConnectionTest
@Test
public void testHelloWorld() throws Exception
{
startSSL();
try (Socket client = newClient())
{
client.setSoTimeout(TIMEOUT);
@ -289,6 +291,7 @@ public class SslConnectionTest
@Test
public void testRenegotiate() throws Exception
{
startSSL();
try (SSLSocket client = newClient())
{
client.setSoTimeout(TIMEOUT);
@ -316,8 +319,10 @@ public class SslConnectionTest
@Test
public void testRenegotiateNotAllowed() throws Exception
{
__sslCtxFactory.setRenegotiationAllowed(false);
// TLS 1.3 and beyond do not support renegotiation.
_sslCtxFactory.setIncludeProtocols("TLSv1.2");
_sslCtxFactory.setRenegotiationAllowed(false);
startSSL();
try (SSLSocket client = newClient())
{
client.setSoTimeout(TIMEOUT);
@ -332,6 +337,7 @@ public class SslConnectionTest
Assert.assertEquals(5, len);
Assert.assertEquals("Hello", new String(buffer, 0, len, StandardCharsets.UTF_8));
// Try to renegotiate, must fail.
client.startHandshake();
client.getOutputStream().write("World".getBytes(StandardCharsets.UTF_8));
@ -351,9 +357,11 @@ public class SslConnectionTest
@Test
public void testRenegotiateLimit() throws Exception
{
__sslCtxFactory.setRenegotiationAllowed(true);
__sslCtxFactory.setRenegotiationLimit(2);
// TLS 1.3 and beyond do not support renegotiation.
_sslCtxFactory.setIncludeProtocols("TLSv1.2");
_sslCtxFactory.setRenegotiationAllowed(true);
_sslCtxFactory.setRenegotiationLimit(2);
startSSL();
try (SSLSocket client = newClient())
{
client.setSoTimeout(TIMEOUT);
@ -403,7 +411,7 @@ public class SslConnectionTest
{
_testFill=false;
_writeCallback = new FutureCallback();
startSSL();
try (SSLSocket client = newClient())
{
client.setSoTimeout(TIMEOUT);
@ -428,6 +436,7 @@ public class SslConnectionTest
@Test
public void testBlockedWrite() throws Exception
{
startSSL();
try (Socket client = newClient())
{
client.setSoTimeout(5000);
@ -458,6 +467,7 @@ public class SslConnectionTest
@Test
public void testManyLines() throws Exception
{
startSSL();
try (Socket client = newClient())
{
client.setSoTimeout(10000);

View File

@ -4,14 +4,16 @@
<artifactId>jetty-project</artifactId>
<version>9.4.12-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-jaspi</artifactId>
<name>Jetty :: JASPI Security</name>
<description>Jetty security infrastructure</description>
<url>http://www.eclipse.org/jetty</url>
<properties>
<bundle-symbolic-name>${project.groupId}.security.jaspi</bundle-symbolic-name>
</properties>
<build>
<plugins>
<plugin>
@ -23,24 +25,7 @@
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>jdk9</id>
<activation>
<jdk>[1.9,)</jdk>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>@{argLine} --add-modules java.se.ee</argLine>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
@ -56,6 +41,24 @@
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.security.auth.message</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>javax.activation-api</artifactId>
<version>1.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.geronimo.components</groupId>
<artifactId>geronimo-jaspi</artifactId>

View File

@ -59,10 +59,12 @@ import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.JavaVersion;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.hamcrest.Matchers;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -74,12 +76,12 @@ import static org.junit.Assert.assertThat;
@RunWith(Parameterized.class)
public class HttpInputIntegrationTest
{
enum Mode { BLOCKING, ASYNC_DISPATCHED, ASYNC_OTHER_DISPATCHED, ASYNC_OTHER_WAIT }
public final static String EOF = "__EOF__";
public final static String DELAY = "__DELAY__";
public final static String ABORT = "__ABORT__";
private static Server __server;
private static HttpConfiguration __config;
private static HttpConfiguration __sslConfig;
@ -342,10 +344,13 @@ public class HttpInputIntegrationTest
assertThat(response,Matchers.containsString("read="+_read));
assertThat(response,Matchers.containsString("sum="+sum));
}
@Test
public void testStress() throws Exception
{
// JDK 11's SSLSocket is not reliable enough to run this test.
Assume.assumeThat(JavaVersion.VERSION.getPlatform(), Matchers.lessThan(11));
System.err.printf("[%d] STRESS c=%s, m=%s, delayDispatch=%b delayInFrame=%s content-length:%d expect=%d read=%d content:%s%n",_id,_client.getSimpleName(),_mode,__config.isDelayDispatchUntilContent(),_delay,_length,_status,_read,_send);
int sum=0;