All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.neo4j.driver.internal.security.TrustOnFirstUseTrustManager Maven / Gradle / Ivy

/*
 * 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.internal.security;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;

import org.neo4j.driver.internal.net.BoltServerAddress;
import org.neo4j.driver.internal.util.BytePrinter;
import org.neo4j.driver.v1.Logger;

import static java.lang.String.format;
import static org.neo4j.driver.internal.util.CertificateTool.X509CertToString;

/**
 * References:
 * http://stackoverflow.com/questions/6802421/how-to-compare-distinct-implementations-of-java-security-cert-x509certificate?answertab=votes#tab-top
 */
public class TrustOnFirstUseTrustManager implements X509TrustManager
{
    /**
     * A list of pairs (known_server certificate) are stored in this file.
     * When establishing a SSL connection to a new server, we will save the server's host:port and its certificate in this
     * file.
     * Then when we try to connect to a known server again, we will authenticate the server by checking if it provides
     * the same certificate as the one saved in this file.
     */
    private final File knownHosts;

    /** The server ip:port (in digits) of the server that we are currently connected to */
    private final String serverId;
    private final Logger logger;

    /** The known certificate we've registered for this server */
    private String fingerprint;

    TrustOnFirstUseTrustManager( BoltServerAddress address, File knownHosts, Logger logger ) throws IOException
    {
        this.logger = logger;
        this.serverId = address.toString();
        this.knownHosts = knownHosts;
        load();
    }

    /**
     * Try to load the certificate form the file if the server we've connected is a known server.
     *
     * @throws IOException
     */
    private void load() throws IOException
    {
        if ( !knownHosts.exists() )
        {
            return;
        }

        assertKnownHostFileReadable();

        BufferedReader reader = new BufferedReader( new FileReader( knownHosts ) );
        String line;
        while ( (line = reader.readLine()) != null )
        {
            if ( (!line.trim().startsWith( "#" )) )
            {
                String[] strings = line.split( " " );
                if ( strings[0].trim().equals( serverId ) )
                {
                    // load the certificate
                    fingerprint = strings[1].trim();
                    return;
                }
            }
        }
        reader.close();
    }

    /**
     * Save a new (server_ip, cert) pair into knownHosts file
     *
     * @param fingerprint the SHA-512 fingerprint of the host certificate
     */
    private void saveTrustedHost( String fingerprint ) throws IOException
    {
        this.fingerprint = fingerprint;

        logger.info( "Adding %s as known and trusted certificate for %s.", fingerprint, serverId );
        createKnownCertFileIfNotExists();

        assertKnownHostFileWritable();
        BufferedWriter writer = new BufferedWriter( new FileWriter( knownHosts, true ) );
        writer.write( serverId + " " + this.fingerprint );
        writer.newLine();
        writer.close();
    }


    private void assertKnownHostFileReadable() throws IOException
    {
        if( !knownHosts.canRead() )
        {
            throw new IOException( format(
                    "Failed to load certificates from file %s as you have no read permissions to it.\n" +
                    "Try configuring the Neo4j driver to use a file system location you do have read permissions to.",
                    knownHosts.getAbsolutePath()
            ) );
        }
    }

    private void assertKnownHostFileWritable() throws IOException
    {
        if( !knownHosts.canWrite() )
        {
            throw new IOException( format(
                    "Failed to write certificates to file %s as you have no write permissions to it.\n" +
                    "Try configuring the Neo4j driver to use a file system location you do have write permissions to.",
                    knownHosts.getAbsolutePath()
            ) );
        }
    }

    /*
     * Disallow all client connection to this client
     */
    public void checkClientTrusted( X509Certificate[] chain, String authType )
            throws CertificateException
    {
        throw new CertificateException( "All client connections to this client are forbidden." );
    }

    /*
     * Trust the cert if it is seen first time for this server or it is the same with the one registered.
     */
    public void checkServerTrusted( X509Certificate[] chain, String authType )
            throws CertificateException
    {
        X509Certificate certificate = chain[0];

        String cert = fingerprint( certificate );

        if ( this.fingerprint == null )
        {
            try
            {
                saveTrustedHost( cert );
            }
            catch ( IOException e )
            {
                throw new CertificateException( format(
                        "Failed to save the server ID and the certificate received from the server to file %s.\n" +
                        "Server ID: %s\nReceived cert:\n%s",
                        knownHosts.getAbsolutePath(), serverId, X509CertToString( cert ) ), e );
            }
        }
        else
        {
            if ( !this.fingerprint.equals( cert ) )
            {
                throw new CertificateException( format(
                        "Unable to connect to neo4j at `%s`, because the certificate the server uses has changed. " +
                        "This is a security feature to protect against man-in-the-middle attacks.\n" +
                        "If you trust the certificate the server uses now, simply remove the line that starts with " +
                        "`%s` " +
                        "in the file `%s`.\n" +
                        "The old certificate saved in file is:\n%sThe New certificate received is:\n%s",
                        serverId, serverId, knownHosts.getAbsolutePath(),
                        X509CertToString( this.fingerprint ), X509CertToString( cert ) ) );
            }
        }
    }

    /**
     * Calculate the certificate fingerprint - simply the SHA-512 hash of the DER-encoded certificate.
     */
    public static String fingerprint( X509Certificate cert ) throws CertificateException
    {
        try
        {
            MessageDigest md = MessageDigest.getInstance( "SHA-512" );
            md.update( cert.getEncoded() );
            return BytePrinter.compactHex( md.digest() );
        }
        catch( NoSuchAlgorithmException e )
        {
            // SHA-1 not available
            throw new CertificateException( "Cannot use TLS on this platform, because SHA-512 message digest algorithm is not available: " + e.getMessage(), e );
        }
    }

    private File createKnownCertFileIfNotExists() throws IOException
    {
        if ( !knownHosts.exists() )
        {
            File parentDir = knownHosts.getParentFile();
            try
            {
                if ( parentDir != null && !parentDir.exists() )
                {
                    if ( !parentDir.mkdirs() )
                    {
                        throw new IOException( "Failed to create directories for the known hosts file in " + knownHosts.getAbsolutePath() +
                                               ". This is usually because you do not have write permissions to the directory. " +
                                               "Try configuring the Neo4j driver to use a file system location you do have write permissions to." );
                    }
                }
                if ( !knownHosts.createNewFile() )
                {
                    throw new IOException( "Failed to create a known hosts file at " + knownHosts.getAbsolutePath() +
                                           ". This is usually because you do not have write permissions to the directory. " +
                                           "Try configuring the Neo4j driver to use a file system location you do have write permissions to." );
                }
            }
            catch( SecurityException e )
            {
                throw new IOException( "Failed to create known host file and/or parent directories at " + knownHosts.getAbsolutePath() +
                                       ". This is usually because you do not have write permission to the directory. " +
                                       "Try configuring the Neo4j driver to use a file location you have write permissions to." );
            }
            BufferedWriter writer = new BufferedWriter( new FileWriter( knownHosts ) );
            writer.write( "# This file contains trusted certificates for Neo4j servers, it's created by Neo4j drivers." );
            writer.newLine();
            writer.write( "# You can configure the location of this file in `org.neo4j.driver.Config`" );
            writer.newLine();
            writer.close();
        }

        return knownHosts;
    }

    /**
     * No issuer is trusted.
     */
    public X509Certificate[] getAcceptedIssuers()
    {
        return new X509Certificate[0];
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy