/* * 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.tck; import cucumber.api.java.After; import cucumber.api.java.en.And; import cucumber.api.java.en.Given; import cucumber.api.java.en.Then; import cucumber.api.java.en.When; import java.io.File; import java.security.cert.X509Certificate; import org.neo4j.driver.v1.Config; import org.neo4j.driver.v1.Config.EncryptionLevel; import org.neo4j.driver.v1.Driver; import org.neo4j.driver.v1.GraphDatabase; 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.CertificateSigningRequestGenerator; import org.neo4j.driver.v1.util.CertificateToolTest.SelfSignedCertificateGenerator; import org.neo4j.driver.v1.util.Neo4jRunner; import static java.io.File.createTempFile; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.Assert.assertEquals; 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.tck.DriverComplianceIT.neo4j; import static org.neo4j.driver.v1.util.CertificateToolTest.generateSelfSignedCertificate; import static org.neo4j.driver.v1.util.Neo4jRunner.HOME_DIR; import static org.neo4j.driver.v1.util.Neo4jSettings.DEFAULT_TLS_CERT_PATH; public class DriverSecurityComplianceSteps { private Driver driver; private File knownHostsFile; private Throwable exception; private Driver driverKitten; // well, just a reference to another driver // first use @Given( "^a running Neo4j Database$" ) public void aRunningDatabase() throws Throwable { } @SuppressWarnings( "deprecation" ) @When( "I connect via a TLS-enabled transport for the first time for the given hostname and port$" ) public void firstUseConnect() throws Throwable { knownHostsFile = tempFile( "known_hosts", ".tmp" ); driver = GraphDatabase.driver( Neo4jRunner.DEFAULT_URI, Neo4jRunner.DEFAULT_AUTH_TOKEN, Config.build().withEncryptionLevel( EncryptionLevel.REQUIRED ) .withTrustStrategy( trustOnFirstUse( knownHostsFile ) ).toConfig() ); } @Then( "sessions should simply work$" ) public void sessionsShouldSimplyWork() throws Throwable { try ( Session session = driver.session() ) { StatementResult statementResult = session.run( "RETURN 1" ); assertEquals( statementResult.single().get( 0 ).asInt(), 1 ); } } // subsequent use @Given( "^a running Neo4j Database that I have connected to with a TLS-enabled transport in the past$" ) public void aRunningNeoJDatabaseThatIHaveConnectedTo() throws Throwable { firstUseConnect(); sessionsShouldSimplyWork(); } @SuppressWarnings( "deprecation" ) @When( "^I connect via a TLS-enabled transport again$" ) public void iConnectViaATlsEnabledTransportAgain() throws Throwable { driver = GraphDatabase.driver( Neo4jRunner.DEFAULT_URI, Neo4jRunner.DEFAULT_AUTH_TOKEN, Config.build().withEncryptionLevel( EncryptionLevel.REQUIRED ) .withTrustStrategy( trustOnFirstUse( knownHostsFile ) ).toConfig() ); } // man in the middle attack @And( "^the database has changed which certificate it uses$" ) public void theDatabaseHasChangedWhichCertificateItUses() throws Throwable { driver.close(); // create new certificate File cert = tempFile( "temp_cert", ".cert" ); File key = tempFile( "temp_key", ".key" ); SelfSignedCertificateGenerator generator = new SelfSignedCertificateGenerator(); generator.saveSelfSignedCertificate( cert ); generator.savePrivateKey( key ); neo4j.updateEncryptionKeyAndCert( key, cert ); } @Then( "^creating sessions should fail$" ) public void creatingSessionsShouldFail() throws Throwable { try ( Session session = driver.session() ) { session.run( "RETURN 1" ); } catch ( Exception e ) { exception = e; } } @And( "^I should get a helpful error explaining that the certificate has changed$" ) public void iShouldGetAHelpfulErrorExplainingThatCertificateChanged( String str ) throws Throwable { assertThat( exception, notNullValue() ); assertThat( exception, instanceOf( SecurityException.class ) ); Throwable rootCause = getRootCause( exception ); assertThat( rootCause.toString(), containsString( "Unable to connect to neo4j at `localhost:7687`, " + "because the certificate the server uses has changed. " + "This is a security feature to protect against man-in-the-middle attacks." ) ); assertThat( rootCause.toString(), containsString( "If you trust the certificate the server uses now, simply remove the line that starts with " + "`localhost:7687` in the file" ) ); assertThat( rootCause.toString(), containsString( "The old certificate saved in file is:" ) ); assertThat( rootCause.toString(), containsString( "The New certificate received is:" ) ); } // modified trusted certificate file location @Given( "^two drivers" ) public void twoDrivers() { } @SuppressWarnings( "deprecation" ) @When( "^I configure one of them to use a different location for its known hosts storage$" ) public void twoDriversWithDifferentKnownHostsFiles() throws Throwable { firstUseConnect(); sessionsShouldSimplyWork(); File tempFile = tempFile( "known_hosts", ".tmp" ); driverKitten = GraphDatabase.driver( Neo4jRunner.DEFAULT_URI, Neo4jRunner.DEFAULT_AUTH_TOKEN, Config.build().withEncryptionLevel( EncryptionLevel.REQUIRED ) .withTrustStrategy( trustOnFirstUse( tempFile ) ).toConfig() ); } @Then( "^the two drivers should not interfere with one another's known hosts files$" ) public void twoDriversShouldNotInterfereWithEachOther() throws Throwable { // if I change the cert of the server, as driver has already connected, so driver will fall to connect theDatabaseHasChangedWhichCertificateItUses(); iConnectViaATlsEnabledTransportAgain(); creatingSessionsShouldFail(); iShouldGetAHelpfulErrorExplainingThatCertificateChanged( "nah" ); // However as driverKitten has not connected to the server, so driverKitten should just simply connect! try ( Session session = driverKitten.session() ) { StatementResult statementResult = session.run( "RETURN 1" ); assertEquals( statementResult.single().get( 0 ).asInt(), 1 ); } } // signed certificate @Given( "^a driver configured to use a trusted certificate$" ) public void aDriverConfiguredToUseATrustedCertificate() throws Throwable { } @And( "^a running Neo4j Database using a certificate signed by the same trusted certificate$" ) public void aRunningNeo4jDatabaseUsingACertificateSignedByTheSameTrustedCertificate() throws Throwable { // create new root certificate File rootCert = tempFile( "temp_root_cert", ".cert" ); File rootKey = tempFile( "temp_root_key", ".key" ); SelfSignedCertificateGenerator certGenerator = new SelfSignedCertificateGenerator(); certGenerator.saveSelfSignedCertificate( rootCert ); certGenerator.savePrivateKey( rootKey ); // give root certificate to driver driver = GraphDatabase.driver( Neo4jRunner.DEFAULT_URI, Neo4jRunner.DEFAULT_AUTH_TOKEN, Config.build().withEncryption() .withTrustStrategy( trustCustomCertificateSignedBy( rootCert ) ).toConfig() ); // generate certificate signing request and get a certificate signed by the root private key File cert = tempFile( "temp_cert", ".cert" ); File key = tempFile( "temp_key", ".key" ); CertificateSigningRequestGenerator csrGenerator = new CertificateSigningRequestGenerator(); X509Certificate signedCert = certGenerator.sign( csrGenerator.certificateSigningRequest(), csrGenerator.publicKey() ); csrGenerator.savePrivateKey( key ); saveX509Cert( signedCert, cert ); neo4j.updateEncryptionKeyAndCert( key, cert ); } @When( "^I connect via a TLS-enabled transport$" ) public void iConnectViaATlsEnabledTransport() { } // same certificate @And( "^a running Neo4j Database using that exact trusted certificate$" ) public void aRunningNeo4jDatabaseUsingThatExactTrustedCertificate() { driver = GraphDatabase.driver( Neo4jRunner.DEFAULT_URI, Neo4jRunner.DEFAULT_AUTH_TOKEN, Config.build().withEncryption() .withTrustStrategy( trustCustomCertificateSignedBy( new File( HOME_DIR, DEFAULT_TLS_CERT_PATH ) ) ) .toConfig() ); } // invalid cert @And( "^a running Neo4j Database using a certificate not signed by the trusted certificate$" ) public void aRunningNeo4jDatabaseUsingACertNotSignedByTheTrustedCertificates() throws Throwable { File cert = tempFile( "temp_cert", ".cert" ); saveX509Cert( generateSelfSignedCertificate(), cert ); // give root certificate to driver driver = GraphDatabase.driver( Neo4jRunner.DEFAULT_URI, Neo4jRunner.DEFAULT_AUTH_TOKEN, Config.build().withEncryption() .withTrustStrategy( trustCustomCertificateSignedBy( cert ) ).toConfig() ); } @And( "^I should get a helpful error explaining that no trusted certificate found$" ) public void iShouldGetAHelpfulErrorExplainingThatCertificatedNotSigned() throws Throwable { assertThat( exception, notNullValue() ); assertThat( exception, instanceOf( SecurityException.class ) ); Throwable rootCause = getRootCause( exception ); assertThat( rootCause.toString(), containsString( "Signature does not match." ) ); } @After( "@tls" ) public void clearAfterEachScenario() throws Throwable { driver.close(); driver = null; knownHostsFile = null; exception = null; if ( driverKitten != null ) { driverKitten.close(); driverKitten = null; } } @After( "@modifies_db_config" ) public void resetDbWithDefaultSettings() throws Throwable { neo4j.restart(); } private File tempFile( String prefix, String suffix ) throws Throwable { File file = createTempFile( prefix, suffix ); file.deleteOnExit(); return file; } private Throwable getRootCause( Throwable th ) { Throwable cause = th; while ( cause.getCause() != null ) { cause = cause.getCause(); } return cause; } }