diff --git a/driver/src/main/java/org/neo4j/driver/internal/connector/socket/SSLContextFactory.java b/driver/src/main/java/org/neo4j/driver/internal/connector/socket/SSLContextFactory.java index 48d7bfa429..9bc0ff8bf7 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/connector/socket/SSLContextFactory.java +++ b/driver/src/main/java/org/neo4j/driver/internal/connector/socket/SSLContextFactory.java @@ -27,8 +27,8 @@ import javax.net.ssl.TrustManagerFactory; import org.neo4j.driver.v1.Config; -import org.neo4j.driver.v1.exceptions.ClientException; import org.neo4j.driver.v1.Logger; +import org.neo4j.driver.v1.exceptions.ClientException; import static org.neo4j.driver.internal.util.CertificateTool.loadX509Cert; @@ -55,6 +55,10 @@ public SSLContext create() switch ( authConfig.strategy() ) { case TRUST_SIGNED_CERTIFICATES: + logger.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." ); + //intentional fallthrough + case TRUST_CUSTOM_CA_SIGNED_CERTIFICATES: // A certificate file is specified so we will load the certificates in the file // Init a in memory TrustedKeyStore KeyStore trustedKeyStore = KeyStore.getInstance( "JKS" ); @@ -67,7 +71,13 @@ public SSLContext create() TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( "SunX509" ); trustManagerFactory.init( trustedKeyStore ); trustManagers = trustManagerFactory.getTrustManagers(); + break; + + //just rely on system defaults + case TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: + return SSLContext.getDefault(); + case TRUST_ON_FIRST_USE: trustManagers = new TrustManager[]{new TrustOnFirstUseTrustManager( host, port, authConfig.certFile(), logger )}; break; diff --git a/driver/src/main/java/org/neo4j/driver/v1/Config.java b/driver/src/main/java/org/neo4j/driver/v1/Config.java index 3bb14dfb0c..a847cb7d06 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/Config.java +++ b/driver/src/main/java/org/neo4j/driver/v1/Config.java @@ -25,7 +25,7 @@ import org.neo4j.driver.v1.util.Immutable; import static java.lang.System.getProperty; -import static org.neo4j.driver.v1.Config.TrustStrategy.*; +import static org.neo4j.driver.v1.Config.TrustStrategy.trustOnFirstUse; /** * A configuration class to config driver properties. @@ -243,7 +243,7 @@ public ConfigBuilder withEncryptionLevel( EncryptionLevel level ) /** * Specify how to determine the authenticity of an encryption certificate provided by the Neo4j instance we are connecting to. * This defaults to {@link TrustStrategy#trustOnFirstUse(File)}. - * See {@link TrustStrategy#trustSignedBy(File)} for using certificate signatures instead to verify + * See {@link TrustStrategy#trustCustomCertificateSignedBy(File)} for using certificate signatures instead to verify * trust. *

* This is an important setting to understand, because unless we know that the remote server we have an encrypted connection to @@ -290,12 +290,20 @@ public static class TrustStrategy public enum Strategy { TRUST_ON_FIRST_USE, - TRUST_SIGNED_CERTIFICATES + @Deprecated + TRUST_SIGNED_CERTIFICATES, + TRUST_CUSTOM_CA_SIGNED_CERTIFICATES, + TRUST_SYSTEM_CA_SIGNED_CERTIFICATES } private final Strategy strategy; private final File certFile; + private TrustStrategy( Strategy strategy ) + { + this( strategy, null ); + } + private TrustStrategy( Strategy strategy, File certFile ) { this.strategy = strategy; @@ -316,6 +324,15 @@ public File certFile() return certFile; } + /** + * Use {@link #trustCustomCertificateSignedBy(File)} instead. + */ + @Deprecated + public static TrustStrategy trustSignedBy( File certFile ) + { + return new TrustStrategy( Strategy.TRUST_SIGNED_CERTIFICATES, certFile ); + } + /** * Only encrypted connections to Neo4j instances with certificates signed by a trusted certificate will be accepted. * The file specified should contain one or more trusted X.509 certificates. @@ -326,9 +343,14 @@ public File certFile() * @param certFile the trusted certificate file * @return an authentication config */ - public static TrustStrategy trustSignedBy( File certFile ) + public static TrustStrategy trustCustomCertificateSignedBy( File certFile ) { - return new TrustStrategy( Strategy.TRUST_SIGNED_CERTIFICATES, certFile ); + return new TrustStrategy( Strategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES, certFile ); + } + + public static TrustStrategy trustSystemCertifcates() + { + return new TrustStrategy( Strategy.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES ); } /** @@ -339,7 +361,7 @@ public static TrustStrategy trustSignedBy( File certFile ) * Each time we reconnect to a known host, we verify that its certificate remains the same, guarding against attackers intercepting our communication. *

* Note that this approach is vulnerable to man-in-the-middle attacks the very first time you connect to a new Neo4j instance. - * If you do not trust the network you are connecting over, consider using {@link #trustSignedBy(File) signed certificates} instead, or manually adding the + * If you do not trust the network you are connecting over, consider using {@link #trustCustomCertificateSignedBy(File)} signed certificates} instead, or manually adding the * trusted host line into the specified file. * * @param knownHostsFile a file where known certificates are stored. diff --git a/driver/src/test/java/org/neo4j/driver/internal/ConfigTest.java b/driver/src/test/java/org/neo4j/driver/internal/ConfigTest.java index 33e3dec887..363d6ce783 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/ConfigTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/ConfigTest.java @@ -69,13 +69,13 @@ public void shouldChangeToTrustedCert() { // Given File trustedCert = new File( "trusted_cert" ); - Config config = Config.build().withTrustStrategy( Config.TrustStrategy.trustSignedBy( trustedCert ) ).toConfig(); + Config config = Config.build().withTrustStrategy( Config.TrustStrategy.trustCustomCertificateSignedBy( trustedCert ) ).toConfig(); // When Config.TrustStrategy authConfig = config.trustStrategy(); // Then - assertEquals( authConfig.strategy(), Config.TrustStrategy.Strategy.TRUST_SIGNED_CERTIFICATES ); + assertEquals( authConfig.strategy(), Config.TrustStrategy.Strategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES ); assertEquals( trustedCert.getAbsolutePath(), authConfig.certFile().getAbsolutePath() ); } @@ -86,7 +86,7 @@ public void shouldConfigureMinIdleTime() throws Throwable Config config = Config.build().withSessionLivenessCheckTimeout( 1337 ).toConfig(); // then - assertThat( config.idleTimeBeforeConnectionTest(), equalTo( 1337l ) ); + assertThat( config.idleTimeBeforeConnectionTest(), equalTo( 1337L ) ); } public static void deleteDefaultKnownCertFileIfExists() diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/TLSSocketChannelIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/TLSSocketChannelIT.java index c68ba76db6..b81486206a 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/TLSSocketChannelIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/TLSSocketChannelIT.java @@ -47,6 +47,8 @@ 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.assertTrue; @@ -61,7 +63,7 @@ public class TLSSocketChannelIT public TestNeo4j neo4j = new TestNeo4j(); @Rule - public TemporaryFolder folder = new TemporaryFolder( ); + public TemporaryFolder folder = new TemporaryFolder(); @BeforeClass public static void setup() throws IOException, InterruptedException @@ -79,21 +81,6 @@ public void shouldPerformTLSHandshakeWithEmptyKnownCertsFile() throws Throwable performTLSHandshakeUsingKnownCerts( knownCerts ); } - private void performTLSHandshakeUsingKnownCerts( File knownCerts ) throws Throwable - { - // Given - Logger logger = mock( Logger.class ); - SocketChannel channel = SocketChannel.open(); - channel.connect( new InetSocketAddress( "localhost", 7687 ) ); - - // When - TLSSocketChannel sslChannel = - new TLSSocketChannel( "localhost", 7687, channel, logger, Config.TrustStrategy.trustOnFirstUse( knownCerts ) ); - sslChannel.close(); - - // Then - verify( logger, atLeastOnce() ).debug( "TLS connection closed" ); - } @Test public void shouldPerformTLSHandshakeWithTrustedCert() throws Throwable @@ -102,26 +89,7 @@ public void shouldPerformTLSHandshakeWithTrustedCert() throws Throwable { // Given // 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 ); + File rootCert = installRootCertificate(); Logger logger = mock( Logger.class ); SocketChannel channel = SocketChannel.open(); @@ -130,7 +98,7 @@ public void shouldPerformTLSHandshakeWithTrustedCert() throws Throwable // When TLSSocketChannel sslChannel = new TLSSocketChannel( "localhost", 7687, channel, logger, - Config.TrustStrategy.trustSignedBy( rootCert ) ); + Config.TrustStrategy.trustCustomCertificateSignedBy( rootCert ) ); sslChannel.close(); // Then @@ -143,6 +111,42 @@ public void shouldPerformTLSHandshakeWithTrustedCert() throws Throwable } } + @Test + public void shouldNotPerformTLSHandshakeWithNonSystemCert() throws Throwable + { + try + { + // Given + // 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 ) ); + + // When + try + { + TLSSocketChannel sslChannel = + new TLSSocketChannel( "localhost", 7687, channel, logger, + Config.TrustStrategy.trustSystemCertifcates() ); + sslChannel.close(); + } + catch ( SSLHandshakeException e ) + { + assertEquals( "General SSLEngine problem", e.getMessage() ); + assertEquals( "General SSLEngine problem", e.getCause().getMessage() ); + assertThat( e.getCause().getCause().getMessage(), + containsString( "unable to find valid certification path to requested target" ) ); + } + } + finally + { + // always restore the db default settings + neo4j.restart(); + } + } + @Test public void shouldFailTLSHandshakeDueToWrongCertInKnownCertsFile() throws Throwable { @@ -179,21 +183,6 @@ public void shouldFailTLSHandshakeDueToWrongCertInKnownCertsFile() throws Throwa } } - private void createFakeServerCertPairInKnownCerts( String host, int port, File knownCerts ) - throws Throwable - { - String ip = InetAddress.getByName( host ).getHostAddress(); // localhost -> 127.0.0.1 - String serverId = ip + ":" + port; - - 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 { @@ -201,7 +190,7 @@ public void shouldFailTLSHandshakeDueToServerCertNotSignedByKnownCA() throws Thr neo4j.restart( Neo4jSettings.TEST_SETTINGS.updateWith( Neo4jSettings.CERT_DIR, - folder.getRoot().getAbsolutePath().replace("\\", "/") ) ); + folder.getRoot().getAbsolutePath().replace( "\\", "/" ) ) ); SocketChannel channel = SocketChannel.open(); channel.connect( new InetSocketAddress( "localhost", 7687 ) ); File trustedCertFile = folder.newFile( "neo4j_trusted_cert.tmp" ); @@ -213,7 +202,7 @@ public void shouldFailTLSHandshakeDueToServerCertNotSignedByKnownCA() throws Thr try { sslChannel = new TLSSocketChannel( "localhost", 7687, channel, mock( Logger.class ), - Config.TrustStrategy.trustSignedBy( trustedCertFile ) ); + Config.TrustStrategy.trustCustomCertificateSignedBy( trustedCertFile ) ); sslChannel.close(); } catch ( SSLHandshakeException e ) @@ -241,8 +230,8 @@ public void shouldPerformTLSHandshakeWithTheSameTrustedServerCert() throws Throw // When TLSSocketChannel sslChannel = new TLSSocketChannel( "localhost", 7687, channel, logger, - Config.TrustStrategy.trustSignedBy( - Neo4jSettings.DEFAULT_TLS_CERT_FILE ) ); + Config.TrustStrategy.trustCustomCertificateSignedBy( + Neo4jSettings.DEFAULT_TLS_CERT_FILE ) ); sslChannel.close(); // Then @@ -255,12 +244,91 @@ public void shouldEstablishTLSConnection() throws Throwable Config config = Config.build().withEncryptionLevel( Config.EncryptionLevel.REQUIRED ).toConfig(); - try( Driver driver = GraphDatabase.driver( URI.create( Neo4jRunner.DEFAULT_URL ), config ); - Session session = driver.session() ) + try ( Driver driver = GraphDatabase.driver( URI.create( Neo4jRunner.DEFAULT_URL ), config ); + Session session = driver.session() ) { StatementResult result = session.run( "RETURN 1" ); assertEquals( 1, result.next().get( 0 ).asInt() ); assertFalse( result.hasNext() ); } } + + @Test + public void shouldWarnIfUsingDeprecatedTLSOption() throws Throwable + { + + Logger logger = mock( Logger.class ); + SocketChannel channel = SocketChannel.open(); + channel.connect( new InetSocketAddress( "localhost", 7687 ) ); + + // When + TLSSocketChannel sslChannel = new TLSSocketChannel( "localhost", 7687, channel, logger, + Config.TrustStrategy.trustSignedBy( + Neo4jSettings.DEFAULT_TLS_CERT_FILE ) ); + sslChannel.close(); + + // 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." ); + } + + private void performTLSHandshakeUsingKnownCerts( File knownCerts ) throws Throwable + { + // Given + Logger logger = mock( Logger.class ); + SocketChannel channel = SocketChannel.open(); + channel.connect( new InetSocketAddress( "localhost", 7687 ) ); + + // When + TLSSocketChannel sslChannel = + new TLSSocketChannel( "localhost", 7687, channel, logger, + Config.TrustStrategy.trustOnFirstUse( knownCerts ) ); + sslChannel.close(); + + // Then + verify( logger, atLeastOnce() ).debug( "TLS connection closed" ); + } + + 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; + } + + private void createFakeServerCertPairInKnownCerts( String host, int port, File knownCerts ) + throws Throwable + { + String ip = InetAddress.getByName( host ).getHostAddress(); // localhost -> 127.0.0.1 + String serverId = ip + ":" + port; + + X509Certificate cert = CertificateToolTest.generateSelfSignedCertificate(); + String certStr = fingerprint( cert ); + + BufferedWriter writer = new BufferedWriter( new FileWriter( knownCerts, true ) ); + writer.write( serverId + "," + certStr ); + writer.newLine(); + writer.close(); + } + } diff --git a/driver/src/test/java/org/neo4j/driver/v1/tck/DriverSecurityComplianceSteps.java b/driver/src/test/java/org/neo4j/driver/v1/tck/DriverSecurityComplianceSteps.java index 3d4f886042..fb57ebfeb2 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/tck/DriverSecurityComplianceSteps.java +++ b/driver/src/test/java/org/neo4j/driver/v1/tck/DriverSecurityComplianceSteps.java @@ -46,8 +46,8 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.Assert.assertThat; import static org.neo4j.driver.internal.util.CertificateTool.saveX509Cert; +import static org.neo4j.driver.v1.Config.TrustStrategy.trustCustomCertificateSignedBy; import static org.neo4j.driver.v1.Config.TrustStrategy.trustOnFirstUse; -import static org.neo4j.driver.v1.Config.TrustStrategy.trustSignedBy; import static org.neo4j.driver.v1.tck.DriverComplianceIT.neo4j; import static org.neo4j.driver.v1.util.CertificateToolTest.generateSelfSignedCertificate; @@ -199,7 +199,7 @@ public void aRunningNeo4jDatabaseUsingACertificateSignedByTheSameTrustedCertific driver = GraphDatabase.driver( Neo4jRunner.DEFAULT_URL, Config.build().withEncryptionLevel( EncryptionLevel.REQUIRED ) - .withTrustStrategy( trustSignedBy( rootCert ) ).toConfig() ); + .withTrustStrategy( trustCustomCertificateSignedBy( rootCert ) ).toConfig() ); // generate certificate signing request and get a certificate signed by the root private key File cert = tempFile( "temp_cert", ".cert" ); @@ -223,7 +223,7 @@ public void aRunningNeo4jDatabaseUsingThatExactTrustedCertificate() driver = GraphDatabase.driver( Neo4jRunner.DEFAULT_URL, Config.build().withEncryptionLevel( EncryptionLevel.REQUIRED ) - .withTrustStrategy( trustSignedBy( Neo4jSettings.DEFAULT_TLS_CERT_FILE ) ).toConfig() ); + .withTrustStrategy( trustCustomCertificateSignedBy( Neo4jSettings.DEFAULT_TLS_CERT_FILE ) ).toConfig() ); } // invalid cert @@ -237,7 +237,7 @@ public void aRunningNeo4jDatabaseUsingACertNotSignedByTheTrustedCertificates() t driver = GraphDatabase.driver( Neo4jRunner.DEFAULT_URL, Config.build().withEncryptionLevel( EncryptionLevel.REQUIRED ) - .withTrustStrategy( trustSignedBy( cert ) ).toConfig() ); + .withTrustStrategy( trustCustomCertificateSignedBy( cert ) ).toConfig() ); } @And( "^I should get a helpful error explaining that no trusted certificate found$" ) diff --git a/examples/src/main/java/org/neo4j/docs/driver/Examples.java b/examples/src/main/java/org/neo4j/docs/driver/Examples.java index 188448f344..7d7461ee34 100644 --- a/examples/src/main/java/org/neo4j/docs/driver/Examples.java +++ b/examples/src/main/java/org/neo4j/docs/driver/Examples.java @@ -239,7 +239,7 @@ public static Driver trustSignedCertificates() throws Exception // tag::tls-signed[] Driver driver = GraphDatabase.driver( "bolt://localhost", AuthTokens.basic("neo4j", "neo4j"), Config.build() .withEncryptionLevel( Config.EncryptionLevel.REQUIRED ) - .withTrustStrategy( Config.TrustStrategy.trustSignedBy( new File( "/path/to/ca-certificate.pem") ) ) + .withTrustStrategy( Config.TrustStrategy.trustCustomCertificateSignedBy( new File( "/path/to/ca-certificate.pem") ) ) .toConfig() ); // end::tls-signed[]