Skip to content

Commit 10ed730

Browse files
committed
Add support for multiple certificate files (neo4j#1153)
* Add support for multiple certificate files This update brings support for specifying multiple certificate files via `TrustStrategy#trustCustomCertificateSignedBy(File...)`. Also, it deprecates `TrustStrategy#certFiles()` that is superseded by `TrustStrategy#certFiles()`. In addition, it adds support for the following Testkit feature flags: - `Feature:API:SSLConfig` - `Detail:DefaultSecurityConfigValueEquality` * Updated certFiles management
1 parent 693b836 commit 10ed730

File tree

11 files changed

+128
-51
lines changed

11 files changed

+128
-51
lines changed

driver/clirr-ignored-differences.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,11 @@
5555
<method>java.lang.Iterable values()</method>
5656
</difference>
5757

58+
<difference>
59+
<className>org/neo4j/driver/Config$TrustStrategy</className>
60+
<differenceType>7005</differenceType>
61+
<method>org.neo4j.driver.Config$TrustStrategy trustCustomCertificateSignedBy(java.io.File)</method>
62+
<to>org.neo4j.driver.Config$TrustStrategy trustCustomCertificateSignedBy(java.io.File[])</to>
63+
</difference>
64+
5865
</differences>

driver/src/main/java/org/neo4j/driver/Config.java

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
import java.io.File;
2222
import java.io.Serializable;
2323
import java.net.InetAddress;
24+
import java.util.ArrayList;
25+
import java.util.Arrays;
26+
import java.util.Collections;
27+
import java.util.List;
2428
import java.util.Objects;
2529
import java.util.concurrent.TimeUnit;
2630
import java.util.logging.Level;
@@ -469,7 +473,7 @@ public ConfigBuilder withoutEncryption()
469473

470474
/**
471475
* Specify how to determine the authenticity of an encryption certificate provided by the Neo4j instance we are connecting to. This defaults to {@link
472-
* TrustStrategy#trustSystemCertificates()}. See {@link TrustStrategy#trustCustomCertificateSignedBy(File)} for using certificate signatures instead to
476+
* TrustStrategy#trustSystemCertificates()}. See {@link TrustStrategy#trustCustomCertificateSignedBy(File...)} for using certificate signatures instead to
473477
* verify trust.
474478
* <p>
475479
* This is an important setting to understand, because unless we know that the remote server we have an encrypted connection to is really Neo4j, there
@@ -798,19 +802,20 @@ public enum Strategy
798802
}
799803

800804
private final Strategy strategy;
801-
private final File certFile;
805+
private final List<File> certFiles;
802806
private boolean hostnameVerificationEnabled = true;
803807
private RevocationStrategy revocationStrategy = RevocationStrategy.NO_CHECKS;
804808

805809
private TrustStrategy( Strategy strategy )
806810
{
807-
this( strategy, null );
811+
this( strategy, Collections.emptyList() );
808812
}
809813

810-
private TrustStrategy( Strategy strategy, File certFile )
814+
private TrustStrategy( Strategy strategy, List<File> certFiles )
811815
{
816+
Objects.requireNonNull( certFiles, "certFiles can't be null" );
812817
this.strategy = strategy;
813-
this.certFile = certFile;
818+
this.certFiles = Collections.unmodifiableList( new ArrayList<>( certFiles ) );
814819
}
815820

816821
/**
@@ -827,10 +832,22 @@ public Strategy strategy()
827832
* Return the configured certificate file.
828833
*
829834
* @return configured certificate or {@code null} if trust strategy does not require a certificate.
835+
* @deprecated superseded by {@link TrustStrategy#certFiles()}
830836
*/
837+
@Deprecated
831838
public File certFile()
832839
{
833-
return certFile;
840+
return certFiles.isEmpty() ? null : certFiles.get( 0 );
841+
}
842+
843+
/**
844+
* Return the configured certificate files.
845+
*
846+
* @return configured certificate files or empty list if trust strategy does not require certificates.
847+
*/
848+
public List<File> certFiles()
849+
{
850+
return certFiles;
834851
}
835852

836853
/**
@@ -866,18 +883,18 @@ public TrustStrategy withoutHostnameVerification()
866883
}
867884

868885
/**
869-
* Only encrypted connections to Neo4j instances with certificates signed by a trusted certificate will be accepted.
870-
* The file specified should contain one or more trusted X.509 certificates.
886+
* Only encrypted connections to Neo4j instances with certificates signed by a trusted certificate will be accepted. The file(s) specified should
887+
* contain one or more trusted X.509 certificates.
871888
* <p>
872-
* The certificate(s) in the file must be encoded using PEM encoding, meaning the certificates in the file should be encoded using Base64,
873-
* and each certificate is bounded at the beginning by "-----BEGIN CERTIFICATE-----", and bounded at the end by "-----END CERTIFICATE-----".
889+
* The certificate(s) in the file(s) must be encoded using PEM encoding, meaning the certificates in the file(s) should be encoded using Base64, and
890+
* each certificate is bounded at the beginning by "-----BEGIN CERTIFICATE-----", and bounded at the end by "-----END CERTIFICATE-----".
874891
*
875-
* @param certFile the trusted certificate file
892+
* @param certFiles the trusted certificate files
876893
* @return an authentication config
877894
*/
878-
public static TrustStrategy trustCustomCertificateSignedBy( File certFile )
895+
public static TrustStrategy trustCustomCertificateSignedBy( File... certFiles )
879896
{
880-
return new TrustStrategy( Strategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES, certFile );
897+
return new TrustStrategy( Strategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES, Arrays.asList( certFiles ) );
881898
}
882899

883900
/**

driver/src/main/java/org/neo4j/driver/internal/SecuritySettings.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.io.IOException;
2222
import java.io.Serializable;
2323
import java.security.GeneralSecurityException;
24-
import java.util.Objects;
2524

2625
import org.neo4j.driver.Config;
2726
import org.neo4j.driver.exceptions.ClientException;
@@ -73,7 +72,7 @@ private boolean hasEqualTrustStrategy( SecuritySettings other )
7372
}
7473

7574
return t1.isHostnameVerificationEnabled() == t2.isHostnameVerificationEnabled() && t1.strategy() == t2.strategy() &&
76-
Objects.equals( t1.certFile(), t2.certFile() ) && t1.revocationStrategy() == t2.revocationStrategy();
75+
t1.certFiles().equals( t2.certFiles() ) && t1.revocationStrategy() == t2.revocationStrategy();
7776
}
7877

7978
public SecurityPlan createSecurityPlan( String uriScheme )
@@ -131,7 +130,7 @@ private static SecurityPlan createSecurityPlanImpl( boolean encrypted, Config.Tr
131130
switch ( trustStrategy.strategy() )
132131
{
133132
case TRUST_CUSTOM_CA_SIGNED_CERTIFICATES:
134-
return SecurityPlanImpl.forCustomCASignedCertificates( trustStrategy.certFile(), hostnameVerificationEnabled, revocationStrategy );
133+
return SecurityPlanImpl.forCustomCASignedCertificates( trustStrategy.certFiles(), hostnameVerificationEnabled, revocationStrategy );
135134
case TRUST_SYSTEM_CA_SIGNED_CERTIFICATES:
136135
return SecurityPlanImpl.forSystemCASignedCertificates( hostnameVerificationEnabled, revocationStrategy );
137136
case TRUST_ALL_CERTIFICATES:

driver/src/main/java/org/neo4j/driver/internal/security/SecurityPlanImpl.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import java.security.cert.PKIXBuilderParameters;
2828
import java.security.cert.X509CertSelector;
2929
import java.security.cert.X509Certificate;
30+
import java.util.Collections;
31+
import java.util.List;
3032
import javax.net.ssl.CertPathTrustManagerParameters;
3133
import javax.net.ssl.KeyManager;
3234
import javax.net.ssl.SSLContext;
@@ -53,31 +55,31 @@ public static SecurityPlan forAllCertificates( boolean requiresHostnameVerificat
5355
return new SecurityPlanImpl( true, sslContext, requiresHostnameVerification, revocationStrategy );
5456
}
5557

56-
public static SecurityPlan forCustomCASignedCertificates( File certFile, boolean requiresHostnameVerification,
58+
public static SecurityPlan forCustomCASignedCertificates( List<File> certFiles, boolean requiresHostnameVerification,
5759
RevocationStrategy revocationStrategy )
5860
throws GeneralSecurityException, IOException
5961
{
60-
SSLContext sslContext = configureSSLContext( certFile, revocationStrategy );
62+
SSLContext sslContext = configureSSLContext( certFiles, revocationStrategy );
6163
return new SecurityPlanImpl( true, sslContext, requiresHostnameVerification, revocationStrategy );
6264
}
6365

6466
public static SecurityPlan forSystemCASignedCertificates( boolean requiresHostnameVerification, RevocationStrategy revocationStrategy )
6567
throws GeneralSecurityException, IOException
6668
{
67-
SSLContext sslContext = configureSSLContext( null, revocationStrategy );
69+
SSLContext sslContext = configureSSLContext( Collections.emptyList(), revocationStrategy );
6870
return new SecurityPlanImpl( true, sslContext, requiresHostnameVerification, revocationStrategy );
6971
}
7072

71-
private static SSLContext configureSSLContext( File customCertFile, RevocationStrategy revocationStrategy )
73+
private static SSLContext configureSSLContext( List<File> customCertFiles, RevocationStrategy revocationStrategy )
7274
throws GeneralSecurityException, IOException
7375
{
7476
KeyStore trustedKeyStore = KeyStore.getInstance( KeyStore.getDefaultType() );
7577
trustedKeyStore.load( null, null );
7678

77-
if ( customCertFile != null )
79+
if ( !customCertFiles.isEmpty() )
7880
{
79-
// A certificate file is specified so we will load the certificates in the file
80-
loadX509Cert( customCertFile, trustedKeyStore );
81+
// Certificate files are specified, so we will load the certificates in the file
82+
loadX509Cert( customCertFiles, trustedKeyStore );
8183
}
8284
else
8385
{

driver/src/main/java/org/neo4j/driver/internal/util/CertificateTool.java

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.security.cert.CertificateFactory;
3333
import java.security.cert.X509Certificate;
3434
import java.util.Base64;
35+
import java.util.List;
3536

3637
/**
3738
* A tool used to save, load certs, etc.
@@ -106,35 +107,39 @@ public static void saveX509Cert( Certificate[] certs, File certFile ) throws Gen
106107
/**
107108
* Load the certificates written in X.509 format in a file to a key store.
108109
*
109-
* @param certFile
110+
* @param certFiles
110111
* @param keyStore
111112
* @throws GeneralSecurityException
112113
* @throws IOException
113114
*/
114-
public static void loadX509Cert( File certFile, KeyStore keyStore ) throws GeneralSecurityException, IOException
115+
public static void loadX509Cert( List<File> certFiles, KeyStore keyStore ) throws GeneralSecurityException, IOException
115116
{
116-
try ( BufferedInputStream inputStream = new BufferedInputStream( new FileInputStream( certFile ) ) )
117+
int certCount = 0; // The files might contain multiple certs
118+
for ( File certFile : certFiles )
117119
{
118-
CertificateFactory certFactory = CertificateFactory.getInstance( "X.509" );
119-
120-
int certCount = 0; // The file might contain multiple certs
121-
while ( inputStream.available() > 0 )
120+
try ( BufferedInputStream inputStream = new BufferedInputStream( new FileInputStream( certFile ) ) )
122121
{
123-
try
124-
{
125-
Certificate cert = certFactory.generateCertificate( inputStream );
126-
certCount++;
127-
loadX509Cert( cert, "neo4j.javadriver.trustedcert." + certCount, keyStore );
128-
}
129-
catch ( CertificateException e )
122+
CertificateFactory certFactory = CertificateFactory.getInstance( "X.509" );
123+
124+
while ( inputStream.available() > 0 )
130125
{
131-
if ( e.getCause() != null && e.getCause().getMessage().equals( "Empty input" ) )
126+
try
127+
{
128+
Certificate cert = certFactory.generateCertificate( inputStream );
129+
certCount++;
130+
loadX509Cert( cert, "neo4j.javadriver.trustedcert." + certCount, keyStore );
131+
}
132+
catch ( CertificateException e )
132133
{
133-
// This happens if there is whitespace at the end of the certificate - we load one cert, and then try and load a
134-
// second cert, at which point we fail
135-
return;
134+
if ( e.getCause() != null && e.getCause().getMessage().equals( "Empty input" ) )
135+
{
136+
// This happens if there is whitespace at the end of the certificate - we load one cert, and then try and load a
137+
// second cert, at which point we fail
138+
return;
139+
}
140+
throw new IOException( "Failed to load certificate from `" + certFile.getAbsolutePath() + "`: " + certCount + " : " + e.getMessage(),
141+
e );
136142
}
137-
throw new IOException( "Failed to load certificate from `" + certFile.getAbsolutePath() + "`: " + certCount + " : " + e.getMessage(), e );
138143
}
139144
}
140145
}

driver/src/test/java/org/neo4j/driver/ConfigTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ void shouldChangeToTrustedCert()
8080

8181
// Then
8282
assertEquals( authConfig.strategy(), Config.TrustStrategy.Strategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES );
83-
assertEquals( trustedCert.getAbsolutePath(), authConfig.certFile().getAbsolutePath() );
83+
assertEquals( trustedCert.getAbsolutePath(), authConfig.certFiles().get( 0 ).getAbsolutePath() );
8484
}
8585

8686
@Test
@@ -401,7 +401,7 @@ void shouldSerialize() throws Exception
401401
assertEquals( config.eventLoopThreads(), verify.eventLoopThreads() );
402402
assertEquals( config.encrypted(), verify.encrypted() );
403403
assertEquals( config.trustStrategy().strategy(), verify.trustStrategy().strategy() );
404-
assertEquals( config.trustStrategy().certFile(), verify.trustStrategy().certFile() );
404+
assertEquals( config.trustStrategy().certFiles(), verify.trustStrategy().certFiles() );
405405
assertEquals( config.trustStrategy().isHostnameVerificationEnabled(), verify.trustStrategy().isHostnameVerificationEnabled() );
406406
assertEquals( config.trustStrategy().revocationStrategy(), verify.trustStrategy().revocationStrategy() );
407407
assertEquals( config.userAgent(), verify.userAgent() );

driver/src/test/java/org/neo4j/driver/util/CertificateUtilTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.security.KeyStore;
2525
import java.security.cert.Certificate;
2626
import java.security.cert.X509Certificate;
27+
import java.util.Collections;
2728
import java.util.Enumeration;
2829

2930
import org.neo4j.driver.internal.util.CertificateTool;
@@ -52,7 +53,7 @@ void shouldLoadMultipleCertsIntoKeyStore() throws Throwable
5253
keyStore.load( null, null );
5354

5455
// When
55-
CertificateTool.loadX509Cert( certFile, keyStore );
56+
CertificateTool.loadX509Cert( Collections.singletonList( certFile ), keyStore );
5657

5758
// Then
5859
Enumeration<String> aliases = keyStore.aliases();

testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ public class GetFeatures implements TestkitRequest
6161
"Temporary:FullSummary",
6262
"Temporary:ResultKeys",
6363
"Temporary:TransactionClose",
64-
"Optimization:EagerTransactionBegin"
64+
"Optimization:EagerTransactionBegin",
65+
"Feature:API:SSLConfig",
66+
"Detail:DefaultSecurityConfigValueEquality"
6567
) );
6668

6769
private static final Set<String> SYNC_FEATURES = new HashSet<>( Arrays.asList(

testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewDriver.java

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@
3131
import neo4j.org.testkit.backend.messages.responses.TestkitResponse;
3232
import reactor.core.publisher.Mono;
3333

34+
import java.io.File;
3435
import java.net.InetAddress;
3536
import java.net.URI;
3637
import java.net.UnknownHostException;
38+
import java.nio.file.Path;
39+
import java.nio.file.Paths;
3740
import java.util.LinkedHashSet;
41+
import java.util.List;
3842
import java.util.Optional;
3943
import java.util.concurrent.CompletableFuture;
4044
import java.util.concurrent.CompletionStage;
@@ -117,7 +121,8 @@ public TestkitResponse process( TestkitState testkitState )
117121
Config config = configBuilder.build();
118122
try
119123
{
120-
driver = driver( URI.create( data.uri ), authToken, config, retrySettings, domainNameResolver, testkitState, id );
124+
driver = driver( URI.create( data.uri ), authToken, config, retrySettings, domainNameResolver, configureSecuritySettingsBuilder(), testkitState,
125+
id );
121126
}
122127
catch ( RuntimeException e )
123128
{
@@ -223,11 +228,9 @@ private CompletionStage<TestkitCallbackResult> dispatchTestkitCallback( TestkitS
223228
}
224229

225230
private org.neo4j.driver.Driver driver( URI uri, AuthToken authToken, Config config, RetrySettings retrySettings, DomainNameResolver domainNameResolver,
226-
TestkitState testkitState,
227-
String driverId )
231+
SecuritySettings.SecuritySettingsBuilder securitySettingsBuilder, TestkitState testkitState, String driverId )
228232
{
229233
RoutingSettings routingSettings = RoutingSettings.DEFAULT;
230-
SecuritySettings.SecuritySettingsBuilder securitySettingsBuilder = new SecuritySettings.SecuritySettingsBuilder();
231234
SecuritySettings securitySettings = securitySettingsBuilder.build();
232235
SecurityPlan securityPlan = securitySettings.createSecurityPlan( uri.getScheme() );
233236
return new DriverFactoryWithDomainNameResolver( domainNameResolver, testkitState, driverId )
@@ -248,6 +251,41 @@ private Optional<TestkitResponse> handleExceptionAsErrorResponse( TestkitState t
248251
return response;
249252
}
250253

254+
private SecuritySettings.SecuritySettingsBuilder configureSecuritySettingsBuilder()
255+
{
256+
SecuritySettings.SecuritySettingsBuilder securitySettingsBuilder = new SecuritySettings.SecuritySettingsBuilder();
257+
if ( data.encrypted )
258+
{
259+
securitySettingsBuilder.withEncryption();
260+
}
261+
else
262+
{
263+
securitySettingsBuilder.withoutEncryption();
264+
}
265+
266+
if ( data.trustedCertificates != null )
267+
{
268+
if ( !data.trustedCertificates.isEmpty() )
269+
{
270+
File[] certs = data.trustedCertificates.stream()
271+
.map( cert -> "/usr/local/share/custom-ca-certificates/" + cert )
272+
.map( Paths::get )
273+
.map( Path::toFile )
274+
.toArray( File[]::new );
275+
securitySettingsBuilder.withTrustStrategy( Config.TrustStrategy.trustCustomCertificateSignedBy( certs ) );
276+
}
277+
else
278+
{
279+
securitySettingsBuilder.withTrustStrategy( Config.TrustStrategy.trustAllCertificates() );
280+
}
281+
}
282+
else
283+
{
284+
securitySettingsBuilder.withTrustStrategy( Config.TrustStrategy.trustSystemCertificates() );
285+
}
286+
return securitySettingsBuilder;
287+
}
288+
251289
@Setter
252290
@Getter
253291
public static class NewDriverBody
@@ -263,6 +301,8 @@ public static class NewDriverBody
263301
private Long livenessCheckTimeoutMs;
264302
private Integer maxConnectionPoolSize;
265303
private Long connectionAcquisitionTimeoutMs;
304+
private boolean encrypted;
305+
private List<String> trustedCertificates;
266306
}
267307

268308
@RequiredArgsConstructor

0 commit comments

Comments
 (0)