diff --git a/community/server/src/main/java/org/neo4j/server/security/ssl/SslSocketConnectorFactory.java b/community/server/src/main/java/org/neo4j/server/security/ssl/SslSocketConnectorFactory.java index fc08419cfe4ed..bd6ebbbb09767 100644 --- a/community/server/src/main/java/org/neo4j/server/security/ssl/SslSocketConnectorFactory.java +++ b/community/server/src/main/java/org/neo4j/server/security/ssl/SslSocketConnectorFactory.java @@ -74,10 +74,10 @@ private SslConnectionFactory createSslConnectionFactory( SslPolicy sslPolicy ) sslContextFactory.setIncludeCipherSuites( ciphers.toArray( new String[ciphers.size()] ) ); } - List protocols = sslPolicy.getTlsVersions(); + String[] protocols = sslPolicy.getTlsVersions(); if ( protocols != null ) { - sslContextFactory.setIncludeProtocols( protocols.toArray( new String[protocols.size()] ) ); + sslContextFactory.setIncludeProtocols( protocols ); } switch ( sslPolicy.getClientAuth() ) diff --git a/community/ssl/src/main/java/org/neo4j/ssl/SslPolicy.java b/community/ssl/src/main/java/org/neo4j/ssl/SslPolicy.java index 5060b086131d3..de8e9f75ce1c9 100644 --- a/community/ssl/src/main/java/org/neo4j/ssl/SslPolicy.java +++ b/community/ssl/src/main/java/org/neo4j/ssl/SslPolicy.java @@ -43,7 +43,7 @@ public class SslPolicy /* cryptographic parameters */ private final List ciphers; - private final List tlsVersions; + private final String[] tlsVersions; private final ClientAuth clientAuth; private final TrustManagerFactory trustManagerFactory; @@ -55,7 +55,7 @@ public SslPolicy( PrivateKey privateKey, X509Certificate[] keyCertChain, { this.privateKey = privateKey; this.keyCertChain = keyCertChain; - this.tlsVersions = tlsVersions; + this.tlsVersions = tlsVersions == null ? null : tlsVersions.toArray( new String[tlsVersions.size()] ); this.ciphers = ciphers; this.clientAuth = clientAuth; this.trustManagerFactory = trustManagerFactory; @@ -67,6 +67,7 @@ public SslContext nettyServerContext() throws SSLException return SslContextBuilder.forServer( privateKey, keyCertChain ) .sslProvider( sslProvider ) .clientAuth( forNetty( clientAuth ) ) + .protocols( tlsVersions ) .ciphers( ciphers ) .trustManager( trustManagerFactory ) .build(); @@ -77,6 +78,7 @@ public SslContext nettyClientContext() throws SSLException return SslContextBuilder.forClient() .sslProvider( sslProvider ) .keyManager( privateKey, keyCertChain ) + .protocols( tlsVersions ) .ciphers( ciphers ) .trustManager( trustManagerFactory ) .build(); @@ -112,7 +114,7 @@ private SslHandler makeNettyHandler( Channel channel, SslContext sslContext ) SSLEngine sslEngine = sslContext.newEngine( channel.alloc() ); if ( tlsVersions != null ) { - sslEngine.setEnabledProtocols( tlsVersions.toArray( new String[tlsVersions.size()] ) ); + sslEngine.setEnabledProtocols( tlsVersions ); } return new SslHandler( sslEngine ); } @@ -154,7 +156,7 @@ public List getCipherSuites() return ciphers; } - public List getTlsVersions() + public String[] getTlsVersions() { return tlsVersions; } @@ -170,7 +172,7 @@ public String toString() return "SslPolicy{" + "keyCertChain=" + describeCertChain() + ", ciphers=" + ciphers + - ", tlsVersions=" + tlsVersions + + ", tlsVersions=" + Arrays.toString( tlsVersions ) + ", clientAuth=" + clientAuth + '}'; } diff --git a/integrationtests/src/test/java/org/neo4j/ssl/SecureClient.java b/integrationtests/src/test/java/org/neo4j/ssl/SecureClient.java new file mode 100644 index 0000000000000..99c0af4751757 --- /dev/null +++ b/integrationtests/src/test/java/org/neo4j/ssl/SecureClient.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.ssl; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.concurrent.Future; + +import javax.net.ssl.SSLEngine; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.hamcrest.Matchers.equalTo; +import static org.neo4j.test.assertion.Assert.assertEventually; + +class SecureClient +{ + private Bootstrap bootstrap; + private ClientInitializer clientInitializer; + private NioEventLoopGroup eventLoopGroup; + private Channel channel; + private Bucket bucket = new Bucket(); + + SecureClient( SslContext sslContext ) + { + eventLoopGroup = new NioEventLoopGroup(); + clientInitializer = new ClientInitializer( sslContext, bucket ); + bootstrap = new Bootstrap() + .group( eventLoopGroup ) + .channel( NioSocketChannel.class ) + .handler( clientInitializer ); + } + + @SuppressWarnings( "SameParameterValue" ) + void connect( int port ) + { + ChannelFuture channelFuture = bootstrap.connect( "localhost", port ).awaitUninterruptibly(); + channel = channelFuture.channel(); + } + + void disconnect() + { + if ( channel != null ) + { + channel.close().awaitUninterruptibly(); + eventLoopGroup.shutdownGracefully( 0, 0, SECONDS ); + } + + bucket.collectedData.release(); + } + + void assertResponse( ByteBuf expected ) throws InterruptedException + { + assertEventually( channel.toString(), () -> bucket.collectedData, equalTo( expected ), 5, SECONDS ); + } + + Channel channel() + { + return channel; + } + + Future sslHandshakeFuture() + { + return clientInitializer.handshakeFuture; + } + + String ciphers() + { + return clientInitializer.sslEngine.getSession().getCipherSuite(); + } + + String protocol() + { + return clientInitializer.sslEngine.getSession().getProtocol(); + } + + static class Bucket extends SimpleChannelInboundHandler + { + private final ByteBuf collectedData; + + Bucket() + { + collectedData = ByteBufAllocator.DEFAULT.buffer(); + } + + @Override + protected void channelRead0( ChannelHandlerContext ctx, ByteBuf msg ) throws Exception + { + collectedData.writeBytes( msg ); + } + + @Override + public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause ) throws Exception + { + //cause.printStackTrace(); // for debugging + } + } + + public static class ClientInitializer extends ChannelInitializer + { + private SslContext sslContext; + private final Bucket bucket; + private Future handshakeFuture; + private SSLEngine sslEngine; + + ClientInitializer( SslContext sslContext, Bucket bucket ) + { + this.sslContext = sslContext; + this.bucket = bucket; + } + + @Override + protected void initChannel( SocketChannel channel ) throws Exception + { + ChannelPipeline pipeline = channel.pipeline(); + + this.sslEngine = sslContext.newEngine( channel.alloc() ); + this.sslEngine.setUseClientMode( true ); + + SslHandler sslHandler = new SslHandler( sslEngine ); + handshakeFuture = sslHandler.handshakeFuture(); + + pipeline.addLast( sslHandler ); + pipeline.addLast( bucket ); + } + } +} diff --git a/integrationtests/src/test/java/org/neo4j/ssl/SecureCommunicationsTest.java b/integrationtests/src/test/java/org/neo4j/ssl/SecureCommunicationsTest.java deleted file mode 100644 index 3732a7c06cc67..0000000000000 --- a/integrationtests/src/test/java/org/neo4j/ssl/SecureCommunicationsTest.java +++ /dev/null @@ -1,469 +0,0 @@ -/* - * Copyright (c) 2002-2018 "Neo Technology," - * Network Engine for Objects in Lund AB [http://neotechnology.com] - * - * This file is part of Neo4j. - * - * Neo4j is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package org.neo4j.ssl; - -import io.netty.bootstrap.Bootstrap; -import io.netty.bootstrap.ServerBootstrap; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelOption; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslHandler; -import io.netty.handler.ssl.SslProvider; -import org.apache.commons.lang3.SystemUtils; -import org.junit.After; -import org.junit.Rule; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.security.cert.CertificateException; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLException; - -import org.neo4j.kernel.configuration.Config; -import org.neo4j.kernel.configuration.ssl.SslPolicyConfig; -import org.neo4j.kernel.configuration.ssl.SslPolicyLoader; -import org.neo4j.kernel.configuration.ssl.SslSystemSettings; -import org.neo4j.logging.NullLogProvider; -import org.neo4j.test.rule.TestDirectory; -import org.neo4j.test.rule.fs.DefaultFileSystemRule; - -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.isOneOf; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; -import static org.junit.Assume.assumeThat; -import static org.junit.Assume.assumeTrue; -import static org.neo4j.ssl.SslResourceBuilder.caSignedKeyId; -import static org.neo4j.ssl.SslResourceBuilder.selfSignedKeyId; -import static org.neo4j.test.assertion.Assert.assertEventually; - -/** - * This test mainly tests the SslContextFactory when it comes to production - * code and serves as baseline implementation for how servers should inject - * SSL into the pipeline utilizing the a security defining context. - */ -public class SecureCommunicationsTest -{ - private static final int UNRELATED_ID = 5; // SslContextFactory requires us to trust something - - private static final byte[] REQUEST = {1, 2, 3, 4}; - private static final byte[] RESPONSE = {5, 6, 7, 8}; - - @Rule - public TestDirectory testDir = TestDirectory.testDirectory(); - - @Rule - public DefaultFileSystemRule fsRule = new DefaultFileSystemRule(); - - private SecureServer server; - private SecureClient client; - private ByteBuf expected; - - @After - public void cleanup() - { - if ( expected != null ) - { - expected.release(); - } - if ( client != null ) - { - client.disconnect(); - } - if ( server != null ) - { - server.stop(); - } - } - - @Test - public void partiesWithMutualTrustShouldCommunicate() throws Exception - { - // given - SslResource sslServerResource = selfSignedKeyId( 0 ).trustKeyId( 1 ).install( testDir.directory( "server" ) ); - SslResource sslClientResource = selfSignedKeyId( 1 ).trustKeyId( 0 ).install( testDir.directory( "client" ) ); - - server = new SecureServer( makeSslContext( sslServerResource, true ) ); - - server.start(); - client = new SecureClient( makeSslContext( sslClientResource, false ) ); - client.connect( server.port() ); - - // when - ByteBuf request = ByteBufAllocator.DEFAULT.buffer().writeBytes( REQUEST ); - client.channel.writeAndFlush( request ); - - // then - expected = ByteBufAllocator.DEFAULT.buffer().writeBytes( RESPONSE ); - client.clientInitializer.handshakeFuture.get(); - client.assertResponse( expected ); - } - - @Test - public void partiesWithMutualTrustThroughCAShouldCommunicate() throws Exception - { - // given - SslResource sslServerResource = caSignedKeyId( 0 ).trustSignedByCA().install( testDir.directory( "server" ) ); - SslResource sslClientResource = caSignedKeyId( 1 ).trustSignedByCA().install( testDir.directory( "client" ) ); - - server = new SecureServer( makeSslContext( sslServerResource, true ) ); - - server.start(); - client = new SecureClient( makeSslContext( sslClientResource, false ) ); - client.connect( server.port() ); - - // when - ByteBuf request = ByteBufAllocator.DEFAULT.buffer().writeBytes( REQUEST ); - client.channel.writeAndFlush( request ); - - // then - expected = ByteBufAllocator.DEFAULT.buffer().writeBytes( RESPONSE ); - client.clientInitializer.handshakeFuture.get(); - client.assertResponse( expected ); - } - - @Test - public void serverShouldNotCommunicateWithUntrustedClient() throws Exception - { - // given - SslResource sslClientResource = selfSignedKeyId( 1 ).trustKeyId( 0 ).install( testDir.directory( "client" ) ); - SslResource sslServerResource = selfSignedKeyId( 0 ).trustKeyId( UNRELATED_ID ).install( testDir.directory( "server" ) ); - - server = new SecureServer( makeSslContext( sslServerResource, true ) ); - - server.start(); - client = new SecureClient( makeSslContext( sslClientResource, false ) ); - client.connect( server.port() ); - - try - { - // when - client.clientInitializer.handshakeFuture.get(); - fail(); - } - catch ( ExecutionException e ) - { - assertThat( e.getCause(), instanceOf( SSLException.class ) ); - } - } - - @Test - public void clientShouldNotCommunicateWithUntrustedServer() throws Exception - { - // given - SslResource sslClientResource = selfSignedKeyId( 0 ).trustKeyId( UNRELATED_ID ).install( testDir.directory( "client" ) ); - SslResource sslServerResource = selfSignedKeyId( 1 ).trustKeyId( 0 ).install( testDir.directory( "server" ) ); - - server = new SecureServer( makeSslContext( sslServerResource, true ) ); - - server.start(); - client = new SecureClient( makeSslContext( sslClientResource, false ) ); - client.connect( server.port() ); - - try - { - client.clientInitializer.handshakeFuture.get(); - fail(); - } - catch ( ExecutionException e ) - { - assertThat( e.getCause(), instanceOf( SSLException.class ) ); - } - } - - @Test - public void partiesWithMutualTrustThroughCAShouldNotCommunicateWhenServerRevoked() throws Exception - { - // given - SslResource sslServerResource = caSignedKeyId( 0 ).trustSignedByCA().install( testDir.directory( "server" ) ); - SslResource sslClientResource = caSignedKeyId( 1 ).trustSignedByCA().revoke( 0 ).install( testDir.directory( "client" ) ); - - server = new SecureServer( makeSslContext( sslServerResource, true ) ); - - server.start(); - client = new SecureClient( makeSslContext( sslClientResource, false ) ); - client.connect( server.port() ); - - try - { - client.clientInitializer.handshakeFuture.get(); - fail( "Server should have been revoked" ); - } - catch ( ExecutionException e ) - { - assertThat( e.getCause(), instanceOf( SSLException.class ) ); - } - } - - @Test - public void partiesWithMutualTrustThroughCAShouldNotCommunicateWhenClientRevoked() throws Exception - { - // given - SslResource sslServerResource = caSignedKeyId( 0 ).trustSignedByCA().revoke( 1 ).install( testDir.directory( "server" ) ); - SslResource sslClientResource = caSignedKeyId( 1 ).trustSignedByCA().install( testDir.directory( "client" ) ); - - server = new SecureServer( makeSslContext( sslServerResource, true ) ); - - server.start(); - client = new SecureClient( makeSslContext( sslClientResource, false ) ); - client.connect( server.port() ); - - try - { - client.clientInitializer.handshakeFuture.get(); - fail( "Client should have been revoked" ); - } - catch ( ExecutionException e ) - { - assertThat( e.getCause(), instanceOf( SSLException.class ) ); - } - } - - @Test - public void shouldSupportOpenSSLOnSupportedPlatforms() throws Exception - { - // depends on the statically linked uber-jar with boring ssl: http://netty.io/wiki/forked-tomcat-native.html - assumeTrue( SystemUtils.IS_OS_WINDOWS || SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC_OSX ); - assumeThat( System.getProperty( "os.arch" ), equalTo( "x86_64" ) ); - assumeThat( SystemUtils.JAVA_VENDOR, isOneOf( "Oracle Corporation", "Sun Microsystems Inc." ) ); - - // given - SslResource sslServerResource = selfSignedKeyId( 0 ).trustKeyId( 1 ).install( testDir.directory( "server" ) ); - SslResource sslClientResource = selfSignedKeyId( 1 ).trustKeyId( 0 ).install( testDir.directory( "client" ) ); - - server = new SecureServer( makeSslContext( sslServerResource, true, SslProvider.OPENSSL.name() ) ); - - server.start(); - client = new SecureClient( makeSslContext( sslClientResource, false, SslProvider.OPENSSL.name() ) ); - client.connect( server.port() ); - - // when - ByteBuf request = ByteBufAllocator.DEFAULT.buffer().writeBytes( REQUEST ); - client.channel.writeAndFlush( request ); - - // then - expected = ByteBufAllocator.DEFAULT.buffer().writeBytes( RESPONSE ); - client.clientInitializer.handshakeFuture.get(); - client.assertResponse( expected ); - } - - private SslContext makeSslContext( SslResource sslResource, boolean forServer ) throws CertificateException, IOException - { - return makeSslContext( sslResource, forServer, SslProvider.JDK.name() ); - } - - private SslContext makeSslContext( SslResource sslResource, boolean forServer, String sslProvider ) throws CertificateException, IOException - { - Map config = new HashMap<>(); - config.put( SslSystemSettings.netty_ssl_provider.name(), sslProvider ); - - SslPolicyConfig policyConfig = new SslPolicyConfig( "default" ); - File baseDirectory = sslResource.privateKey().getParentFile(); - new File( baseDirectory, "trusted" ).mkdirs(); - new File( baseDirectory, "revoked" ).mkdirs(); - - config.put( policyConfig.base_directory.name(), baseDirectory.getPath() ); - config.put( policyConfig.private_key.name(), sslResource.privateKey().getPath() ); - config.put( policyConfig.public_certificate.name(), sslResource.publicCertificate().getPath() ); - config.put( policyConfig.trusted_dir.name(), sslResource.trustedDirectory().getPath() ); - config.put( policyConfig.revoked_dir.name(), sslResource.revokedDirectory().getPath() ); - - SslPolicyLoader sslPolicyFactory = SslPolicyLoader.create( Config.serverDefaults( config ), NullLogProvider.getInstance() ); - - SslPolicy sslPolicy = sslPolicyFactory.getPolicy( "default" ); - return forServer ? sslPolicy.nettyServerContext() : sslPolicy.nettyClientContext(); - } - - private class SecureServer - { - SslContext sslContext; - Channel channel; - NioEventLoopGroup eventLoopGroup; - - SecureServer( SslContext sslContext ) - { - this.sslContext = sslContext; - } - - private void start() - { - eventLoopGroup = new NioEventLoopGroup(); - ServerBootstrap bootstrap = new ServerBootstrap() - .group( eventLoopGroup ) - .channel( NioServerSocketChannel.class ) - .option( ChannelOption.SO_REUSEADDR, true ) - .localAddress( 0 ) - .childHandler( new ChannelInitializer() - { - @Override - protected void initChannel( SocketChannel ch ) throws Exception - { - ChannelPipeline pipeline = ch.pipeline(); - - SSLEngine sslEngine = sslContext.newEngine( ch.alloc() ); - sslEngine.setNeedClientAuth( true ); - SslHandler sslHandler = new SslHandler( sslEngine ); - pipeline.addLast( sslHandler ); - - pipeline.addLast( new Responder() ); - } - } ); - - channel = bootstrap.bind().syncUninterruptibly().channel(); - } - - private void stop() - { - channel.close().awaitUninterruptibly(); - channel = null; - eventLoopGroup.shutdownGracefully( 0, 0, SECONDS ); - } - - private int port() - { - return ((InetSocketAddress) channel.localAddress()).getPort(); - } - } - - private class SecureClient - { - ClientInitializer clientInitializer; - Bootstrap bootstrap; - NioEventLoopGroup eventLoopGroup; - Channel channel; - Bucket bucket = new Bucket(); - - SecureClient( SslContext sslContext ) - { - eventLoopGroup = new NioEventLoopGroup(); - clientInitializer = new ClientInitializer( sslContext, bucket ); - bootstrap = new Bootstrap() - .group( eventLoopGroup ) - .channel( NioSocketChannel.class ) - .handler( clientInitializer ); - } - - @SuppressWarnings( "SameParameterValue" ) - void connect( int port ) - { - ChannelFuture channelFuture = bootstrap.connect( "localhost", port ).awaitUninterruptibly(); - channel = channelFuture.channel(); - } - - void disconnect() - { - if ( channel != null ) - { - channel.close().awaitUninterruptibly(); - eventLoopGroup.shutdownGracefully( 0, 0, SECONDS ); - } - - bucket.collectedData.release(); - } - - void assertResponse( ByteBuf expected ) throws InterruptedException - { - assertEventually( channel.toString(), () -> bucket.collectedData, equalTo( expected ), 5, SECONDS ); - } - } - - public class ClientInitializer extends ChannelInitializer - { - SslContext sslContext; - private final Bucket bucket; - private io.netty.util.concurrent.Future handshakeFuture; - - ClientInitializer( SslContext sslContext, Bucket bucket ) - { - this.sslContext = sslContext; - this.bucket = bucket; - } - - @Override - protected void initChannel( SocketChannel channel ) throws Exception - { - ChannelPipeline pipeline = channel.pipeline(); - - SSLEngine sslEngine = sslContext.newEngine( channel.alloc() ); - sslEngine.setUseClientMode( true ); - - SslHandler sslHandler = new SslHandler( sslEngine ); - handshakeFuture = sslHandler.handshakeFuture(); - - pipeline.addLast( sslHandler ); - pipeline.addLast( bucket ); - } - } - - class Bucket extends SimpleChannelInboundHandler - { - private final ByteBuf collectedData; - - Bucket() - { - collectedData = ByteBufAllocator.DEFAULT.buffer(); - } - - @Override - protected void channelRead0( ChannelHandlerContext ctx, ByteBuf msg ) throws Exception - { - collectedData.writeBytes( msg ); - } - - @Override - public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause ) throws Exception - { - //cause.printStackTrace(); - } - } - - private class Responder extends SimpleChannelInboundHandler - { - @Override - protected void channelRead0( ChannelHandlerContext ctx, ByteBuf msg ) throws Exception - { - ctx.channel().writeAndFlush( ctx.alloc().buffer().writeBytes( RESPONSE ) ); - } - - @Override - public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause ) throws Exception - { - //cause.printStackTrace(); - } - } -} diff --git a/integrationtests/src/test/java/org/neo4j/ssl/SecureServer.java b/integrationtests/src/test/java/org/neo4j/ssl/SecureServer.java new file mode 100644 index 0000000000000..87b058fefeae3 --- /dev/null +++ b/integrationtests/src/test/java/org/neo4j/ssl/SecureServer.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.ssl; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; + +import java.net.InetSocketAddress; +import javax.net.ssl.SSLEngine; + +import static java.util.concurrent.TimeUnit.SECONDS; + +class SecureServer +{ + static final byte[] RESPONSE = {5, 6, 7, 8}; + + private SslContext sslContext; + private Channel channel; + private NioEventLoopGroup eventLoopGroup; + + SecureServer( SslContext sslContext ) + { + this.sslContext = sslContext; + } + + void start() + { + eventLoopGroup = new NioEventLoopGroup(); + ServerBootstrap bootstrap = new ServerBootstrap() + .group( eventLoopGroup ) + .channel( NioServerSocketChannel.class ) + .option( ChannelOption.SO_REUSEADDR, true ) + .localAddress( 0 ) + .childHandler( new ChannelInitializer() + { + @Override + protected void initChannel( SocketChannel ch ) throws Exception + { + ChannelPipeline pipeline = ch.pipeline(); + + SSLEngine sslEngine = sslContext.newEngine( ch.alloc() ); + sslEngine.setNeedClientAuth( true ); + SslHandler sslHandler = new SslHandler( sslEngine ); + pipeline.addLast( sslHandler ); + //sslHandler.handshakeFuture().addListener( f -> f.cause().printStackTrace() ); // for debugging + + pipeline.addLast( new Responder() ); + } + } ); + + channel = bootstrap.bind().syncUninterruptibly().channel(); + } + + void stop() + { + channel.close().awaitUninterruptibly(); + channel = null; + eventLoopGroup.shutdownGracefully( 0, 0, SECONDS ); + } + + int port() + { + return ((InetSocketAddress) channel.localAddress()).getPort(); + } + + static class Responder extends SimpleChannelInboundHandler + { + @Override + protected void channelRead0( ChannelHandlerContext ctx, ByteBuf msg ) throws Exception + { + ctx.channel().writeAndFlush( ctx.alloc().buffer().writeBytes( RESPONSE ) ); + } + + @Override + public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause ) throws Exception + { + //cause.printStackTrace(); // for debugging + } + } +} diff --git a/integrationtests/src/test/java/org/neo4j/ssl/SslContextFactory.java b/integrationtests/src/test/java/org/neo4j/ssl/SslContextFactory.java new file mode 100644 index 0000000000000..2d64100bdf020 --- /dev/null +++ b/integrationtests/src/test/java/org/neo4j/ssl/SslContextFactory.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.ssl; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslProvider; + +import java.io.File; +import java.io.IOException; +import java.security.cert.CertificateException; +import java.util.HashMap; +import java.util.Map; + +import org.neo4j.kernel.configuration.Config; +import org.neo4j.kernel.configuration.ssl.SslPolicyConfig; +import org.neo4j.kernel.configuration.ssl.SslPolicyLoader; +import org.neo4j.kernel.configuration.ssl.SslSystemSettings; +import org.neo4j.logging.NullLogProvider; + +class SslContextFactory +{ + interface Ciphers + { + SslParameters ciphers( String... ciphers ); + } + + static class SslParameters implements Ciphers + { + private String protocols; + private String ciphers; + + private SslParameters( String protocols, String ciphers ) + { + this.protocols = protocols; + this.ciphers = ciphers; + } + + static Ciphers protocols( String... protocols ) + { + return new SslParameters( String.join( ",", protocols ), null ); + } + + @Override + public SslParameters ciphers( String... ciphers ) + { + this.ciphers = String.join( ",", ciphers ); + return this; + } + + @Override + public String toString() + { + return "SslParameters{" + "protocols='" + protocols + '\'' + ", ciphers='" + ciphers + '\'' + '}'; + } + } + + static SslContext makeSslContext( SslResource sslResource, boolean forServer, SslParameters params ) throws CertificateException, IOException + { + return makeSslContext( sslResource, forServer, SslProvider.JDK.name(), params.protocols, params.ciphers ); + } + + static SslContext makeSslContext( SslResource sslResource, boolean forServer, String sslProvider ) throws CertificateException, IOException + { + return makeSslContext( sslResource, forServer, sslProvider, null, null ); + } + + static SslContext makeSslContext( SslResource sslResource, boolean forServer ) throws CertificateException, IOException + { + return makeSslContext( sslResource, forServer, SslProvider.JDK.name(), null, null ); + } + + static SslContext makeSslContext( SslResource sslResource, boolean forServer, String sslProvider, String protocols, String ciphers ) throws CertificateException, IOException + { + Map config = new HashMap<>(); + config.put( SslSystemSettings.netty_ssl_provider.name(), sslProvider ); + + SslPolicyConfig policyConfig = new SslPolicyConfig( "default" ); + File baseDirectory = sslResource.privateKey().getParentFile(); + new File( baseDirectory, "trusted" ).mkdirs(); + new File( baseDirectory, "revoked" ).mkdirs(); + + config.put( policyConfig.base_directory.name(), baseDirectory.getPath() ); + config.put( policyConfig.private_key.name(), sslResource.privateKey().getPath() ); + config.put( policyConfig.public_certificate.name(), sslResource.publicCertificate().getPath() ); + config.put( policyConfig.trusted_dir.name(), sslResource.trustedDirectory().getPath() ); + config.put( policyConfig.revoked_dir.name(), sslResource.revokedDirectory().getPath() ); + + if ( protocols != null ) + { + config.put( policyConfig.tls_versions.name(), protocols ); + } + + if ( ciphers != null ) + { + config.put( policyConfig.ciphers.name(), ciphers ); + } + + SslPolicyLoader sslPolicyFactory = SslPolicyLoader.create( Config.serverDefaults( config ), NullLogProvider.getInstance() ); + + SslPolicy sslPolicy = sslPolicyFactory.getPolicy( "default" ); + return forServer ? sslPolicy.nettyServerContext() : sslPolicy.nettyClientContext(); + } +} diff --git a/integrationtests/src/test/java/org/neo4j/ssl/SslNegotiationTest.java b/integrationtests/src/test/java/org/neo4j/ssl/SslNegotiationTest.java new file mode 100644 index 0000000000000..877db3b0417ec --- /dev/null +++ b/integrationtests/src/test/java/org/neo4j/ssl/SslNegotiationTest.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.ssl; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.neo4j.ssl.SslContextFactory.SslParameters; +import org.neo4j.test.rule.TestDirectory; +import org.neo4j.test.rule.fs.DefaultFileSystemRule; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.neo4j.ssl.SslContextFactory.SslParameters.protocols; +import static org.neo4j.ssl.SslContextFactory.makeSslContext; +import static org.neo4j.ssl.SslResourceBuilder.selfSignedKeyId; + +@RunWith( Parameterized.class ) +public class SslNegotiationTest +{ + private static final String OLD_CIPHER_A = "SSL_DHE_RSA_WITH_DES_CBC_SHA"; + private static final String OLD_CIPHER_B = "SSL_RSA_WITH_RC4_128_MD5"; + private static final String OLD_CIPHER_C = "SSL_RSA_WITH_3DES_EDE_CBC_SHA"; + + private static final String NEW_CIPHER_A = "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"; + private static final String NEW_CIPHER_B = "TLS_RSA_WITH_AES_128_CBC_SHA256"; + private static final String NEW_CIPHER_C = "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256"; + + private static final String TLSv10 = "TLSv1"; + private static final String TLSv11 = "TLSv1.1"; + private static final String TLSv12 = "TLSv1.2"; + + @Rule + public TestDirectory testDir = TestDirectory.testDirectory(); + + @Rule + public DefaultFileSystemRule fsRule = new DefaultFileSystemRule(); + + @Parameterized.Parameter + public TestSetup setup; + + private SecureServer server; + private SecureClient client; + + @Parameterized.Parameters( name = "{0}" ) + public static Object[] params() + { + return new TestSetup[]{ + // succeeding exact matches + new TestSetup( + protocols( TLSv10 ).ciphers( OLD_CIPHER_A ), + protocols( TLSv10 ).ciphers( OLD_CIPHER_A ), + true, TLSv10, OLD_CIPHER_A ), + new TestSetup( + protocols( TLSv10 ).ciphers( NEW_CIPHER_A ), + protocols( TLSv10 ).ciphers( NEW_CIPHER_A ), + true, TLSv10, NEW_CIPHER_A ), + new TestSetup( + protocols( TLSv11 ).ciphers( OLD_CIPHER_A ), + protocols( TLSv11 ).ciphers( OLD_CIPHER_A ), + true, TLSv11, OLD_CIPHER_A ), + new TestSetup( + protocols( TLSv11 ).ciphers( NEW_CIPHER_A ), + protocols( TLSv11 ).ciphers( NEW_CIPHER_A ), + true, TLSv11, NEW_CIPHER_A ), + new TestSetup( + protocols( TLSv12 ).ciphers( NEW_CIPHER_A ), + protocols( TLSv12 ).ciphers( NEW_CIPHER_A ), + true, TLSv12, NEW_CIPHER_A ), + + // failing protocol matches + new TestSetup( + protocols( TLSv10 ).ciphers( OLD_CIPHER_A ), + protocols( TLSv11 ).ciphers( OLD_CIPHER_A ), + false ), + new TestSetup( + protocols( TLSv11 ).ciphers( OLD_CIPHER_A ), + protocols( TLSv10 ).ciphers( OLD_CIPHER_A ), + false ), + new TestSetup( + protocols( TLSv11 ).ciphers( NEW_CIPHER_A ), + protocols( TLSv12 ).ciphers( NEW_CIPHER_A ), + false ), + new TestSetup( + protocols( TLSv12 ).ciphers( NEW_CIPHER_A ), + protocols( TLSv11 ).ciphers( NEW_CIPHER_A ), + false ), + + // failing cipher matches + new TestSetup( + protocols( TLSv10 ).ciphers( OLD_CIPHER_A ), + protocols( TLSv10 ).ciphers( OLD_CIPHER_B ), + false ), + new TestSetup( + protocols( TLSv11 ).ciphers( NEW_CIPHER_A ), + protocols( TLSv11 ).ciphers( NEW_CIPHER_B ), + false ), + new TestSetup( + protocols( TLSv12 ).ciphers( NEW_CIPHER_A ), + protocols( TLSv12 ).ciphers( NEW_CIPHER_B ), + false ), + + // overlapping cipher success + new TestSetup( + protocols( TLSv10 ).ciphers( OLD_CIPHER_B, OLD_CIPHER_A ), + protocols( TLSv10 ).ciphers( OLD_CIPHER_C, OLD_CIPHER_A ), + true, TLSv10, OLD_CIPHER_A ), + new TestSetup( + protocols( TLSv11 ).ciphers( NEW_CIPHER_B, NEW_CIPHER_A ), + protocols( TLSv11 ).ciphers( NEW_CIPHER_C, NEW_CIPHER_A ), + true, TLSv11, NEW_CIPHER_A ), + new TestSetup( + protocols( TLSv12 ).ciphers( NEW_CIPHER_B, NEW_CIPHER_A ), + protocols( TLSv12 ).ciphers( NEW_CIPHER_C, NEW_CIPHER_A ), + true, TLSv12, NEW_CIPHER_A ), + + // overlapping protocol success + new TestSetup( + protocols( TLSv10, TLSv11 ).ciphers( OLD_CIPHER_A ), + protocols( TLSv11, TLSv12 ).ciphers( OLD_CIPHER_A ), + true, TLSv11, OLD_CIPHER_A ), + new TestSetup( + protocols( TLSv11, TLSv12 ).ciphers( OLD_CIPHER_A ), + protocols( TLSv10, TLSv11 ).ciphers( OLD_CIPHER_A ), + true, TLSv11, OLD_CIPHER_A ), + new TestSetup( + protocols( TLSv10, TLSv11, TLSv12 ).ciphers( NEW_CIPHER_B ), + protocols( TLSv10, TLSv11, TLSv12 ).ciphers( NEW_CIPHER_B ), + true, TLSv12, NEW_CIPHER_B ), + }; + } + + @After + public void cleanup() + { + if ( client != null ) + { + client.disconnect(); + } + if ( server != null ) + { + server.stop(); + } + } + + @Test + public void shouldNegotiateCorrectly() throws Exception + { + SslResource sslServerResource = selfSignedKeyId( 0 ).trustKeyId( 1 ).install( testDir.directory( "server" ) ); + SslResource sslClientResource = selfSignedKeyId( 1 ).trustKeyId( 0 ).install( testDir.directory( "client" ) ); + + server = new SecureServer( makeSslContext( sslServerResource, true, setup.serverParams ) ); + + server.start(); + client = new SecureClient( makeSslContext( sslClientResource, false, setup.clientParams ) ); + client.connect( server.port() ); + + assertTrue( client.sslHandshakeFuture().await( 1, MINUTES ) ); + + if ( setup.expectedSuccess ) + { + assertNull( client.sslHandshakeFuture().cause() ); + assertEquals( setup.expectedProtocol, client.protocol() ); + assertEquals( setup.expectedCipher, client.ciphers() ); + } + else + { + assertNotNull( client.sslHandshakeFuture().cause() ); + } + } + + private static class TestSetup + { + private final SslParameters serverParams; + private final SslParameters clientParams; + + private final boolean expectedSuccess; + private final String expectedProtocol; + private final String expectedCipher; + + private TestSetup( SslParameters serverParams, SslParameters clientParams, boolean expectedSuccess ) + { + this( serverParams, clientParams, expectedSuccess, null, null ); + } + + private TestSetup( SslParameters serverParams, SslParameters clientParams, boolean expectedSuccess, String expectedProtocol, String expectedCipher ) + { + this.serverParams = serverParams; + this.clientParams = clientParams; + this.expectedSuccess = expectedSuccess; + this.expectedProtocol = expectedProtocol; + this.expectedCipher = expectedCipher; + } + + @Override + public String toString() + { + return "TestSetup{" + "serverParams=" + serverParams + ", clientParams=" + clientParams + ", expectedSuccess=" + expectedSuccess + ", expectedProtocol='" + + expectedProtocol + '\'' + ", expectedCipher='" + expectedCipher + '\'' + '}'; + } + } +} diff --git a/integrationtests/src/test/java/org/neo4j/ssl/SslPlatformTest.java b/integrationtests/src/test/java/org/neo4j/ssl/SslPlatformTest.java new file mode 100644 index 0000000000000..e9106bc72a10d --- /dev/null +++ b/integrationtests/src/test/java/org/neo4j/ssl/SslPlatformTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.ssl; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.SslProvider; +import org.apache.commons.lang3.SystemUtils; +import org.junit.Rule; +import org.junit.Test; + +import org.neo4j.test.rule.TestDirectory; +import org.neo4j.test.rule.fs.DefaultFileSystemRule; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.isOneOf; +import static org.junit.Assume.assumeThat; +import static org.junit.Assume.assumeTrue; +import static org.neo4j.ssl.SslContextFactory.makeSslContext; +import static org.neo4j.ssl.SslResourceBuilder.selfSignedKeyId; + +@SuppressWarnings( "FieldCanBeLocal" ) +public class SslPlatformTest +{ + private static final byte[] REQUEST = {1, 2, 3, 4}; + + @Rule + public TestDirectory testDir = TestDirectory.testDirectory(); + + @Rule + public DefaultFileSystemRule fsRule = new DefaultFileSystemRule(); + + private SecureServer server; + private SecureClient client; + private ByteBuf expected; + + @Test + public void shouldSupportOpenSSLOnSupportedPlatforms() throws Exception + { + // depends on the statically linked uber-jar with boring ssl: http://netty.io/wiki/forked-tomcat-native.html + assumeTrue( SystemUtils.IS_OS_WINDOWS || SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC_OSX ); + assumeThat( System.getProperty( "os.arch" ), equalTo( "x86_64" ) ); + assumeThat( SystemUtils.JAVA_VENDOR, isOneOf( "Oracle Corporation", "Sun Microsystems Inc." ) ); + + // given + SslResource sslServerResource = selfSignedKeyId( 0 ).trustKeyId( 1 ).install( testDir.directory( "server" ) ); + SslResource sslClientResource = selfSignedKeyId( 1 ).trustKeyId( 0 ).install( testDir.directory( "client" ) ); + + server = new SecureServer( makeSslContext( sslServerResource, true, SslProvider.OPENSSL.name() ) ); + + server.start(); + client = new SecureClient( makeSslContext( sslClientResource, false, SslProvider.OPENSSL.name() ) ); + client.connect( server.port() ); + + // when + ByteBuf request = ByteBufAllocator.DEFAULT.buffer().writeBytes( REQUEST ); + client.channel().writeAndFlush( request ); + + // then + expected = ByteBufAllocator.DEFAULT.buffer().writeBytes( SecureServer.RESPONSE ); + client.sslHandshakeFuture().get( 1, MINUTES ); + client.assertResponse( expected ); + } +} diff --git a/integrationtests/src/test/java/org/neo4j/ssl/SslTrustTest.java b/integrationtests/src/test/java/org/neo4j/ssl/SslTrustTest.java new file mode 100644 index 0000000000000..0aad1f962b0ae --- /dev/null +++ b/integrationtests/src/test/java/org/neo4j/ssl/SslTrustTest.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.ssl; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; + +import java.util.concurrent.ExecutionException; +import javax.net.ssl.SSLException; + +import org.neo4j.test.rule.TestDirectory; +import org.neo4j.test.rule.fs.DefaultFileSystemRule; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.neo4j.ssl.SslContextFactory.makeSslContext; +import static org.neo4j.ssl.SslResourceBuilder.caSignedKeyId; +import static org.neo4j.ssl.SslResourceBuilder.selfSignedKeyId; + +public class SslTrustTest +{ + private static final int UNRELATED_ID = 5; // SslContextFactory requires us to trust something + + private static final byte[] REQUEST = {1, 2, 3, 4}; + + @Rule + public TestDirectory testDir = TestDirectory.testDirectory(); + + @Rule + public DefaultFileSystemRule fsRule = new DefaultFileSystemRule(); + + private SecureServer server; + private SecureClient client; + private ByteBuf expected; + + @After + public void cleanup() + { + if ( expected != null ) + { + expected.release(); + } + if ( client != null ) + { + client.disconnect(); + } + if ( server != null ) + { + server.stop(); + } + } + + @Test + public void partiesWithMutualTrustShouldCommunicate() throws Exception + { + // given + SslResource sslServerResource = selfSignedKeyId( 0 ).trustKeyId( 1 ).install( testDir.directory( "server" ) ); + SslResource sslClientResource = selfSignedKeyId( 1 ).trustKeyId( 0 ).install( testDir.directory( "client" ) ); + + server = new SecureServer( makeSslContext( sslServerResource, true ) ); + + server.start(); + client = new SecureClient( makeSslContext( sslClientResource, false ) ); + client.connect( server.port() ); + + // when + ByteBuf request = ByteBufAllocator.DEFAULT.buffer().writeBytes( REQUEST ); + client.channel().writeAndFlush( request ); + + // then + expected = ByteBufAllocator.DEFAULT.buffer().writeBytes( SecureServer.RESPONSE ); + client.sslHandshakeFuture().get( 1, MINUTES ); + client.assertResponse( expected ); + } + + @Test + public void partiesWithMutualTrustThroughCAShouldCommunicate() throws Exception + { + // given + SslResource sslServerResource = caSignedKeyId( 0 ).trustSignedByCA().install( testDir.directory( "server" ) ); + SslResource sslClientResource = caSignedKeyId( 1 ).trustSignedByCA().install( testDir.directory( "client" ) ); + + server = new SecureServer( makeSslContext( sslServerResource, true ) ); + + server.start(); + client = new SecureClient( makeSslContext( sslClientResource, false ) ); + client.connect( server.port() ); + + // when + ByteBuf request = ByteBufAllocator.DEFAULT.buffer().writeBytes( REQUEST ); + client.channel().writeAndFlush( request ); + + // then + expected = ByteBufAllocator.DEFAULT.buffer().writeBytes( SecureServer.RESPONSE ); + client.sslHandshakeFuture().get( 1, MINUTES ); + client.assertResponse( expected ); + } + + @Test + public void serverShouldNotCommunicateWithUntrustedClient() throws Exception + { + // given + SslResource sslClientResource = selfSignedKeyId( 1 ).trustKeyId( 0 ).install( testDir.directory( "client" ) ); + SslResource sslServerResource = selfSignedKeyId( 0 ).trustKeyId( UNRELATED_ID ).install( testDir.directory( "server" ) ); + + server = new SecureServer( makeSslContext( sslServerResource, true ) ); + + server.start(); + client = new SecureClient( makeSslContext( sslClientResource, false ) ); + client.connect( server.port() ); + + try + { + // when + client.sslHandshakeFuture().get( 1, MINUTES ); + fail(); + } + catch ( ExecutionException e ) + { + assertThat( e.getCause(), instanceOf( SSLException.class ) ); + } + } + + @Test + public void clientShouldNotCommunicateWithUntrustedServer() throws Exception + { + // given + SslResource sslClientResource = selfSignedKeyId( 0 ).trustKeyId( UNRELATED_ID ).install( testDir.directory( "client" ) ); + SslResource sslServerResource = selfSignedKeyId( 1 ).trustKeyId( 0 ).install( testDir.directory( "server" ) ); + + server = new SecureServer( makeSslContext( sslServerResource, true ) ); + + server.start(); + client = new SecureClient( makeSslContext( sslClientResource, false ) ); + client.connect( server.port() ); + + try + { + client.sslHandshakeFuture().get( 1, MINUTES ); + fail(); + } + catch ( ExecutionException e ) + { + assertThat( e.getCause(), instanceOf( SSLException.class ) ); + } + } + + @Test + public void partiesWithMutualTrustThroughCAShouldNotCommunicateWhenServerRevoked() throws Exception + { + // given + SslResource sslServerResource = caSignedKeyId( 0 ).trustSignedByCA().install( testDir.directory( "server" ) ); + SslResource sslClientResource = caSignedKeyId( 1 ).trustSignedByCA().revoke( 0 ).install( testDir.directory( "client" ) ); + + server = new SecureServer( makeSslContext( sslServerResource, true ) ); + + server.start(); + client = new SecureClient( makeSslContext( sslClientResource, false ) ); + client.connect( server.port() ); + + try + { + client.sslHandshakeFuture().get( 1, MINUTES ); + fail( "Server should have been revoked" ); + } + catch ( ExecutionException e ) + { + assertThat( e.getCause(), instanceOf( SSLException.class ) ); + } + } + + @Test + public void partiesWithMutualTrustThroughCAShouldNotCommunicateWhenClientRevoked() throws Exception + { + // given + SslResource sslServerResource = caSignedKeyId( 0 ).trustSignedByCA().revoke( 1 ).install( testDir.directory( "server" ) ); + SslResource sslClientResource = caSignedKeyId( 1 ).trustSignedByCA().install( testDir.directory( "client" ) ); + + server = new SecureServer( makeSslContext( sslServerResource, true ) ); + + server.start(); + client = new SecureClient( makeSslContext( sslClientResource, false ) ); + client.connect( server.port() ); + + try + { + client.sslHandshakeFuture().get( 1, MINUTES ); + fail( "Client should have been revoked" ); + } + catch ( ExecutionException e ) + { + assertThat( e.getCause(), instanceOf( SSLException.class ) ); + } + } +}