/*
* Copyright (c) 2002-2017 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.driver.v1.integration;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.security.cert.X509Certificate;
import org.neo4j.driver.internal.net.BoltServerAddress;
import org.neo4j.driver.internal.security.SecurityPlan;
import org.neo4j.driver.internal.security.TLSSocketChannel;
import org.neo4j.driver.internal.util.CertificateTool;
import org.neo4j.driver.v1.Config;
import org.neo4j.driver.v1.Driver;
import org.neo4j.driver.v1.GraphDatabase;
import org.neo4j.driver.v1.Logger;
import org.neo4j.driver.v1.Logging;
import org.neo4j.driver.v1.Session;
import org.neo4j.driver.v1.StatementResult;
import org.neo4j.driver.v1.exceptions.SecurityException;
import org.neo4j.driver.v1.util.CertificateToolTest;
import org.neo4j.driver.v1.util.Neo4jSettings;
import org.neo4j.driver.v1.util.TestNeo4j;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.neo4j.driver.internal.logging.DevNullLogger.DEV_NULL_LOGGER;
import static org.neo4j.driver.internal.security.TrustOnFirstUseTrustManager.fingerprint;
public class TLSSocketChannelIT
{
@Rule
public TestNeo4j neo4j = new TestNeo4j();
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@BeforeClass
public static void setup() throws IOException, InterruptedException
{
/* uncomment for JSSE debugging info */
// System.setProperty( "javax.net.debug", "all" );
}
@Test
public void shouldPerformTLSHandshakeWithEmptyKnownCertsFile() throws Throwable
{
File knownCerts = File.createTempFile( "neo4j_known_hosts", ".tmp" );
knownCerts.deleteOnExit();
performTLSHandshakeUsingKnownCerts( knownCerts );
}
@Test
public void shouldPerformTLSHandshakeWithTrustedCert() throws Throwable
{
try
{
// Given
BoltServerAddress address = BoltServerAddress.LOCAL_DEFAULT;
// Create root certificate
File rootCert = folder.newFile( "temp_root_cert.cert" );
File rootKey = folder.newFile( "temp_root_key.key" );
CertificateToolTest.SelfSignedCertificateGenerator
certGenerator = new CertificateToolTest.SelfSignedCertificateGenerator();
certGenerator.saveSelfSignedCertificate( rootCert );
certGenerator.savePrivateKey( rootKey );
// Generate certificate signing request and get a certificate signed by the root private key
File cert = folder.newFile( "temp_cert.cert" );
File key = folder.newFile( "temp_key.key" );
CertificateToolTest.CertificateSigningRequestGenerator
csrGenerator = new CertificateToolTest.CertificateSigningRequestGenerator();
X509Certificate signedCert = certGenerator.sign(
csrGenerator.certificateSigningRequest(), csrGenerator.publicKey() );
csrGenerator.savePrivateKey( key );
CertificateTool.saveX509Cert( signedCert, cert );
// Give the server certs to database
neo4j.updateEncryptionKeyAndCert( key, cert );
Logger logger = mock( Logger.class );
SocketChannel channel = SocketChannel.open();
channel.connect( address.toSocketAddress() );
// When
SecurityPlan securityPlan = SecurityPlan.forCustomCASignedCertificates( rootCert );
TLSSocketChannel sslChannel = TLSSocketChannel.create( address, securityPlan, channel, logger );
sslChannel.close();
// Then
verify( logger, atLeastOnce() ).debug( "~~ [OPENING SECURE CHANNEL]" );
}
finally
{
// always restore the db default settings
neo4j.restart();
}
}
@Test
public void shouldNotPerformTLSHandshakeWithNonSystemCert() throws Throwable
{
try
{
// Given
BoltServerAddress address = BoltServerAddress.LOCAL_DEFAULT;
// Install a root certificate unknown by the system certificate chain
installRootCertificate();
Logger logger = mock( Logger.class );
SocketChannel channel = SocketChannel.open();
channel.connect( new InetSocketAddress( "localhost", 7687 ) );
SecurityPlan securityPlan = SecurityPlan.forSystemCASignedCertificates();
// When
TLSSocketChannel sslChannel = null;
try
{
sslChannel = TLSSocketChannel.create( address, securityPlan, channel, logger );
fail( "Should have thrown exception" );
}
catch ( SecurityException e )
{
assertThat( e.getMessage(), containsString( "General SSLEngine problem" ) );
assertThat( getRootCause( e ).getMessage(),
containsString( "unable to find valid certification path to requested target" ) );
}
finally
{
if ( sslChannel != null )
{
sslChannel.close();
}
}
}
finally
{
// always restore the db default settings
neo4j.restart();
}
}
@SuppressWarnings( "deprecation" )
@Test
public void shouldFailTLSHandshakeDueToWrongCertInKnownCertsFile() throws Throwable
{
// Given
BoltServerAddress address = BoltServerAddress.LOCAL_DEFAULT;
SocketChannel channel = SocketChannel.open();
channel.connect( address.toSocketAddress() );
File knownCerts = File.createTempFile( "neo4j_known_hosts", ".tmp" );
knownCerts.deleteOnExit();
//create a Fake Cert for the server in knownCert
createFakeServerCertPairInKnownCerts( address, knownCerts );
// When & Then
SecurityPlan securityPlan = SecurityPlan.forTrustOnFirstUse( knownCerts, address, DEV_NULL_LOGGER );
TLSSocketChannel sslChannel = null;
try
{
sslChannel = TLSSocketChannel.create( address, securityPlan, channel, DEV_NULL_LOGGER );
fail( "Should have thrown exception" );
}
catch ( SecurityException e )
{
assertThat( e.getMessage(), containsString( "General SSLEngine problem" ) );
assertThat( getRootCause( e ).getMessage(), containsString(
"If you trust the certificate the server uses now, simply remove the line that starts with" ) );
}
finally
{
if ( sslChannel != null )
{
sslChannel.close();
}
}
}
private void createFakeServerCertPairInKnownCerts( BoltServerAddress address, File knownCerts )
throws Throwable
{
String serverId = address.toString();
X509Certificate cert = CertificateToolTest.generateSelfSignedCertificate();
String certStr = fingerprint( cert );
BufferedWriter writer = new BufferedWriter( new FileWriter( knownCerts, true ) );
writer.write( serverId + " " + certStr );
writer.newLine();
writer.close();
}
@Test
public void shouldFailTLSHandshakeDueToServerCertNotSignedByKnownCA() throws Throwable
{
// Given
neo4j.restart( Neo4jSettings.TEST_SETTINGS.updateWith(
Neo4jSettings.CERT_DIR,
folder.getRoot().getAbsolutePath().replace( "\\", "/" ) ) );
SocketChannel channel = SocketChannel.open();
channel.connect( neo4j.address().toSocketAddress() );
File trustedCertFile = folder.newFile( "neo4j_trusted_cert.tmp" );
X509Certificate aRandomCert = CertificateToolTest.generateSelfSignedCertificate();
CertificateTool.saveX509Cert( aRandomCert, trustedCertFile );
// When & Then
SecurityPlan securityPlan = SecurityPlan.forCustomCASignedCertificates( trustedCertFile );
TLSSocketChannel sslChannel = null;
try
{
sslChannel = TLSSocketChannel.create( neo4j.address(), securityPlan, channel, mock( Logger.class ) );
fail( "Should have thrown exception" );
}
catch ( SecurityException e )
{
assertThat( e.getMessage(), containsString( "General SSLEngine problem" ) );
assertThat( getRootCause( e ).getMessage(), containsString( "No trusted certificate found" ) );
}
finally
{
if ( sslChannel != null )
{
sslChannel.close();
}
}
}
private Throwable getRootCause( Throwable e )
{
Throwable parentError = e;
Throwable error = null;
do
{
error = parentError;
parentError = error.getCause();
}
while ( parentError != null );
return error;
}
@Test
public void shouldPerformTLSHandshakeWithTheSameTrustedServerCert() throws Throwable
{
BoltServerAddress address = BoltServerAddress.LOCAL_DEFAULT;
Logger logger = mock( Logger.class );
SocketChannel channel = SocketChannel.open();
channel.connect( address.toSocketAddress() );
// When
SecurityPlan securityPlan = SecurityPlan.forCustomCASignedCertificates( neo4j.tlsCertFile() );
TLSSocketChannel sslChannel = TLSSocketChannel.create( address, securityPlan, channel, logger );
sslChannel.close();
// Then
verify( logger, atLeastOnce() ).debug( "~~ [OPENING SECURE CHANNEL]" );
}
@Test
public void shouldEstablishTLSConnection() throws Throwable
{
Config config = Config.build().withEncryption().toConfig();
try ( Driver driver = GraphDatabase.driver( neo4j.uri(), neo4j.authToken(), config );
Session session = driver.session() )
{
StatementResult result = session.run( "RETURN 1" );
assertEquals( 1, result.next().get( 0 ).asInt() );
assertFalse( result.hasNext() );
}
}
@SuppressWarnings( "deprecation" )
@Test
public void shouldWarnIfUsingDeprecatedTLSOption() throws Throwable
{
Logger logger = mock( Logger.class );
Logging logging = mock( Logging.class );
when( logging.getLog( anyString() ) ).thenReturn( logger );
SocketChannel channel = SocketChannel.open();
channel.connect( new InetSocketAddress( "localhost", 7687 ) );
Config config = Config.build()
.withEncryptionLevel( Config.EncryptionLevel.REQUIRED )
.withTrustStrategy( Config.TrustStrategy.trustSignedBy( neo4j.tlsCertFile() ) )
.withLogging( logging )
.toConfig();
// When
try ( Driver driver = GraphDatabase.driver( neo4j.uri(), neo4j.authToken(), config );
Session session = driver.session() )
{
session.run( "RETURN 1" ).consume();
}
// Then
verify( logger, atLeastOnce() )
.warn( "Option `TRUST_SIGNED_CERTIFICATE` has been deprecated and will be removed " +
"in a future version of the driver. Please switch to use " +
"`TRUST_CUSTOM_CA_SIGNED_CERTIFICATES` instead." );
}
@SuppressWarnings( "deprecation" )
private void performTLSHandshakeUsingKnownCerts( File knownCerts ) throws Throwable
{
// Given
Logger logger = mock( Logger.class );
BoltServerAddress address = BoltServerAddress.LOCAL_DEFAULT;
SocketChannel channel = SocketChannel.open();
channel.connect( address.toSocketAddress() );
// When
SecurityPlan securityPlan = SecurityPlan.forTrustOnFirstUse( knownCerts, address, DEV_NULL_LOGGER );
TLSSocketChannel sslChannel =
TLSSocketChannel.create( address, securityPlan, channel, logger );
sslChannel.close();
// Then
verify( logger, atLeastOnce() ).debug( "~~ [CLOSED SECURE CHANNEL]" );
}
private File installRootCertificate() throws Exception
{
File rootCert = folder.newFile( "temp_root_cert.cert" );
File rootKey = folder.newFile( "temp_root_key.key" );
CertificateToolTest.SelfSignedCertificateGenerator
certGenerator = new CertificateToolTest.SelfSignedCertificateGenerator();
certGenerator.saveSelfSignedCertificate( rootCert );
certGenerator.savePrivateKey( rootKey );
// Generate certificate signing request and get a certificate signed by the root private key
File cert = folder.newFile( "temp_cert.cert" );
File key = folder.newFile( "temp_key.key" );
CertificateToolTest.CertificateSigningRequestGenerator
csrGenerator = new CertificateToolTest.CertificateSigningRequestGenerator();
X509Certificate signedCert = certGenerator.sign(
csrGenerator.certificateSigningRequest(), csrGenerator.publicKey() );
csrGenerator.savePrivateKey( key );
CertificateTool.saveX509Cert( signedCert, cert );
// Give the server certs to database
neo4j.updateEncryptionKeyAndCert( key, cert );
return rootCert;
}
}