From d00310c81995f55568d09c5f09c77da4e81e45ce Mon Sep 17 00:00:00 2001 From: Martin Furmanski Date: Thu, 22 Feb 2018 17:17:18 +0100 Subject: [PATCH] Fix ssl protocol config for ssl context Using the raw ssl context instead of the netty handlers missed the configuration of protocols. The ssl testing is broken out into submodules and a new exhaustive set of negotiation circumstances are tested. This affected the bolt server which instantiated its handlers using the context. --- .../ssl/SslSocketConnectorFactory.java | 4 +- .../main/java/org/neo4j/ssl/SslPolicy.java | 12 +- .../test/java/org/neo4j/ssl/SecureClient.java | 155 ++++++ .../neo4j/ssl/SecureCommunicationsTest.java | 469 ------------------ .../test/java/org/neo4j/ssl/SecureServer.java | 108 ++++ .../java/org/neo4j/ssl/SslContextFactory.java | 120 +++++ .../org/neo4j/ssl/SslNegotiationTest.java | 225 +++++++++ .../java/org/neo4j/ssl/SslPlatformTest.java | 82 +++ .../test/java/org/neo4j/ssl/SslTrustTest.java | 217 ++++++++ 9 files changed, 916 insertions(+), 476 deletions(-) create mode 100644 integrationtests/src/test/java/org/neo4j/ssl/SecureClient.java delete mode 100644 integrationtests/src/test/java/org/neo4j/ssl/SecureCommunicationsTest.java create mode 100644 integrationtests/src/test/java/org/neo4j/ssl/SecureServer.java create mode 100644 integrationtests/src/test/java/org/neo4j/ssl/SslContextFactory.java create mode 100644 integrationtests/src/test/java/org/neo4j/ssl/SslNegotiationTest.java create mode 100644 integrationtests/src/test/java/org/neo4j/ssl/SslPlatformTest.java create mode 100644 integrationtests/src/test/java/org/neo4j/ssl/SslTrustTest.java 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 ) ); + } + } +}