24
24
import java .io .FileReader ;
25
25
import java .io .FileWriter ;
26
26
import java .io .IOException ;
27
- import java .net .InetAddress ;
27
+ import java .security .MessageDigest ;
28
+ import java .security .NoSuchAlgorithmException ;
28
29
import java .security .cert .CertificateException ;
29
30
import java .security .cert .X509Certificate ;
30
31
import javax .net .ssl .X509TrustManager ;
31
- import javax .xml .bind .DatatypeConverter ;
32
+
33
+ import org .neo4j .driver .internal .spi .Logger ;
34
+ import org .neo4j .driver .internal .util .BytePrinter ;
32
35
33
36
import static org .neo4j .driver .internal .util .CertificateTool .X509CertToString ;
34
37
35
38
/**
36
39
* References:
37
40
* http://stackoverflow.com/questions/6802421/how-to-compare-distinct-implementations-of-java-security-cert-x509certificate?answertab=votes#tab-top
38
41
*/
39
-
40
42
class TrustOnFirstUseTrustManager implements X509TrustManager
41
43
{
42
44
/**
43
- * A list of pairs (known_server, certificate) are stored in this file.
44
- * When establishing a SSL connection to a new server, we will save the server's ip :port and its certificate in this
45
+ * A list of pairs (known_server certificate) are stored in this file.
46
+ * When establishing a SSL connection to a new server, we will save the server's host :port and its certificate in this
45
47
* file.
46
48
* Then when we try to connect to a known server again, we will authenticate the server by checking if it provides
47
49
* the same certificate as the one saved in this file.
@@ -50,15 +52,15 @@ class TrustOnFirstUseTrustManager implements X509TrustManager
50
52
51
53
/** The server ip:port (in digits) of the server that we are currently connected to */
52
54
private final String serverId ;
55
+ private final Logger logger ;
53
56
54
57
/** The known certificate we've registered for this server */
55
- private String cert ;
58
+ private String fingerprint ;
56
59
57
- TrustOnFirstUseTrustManager ( String host , int port , File knownCerts ) throws IOException
60
+ TrustOnFirstUseTrustManager ( String host , int port , File knownCerts , Logger logger ) throws IOException
58
61
{
59
- String ip = InetAddress .getByName ( host ).getHostAddress (); // localhost -> 127.0.0.1
60
- this .serverId = ip + ":" + port ;
61
-
62
+ this .logger = logger ;
63
+ this .serverId = host + ":" + port ;
62
64
this .knownCerts = knownCerts ;
63
65
load ();
64
66
}
@@ -76,16 +78,16 @@ private void load() throws IOException
76
78
}
77
79
78
80
BufferedReader reader = new BufferedReader ( new FileReader ( knownCerts ) );
79
- String line = null ;
81
+ String line ;
80
82
while ( (line = reader .readLine ()) != null )
81
83
{
82
- if ( (!line .trim ().startsWith ( "#" )) && line . contains ( "," ) )
84
+ if ( (!line .trim ().startsWith ( "#" )) )
83
85
{
84
- String [] strings = line .split ( ", " );
86
+ String [] strings = line .split ( " " );
85
87
if ( strings [0 ].trim ().equals ( serverId ) )
86
88
{
87
89
// load the certificate
88
- cert = strings [1 ].trim ();
90
+ fingerprint = strings [1 ].trim ();
89
91
return ;
90
92
}
91
93
}
@@ -96,16 +98,17 @@ private void load() throws IOException
96
98
/**
97
99
* Save a new (server_ip, cert) pair into knownCerts file
98
100
*
99
- * @param cert
101
+ * @param fingerprint
100
102
*/
101
- private void save ( String cert ) throws IOException
103
+ private void saveTrustedHost ( String fingerprint ) throws IOException
102
104
{
103
- this .cert = cert ;
105
+ this .fingerprint = fingerprint ;
104
106
107
+ logger .warn ( "Adding %s as known and trusted certificate for %s." , fingerprint , serverId );
105
108
createKnownCertFileIfNotExists ();
106
109
107
110
BufferedWriter writer = new BufferedWriter ( new FileWriter ( knownCerts , true ) );
108
- writer .write ( serverId + ", " + this .cert );
111
+ writer .write ( serverId + " " + this .fingerprint );
109
112
writer .newLine ();
110
113
writer .close ();
111
114
}
@@ -126,15 +129,14 @@ public void checkServerTrusted( X509Certificate[] chain, String authType )
126
129
throws CertificateException
127
130
{
128
131
X509Certificate certificate = chain [0 ];
129
- byte [] encoded = certificate .getEncoded ();
130
132
131
- String cert = DatatypeConverter . printBase64Binary ( encoded );
133
+ String cert = fingerprint ( certificate );
132
134
133
- if ( this .cert == null )
135
+ if ( this .fingerprint == null )
134
136
{
135
137
try
136
138
{
137
- save ( cert );
139
+ saveTrustedHost ( cert );
138
140
}
139
141
catch ( IOException e )
140
142
{
@@ -146,7 +148,7 @@ public void checkServerTrusted( X509Certificate[] chain, String authType )
146
148
}
147
149
else
148
150
{
149
- if ( !this .cert .equals ( cert ) )
151
+ if ( !this .fingerprint .equals ( cert ) )
150
152
{
151
153
throw new CertificateException ( String .format (
152
154
"Unable to connect to neo4j at `%s`, because the certificate the server uses has changed. " +
@@ -156,21 +158,47 @@ public void checkServerTrusted( X509Certificate[] chain, String authType )
156
158
"in the file `%s`.\n " +
157
159
"The old certificate saved in file is:\n %sThe New certificate received is:\n %s" ,
158
160
serverId , serverId , knownCerts .getAbsolutePath (),
159
- X509CertToString ( this .cert ), X509CertToString ( cert ) ) );
161
+ X509CertToString ( this .fingerprint ), X509CertToString ( cert ) ) );
160
162
}
161
163
}
162
164
}
163
165
166
+ /**
167
+ * Calculate the certificate fingerprint - simply the SHA-1 hash of the DER-encoded certificate.
168
+ */
169
+ public static String fingerprint ( X509Certificate cert ) throws CertificateException
170
+ {
171
+ try
172
+ {
173
+ MessageDigest md = MessageDigest .getInstance ( "SHA-1" );
174
+ md .update ( cert .getEncoded () );
175
+ return BytePrinter .compactHex ( md .digest () );
176
+ }
177
+ catch ( NoSuchAlgorithmException e )
178
+ {
179
+ // SHA-1 not available
180
+ throw new CertificateException ( "Cannot use TLS on this platform, because SHA-1 message digest algorithm is not available: " + e .getMessage (), e );
181
+ }
182
+ }
183
+
164
184
private File createKnownCertFileIfNotExists () throws IOException
165
185
{
166
186
if ( !knownCerts .exists () )
167
187
{
168
188
File parentDir = knownCerts .getParentFile ();
169
189
if ( parentDir != null && !parentDir .exists () )
170
190
{
171
- parentDir .mkdirs ();
191
+ if (!parentDir .mkdirs ()) {
192
+ throw new IOException ( "Failed to create directories for the known hosts file in " + knownCerts .getAbsolutePath () + ". This is usually " +
193
+ "because you do not have write permissions to the directory. Try configuring the Neo4j driver to use a file " +
194
+ "system location you do have write permissions to." );
195
+ }
196
+ }
197
+ if (!knownCerts .createNewFile ()) {
198
+ throw new IOException ( "Failed to create a known hosts file at " + knownCerts .getAbsolutePath () + ". This is usually " +
199
+ "because you do not have write permissions to the directory. Try configuring the Neo4j driver to use a file " +
200
+ "system location you do have write permissions to." );
172
201
}
173
- knownCerts .createNewFile ();
174
202
BufferedWriter writer = new BufferedWriter ( new FileWriter ( knownCerts ) );
175
203
writer .write ( "# This file contains trusted certificates for Neo4j servers, it's created by Neo4j drivers." );
176
204
writer .newLine ();
0 commit comments