From 2db210a898dcfcdd2fdc25207687c94f6e644b70 Mon Sep 17 00:00:00 2001
From: Dmitriy Tverdiakov <11927660+injectives@users.noreply.github.com>
Date: Wed, 6 Mar 2024 17:05:23 +0000
Subject: [PATCH] Introduce mTLS support (#1543)
* Introduce mTLS support
Please note that this feature is in preview.
* Make hasUpdate unnecessary
* Fix inspection errors
---
benchkit-backend/pom.xml | 2 +-
bundle/pom.xml | 2 +-
driver/pom.xml | 2 +-
.../org/neo4j/driver/ClientCertificate.java | 29 ++
.../driver/ClientCertificateManager.java | 48 ++
.../driver/ClientCertificateManagers.java | 41 ++
.../org/neo4j/driver/ClientCertificates.java | 54 ++
.../java/org/neo4j/driver/GraphDatabase.java | 244 ++++++++-
.../RotatingClientCertificateManager.java | 33 ++
.../neo4j/driver/internal/DriverFactory.java | 13 +-
.../internal/InternalClientCertificate.java | 23 +
...ernalRotatingClientCertificateManager.java | 57 +++
.../ValidatingClientCertificateManager.java | 43 ++
.../async/connection/ChannelConnector.java | 6 +-
.../connection/ChannelConnectorImpl.java | 66 ++-
.../connection/DeferredChannelFuture.java | 296 +++++++++++
.../connection/NettyChannelInitializer.java | 6 +-
.../internal/async/pool/NettyChannelPool.java | 33 +-
.../neo4j/driver/internal/pki/DerUtils.java | 204 ++++++++
.../neo4j/driver/internal/pki/PemFormats.java | 463 ++++++++++++++++++
.../neo4j/driver/internal/pki/PemParser.java | 150 ++++++
.../internal/security/SSLContextManager.java | 154 ++++++
.../internal/security/SecurityPlan.java | 12 +-
.../internal/security/SecurityPlanImpl.java | 103 +++-
.../internal/security/SecurityPlans.java | 38 +-
.../driver/ClientCertificateManagersTest.java | 34 ++
.../integration/ChannelConnectorImplIT.java | 60 ++-
.../integration/ConnectionHandlingIT.java | 1 +
.../driver/integration/ConnectionPoolIT.java | 2 +-
.../org/neo4j/driver/integration/ErrorIT.java | 4 +-
.../driver/integration/ServerKilledIT.java | 2 +-
.../driver/integration/SessionBoltV3IT.java | 8 +-
.../neo4j/driver/integration/SessionIT.java | 10 +-
.../driver/integration/SharedEventLoopIT.java | 1 +
.../driver/integration/TransactionIT.java | 2 +-
.../integration/UnmanagedTransactionIT.java | 2 +-
.../internal/CustomSecurityPlanTest.java | 1 +
.../driver/internal/DriverFactoryTest.java | 4 +-
...RotatingClientCertificateManagerTests.java | 76 +++
.../NettyChannelInitializerTest.java | 31 +-
.../security/SSLContextManagerTests.java | 214 ++++++++
.../internal/security/SecurityPlans.java | 31 +-
.../util/io/ChannelTrackingConnector.java | 13 +-
.../neo4j/driver/ClientCertificatesTests.java | 62 +++
examples/pom.xml | 2 +-
pom.xml | 2 +-
testkit-backend/pom.xml | 2 +-
.../org/testkit/backend/TestkitState.java | 18 +
.../messages/requests/ClientCertificate.java | 38 ++
.../ClientCertificateProviderClose.java | 45 ++
.../ClientCertificateProviderCompleted.java | 39 ++
.../messages/requests/GetFeatures.java | 3 +-
.../NewClientCertificateProvider.java | 94 ++++
.../backend/messages/requests/NewDriver.java | 22 +-
.../messages/requests/TestkitRequest.java | 5 +-
.../responses/ClientCertificateProvider.java | 37 ++
.../ClientCertificateProviderRequest.java | 43 ++
testkit-tests/pom.xml | 2 +-
58 files changed, 2865 insertions(+), 167 deletions(-)
create mode 100644 driver/src/main/java/org/neo4j/driver/ClientCertificate.java
create mode 100644 driver/src/main/java/org/neo4j/driver/ClientCertificateManager.java
create mode 100644 driver/src/main/java/org/neo4j/driver/ClientCertificateManagers.java
create mode 100644 driver/src/main/java/org/neo4j/driver/ClientCertificates.java
create mode 100644 driver/src/main/java/org/neo4j/driver/RotatingClientCertificateManager.java
create mode 100644 driver/src/main/java/org/neo4j/driver/internal/InternalClientCertificate.java
create mode 100644 driver/src/main/java/org/neo4j/driver/internal/InternalRotatingClientCertificateManager.java
create mode 100644 driver/src/main/java/org/neo4j/driver/internal/ValidatingClientCertificateManager.java
create mode 100644 driver/src/main/java/org/neo4j/driver/internal/async/connection/DeferredChannelFuture.java
create mode 100644 driver/src/main/java/org/neo4j/driver/internal/pki/DerUtils.java
create mode 100644 driver/src/main/java/org/neo4j/driver/internal/pki/PemFormats.java
create mode 100644 driver/src/main/java/org/neo4j/driver/internal/pki/PemParser.java
create mode 100644 driver/src/main/java/org/neo4j/driver/internal/security/SSLContextManager.java
create mode 100644 driver/src/test/java/org/neo4j/driver/ClientCertificateManagersTest.java
create mode 100644 driver/src/test/java/org/neo4j/driver/internal/InternalRotatingClientCertificateManagerTests.java
create mode 100644 driver/src/test/java/org/neo4j/driver/internal/security/SSLContextManagerTests.java
create mode 100644 driver/src/test/java/org/neo4j/driver/org/neo4j/driver/ClientCertificatesTests.java
create mode 100644 testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificate.java
create mode 100644 testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderClose.java
create mode 100644 testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderCompleted.java
create mode 100644 testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewClientCertificateProvider.java
create mode 100644 testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProvider.java
create mode 100644 testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProviderRequest.java
diff --git a/benchkit-backend/pom.xml b/benchkit-backend/pom.xml
index 442e88a26d..445a078b33 100644
--- a/benchkit-backend/pom.xml
+++ b/benchkit-backend/pom.xml
@@ -7,7 +7,7 @@
neo4j-java-driver-parent
org.neo4j.driver
- 5.18-SNAPSHOT
+ 5.19-SNAPSHOT
benchkit-backend
diff --git a/bundle/pom.xml b/bundle/pom.xml
index 2d108187c9..b3881b283d 100644
--- a/bundle/pom.xml
+++ b/bundle/pom.xml
@@ -6,7 +6,7 @@
org.neo4j.driver
neo4j-java-driver-parent
- 5.18-SNAPSHOT
+ 5.19-SNAPSHOT
..
diff --git a/driver/pom.xml b/driver/pom.xml
index 6cee687590..37433c31b2 100644
--- a/driver/pom.xml
+++ b/driver/pom.xml
@@ -6,7 +6,7 @@
org.neo4j.driver
neo4j-java-driver-parent
- 5.18-SNAPSHOT
+ 5.19-SNAPSHOT
neo4j-java-driver
diff --git a/driver/src/main/java/org/neo4j/driver/ClientCertificate.java b/driver/src/main/java/org/neo4j/driver/ClientCertificate.java
new file mode 100644
index 0000000000..e8f65882d0
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/ClientCertificate.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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;
+
+import org.neo4j.driver.internal.InternalClientCertificate;
+import org.neo4j.driver.util.Preview;
+
+/**
+ * An opaque container for client certificate used for mTLS.
+ *
+ * Use {@link ClientCertificates} to create new instances.
+ * @since 5.19
+ */
+@Preview(name = "mTLS")
+public sealed interface ClientCertificate permits InternalClientCertificate {}
diff --git a/driver/src/main/java/org/neo4j/driver/ClientCertificateManager.java b/driver/src/main/java/org/neo4j/driver/ClientCertificateManager.java
new file mode 100644
index 0000000000..772b71400e
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/ClientCertificateManager.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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;
+
+import java.util.concurrent.CompletionStage;
+import org.neo4j.driver.util.Preview;
+
+/**
+ * A manager of {@link ClientCertificate} instances used by the driver for mTLS.
+ *
+ * The driver uses the {@link ClientCertificate} supplied by the manager for setting up new connections. Therefore,
+ * a change of the certificate affects subsequent new connections only.
+ *
+ * The manager must never return {@literal null}. Exceptions must be emitted via the {@link CompletionStage} only.
+ *
+ * All implementations of this interface must be thread-safe and non-blocking for caller threads. For instance, IO
+ * operations must not done on the calling thread.
+ * @since 5.19
+ */
+@Preview(name = "mTLS")
+public interface ClientCertificateManager {
+ /**
+ * Returns a {@link CompletionStage} of a new {@link ClientCertificate}.
+ *
+ * The first {@link CompletionStage} supplied to the driver must not complete with {@literal null} to ensure the
+ * driver has the initial {@link ClientCertificate}.
+ *
+ * Afterwards, the {@link CompletionStage} may complete with {@literal null} to indicate no update. If the
+ * {@link CompletionStage} completes with {@link ClientCertificate}, the driver loads the supplied
+ * {@link ClientCertificate}.
+ * @return the certificate stage, must not be {@literal null}
+ */
+ CompletionStage getClientCertificate();
+}
diff --git a/driver/src/main/java/org/neo4j/driver/ClientCertificateManagers.java b/driver/src/main/java/org/neo4j/driver/ClientCertificateManagers.java
new file mode 100644
index 0000000000..b475092728
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/ClientCertificateManagers.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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;
+
+import org.neo4j.driver.internal.InternalRotatingClientCertificateManager;
+import org.neo4j.driver.util.Preview;
+
+/**
+ * Implementations of {@link ClientCertificateManager}.
+ *
+ * @since 5.19
+ */
+@Preview(name = "mTLS")
+public final class ClientCertificateManagers {
+ private ClientCertificateManagers() {}
+
+ /**
+ * Returns a {@link RotatingClientCertificateManager} that supports rotating its {@link ClientCertificate} using the
+ * {@link RotatingClientCertificateManager#rotate(ClientCertificate)} method.
+ *
+ * @param clientCertificate an initial certificate, must not be {@literal null}
+ * @return a new manager
+ */
+ public static RotatingClientCertificateManager rotating(ClientCertificate clientCertificate) {
+ return new InternalRotatingClientCertificateManager(clientCertificate);
+ }
+}
diff --git a/driver/src/main/java/org/neo4j/driver/ClientCertificates.java b/driver/src/main/java/org/neo4j/driver/ClientCertificates.java
new file mode 100644
index 0000000000..264be1ae4d
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/ClientCertificates.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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;
+
+import java.io.File;
+import java.util.Objects;
+import org.neo4j.driver.internal.InternalClientCertificate;
+import org.neo4j.driver.util.Preview;
+
+/**
+ * Creates new instances of {@link ClientCertificate}.
+ * @since 5.19
+ */
+@Preview(name = "mTLS")
+public final class ClientCertificates {
+ private ClientCertificates() {}
+
+ /**
+ * Creates a new instance of {@link ClientCertificate} with certificate {@link File} and private key {@link File}.
+ * @param certificate the certificate file, must not be {@literal null}
+ * @param privateKey the key file, must not be {@literal null}
+ * @return the client certificate
+ */
+ public static ClientCertificate of(File certificate, File privateKey) {
+ return of(certificate, privateKey, null);
+ }
+
+ /**
+ * Creates a new instance of {@link ClientCertificate} with certificate {@link File}, private key {@link File} and key password.
+ * @param certificate the certificate file, must not be {@literal null}
+ * @param privateKey the key file, must not be {@literal null}
+ * @param password the key password
+ * @return the client certificate
+ */
+ public static ClientCertificate of(File certificate, File privateKey, String password) {
+ Objects.requireNonNull(certificate);
+ Objects.requireNonNull(privateKey);
+ return new InternalClientCertificate(certificate, privateKey, password);
+ }
+}
diff --git a/driver/src/main/java/org/neo4j/driver/GraphDatabase.java b/driver/src/main/java/org/neo4j/driver/GraphDatabase.java
index 4f068f2ac4..d2c5263118 100644
--- a/driver/src/main/java/org/neo4j/driver/GraphDatabase.java
+++ b/driver/src/main/java/org/neo4j/driver/GraphDatabase.java
@@ -20,11 +20,14 @@
import java.net.URI;
import org.neo4j.driver.internal.DriverFactory;
+import org.neo4j.driver.internal.ValidatingClientCertificateManager;
import org.neo4j.driver.internal.security.StaticAuthTokenManager;
import org.neo4j.driver.internal.security.ValidatingAuthTokenManager;
+import org.neo4j.driver.util.Preview;
/**
* Creates {@link Driver drivers}, optionally letting you {@link #driver(URI, Config)} to configure them.
+ *
* @see Driver
* @since 1.0
*/
@@ -54,7 +57,7 @@ public static Driver driver(URI uri) {
/**
* Return a driver for a Neo4j instance with custom configuration.
*
- * @param uri the URL to a Neo4j instance
+ * @param uri the URL to a Neo4j instance
* @param config user defined configuration
* @return a new driver to the database instance specified by the URL
*/
@@ -65,7 +68,7 @@ public static Driver driver(URI uri, Config config) {
/**
* Return a driver for a Neo4j instance with custom configuration.
*
- * @param uri the URL to a Neo4j instance
+ * @param uri the URL to a Neo4j instance
* @param config user defined configuration
* @return a new driver to the database instance specified by the URL
*/
@@ -76,7 +79,7 @@ public static Driver driver(String uri, Config config) {
/**
* Return a driver for a Neo4j instance with the default configuration settings
*
- * @param uri the URL to a Neo4j instance
+ * @param uri the URL to a Neo4j instance
* @param authToken authentication to use, see {@link AuthTokens}
* @return a new driver to the database instance specified by the URL
*/
@@ -87,7 +90,7 @@ public static Driver driver(String uri, AuthToken authToken) {
/**
* Return a driver for a Neo4j instance with the default configuration settings
*
- * @param uri the URL to a Neo4j instance
+ * @param uri the URL to a Neo4j instance
* @param authToken authentication to use, see {@link AuthTokens}
* @return a new driver to the database instance specified by the URL
*/
@@ -98,9 +101,9 @@ public static Driver driver(URI uri, AuthToken authToken) {
/**
* Return a driver for a Neo4j instance with custom configuration.
*
- * @param uri the URL to a Neo4j instance
+ * @param uri the URL to a Neo4j instance
* @param authToken authentication to use, see {@link AuthTokens}
- * @param config user defined configuration
+ * @param config user defined configuration
* @return a new driver to the database instance specified by the URL
*/
public static Driver driver(String uri, AuthToken authToken, Config config) {
@@ -119,18 +122,18 @@ public static Driver driver(URI uri, AuthToken authToken, Config config) {
if (authToken == null) {
authToken = AuthTokens.none();
}
- return driver(uri, authToken, config, new DriverFactory());
+ return driver(uri, authToken, null, config, new DriverFactory());
}
/**
* Returns a driver for a Neo4j instance with the default configuration settings and the provided
* {@link AuthTokenManager}.
*
- * @param uri the URL to a Neo4j instance
+ * @param uri the URL to a Neo4j instance
* @param authTokenManager manager to use
* @return a new driver to the database instance specified by the URL
- * @since 5.8
* @see AuthTokenManager
+ * @since 5.8
*/
public static Driver driver(URI uri, AuthTokenManager authTokenManager) {
return driver(uri, authTokenManager, Config.defaultConfig());
@@ -140,11 +143,11 @@ public static Driver driver(URI uri, AuthTokenManager authTokenManager) {
* Returns a driver for a Neo4j instance with the default configuration settings and the provided
* {@link AuthTokenManager}.
*
- * @param uri the URL to a Neo4j instance
+ * @param uri the URL to a Neo4j instance
* @param authTokenManager manager to use
* @return a new driver to the database instance specified by the URL
- * @since 5.8
* @see AuthTokenManager
+ * @since 5.8
*/
public static Driver driver(String uri, AuthTokenManager authTokenManager) {
return driver(URI.create(uri), authTokenManager);
@@ -153,42 +156,239 @@ public static Driver driver(String uri, AuthTokenManager authTokenManager) {
/**
* Returns a driver for a Neo4j instance with the provided {@link AuthTokenManager} and custom configuration.
*
- * @param uri the URL to a Neo4j instance
+ * @param uri the URL to a Neo4j instance
* @param authTokenManager manager to use
- * @param config user defined configuration
+ * @param config user defined configuration
* @return a new driver to the database instance specified by the URL
- * @since 5.8
* @see AuthTokenManager
+ * @since 5.8
*/
public static Driver driver(URI uri, AuthTokenManager authTokenManager, Config config) {
- return driver(uri, authTokenManager, config, new DriverFactory());
+ return driver(uri, authTokenManager, null, config, new DriverFactory());
}
/**
* Returns a driver for a Neo4j instance with the provided {@link AuthTokenManager} and custom configuration.
*
- * @param uri the URL to a Neo4j instance
+ * @param uri the URL to a Neo4j instance
* @param authTokenManager manager to use
- * @param config user defined configuration
+ * @param config user defined configuration
* @return a new driver to the database instance specified by the URL
- * @since 5.8
* @see AuthTokenManager
+ * @since 5.8
*/
public static Driver driver(String uri, AuthTokenManager authTokenManager, Config config) {
return driver(URI.create(uri), authTokenManager, config);
}
- private static Driver driver(URI uri, AuthToken authToken, Config config, DriverFactory driverFactory) {
+ /**
+ * Returns a driver for a Neo4j instance with the provided {@link ClientCertificateManager}.
+ * @param uri the URL to a Neo4j instance
+ * @param clientCertificateManager the client certificate manager
+ * @return a new driver to the database instance specified by the URL
+ * @since 5.19
+ * @see ClientCertificateManager
+ */
+ @Preview(name = "mTLS")
+ public static Driver driver(String uri, ClientCertificateManager clientCertificateManager) {
+ return driver(URI.create(uri), clientCertificateManager);
+ }
+
+ /**
+ * Returns a driver for a Neo4j instance with the provided {@link ClientCertificateManager} and driver
+ * {@link Config}.
+ * @param uri the URL to a Neo4j instance
+ * @param clientCertificateManager the client certificate manager
+ * @param config the driver config
+ * @return a new driver to the database instance specified by the URL
+ * @since 5.19
+ */
+ @Preview(name = "mTLS")
+ public static Driver driver(String uri, ClientCertificateManager clientCertificateManager, Config config) {
+ return driver(URI.create(uri), clientCertificateManager, config);
+ }
+
+ /**
+ * Returns a driver for a Neo4j instance with the provided {@link AuthToken} and {@link ClientCertificateManager}.
+ * @param uri the URL to a Neo4j instance
+ * @param authToken the auth token
+ * @param clientCertificateManager the client certificate manager
+ * @return a new driver to the database instance specified by the URL
+ * @since 5.19
+ */
+ @Preview(name = "mTLS")
+ public static Driver driver(String uri, AuthToken authToken, ClientCertificateManager clientCertificateManager) {
+ return driver(URI.create(uri), authToken, clientCertificateManager);
+ }
+
+ /**
+ * Returns a driver for a Neo4j instance with the provided {@link AuthToken}, {@link ClientCertificateManager} and
+ * driver {@link Config}.
+ * @param uri the URL to a Neo4j instance
+ * @param authToken the auth token
+ * @param clientCertificateManager the client certificate manager
+ * @param config the driver config
+ * @return a new driver to the database instance specified by the URL
+ * @since 5.19
+ */
+ @Preview(name = "mTLS")
+ public static Driver driver(
+ String uri, AuthToken authToken, ClientCertificateManager clientCertificateManager, Config config) {
+ return driver(URI.create(uri), authToken, clientCertificateManager, config);
+ }
+
+ /**
+ * Returns a driver for a Neo4j instance with the provided {@link AuthTokenManager} and
+ * {@link ClientCertificateManager}.
+ * @param uri the URL to a Neo4j instance
+ * @param authTokenManager the auth token manager
+ * @param clientCertificateManager the client certificate manager
+ * @return a new driver to the database instance specified by the URL
+ * @since 5.19
+ */
+ @Preview(name = "mTLS")
+ public static Driver driver(
+ String uri, AuthTokenManager authTokenManager, ClientCertificateManager clientCertificateManager) {
+ return driver(URI.create(uri), authTokenManager, clientCertificateManager);
+ }
+
+ /**
+ * Returns a driver for a Neo4j instance with the provided {@link AuthTokenManager},
+ * {@link ClientCertificateManager} and driver {@link Config}.
+ * @param uri the URL to a Neo4j instance
+ * @param authTokenManager the auth token manager
+ * @param clientCertificateManager the client certificate manager
+ * @param config the driver config
+ * @return a new driver to the database instance specified by the URL
+ * @since 5.19
+ */
+ @Preview(name = "mTLS")
+ public static Driver driver(
+ String uri,
+ AuthTokenManager authTokenManager,
+ ClientCertificateManager clientCertificateManager,
+ Config config) {
+ return driver(URI.create(uri), authTokenManager, clientCertificateManager, config);
+ }
+
+ /**
+ * Returns a driver for a Neo4j instance with the provided {@link ClientCertificateManager}.
+ * @param uri the URL to a Neo4j instance
+ * @param clientCertificateManager the client certificate manager
+ * @return a new driver to the database instance specified by the URL
+ * @since 5.19
+ */
+ @Preview(name = "mTLS")
+ public static Driver driver(URI uri, ClientCertificateManager clientCertificateManager) {
+ return driver(uri, clientCertificateManager, Config.defaultConfig());
+ }
+
+ /**
+ * Returns a driver for a Neo4j instance with the provided {@link ClientCertificateManager} and driver
+ * {@link Config}.
+ * @param uri the URL to a Neo4j instance
+ * @param clientCertificateManager the client certificate manager
+ * @param config the driver config
+ * @return a new driver to the database instance specified by the URL
+ * @since 5.19
+ */
+ @Preview(name = "mTLS")
+ public static Driver driver(URI uri, ClientCertificateManager clientCertificateManager, Config config) {
+ return driver(uri, AuthTokens.none(), clientCertificateManager, config);
+ }
+
+ /**
+ * Returns a driver for a Neo4j instance with the provided {@link AuthToken} and {@link ClientCertificateManager}.
+ * @param uri the URL to a Neo4j instance
+ * @param authToken the auth token
+ * @param clientCertificateManager the client certificate manager
+ * @return a new driver to the database instance specified by the URL
+ * @since 5.19
+ */
+ @Preview(name = "mTLS")
+ public static Driver driver(URI uri, AuthToken authToken, ClientCertificateManager clientCertificateManager) {
+ return driver(uri, authToken, clientCertificateManager, Config.defaultConfig());
+ }
+
+ /**
+ * Returns a driver for a Neo4j instance with the provided {@link AuthToken}, {@link ClientCertificateManager} and
+ * driver {@link Config}.
+ * @param uri the URL to a Neo4j instance
+ * @param authToken the auth token
+ * @param clientCertificateManager the client certificate manager
+ * @param config the driver config
+ * @return a new driver to the database instance specified by the URL
+ * @since 5.19
+ */
+ @Preview(name = "mTLS")
+ public static Driver driver(
+ URI uri, AuthToken authToken, ClientCertificateManager clientCertificateManager, Config config) {
+ return driver(uri, authToken, clientCertificateManager, config, new DriverFactory());
+ }
+
+ /**
+ * Returns a driver for a Neo4j instance with the provided {@link AuthTokenManager} and
+ * {@link ClientCertificateManager}.
+ * @param uri the URL to a Neo4j instance
+ * @param authTokenManager the auth token manager
+ * @param clientCertificateManager the client certificate manager
+ * @return a new driver to the database instance specified by the URL
+ * @since 5.19
+ */
+ @Preview(name = "mTLS")
+ public static Driver driver(
+ URI uri, AuthTokenManager authTokenManager, ClientCertificateManager clientCertificateManager) {
+ return driver(uri, authTokenManager, clientCertificateManager, Config.defaultConfig());
+ }
+
+ /**
+ * Returns a driver for a Neo4j instance with the provided {@link AuthTokenManager},
+ * {@link ClientCertificateManager} and driver {@link Config}.
+ * @param uri the URL to a Neo4j instance
+ * @param authTokenManager the auth token manager
+ * @param clientCertificateManager the client certificate manager
+ * @param config the driver config
+ * @return a new driver to the database instance specified by the URL
+ * @since 5.19
+ */
+ @Preview(name = "mTLS")
+ public static Driver driver(
+ URI uri,
+ AuthTokenManager authTokenManager,
+ ClientCertificateManager clientCertificateManager,
+ Config config) {
+ return driver(uri, authTokenManager, clientCertificateManager, config, new DriverFactory());
+ }
+
+ private static Driver driver(
+ URI uri,
+ AuthToken authToken,
+ ClientCertificateManager clientCertificateManager,
+ Config config,
+ DriverFactory driverFactory) {
+ if (clientCertificateManager != null) {
+ clientCertificateManager = new ValidatingClientCertificateManager(clientCertificateManager);
+ }
config = getOrDefault(config);
- return driverFactory.newInstance(uri, new StaticAuthTokenManager(authToken), config);
+ return driverFactory.newInstance(uri, new StaticAuthTokenManager(authToken), clientCertificateManager, config);
}
private static Driver driver(
- URI uri, AuthTokenManager authTokenManager, Config config, DriverFactory driverFactory) {
+ URI uri,
+ AuthTokenManager authTokenManager,
+ ClientCertificateManager clientCertificateManager,
+ Config config,
+ DriverFactory driverFactory) {
requireNonNull(authTokenManager, "authTokenManager must not be null");
+ if (clientCertificateManager != null) {
+ clientCertificateManager = new ValidatingClientCertificateManager(clientCertificateManager);
+ }
config = getOrDefault(config);
return driverFactory.newInstance(
- uri, new ValidatingAuthTokenManager(authTokenManager, config.logging()), config);
+ uri,
+ new ValidatingAuthTokenManager(authTokenManager, config.logging()),
+ clientCertificateManager,
+ config);
}
private static Config getOrDefault(Config config) {
diff --git a/driver/src/main/java/org/neo4j/driver/RotatingClientCertificateManager.java b/driver/src/main/java/org/neo4j/driver/RotatingClientCertificateManager.java
new file mode 100644
index 0000000000..d52759bab7
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/RotatingClientCertificateManager.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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;
+
+import org.neo4j.driver.util.Preview;
+
+/**
+ * A {@link ClientCertificateManager} that supports rotating its {@link ClientCertificate}.
+ * @since 5.19
+ */
+@Preview(name = "mTLS")
+public sealed interface RotatingClientCertificateManager extends ClientCertificateManager
+ permits org.neo4j.driver.internal.InternalRotatingClientCertificateManager {
+ /**
+ * Rotates the current {@link ClientCertificate}.
+ * @param clientCertificate the new certificate, must not be {@literal null}
+ */
+ void rotate(ClientCertificate clientCertificate);
+}
diff --git a/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java b/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java
index 9167abffe7..273d9f97d5 100644
--- a/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java
+++ b/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java
@@ -29,6 +29,7 @@
import java.time.Clock;
import java.util.function.Supplier;
import org.neo4j.driver.AuthTokenManager;
+import org.neo4j.driver.ClientCertificateManager;
import org.neo4j.driver.Config;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Logging;
@@ -64,13 +65,18 @@ public class DriverFactory {
public static final String NO_ROUTING_CONTEXT_ERROR_MESSAGE =
"Routing parameters are not supported with scheme 'bolt'. Given URI: ";
- public final Driver newInstance(URI uri, AuthTokenManager authTokenManager, Config config) {
- return newInstance(uri, authTokenManager, config, null, null, null);
+ public final Driver newInstance(
+ URI uri,
+ AuthTokenManager authTokenManager,
+ ClientCertificateManager clientCertificateManager,
+ Config config) {
+ return newInstance(uri, authTokenManager, clientCertificateManager, config, null, null, null);
}
public final Driver newInstance(
URI uri,
AuthTokenManager authTokenManager,
+ ClientCertificateManager clientCertificateManager,
Config config,
SecurityPlan securityPlan,
EventLoopGroup eventLoopGroup,
@@ -89,7 +95,8 @@ public final Driver newInstance(
if (securityPlan == null) {
var settings = new SecuritySettings(config.encrypted(), config.trustStrategy());
- securityPlan = SecurityPlans.createSecurityPlan(settings, uri.getScheme());
+ securityPlan = SecurityPlans.createSecurityPlan(
+ settings, uri.getScheme(), clientCertificateManager, config.logging());
}
var address = new BoltServerAddress(uri);
diff --git a/driver/src/main/java/org/neo4j/driver/internal/InternalClientCertificate.java b/driver/src/main/java/org/neo4j/driver/internal/InternalClientCertificate.java
new file mode 100644
index 0000000000..c89f988725
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/internal/InternalClientCertificate.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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;
+
+import java.io.File;
+import org.neo4j.driver.ClientCertificate;
+
+public record InternalClientCertificate(File certificate, File privateKey, String password)
+ implements ClientCertificate {}
diff --git a/driver/src/main/java/org/neo4j/driver/internal/InternalRotatingClientCertificateManager.java b/driver/src/main/java/org/neo4j/driver/internal/InternalRotatingClientCertificateManager.java
new file mode 100644
index 0000000000..2601915cb7
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/internal/InternalRotatingClientCertificateManager.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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;
+
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import org.neo4j.driver.ClientCertificate;
+import org.neo4j.driver.RotatingClientCertificateManager;
+
+public final class InternalRotatingClientCertificateManager implements RotatingClientCertificateManager {
+ private CompletionStage clientCertificateStage;
+ private InternalClientCertificate clientCertificate;
+
+ public InternalRotatingClientCertificateManager(ClientCertificate clientCertificate) {
+ Objects.requireNonNull(clientCertificate);
+ updateState(clientCertificate);
+ }
+
+ @Override
+ public synchronized CompletionStage getClientCertificate() {
+ if (clientCertificate != null) {
+ var stage = clientCertificateStage;
+ updateState(null);
+ return stage;
+ } else {
+ return clientCertificateStage;
+ }
+ }
+
+ @Override
+ public void rotate(ClientCertificate clientCertificate) {
+ Objects.requireNonNull(clientCertificate);
+ synchronized (this) {
+ updateState(clientCertificate);
+ }
+ }
+
+ private void updateState(ClientCertificate clientCertificate) {
+ this.clientCertificateStage = CompletableFuture.completedStage(clientCertificate);
+ this.clientCertificate = (InternalClientCertificate) clientCertificate;
+ }
+}
diff --git a/driver/src/main/java/org/neo4j/driver/internal/ValidatingClientCertificateManager.java b/driver/src/main/java/org/neo4j/driver/internal/ValidatingClientCertificateManager.java
new file mode 100644
index 0000000000..401bd8c357
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/internal/ValidatingClientCertificateManager.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import org.neo4j.driver.ClientCertificate;
+import org.neo4j.driver.ClientCertificateManager;
+import org.neo4j.driver.exceptions.ClientException;
+
+public class ValidatingClientCertificateManager implements ClientCertificateManager {
+ private final ClientCertificateManager delegate;
+
+ public ValidatingClientCertificateManager(ClientCertificateManager delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public CompletionStage getClientCertificate() {
+ CompletionStage certificateStage;
+ try {
+ certificateStage = delegate.getClientCertificate();
+ } catch (Throwable throwable) {
+ return CompletableFuture.failedFuture(
+ new ClientException("An exception has been thrown by the ClientCertificateManager.", throwable));
+ }
+ return certificateStage;
+ }
+}
diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/connection/ChannelConnector.java b/driver/src/main/java/org/neo4j/driver/internal/async/connection/ChannelConnector.java
index 5a64be241e..57dfc36951 100644
--- a/driver/src/main/java/org/neo4j/driver/internal/async/connection/ChannelConnector.java
+++ b/driver/src/main/java/org/neo4j/driver/internal/async/connection/ChannelConnector.java
@@ -18,8 +18,12 @@
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
+import java.util.function.Function;
import org.neo4j.driver.internal.BoltServerAddress;
public interface ChannelConnector {
- ChannelFuture connect(BoltServerAddress address, Bootstrap bootstrap);
+ ChannelFuture connect(
+ BoltServerAddress address,
+ Bootstrap bootstrap,
+ Function channelFutureExtensionMapper);
}
diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/connection/ChannelConnectorImpl.java b/driver/src/main/java/org/neo4j/driver/internal/async/connection/ChannelConnectorImpl.java
index f7e4149dc0..59e7a5fe84 100644
--- a/driver/src/main/java/org/neo4j/driver/internal/async/connection/ChannelConnectorImpl.java
+++ b/driver/src/main/java/org/neo4j/driver/internal/async/connection/ChannelConnectorImpl.java
@@ -26,6 +26,8 @@
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.time.Clock;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
import org.neo4j.driver.AuthTokenManager;
import org.neo4j.driver.Logging;
import org.neo4j.driver.NotificationConfig;
@@ -36,6 +38,7 @@
import org.neo4j.driver.internal.async.inbound.ConnectTimeoutHandler;
import org.neo4j.driver.internal.cluster.RoutingContext;
import org.neo4j.driver.internal.security.SecurityPlan;
+import org.neo4j.driver.internal.util.Futures;
public class ChannelConnectorImpl implements ChannelConnector {
private final String userAgent;
@@ -97,30 +100,45 @@ public ChannelConnectorImpl(
}
@Override
- public ChannelFuture connect(BoltServerAddress address, Bootstrap bootstrap) {
- bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMillis);
- bootstrap.handler(new NettyChannelInitializer(
- address, securityPlan, connectTimeoutMillis, authTokenManager, clock, logging));
- bootstrap.resolver(addressResolverGroup);
-
- SocketAddress socketAddress;
- try {
- socketAddress =
- new InetSocketAddress(domainNameResolver.resolve(address.connectionHost())[0], address.port());
- } catch (Throwable t) {
- socketAddress = InetSocketAddress.createUnresolved(address.connectionHost(), address.port());
- }
-
- var channelConnected = bootstrap.connect(socketAddress);
-
- var channel = channelConnected.channel();
- var handshakeCompleted = channel.newPromise();
- var connectionInitialized = channel.newPromise();
-
- installChannelConnectedListeners(address, channelConnected, handshakeCompleted);
- installHandshakeCompletedListeners(handshakeCompleted, connectionInitialized);
-
- return connectionInitialized;
+ public ChannelFuture connect(
+ BoltServerAddress address,
+ Bootstrap bootstrap,
+ Function channelFutureExtensionMapper) {
+ var sslContextStage = securityPlan.sslContext();
+
+ var channelFutureCompletableFuture = new CompletableFuture();
+
+ sslContextStage.whenComplete((sslContext, throwable) -> {
+ if (throwable != null) {
+ channelFutureCompletableFuture.completeExceptionally(Futures.completionExceptionCause(throwable));
+ } else {
+ bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMillis);
+ bootstrap.handler(new NettyChannelInitializer(
+ address, securityPlan, connectTimeoutMillis, authTokenManager, sslContext, clock, logging));
+ bootstrap.resolver(addressResolverGroup);
+
+ SocketAddress socketAddress;
+ try {
+ socketAddress = new InetSocketAddress(
+ domainNameResolver.resolve(address.connectionHost())[0], address.port());
+ } catch (Throwable t) {
+ socketAddress = InetSocketAddress.createUnresolved(address.connectionHost(), address.port());
+ }
+
+ var channelConnected = bootstrap.connect(socketAddress);
+
+ var channel = channelConnected.channel();
+ var handshakeCompleted = channel.newPromise();
+ var connectionInitialized = channel.newPromise();
+
+ installChannelConnectedListeners(address, channelConnected, handshakeCompleted);
+ installHandshakeCompletedListeners(handshakeCompleted, connectionInitialized);
+
+ channelFutureCompletableFuture.complete(channelFutureExtensionMapper.apply(connectionInitialized));
+ }
+ });
+
+ return new DeferredChannelFuture(channelFutureCompletableFuture, logging);
}
private void installChannelConnectedListeners(
diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/connection/DeferredChannelFuture.java b/driver/src/main/java/org/neo4j/driver/internal/async/connection/DeferredChannelFuture.java
new file mode 100644
index 0000000000..f131c34317
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/internal/async/connection/DeferredChannelFuture.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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.async.connection;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.TimeUnit;
+import org.neo4j.driver.Logger;
+import org.neo4j.driver.Logging;
+import org.neo4j.driver.internal.util.Futures;
+
+/**
+ * This implementation is for {@link io.netty.channel.pool.SimpleChannelPool} usage only. It defers listener
+ * notification until the supplied {@link CompletionStage} completes.
+ */
+class DeferredChannelFuture implements ChannelFuture {
+ private final Logger logger;
+
+ @SuppressWarnings("rawtypes")
+ private final CompletableFuture listenerFuture = new CompletableFuture<>();
+
+ @SuppressWarnings("unchecked")
+ public DeferredChannelFuture(CompletionStage channelFutureCompletionStage, Logging logging) {
+ this.logger = logging.getLog(getClass());
+
+ listenerFuture.thenCompose(ignored -> channelFutureCompletionStage).whenComplete((channelFuture, throwable) -> {
+ var listener = listenerFuture.join();
+ if (throwable != null) {
+ throwable = Futures.completionExceptionCause(throwable);
+ try {
+ listener.operationComplete(new FailedChannelFuture(throwable));
+ } catch (Throwable e) {
+ logger.error("An error occured while notifying listener.", e);
+ }
+ } else {
+ channelFuture.addListener(listener);
+ }
+ });
+ }
+
+ @Override
+ public Channel channel() {
+ return null;
+ }
+
+ @Override
+ public boolean isSuccess() {
+ return false;
+ }
+
+ @Override
+ public boolean isCancellable() {
+ return false;
+ }
+
+ @Override
+ public Throwable cause() {
+ return null;
+ }
+
+ @Override
+ public ChannelFuture addListener(GenericFutureListener extends Future super Void>> listener) {
+ listenerFuture.complete(listener);
+ return this;
+ }
+
+ @SafeVarargs
+ @Override
+ public final ChannelFuture addListeners(GenericFutureListener extends Future super Void>>... listeners) {
+ return null;
+ }
+
+ @Override
+ public ChannelFuture removeListener(GenericFutureListener extends Future super Void>> listener) {
+ return null;
+ }
+
+ @SafeVarargs
+ @Override
+ public final ChannelFuture removeListeners(GenericFutureListener extends Future super Void>>... listeners) {
+ return null;
+ }
+
+ @Override
+ public ChannelFuture sync() {
+ return null;
+ }
+
+ @Override
+ public ChannelFuture syncUninterruptibly() {
+ return null;
+ }
+
+ @Override
+ public ChannelFuture await() {
+ return null;
+ }
+
+ @Override
+ public ChannelFuture awaitUninterruptibly() {
+ return null;
+ }
+
+ @Override
+ public boolean await(long timeout, TimeUnit unit) {
+ return false;
+ }
+
+ @Override
+ public boolean await(long timeoutMillis) {
+ return false;
+ }
+
+ @Override
+ public boolean awaitUninterruptibly(long timeout, TimeUnit unit) {
+ return false;
+ }
+
+ @Override
+ public boolean awaitUninterruptibly(long timeoutMillis) {
+ return false;
+ }
+
+ @Override
+ public Void getNow() {
+ return null;
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return false;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return false;
+ }
+
+ @Override
+ public Void get() {
+ return null;
+ }
+
+ @Override
+ public Void get(long timeout, TimeUnit unit) {
+ return null;
+ }
+
+ @Override
+ public boolean isVoid() {
+ return false;
+ }
+
+ private record FailedChannelFuture(Throwable throwable) implements ChannelFuture {
+
+ @Override
+ public Channel channel() {
+ return null;
+ }
+
+ @Override
+ public boolean isSuccess() {
+ return false;
+ }
+
+ @Override
+ public boolean isCancellable() {
+ return false;
+ }
+
+ @Override
+ public Throwable cause() {
+ return throwable;
+ }
+
+ @Override
+ public ChannelFuture addListener(GenericFutureListener extends Future super Void>> listener) {
+ return null;
+ }
+
+ @SafeVarargs
+ @Override
+ public final ChannelFuture addListeners(GenericFutureListener extends Future super Void>>... listeners) {
+ return null;
+ }
+
+ @Override
+ public ChannelFuture removeListener(GenericFutureListener extends Future super Void>> listener) {
+ return null;
+ }
+
+ @SafeVarargs
+ @Override
+ public final ChannelFuture removeListeners(GenericFutureListener extends Future super Void>>... listeners) {
+ return null;
+ }
+
+ @Override
+ public ChannelFuture sync() {
+ return null;
+ }
+
+ @Override
+ public ChannelFuture syncUninterruptibly() {
+ return null;
+ }
+
+ @Override
+ public ChannelFuture await() {
+ return null;
+ }
+
+ @Override
+ public ChannelFuture awaitUninterruptibly() {
+ return null;
+ }
+
+ @Override
+ public boolean await(long timeout, TimeUnit unit) {
+ return false;
+ }
+
+ @Override
+ public boolean await(long timeoutMillis) {
+ return false;
+ }
+
+ @Override
+ public boolean awaitUninterruptibly(long timeout, TimeUnit unit) {
+ return false;
+ }
+
+ @Override
+ public boolean awaitUninterruptibly(long timeoutMillis) {
+ return false;
+ }
+
+ @Override
+ public Void getNow() {
+ return null;
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return false;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return true;
+ }
+
+ @Override
+ public Void get() {
+ return null;
+ }
+
+ @Override
+ public Void get(long timeout, TimeUnit unit) {
+ return null;
+ }
+
+ @Override
+ public boolean isVoid() {
+ return false;
+ }
+ }
+}
diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/connection/NettyChannelInitializer.java b/driver/src/main/java/org/neo4j/driver/internal/async/connection/NettyChannelInitializer.java
index fa1f4b3c82..4a28b091ea 100644
--- a/driver/src/main/java/org/neo4j/driver/internal/async/connection/NettyChannelInitializer.java
+++ b/driver/src/main/java/org/neo4j/driver/internal/async/connection/NettyChannelInitializer.java
@@ -25,6 +25,7 @@
import io.netty.channel.ChannelInitializer;
import io.netty.handler.ssl.SslHandler;
import java.time.Clock;
+import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import org.neo4j.driver.AuthTokenManager;
import org.neo4j.driver.Logging;
@@ -38,6 +39,7 @@ public class NettyChannelInitializer extends ChannelInitializer {
private final SecurityPlan securityPlan;
private final int connectTimeoutMillis;
private final AuthTokenManager authTokenManager;
+ private final SSLContext sslContext;
private final Clock clock;
private final Logging logging;
@@ -46,12 +48,14 @@ public NettyChannelInitializer(
SecurityPlan securityPlan,
int connectTimeoutMillis,
AuthTokenManager authTokenManager,
+ SSLContext sslContext,
Clock clock,
Logging logging) {
this.address = address;
this.securityPlan = securityPlan;
this.connectTimeoutMillis = connectTimeoutMillis;
this.authTokenManager = authTokenManager;
+ this.sslContext = sslContext;
this.clock = clock;
this.logging = logging;
}
@@ -74,8 +78,8 @@ private SslHandler createSslHandler() {
}
private SSLEngine createSslEngine() {
- var sslContext = securityPlan.sslContext();
var sslEngine = sslContext.createSSLEngine(address.host(), address.port());
+ sslEngine.setNeedClientAuth(securityPlan.requiresClientAuth());
sslEngine.setUseClientMode(true);
if (securityPlan.requiresHostnameVerification()) {
var sslParameters = sslEngine.getSSLParameters();
diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelPool.java b/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelPool.java
index e792bd0c50..1932114ab1 100644
--- a/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelPool.java
+++ b/driver/src/main/java/org/neo4j/driver/internal/async/pool/NettyChannelPool.java
@@ -91,23 +91,24 @@ public class NettyChannelPool implements ExtendedChannelPool {
@Override
protected ChannelFuture connectChannel(Bootstrap bootstrap) {
var creatingEvent = handler.channelCreating(id);
- var connectedChannelFuture = connector.connect(address, bootstrap);
- var channel = connectedChannelFuture.channel();
- // This ensures that handler.channelCreated is called before SimpleChannelPool calls
- // handler.channelAcquired
- var trackedChannelFuture = channel.newPromise();
- connectedChannelFuture.addListener(future -> {
- if (future.isSuccess()) {
- // notify pool handler about a successful connection
- setPoolId(channel, id);
- handler.channelCreated(channel, creatingEvent);
- trackedChannelFuture.setSuccess();
- } else {
- handler.channelFailedToCreate(id);
- trackedChannelFuture.setFailure(future.cause());
- }
+ return connector.connect(address, bootstrap, connectedChannelFuture -> {
+ var channel = connectedChannelFuture.channel();
+ // This ensures that handler.channelCreated is called before SimpleChannelPool calls
+ // handler.channelAcquired
+ var trackedChannelFuture = channel.newPromise();
+ connectedChannelFuture.addListener(future -> {
+ if (future.isSuccess()) {
+ // notify pool handler about a successful connection
+ setPoolId(channel, id);
+ handler.channelCreated(channel, creatingEvent);
+ trackedChannelFuture.setSuccess();
+ } else {
+ handler.channelFailedToCreate(id);
+ trackedChannelFuture.setFailure(future.cause());
+ }
+ });
+ return trackedChannelFuture;
});
- return trackedChannelFuture;
}
};
}
diff --git a/driver/src/main/java/org/neo4j/driver/internal/pki/DerUtils.java b/driver/src/main/java/org/neo4j/driver/internal/pki/DerUtils.java
new file mode 100644
index 0000000000..4bd2d05198
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/internal/pki/DerUtils.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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.pki;
+
+import static java.lang.String.format;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+
+/**
+ * Bare minimum parser for DER-encoded values. The support is sufficient
+ * to parse private and public keys.
+ *
+ * DER(Distinguished Encoding Rules) is a subset of BER(Basic Encoding Rules)
+ * with some limitations; length needs to be in a definite form and string/array
+ * values must use primitive encoding.
+ *
+ * Type structure is very simple: {@code IDENTIFIER LENGTH DATA}
+ */
+final class DerUtils {
+ private static final byte INTEGER_TAG = 0x02;
+ private static final byte OCTET_STRING_TAG = 0x04;
+ private static final byte SEQUENCE_TAG = 0x30;
+
+ private DerUtils() {}
+
+ /**
+ * Read an integer.
+ *
+ * @param input buffer positioned at the beginning of an integer.
+ * @return the integer at the current position.
+ */
+ static BigInteger readDerInteger(ByteBuffer input) {
+ var len = der(input, INTEGER_TAG);
+ var value = new byte[len];
+ input.get(value);
+ return new BigInteger(1, value);
+ }
+
+ /**
+ * Read an octet string, more commonly known as a byte array.
+ *
+ * @param input buffer positioned at the beginning if an octet string.
+ * @return the octet string at the current position.
+ */
+ static byte[] readDerOctetString(ByteBuffer input) {
+ var len = der(input, OCTET_STRING_TAG);
+ var value = new byte[len];
+ input.get(value);
+ return value;
+ }
+
+ /**
+ * Return the context specific data with tag {@code contextTag}.
+ *
+ * To break ambiguity, optional values with the same type can be tagged with numbers.
+ * In this case, the tag value represents the tag number, and the tag type is inferred.
+ *
+ * @param input buffer positioned at the beginning if the context specific data.
+ * @param contextTag the tag to which data we should find.
+ * @return the context data that is tagged with {@code contextTag}, or {@code null} if
+ * the tag is not present.
+ */
+ @SuppressWarnings("SameParameterValue")
+ static byte[] getDerContext(ByteBuffer input, byte contextTag) {
+ var begin = input.position();
+ while (input.hasRemaining()) {
+ var start = input.position();
+ var tag = input.get();
+ var length = getLength(input);
+ if (isContextSpecific(tag, contextTag)) {
+ // Found our tag, copy full block
+ return copyContext(input, start);
+ }
+ // Skip this block
+ input.position(input.position() + length);
+ }
+ input.position(begin); // Reset input
+ return null; // Found no matching tag
+ }
+
+ /**
+ * Begin parsing a sequence.
+ *
+ * @param input buffer positioned at the beginning of a sequence.
+ * @return length of sequence.
+ */
+ static int beginDerSequence(ByteBuffer input) {
+ return der(input, SEQUENCE_TAG);
+ }
+
+ /**
+ * Consumes a tag.
+ *
+ * @param input buffer positions at the beginning of a tag.
+ * @param expectedTag what tag should be found.
+ * @return the length of the data following the tag.
+ * @throws IllegalArgumentException if the correct that is not found.
+ */
+ private static int der(ByteBuffer input, int expectedTag) {
+ var tag = unsignedByte(input);
+ if (tag != expectedTag) {
+ throw new IllegalArgumentException(format("Expected tag '%02X' but found '%02X'", expectedTag, tag));
+ }
+ return getLength(input);
+ }
+
+ private static byte[] copyContext(ByteBuffer input, int dataStart) {
+ input.position(dataStart);
+ var tag = input.get();
+
+ if (isConstructed(tag)) {
+ var length = getLength(input);
+ var data = new byte[length];
+ input.get(data);
+
+ // Technically, we should read all data and concatenate "sub-values". However,
+ // this is not allowed in DER, so the only valid data consists of 0 or 1
+ // "sub-values", which our code works for.
+ return data;
+ }
+ throw new IllegalArgumentException("Unable to extract non-constructed data.");
+ }
+
+ /**
+ * Determines whether a tag matches the context tag. Context is defined as
+ * {@code [10tttttt]}, where 't' is the context tag.
+ *
+ * @param tag read tag from encoding.
+ * @param contextTag the context value we are looking for.
+ * @return {@code true} if {@code tag} matches {@code contextTag}, {@code false} otherwise.
+ */
+ private static boolean isContextSpecific(byte tag, byte contextTag) {
+ if ((tag & 0b1100_0000) == 0b1000_0000) {
+ return (tag & 0b001_1111) == contextTag;
+ }
+ return false;
+ }
+
+ /**
+ * Determines whether a tag is constructed, which is indicated by bit 6 being one.
+ * A constructed value is an encapsulation structure, that contains 0 or more
+ * "sub-values" that should be concatenated to form the final value.
+ *
+ * @return {@code true} if the value is constructed.
+ */
+ private static boolean isConstructed(byte tag) {
+ return ((tag & 0b0010_0000) == 0b0010_0000);
+ }
+
+ /**
+ * Parse the length octet's. They follow the simple scheme of:
+ *
+ * Short form: [0xxxxxxx]
+ * Long form: [1nnnnnnn]...[n-1][n] // where 'n' denotes the number of bytes
+ * n = 0 // indefinite length, not allowed in DER
+ * n = 127 // reserved
+ *
+ *
+ * @return the length of the following content.
+ */
+ private static int getLength(ByteBuffer input) {
+ var lengthByte = unsignedByte(input);
+ // if bit 8 is 0, short form
+ if ((lengthByte & 0b1000_0000) == 0) {
+ return lengthByte;
+ }
+ // otherwise, long form
+ lengthByte &= 0b0111_1111;
+ if (lengthByte == 0) {
+ throw new UnsupportedOperationException("Indefinite length is not allowed in DER.");
+ }
+ if (lengthByte > 2) { // 65mb should be enough, right?
+ throw new IllegalArgumentException("Too big content.");
+ }
+ var len = 0;
+ while (lengthByte-- > 0) {
+ len <<= Byte.SIZE;
+ len |= unsignedByte(input);
+ }
+ return len;
+ }
+
+ /**
+ * Because Java...
+ */
+ private static int unsignedByte(ByteBuffer input) {
+ return input.get() & 0xFF;
+ }
+}
diff --git a/driver/src/main/java/org/neo4j/driver/internal/pki/PemFormats.java b/driver/src/main/java/org/neo4j/driver/internal/pki/PemFormats.java
new file mode 100644
index 0000000000..506fb60ec4
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/internal/pki/PemFormats.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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.pki;
+
+import static java.lang.String.format;
+import static org.neo4j.driver.internal.pki.DerUtils.beginDerSequence;
+import static org.neo4j.driver.internal.pki.DerUtils.getDerContext;
+import static org.neo4j.driver.internal.pki.DerUtils.readDerInteger;
+import static org.neo4j.driver.internal.pki.DerUtils.readDerOctetString;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.AlgorithmParameters;
+import java.security.KeyException;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.DSAPrivateKeySpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPrivateKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.InvalidParameterSpecException;
+import java.security.spec.KeySpec;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.RSAPrivateCrtKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.HexFormat;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.function.Function;
+import javax.crypto.Cipher;
+import javax.crypto.EncryptedPrivateKeyInfo;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Supported PEM format
+ */
+final class PemFormats {
+ private static final KeyFactory RSA_KEY_FACTORY;
+ private static final KeyFactory DSA_KEY_FACTORY;
+ private static final KeyFactory EC_KEY_FACTORY;
+
+ private static final Function ALL_KEY_FACTORIES;
+
+ static {
+ try {
+ // All exists as part of openJDK
+ RSA_KEY_FACTORY = KeyFactory.getInstance("RSA");
+ DSA_KEY_FACTORY = KeyFactory.getInstance("DSA");
+ EC_KEY_FACTORY = KeyFactory.getInstance("EC");
+ ALL_KEY_FACTORIES = keySpec -> {
+ try {
+ return RSA_KEY_FACTORY.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException e) {
+ try {
+ return DSA_KEY_FACTORY.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException ex) {
+ try {
+ return EC_KEY_FACTORY.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException exc) {
+ // We tried...
+ e.addSuppressed(ex);
+ e.addSuppressed(exc);
+ throw new IllegalStateException("Key does not match RSA, DSA or EC spec.", e);
+ }
+ }
+ }
+ };
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("Non-conforming JDK implementation.", e);
+ }
+ }
+
+ private PemFormats() {}
+
+ interface PemFormat {
+ PrivateKey decodePrivate(byte[] der, Map headers, String password) throws KeyException;
+
+ PublicKey decodePublicKey(byte[] der) throws KeyException;
+ }
+
+ /**
+ * PEM format at described by RFC7468 section 10.
+ */
+ static class Pkcs8 implements PemFormat {
+ static final String PRIVATE_LABEL = "PRIVATE KEY";
+ static final String PUBLIC_LABEL = "PUBLIC KEY";
+
+ @Override
+ public PrivateKey decodePrivate(byte[] der, Map headers, String password) throws KeyException {
+ assertNoPassword(password);
+ return ALL_KEY_FACTORIES.apply(new PKCS8EncodedKeySpec(der));
+ }
+
+ @Override
+ public PublicKey decodePublicKey(byte[] der) throws KeyException {
+ KeySpec encodedKeySpec = new X509EncodedKeySpec(der);
+ try {
+ return RSA_KEY_FACTORY.generatePublic(encodedKeySpec);
+ } catch (InvalidKeySpecException e) {
+ try {
+ return DSA_KEY_FACTORY.generatePublic(encodedKeySpec);
+ } catch (InvalidKeySpecException ex) {
+ try {
+ return EC_KEY_FACTORY.generatePublic(encodedKeySpec);
+ } catch (InvalidKeySpecException exc) {
+ // We tried...
+ e.addSuppressed(ex);
+ e.addSuppressed(exc);
+ throw new KeyException("Public key does not match RSA, DSA or EC spec.", e);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * PEM format at described by RFC7468 section 11.
+ */
+ static class Pkcs8Encrypted extends Pkcs8 {
+ static final String ENCRYPTED_LABEL = "ENCRYPTED PRIVATE KEY";
+
+ @Override
+ public PrivateKey decodePrivate(byte[] der, Map headers, String password) throws KeyException {
+ assertPassword(password);
+ try {
+ var keyInfo = new EncryptedPrivateKeyInfo(der);
+ var pbeKey = getSecretKey(keyInfo, password);
+ var cipher = getCipher(keyInfo);
+ cipher.init(Cipher.DECRYPT_MODE, pbeKey, keyInfo.getAlgParameters());
+ return ALL_KEY_FACTORIES.apply(keyInfo.getKeySpec(cipher));
+ } catch (Exception e) {
+ throw new KeyException("Unable to decrypt private key.", e);
+ }
+ }
+
+ private static SecretKey getSecretKey(EncryptedPrivateKeyInfo keyInfo, String password)
+ throws InvalidKeySpecException, NoSuchAlgorithmException {
+ SecretKeyFactory keyFactory;
+ try {
+ // Try to find by algorithm name first
+ keyFactory = SecretKeyFactory.getInstance(keyInfo.getAlgName());
+ } catch (NoSuchAlgorithmException e) {
+ // Maybe the algorithm parameter have a descent toString()?
+ keyFactory =
+ SecretKeyFactory.getInstance(keyInfo.getAlgParameters().toString());
+ }
+ return keyFactory.generateSecret(new PBEKeySpec(password.toCharArray()));
+ }
+
+ private static Cipher getCipher(EncryptedPrivateKeyInfo keyInfo)
+ throws NoSuchPaddingException, NoSuchAlgorithmException {
+ try {
+ return Cipher.getInstance(keyInfo.getAlgName());
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ return Cipher.getInstance(keyInfo.getAlgParameters().toString());
+ }
+ }
+ }
+
+ /**
+ * PEM Legacy described by RFC1421.
+ * Main difference is that the type of key is described in the label, and not as PKCS#8 as part
+ * of the structure. Encryption is supported and is defined in the header.
+ */
+ abstract static class PemLegacy implements PemFormat {
+
+ @Override
+ public PrivateKey decodePrivate(byte[] der, Map headers, String password) throws KeyException {
+ // Handle decryption
+ var procType = headers.get("Proc-Type");
+ if (procType != null && procType.equals("4,ENCRYPTED")) {
+ assertPassword(password);
+ var deckInfo = headers.get("DEK-Info");
+ if (deckInfo == null) {
+ throw new KeyException("Missing 'DEK-Info' in encrypted PRIVATE KEY.");
+ }
+ var tokenizer = new StringTokenizer(deckInfo, ",");
+ var algorithm = tokenizer.nextToken();
+ var iv = HexFormat.of().parseHex(tokenizer.nextToken());
+ der = decryptLegacyPem(der, algorithm, iv, password);
+ } else {
+ assertNoPassword(password);
+ }
+
+ // Here we have an un-encrypted DER that can be parsed by the corresponding algorithm
+ var buffer = ByteBuffer.wrap(der);
+ if (beginDerSequence(buffer) != buffer.remaining()) {
+ throw new IllegalArgumentException("Malformed ASN.1 input.");
+ }
+ if (!version().equals(readDerInteger(buffer))) {
+ throw new IllegalArgumentException("PrivateKey version mismatch.");
+ }
+ try {
+ return decodePrivate0(buffer);
+ } catch (InvalidKeySpecException e) {
+ throw new KeyException(e);
+ }
+ }
+
+ protected abstract PrivateKey decodePrivate0(ByteBuffer buffer) throws InvalidKeySpecException;
+
+ protected abstract BigInteger version();
+ }
+
+ /**
+ * Parser for PKCS1 encoded keys as described by
+ * RFC3447
+ *
+ * Unlike PKCS8, the type of the key is not encoded in the structure, so
+ * we need to parse the ASN.1 structures directly.
+ */
+ static class PemPKCS1Rsa extends PemLegacy {
+ static final String PRIVATE_LABEL = "RSA PRIVATE KEY";
+ static final String PUBLIC_LABEL = "RSA PUBLIC KEY";
+
+ /**
+ *
+ * RSAPublicKey ::= SEQUENCE {
+ * modulus INTEGER, -- n
+ * publicExponent INTEGER -- e
+ * }
+ *
+ */
+ @Override
+ public PublicKey decodePublicKey(byte[] der) throws KeyException {
+ var input = ByteBuffer.wrap(der);
+ if (beginDerSequence(input) != input.remaining()) {
+ throw new IllegalArgumentException("Malformed RSAPublicKey");
+ }
+ var n = readDerInteger(input); // INTEGER modulus
+ var e = readDerInteger(input); // INTEGER publicExponent
+ try {
+ return RSA_KEY_FACTORY.generatePublic(new RSAPublicKeySpec(n, e));
+ } catch (InvalidKeySpecException ex) {
+ throw new KeyException(ex);
+ }
+ }
+
+ /**
+ *
+ * RSAPrivateKey ::= SEQUENCE {
+ * version Version,
+ * modulus INTEGER, -- n
+ * publicExponent INTEGER, -- e
+ * privateExponent INTEGER, -- d
+ * prime1 INTEGER, -- p
+ * prime2 INTEGER, -- q
+ * exponent1 INTEGER, -- d mod (p-1)
+ * exponent2 INTEGER, -- d mod (q-1)
+ * coefficient INTEGER, -- (inverse of q) mod p
+ * otherPrimeInfos OtherPrimeInfos OPTIONAL
+ * }
+ *
+ */
+ @Override
+ protected PrivateKey decodePrivate0(ByteBuffer buffer) throws InvalidKeySpecException {
+ var n = readDerInteger(buffer); // INTEGER modulus
+ var e = readDerInteger(buffer); // INTEGER publicExponent
+ var d = readDerInteger(buffer); // INTEGER privateExponent
+ var p = readDerInteger(buffer); // INTEGER prime1
+ var q = readDerInteger(buffer); // INTEGER prime2
+ var ep = readDerInteger(buffer); // INTEGER exponent1
+ var eq = readDerInteger(buffer); // INTEGER exponent2
+ var c = readDerInteger(buffer); // INTEGER coefficient
+ return RSA_KEY_FACTORY.generatePrivate(new RSAPrivateCrtKeySpec(n, e, d, p, q, ep, eq, c));
+ }
+
+ @Override
+ protected BigInteger version() {
+ return BigInteger.ZERO;
+ }
+ }
+
+ /**
+ * Parser for DSA keys, no available standard but exists as a de facto standard as implemented by openSSL
+ */
+ static class PemPKCS1Dsa extends PemLegacy {
+ static final String PRIVATE_LABEL = "DSA PRIVATE KEY";
+
+ @Override
+ public PublicKey decodePublicKey(byte[] der) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ *
+ * DSSPrivatKey_OpenSSL ::= SEQUENCE
+ * version INTEGER,
+ * p INTEGER,
+ * q INTEGER,
+ * g INTEGER,
+ * y INTEGER,
+ * x INTEGER
+ * }
+ *
+ */
+ @Override
+ protected PrivateKey decodePrivate0(ByteBuffer buffer) throws InvalidKeySpecException {
+ var p = readDerInteger(buffer); // INTEGER p
+ var q = readDerInteger(buffer); // INTEGER q
+ var g = readDerInteger(buffer); // INTEGER g
+ readDerInteger(buffer); // public key 'y' is not used in the private key
+ var x = readDerInteger(buffer); // INTEGER x
+ return DSA_KEY_FACTORY.generatePrivate(new DSAPrivateKeySpec(x, p, q, g));
+ }
+
+ @Override
+ protected BigInteger version() {
+ return BigInteger.ZERO;
+ }
+ }
+
+ /**
+ * Parser of Elliptic Curve keys as described by RFC5915
+ */
+ static class PemPKCS1Ec extends PemLegacy {
+ static final String PRIVATE_LABEL = "EC PRIVATE KEY";
+
+ @Override
+ public PublicKey decodePublicKey(byte[] der) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ *
+ * ECPrivateKey ::= SEQUENCE {
+ * version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
+ * privateKey OCTET STRING,
+ * parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
+ * publicKey [1] BIT STRING OPTIONAL
+ * }
+ *
+ */
+ @Override
+ protected PrivateKey decodePrivate0(ByteBuffer buffer) throws InvalidKeySpecException {
+ try {
+ var s = new BigInteger(1, readDerOctetString(buffer));
+ var parameters = getDerContext(buffer, (byte) 0);
+ var ecParams = AlgorithmParameters.getInstance("EC");
+ ecParams.init(parameters);
+ var parameterSpec = ecParams.getParameterSpec(ECParameterSpec.class);
+ return EC_KEY_FACTORY.generatePrivate(new ECPrivateKeySpec(s, parameterSpec));
+ } catch (NoSuchAlgorithmException | IOException | InvalidParameterSpecException e) {
+ throw new IllegalArgumentException("Failed to decode EC private key", e);
+ }
+ }
+
+ @Override
+ protected BigInteger version() {
+ return BigInteger.ONE;
+ }
+ }
+
+ private static void assertNoPassword(String password) throws KeyException {
+ if (password != null) {
+ throw new KeyException("Passphrase was provided but found un-encrypted private key.");
+ }
+ }
+
+ private static void assertPassword(String password) throws KeyException {
+ if (password == null) {
+ throw new KeyException("Found encrypted private key but no passphrase was provided.");
+ }
+ }
+
+ /**
+ * Supported encryption schemas for legacy PEM.
+ */
+ private enum DecryptSchema {
+ DES_CBC(8, "DES", "DES/CBC/PKCS5Padding"),
+ DES_EDE3_CBC(24, "DESede", "DESede/CBC/PKCS5Padding"),
+ AES_128_CBC(16, "AES", "AES/CBC/PKCS5Padding"),
+ AES_192_CBC(24, "AES", "AES/CBC/PKCS5Padding"),
+ AES_256_CBC(32, "AES", "AES/CBC/PKCS5Padding");
+
+ final int keySize;
+ final String family;
+ final String cipher;
+
+ DecryptSchema(int keySize, String family, String cipher) {
+ this.keySize = keySize;
+ this.family = family;
+ this.cipher = cipher;
+ }
+ }
+
+ private static byte[] decryptLegacyPem(byte[] der, String algorithm, byte[] iv, String password)
+ throws KeyException {
+ try {
+ DecryptSchema decryptSchema;
+ try {
+ decryptSchema = DecryptSchema.valueOf(algorithm.replace("-", "_"));
+ } catch (IllegalArgumentException e) {
+ throw new KeyException(format("Encryption scheme %s is not supported.", algorithm));
+ }
+
+ // Take as many bytes from the digest as needed
+ var kdf = keyDerivationFunction(iv, password);
+ var key = new byte[decryptSchema.keySize];
+ System.arraycopy(kdf, 0, key, 0, decryptSchema.keySize);
+ var secret = new SecretKeySpec(key, decryptSchema.family);
+
+ // Decrypt
+ var cipher = Cipher.getInstance(decryptSchema.cipher);
+ cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
+ return cipher.doFinal(der);
+ } catch (Exception e) {
+ throw new KeyException("Failed to decrypt PEM file.", e);
+ }
+ }
+
+ /**
+ * OpenSSL de facto standard for generating keys for symmetric cyphers for PEM file encryption.
+ * It's basically PBKDF1 as described in PKCS#5 v1.5 with MD5 hash and iteration count of 1.
+ *
+ * @param iv initialization vector.
+ * @param password user password.
+ * @return an array of 32 bytes that should be used as keys for symmetric cyphers.
+ * @throws NoSuchAlgorithmException if MD5 message digest is not available.
+ */
+ private static byte[] keyDerivationFunction(byte[] iv, String password) throws NoSuchAlgorithmException {
+ // https://github.com/openssl/openssl/blob/e4fd3fc379d76d9cd33ea6699268485606447737/crypto/pem/pem_lib.c#L378
+ // https://github.com/openssl/openssl/blob/e4fd3fc379d76d9cd33ea6699268485606447737/crypto/evp/evp_key.c#L78
+ var pw = password.getBytes(StandardCharsets.UTF_8);
+ var md5 = MessageDigest.getInstance("MD5");
+ md5.update(pw);
+ md5.update(iv, 0, 8);
+ var d0 = md5.digest();
+ md5.update(d0);
+ md5.update(pw);
+ md5.update(iv, 0, 8);
+ var d1 = md5.digest();
+ var kdf = new byte[32];
+ System.arraycopy(d0, 0, kdf, 0, 16);
+ System.arraycopy(d1, 0, kdf, 16, 16);
+ return kdf;
+ }
+}
diff --git a/driver/src/main/java/org/neo4j/driver/internal/pki/PemParser.java b/driver/src/main/java/org/neo4j/driver/internal/pki/PemParser.java
new file mode 100644
index 0000000000..11f2f2e5dc
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/internal/pki/PemParser.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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.pki;
+
+import static java.lang.String.format;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyException;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.CertificateFactory;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Parse a public or private key.
+ *
+ * In contrast to {@link CertificateFactory}, Java's {@link KeyFactory}'s does not support reading textual pem files.
+ * PEM format reference: RFC7468
+ * This implementation has support for
+ *
PKCS#8/X.509 Public Key
+ * PKCS#8 and encrypted PKCS#8 private keys
+ * PKCS#1 encoded RSA, DSA and EC.
+ * Encrypted PKCS#1 encoded RSA, DSA and EC.
+ */
+public final class PemParser {
+ private static final String BEGIN = "-----BEGIN ";
+ private static final String END = "-----END ";
+
+ /**
+ * Mime decoder is almost identical to PEM specification.
+ *
+ * RFC2045
+ */
+ private static final Base64.Decoder BASE_64 = Base64.getMimeDecoder();
+
+ private static final Map parsers;
+
+ static {
+ parsers = Map.of(
+ PemFormats.Pkcs8.PUBLIC_LABEL, new PemFormats.Pkcs8(),
+ PemFormats.Pkcs8.PRIVATE_LABEL, new PemFormats.Pkcs8(),
+ PemFormats.Pkcs8Encrypted.ENCRYPTED_LABEL, new PemFormats.Pkcs8Encrypted(),
+ PemFormats.PemPKCS1Rsa.PUBLIC_LABEL, new PemFormats.PemPKCS1Rsa(),
+ PemFormats.PemPKCS1Rsa.PRIVATE_LABEL, new PemFormats.PemPKCS1Rsa(),
+ PemFormats.PemPKCS1Dsa.PRIVATE_LABEL, new PemFormats.PemPKCS1Dsa(),
+ PemFormats.PemPKCS1Ec.PRIVATE_LABEL, new PemFormats.PemPKCS1Ec());
+ }
+
+ private String label;
+ private byte[] der;
+ private final Map headers = new HashMap<>();
+
+ public PemParser(InputStream in) throws IOException {
+ try (var reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.US_ASCII))) {
+ parse(reader);
+ }
+ }
+
+ public PrivateKey getPrivateKey(String password) throws KeyException {
+ var pemFormat = parsers.get(label);
+ if (pemFormat != null) {
+ return pemFormat.decodePrivate(der, headers, password);
+ }
+
+ throw new KeyException(format("Provided PEM does not contain a private key, found '%s'.", label));
+ }
+
+ public PublicKey getPublicKey() throws KeyException {
+ var pemFormat = parsers.get(label);
+ if (pemFormat != null) {
+ return pemFormat.decodePublicKey(der);
+ }
+ throw new KeyException(format("Provided PEM does not contain a public key, found '%s'.", label));
+ }
+
+ /**
+ * Read the input stream and extract all the components.
+ * @throws IOException on failure to read from the provided input stream.
+ */
+ private void parse(BufferedReader reader) throws IOException {
+ // Find header, explanatory text is allowed before and after block, so ignore that
+ var line = reader.readLine();
+ while (line != null && !line.startsWith(BEGIN)) {
+ line = reader.readLine();
+ }
+ if (line == null) {
+ throw new IllegalStateException("File does not contain " + BEGIN + " encapsulation boundary.");
+ }
+
+ // Get label
+ label = extractLabel(line);
+ var endMarker = END + label;
+
+ // Extract base64 encoded DER
+ var sb = new StringBuilder();
+ while ((line = reader.readLine()) != null) {
+ if (line.contains(endMarker)) {
+ der = BASE_64.decode(sb.toString());
+ return;
+ }
+
+ // Look for headers, is allowed in legacy PEM
+ if (line.contains(":")) {
+ var kv = line.split(":");
+ headers.put(kv[0].trim(), kv[1].trim());
+ continue;
+ }
+
+ sb.append(line.trim());
+ }
+ throw new IllegalStateException("Missing footer: " + endMarker + ".");
+ }
+
+ /**
+ * Extract the label from the format {@code -----BEGIN (label)-----}.
+ * @param line line containing the pre-encapsulation boundary.
+ * @return the label for the encapsulation.
+ */
+ private static String extractLabel(String line) {
+ line = line.substring(BEGIN.length());
+ var index = line.indexOf('-');
+
+ if (!line.endsWith("-----") || (line.length() - index) != "-----".length()) {
+ throw new IllegalStateException(
+ format("Unable to find label, expecting '-----BEGIN (label)-----' but found '%s'.", line));
+ }
+ return line.substring(0, index);
+ }
+}
diff --git a/driver/src/main/java/org/neo4j/driver/internal/security/SSLContextManager.java b/driver/src/main/java/org/neo4j/driver/internal/security/SSLContextManager.java
new file mode 100644
index 0000000000..3e8b87240e
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/internal/security/SSLContextManager.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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.FileInputStream;
+import java.io.IOException;
+import java.security.KeyException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CompletionStage;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import org.neo4j.driver.ClientCertificateManager;
+import org.neo4j.driver.Logger;
+import org.neo4j.driver.Logging;
+import org.neo4j.driver.exceptions.ClientException;
+import org.neo4j.driver.internal.InternalClientCertificate;
+import org.neo4j.driver.internal.pki.PemParser;
+import org.neo4j.driver.internal.util.Futures;
+
+class SSLContextManager {
+ private final ClientCertificateManager clientCertificateManager;
+ private final SecurityPlan.SSLContextSupplier sslContextSupplier;
+ private final Logger logger;
+ private CompletableFuture sslContextFuture;
+ private SSLContext sslContext;
+ private Throwable throwable;
+
+ public SSLContextManager(
+ ClientCertificateManager clientCertificateManager,
+ SecurityPlan.SSLContextSupplier sslContextSupplier,
+ Logging logging)
+ throws NoSuchAlgorithmException, KeyManagementException {
+ this.clientCertificateManager = clientCertificateManager;
+ this.sslContextSupplier = sslContextSupplier;
+ logger = logging.getLog(getClass());
+
+ if (clientCertificateManager == null) {
+ var sslContext = sslContextSupplier.get(new KeyManager[0]);
+ sslContextFuture = CompletableFuture.completedFuture(sslContext);
+ }
+ }
+
+ public CompletionStage getSSLContext() {
+ return clientCertificateManager != null ? getSSLContextWithClientCertificate() : sslContextFuture;
+ }
+
+ private CompletionStage getSSLContextWithClientCertificate() {
+ CompletableFuture sslContextFuture;
+ CompletionStage sslContextStage = null;
+ synchronized (this) {
+ if (this.sslContextFuture == null) {
+ this.sslContextFuture = new CompletableFuture<>();
+ sslContextFuture = this.sslContextFuture;
+ var sslContext = this.sslContext;
+ var previousThrowable = this.throwable;
+ sslContextStage = clientCertificateManager
+ .getClientCertificate()
+ .thenApply(clientCertificate -> {
+ if (clientCertificate != null) {
+ var certificate = (InternalClientCertificate) clientCertificate;
+ try {
+ var keyManagers = createKeyManagers(certificate);
+ return sslContextSupplier.get(keyManagers);
+ } catch (Throwable throwable) {
+ var exception = new ClientException(
+ "An error occured while loading client certficate.", throwable);
+ logger.error("An error occured while loading client certficate.", exception);
+ throw new CompletionException(exception);
+ }
+ } else {
+ if (previousThrowable != null) {
+ throw new CompletionException(previousThrowable);
+ } else {
+ if (sslContext == null) {
+ var exception = new ClientException(
+ "The initial client certificate returned by the manager must not be null.");
+ logger.error(
+ "The initial client certificate returned by the manager must not be null.",
+ exception);
+ throw new CompletionException(exception);
+ } else {
+ return sslContext;
+ }
+ }
+ }
+ });
+ } else {
+ sslContextFuture = this.sslContextFuture;
+ }
+ }
+
+ if (sslContextStage != null) {
+ sslContextStage.whenComplete((sslContext, throwable) -> {
+ throwable = Futures.completionExceptionCause(throwable);
+ synchronized (this) {
+ this.sslContextFuture = null;
+ this.sslContext = sslContext;
+ this.throwable = throwable;
+ }
+ if (throwable != null) {
+ sslContextFuture.completeExceptionally(throwable);
+ } else {
+ sslContextFuture.complete(this.sslContext);
+ }
+ });
+ }
+
+ return sslContextFuture;
+ }
+
+ protected KeyManager[] createKeyManagers(InternalClientCertificate clientCertificate)
+ throws CertificateException, IOException, KeyException, KeyStoreException, NoSuchAlgorithmException,
+ UnrecoverableKeyException {
+ var certificateFactory = CertificateFactory.getInstance("X.509");
+ var chain = certificateFactory.generateCertificates(new FileInputStream(clientCertificate.certificate()));
+
+ var password = clientCertificate.password();
+ var key = new PemParser(new FileInputStream(clientCertificate.privateKey())).getPrivateKey(password);
+
+ var clientKeyStore = KeyStore.getInstance("JKS");
+ var pwdChars = password != null ? password.toCharArray() : "password".toCharArray();
+ clientKeyStore.load(null, null);
+ clientKeyStore.setKeyEntry("neo4j.javadriver.clientcert.", key, pwdChars, chain.toArray(new Certificate[0]));
+
+ var keyMgrFactory = KeyManagerFactory.getInstance("SunX509");
+ keyMgrFactory.init(clientKeyStore, pwdChars);
+
+ return keyMgrFactory.getKeyManagers();
+ }
+}
diff --git a/driver/src/main/java/org/neo4j/driver/internal/security/SecurityPlan.java b/driver/src/main/java/org/neo4j/driver/internal/security/SecurityPlan.java
index b832c9cccc..d1ee98c22c 100644
--- a/driver/src/main/java/org/neo4j/driver/internal/security/SecurityPlan.java
+++ b/driver/src/main/java/org/neo4j/driver/internal/security/SecurityPlan.java
@@ -16,6 +16,10 @@
*/
package org.neo4j.driver.internal.security;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.CompletionStage;
+import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import org.neo4j.driver.RevocationCheckingStrategy;
@@ -25,9 +29,15 @@
public interface SecurityPlan {
boolean requiresEncryption();
- SSLContext sslContext();
+ boolean requiresClientAuth();
+
+ CompletionStage sslContext();
boolean requiresHostnameVerification();
RevocationCheckingStrategy revocationCheckingStrategy();
+
+ interface SSLContextSupplier {
+ SSLContext get(KeyManager[] keyManagers) throws NoSuchAlgorithmException, KeyManagementException;
+ }
}
diff --git a/driver/src/main/java/org/neo4j/driver/internal/security/SecurityPlanImpl.java b/driver/src/main/java/org/neo4j/driver/internal/security/SecurityPlanImpl.java
index 7a8e9ba063..2d526b5289 100644
--- a/driver/src/main/java/org/neo4j/driver/internal/security/SecurityPlanImpl.java
+++ b/driver/src/main/java/org/neo4j/driver/internal/security/SecurityPlanImpl.java
@@ -24,8 +24,10 @@
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.PKIXBuilderParameters;
@@ -34,44 +36,72 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.CompletionStage;
+import java.util.function.Supplier;
import javax.net.ssl.CertPathTrustManagerParameters;
-import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
+import org.neo4j.driver.ClientCertificateManager;
+import org.neo4j.driver.Logging;
import org.neo4j.driver.RevocationCheckingStrategy;
+import org.neo4j.driver.internal.util.Futures;
/**
* A SecurityPlan consists of encryption and trust details.
*/
public class SecurityPlanImpl implements SecurityPlan {
public static SecurityPlan forAllCertificates(
- boolean requiresHostnameVerification, RevocationCheckingStrategy revocationCheckingStrategy)
- throws GeneralSecurityException {
- var sslContext = SSLContext.getInstance("TLS");
- sslContext.init(new KeyManager[0], new TrustManager[] {new TrustAllTrustManager()}, null);
-
- return new SecurityPlanImpl(true, sslContext, requiresHostnameVerification, revocationCheckingStrategy);
+ boolean requiresHostnameVerification,
+ RevocationCheckingStrategy revocationCheckingStrategy,
+ ClientCertificateManager clientCertificateManager,
+ Logging logging)
+ throws NoSuchAlgorithmException, KeyManagementException {
+ return new SecurityPlanImpl(
+ keyManagers -> {
+ var sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(keyManagers, new TrustManager[] {new TrustAllTrustManager()}, null);
+ return sslContext;
+ },
+ requiresHostnameVerification,
+ revocationCheckingStrategy,
+ clientCertificateManager,
+ logging);
}
public static SecurityPlan forCustomCASignedCertificates(
List certFiles,
boolean requiresHostnameVerification,
- RevocationCheckingStrategy revocationCheckingStrategy)
+ RevocationCheckingStrategy revocationCheckingStrategy,
+ ClientCertificateManager clientCertificateManager,
+ Logging logging)
throws GeneralSecurityException, IOException {
- var sslContext = configureSSLContext(certFiles, revocationCheckingStrategy);
- return new SecurityPlanImpl(true, sslContext, requiresHostnameVerification, revocationCheckingStrategy);
+ var sslContext = configureSSLContextSupplier(certFiles, revocationCheckingStrategy);
+ return new SecurityPlanImpl(
+ sslContext,
+ requiresHostnameVerification,
+ revocationCheckingStrategy,
+ clientCertificateManager,
+ logging);
}
public static SecurityPlan forSystemCASignedCertificates(
- boolean requiresHostnameVerification, RevocationCheckingStrategy revocationCheckingStrategy)
+ boolean requiresHostnameVerification,
+ RevocationCheckingStrategy revocationCheckingStrategy,
+ ClientCertificateManager clientCertificateManager,
+ Logging logging)
throws GeneralSecurityException, IOException {
- var sslContext = configureSSLContext(Collections.emptyList(), revocationCheckingStrategy);
- return new SecurityPlanImpl(true, sslContext, requiresHostnameVerification, revocationCheckingStrategy);
+ var sslContext = configureSSLContextSupplier(Collections.emptyList(), revocationCheckingStrategy);
+ return new SecurityPlanImpl(
+ sslContext,
+ requiresHostnameVerification,
+ revocationCheckingStrategy,
+ clientCertificateManager,
+ logging);
}
- private static SSLContext configureSSLContext(
+ private static SSLContextSupplier configureSSLContextSupplier(
List customCertFiles, RevocationCheckingStrategy revocationCheckingStrategy)
throws GeneralSecurityException, IOException {
var trustedKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
@@ -86,7 +116,6 @@ private static SSLContext configureSSLContext(
var pkixBuilderParameters = configurePKIXBuilderParameters(trustedKeyStore, revocationCheckingStrategy);
- var sslContext = SSLContext.getInstance("TLS");
var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
if (pkixBuilderParameters == null) {
@@ -95,9 +124,11 @@ private static SSLContext configureSSLContext(
trustManagerFactory.init(new CertPathTrustManagerParameters(pkixBuilderParameters));
}
- sslContext.init(new KeyManager[0], trustManagerFactory.getTrustManagers(), null);
-
- return sslContext;
+ return keyManagers -> {
+ var sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(keyManagers, trustManagerFactory.getTrustManagers(), null);
+ return sslContext;
+ };
}
private static PKIXBuilderParameters configurePKIXBuilderParameters(
@@ -143,23 +174,36 @@ private static void loadSystemCertificates(KeyStore trustedKeyStore) throws Gene
}
public static SecurityPlan insecure() {
- return new SecurityPlanImpl(false, null, false, RevocationCheckingStrategy.NO_CHECKS);
+ return new SecurityPlanImpl();
}
private final boolean requiresEncryption;
- private final SSLContext sslContext;
+ private final boolean requiresClientAuth;
private final boolean requiresHostnameVerification;
private final RevocationCheckingStrategy revocationCheckingStrategy;
+ private final Supplier> sslContextSupplier;
private SecurityPlanImpl(
- boolean requiresEncryption,
- SSLContext sslContext,
+ SSLContextSupplier sslContextSupplier,
boolean requiresHostnameVerification,
- RevocationCheckingStrategy revocationCheckingStrategy) {
- this.requiresEncryption = requiresEncryption;
- this.sslContext = sslContext;
+ RevocationCheckingStrategy revocationCheckingStrategy,
+ ClientCertificateManager clientCertificateManager,
+ Logging logging)
+ throws NoSuchAlgorithmException, KeyManagementException {
+ this.requiresEncryption = true;
this.requiresHostnameVerification = requiresHostnameVerification;
this.revocationCheckingStrategy = revocationCheckingStrategy;
+ var sslContextManager = new SSLContextManager(clientCertificateManager, sslContextSupplier, logging);
+ this.sslContextSupplier = sslContextManager::getSSLContext;
+ this.requiresClientAuth = clientCertificateManager != null;
+ }
+
+ private SecurityPlanImpl() {
+ this.requiresEncryption = false;
+ this.requiresHostnameVerification = false;
+ this.revocationCheckingStrategy = RevocationCheckingStrategy.NO_CHECKS;
+ this.sslContextSupplier = Futures::completedWithNull;
+ this.requiresClientAuth = false;
}
@Override
@@ -168,8 +212,13 @@ public boolean requiresEncryption() {
}
@Override
- public SSLContext sslContext() {
- return sslContext;
+ public boolean requiresClientAuth() {
+ return requiresClientAuth;
+ }
+
+ @Override
+ public CompletionStage sslContext() {
+ return sslContextSupplier.get();
}
@Override
diff --git a/driver/src/main/java/org/neo4j/driver/internal/security/SecurityPlans.java b/driver/src/main/java/org/neo4j/driver/internal/security/SecurityPlans.java
index 353b387a22..62d3809f36 100644
--- a/driver/src/main/java/org/neo4j/driver/internal/security/SecurityPlans.java
+++ b/driver/src/main/java/org/neo4j/driver/internal/security/SecurityPlans.java
@@ -22,21 +22,28 @@
import java.io.IOException;
import java.security.GeneralSecurityException;
+import org.neo4j.driver.ClientCertificateManager;
import org.neo4j.driver.Config;
+import org.neo4j.driver.Logging;
import org.neo4j.driver.RevocationCheckingStrategy;
import org.neo4j.driver.exceptions.ClientException;
import org.neo4j.driver.internal.Scheme;
import org.neo4j.driver.internal.SecuritySettings;
public class SecurityPlans {
- public static SecurityPlan createSecurityPlan(SecuritySettings settings, String uriScheme) {
+ public static SecurityPlan createSecurityPlan(
+ SecuritySettings settings,
+ String uriScheme,
+ ClientCertificateManager clientCertificateManager,
+ Logging logging) {
Scheme.validateScheme(uriScheme);
try {
if (isSecurityScheme(uriScheme)) {
assertSecuritySettingsNotUserConfigured(settings, uriScheme);
- return createSecurityPlanFromScheme(uriScheme);
+ return createSecurityPlanFromScheme(uriScheme, clientCertificateManager, logging);
} else {
- return createSecurityPlanImpl(settings.encrypted(), settings.trustStrategy());
+ return createSecurityPlanImpl(
+ settings.encrypted(), settings.trustStrategy(), clientCertificateManager, logging);
}
} catch (GeneralSecurityException | IOException ex) {
throw new ClientException("Unable to establish SSL parameters", ex);
@@ -68,12 +75,15 @@ private static boolean hasEqualTrustStrategy(SecuritySettings settings) {
&& t1.revocationCheckingStrategy() == t2.revocationCheckingStrategy();
}
- private static SecurityPlan createSecurityPlanFromScheme(String scheme)
+ private static SecurityPlan createSecurityPlanFromScheme(
+ String scheme, ClientCertificateManager clientCertificateManager, Logging logging)
throws GeneralSecurityException, IOException {
if (isHighTrustScheme(scheme)) {
- return SecurityPlanImpl.forSystemCASignedCertificates(true, RevocationCheckingStrategy.NO_CHECKS);
+ return SecurityPlanImpl.forSystemCASignedCertificates(
+ true, RevocationCheckingStrategy.NO_CHECKS, clientCertificateManager, logging);
} else {
- return SecurityPlanImpl.forAllCertificates(false, RevocationCheckingStrategy.NO_CHECKS);
+ return SecurityPlanImpl.forAllCertificates(
+ false, RevocationCheckingStrategy.NO_CHECKS, clientCertificateManager, logging);
}
}
@@ -81,18 +91,26 @@ private static SecurityPlan createSecurityPlanFromScheme(String scheme)
* Establish a complete SecurityPlan based on the details provided for
* driver construction.
*/
- private static SecurityPlan createSecurityPlanImpl(boolean encrypted, Config.TrustStrategy trustStrategy)
+ private static SecurityPlan createSecurityPlanImpl(
+ boolean encrypted,
+ Config.TrustStrategy trustStrategy,
+ ClientCertificateManager clientCertificateManager,
+ Logging logging)
throws GeneralSecurityException, IOException {
if (encrypted) {
var hostnameVerificationEnabled = trustStrategy.isHostnameVerificationEnabled();
var revocationCheckingStrategy = trustStrategy.revocationCheckingStrategy();
return switch (trustStrategy.strategy()) {
case TRUST_CUSTOM_CA_SIGNED_CERTIFICATES -> SecurityPlanImpl.forCustomCASignedCertificates(
- trustStrategy.certFiles(), hostnameVerificationEnabled, revocationCheckingStrategy);
+ trustStrategy.certFiles(),
+ hostnameVerificationEnabled,
+ revocationCheckingStrategy,
+ clientCertificateManager,
+ logging);
case TRUST_SYSTEM_CA_SIGNED_CERTIFICATES -> SecurityPlanImpl.forSystemCASignedCertificates(
- hostnameVerificationEnabled, revocationCheckingStrategy);
+ hostnameVerificationEnabled, revocationCheckingStrategy, clientCertificateManager, logging);
case TRUST_ALL_CERTIFICATES -> SecurityPlanImpl.forAllCertificates(
- hostnameVerificationEnabled, revocationCheckingStrategy);
+ hostnameVerificationEnabled, revocationCheckingStrategy, clientCertificateManager, logging);
};
} else {
return insecure();
diff --git a/driver/src/test/java/org/neo4j/driver/ClientCertificateManagersTest.java b/driver/src/test/java/org/neo4j/driver/ClientCertificateManagersTest.java
new file mode 100644
index 0000000000..6ecaf0065f
--- /dev/null
+++ b/driver/src/test/java/org/neo4j/driver/ClientCertificateManagersTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+
+import java.io.File;
+import org.junit.jupiter.api.Test;
+
+class ClientCertificateManagersTest {
+ @Test
+ void shoudReturnRotatingManager() {
+ var file = mock(File.class);
+ var certificate = ClientCertificates.of(file, file);
+ var manager = ClientCertificateManagers.rotating(certificate);
+
+ assertNotNull(manager);
+ }
+}
diff --git a/driver/src/test/java/org/neo4j/driver/integration/ChannelConnectorImplIT.java b/driver/src/test/java/org/neo4j/driver/integration/ChannelConnectorImplIT.java
index 4b142300ef..d55879a8b1 100644
--- a/driver/src/test/java/org/neo4j/driver/integration/ChannelConnectorImplIT.java
+++ b/driver/src/test/java/org/neo4j/driver/integration/ChannelConnectorImplIT.java
@@ -31,13 +31,17 @@
import static org.neo4j.driver.testutil.TestUtil.await;
import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.ChannelFuture;
import io.netty.handler.ssl.SslHandler;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.ServerSocket;
-import java.security.GeneralSecurityException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
@@ -45,6 +49,7 @@
import org.junit.jupiter.api.extension.RegisterExtension;
import org.neo4j.driver.AuthTokenManager;
import org.neo4j.driver.AuthTokens;
+import org.neo4j.driver.Logging;
import org.neo4j.driver.RevocationCheckingStrategy;
import org.neo4j.driver.exceptions.AuthenticationException;
import org.neo4j.driver.exceptions.ServiceUnavailableException;
@@ -88,7 +93,11 @@ void tearDown() {
void shouldConnect() throws Exception {
ChannelConnector connector = newConnector(neo4j.authTokenManager());
- var channelFuture = connector.connect(neo4j.address(), bootstrap);
+ var ft = new CompletableFuture();
+ connector
+ .connect(neo4j.address(), bootstrap, Function.identity())
+ .addListener(future -> ft.complete((ChannelFuture) future));
+ var channelFuture = ft.join();
assertTrue(channelFuture.await(10, TimeUnit.SECONDS));
var channel = channelFuture.channel();
@@ -100,7 +109,11 @@ void shouldConnect() throws Exception {
void shouldSetupHandlers() throws Exception {
ChannelConnector connector = newConnector(neo4j.authTokenManager(), trustAllCertificates(), 10_000);
- var channelFuture = connector.connect(neo4j.address(), bootstrap);
+ var ft = new CompletableFuture();
+ connector
+ .connect(neo4j.address(), bootstrap, Function.identity())
+ .addListener(future -> ft.complete((ChannelFuture) future));
+ var channelFuture = ft.join();
assertTrue(channelFuture.await(10, TimeUnit.SECONDS));
var channel = channelFuture.channel();
@@ -115,7 +128,11 @@ void shouldSetupHandlers() throws Exception {
void shouldFailToConnectToWrongAddress() throws Exception {
ChannelConnector connector = newConnector(neo4j.authTokenManager());
- var channelFuture = connector.connect(new BoltServerAddress("wrong-localhost"), bootstrap);
+ var ft = new CompletableFuture();
+ connector
+ .connect(new BoltServerAddress("wrong-localhost"), bootstrap, Function.identity())
+ .addListener(future -> ft.complete((ChannelFuture) future));
+ var channelFuture = ft.join();
assertTrue(channelFuture.await(10, TimeUnit.SECONDS));
var channel = channelFuture.channel();
@@ -133,7 +150,11 @@ void shouldFailToConnectWithWrongCredentials() throws Exception {
var authToken = AuthTokens.basic("neo4j", "wrong-password");
ChannelConnector connector = newConnector(new StaticAuthTokenManager(authToken));
- var channelFuture = connector.connect(neo4j.address(), bootstrap);
+ var ft = new CompletableFuture();
+ connector
+ .connect(neo4j.address(), bootstrap, Function.identity())
+ .addListener(future -> ft.complete((ChannelFuture) future));
+ var channelFuture = ft.join();
assertTrue(channelFuture.await(10, TimeUnit.SECONDS));
var channel = channelFuture.channel();
@@ -143,11 +164,15 @@ void shouldFailToConnectWithWrongCredentials() throws Exception {
}
@Test
- void shouldEnforceConnectTimeout() throws Exception {
+ void shouldEnforceConnectTimeout() throws NoSuchAlgorithmException, KeyManagementException {
ChannelConnector connector = newConnector(neo4j.authTokenManager(), 1000);
// try connect to a non-routable ip address 10.0.0.0, it will never respond
- var channelFuture = connector.connect(new BoltServerAddress("10.0.0.0"), bootstrap);
+ var ft = new CompletableFuture();
+ connector
+ .connect(new BoltServerAddress("10.0.0.0"), bootstrap, Function.identity())
+ .addListener(future -> ft.complete((ChannelFuture) future));
+ var channelFuture = ft.join();
assertThrows(ServiceUnavailableException.class, () -> await(channelFuture));
}
@@ -184,7 +209,11 @@ void shouldThrowServiceUnavailableExceptionOnFailureDuringConnect() throws Excep
});
ChannelConnector connector = newConnector(neo4j.authTokenManager());
- var channelFuture = connector.connect(address, bootstrap);
+ var ft = new CompletableFuture();
+ connector
+ .connect(address, bootstrap, Function.identity())
+ .addListener(future -> ft.complete((ChannelFuture) future));
+ var channelFuture = ft.join();
// connect operation should fail with ServiceUnavailableException
assertThrows(ServiceUnavailableException.class, () -> await(channelFuture));
@@ -197,19 +226,24 @@ private void testReadTimeoutOnConnect(SecurityPlan securityPlan) throws IOExcept
var address = new BoltServerAddress("localhost", server.getLocalPort());
ChannelConnector connector = newConnector(neo4j.authTokenManager(), securityPlan, timeoutMillis);
- var channelFuture = connector.connect(address, bootstrap);
+ var ft = new CompletableFuture();
+ connector
+ .connect(address, bootstrap, Function.identity())
+ .addListener(future -> ft.complete((ChannelFuture) future));
+ var channelFuture = ft.join();
var e = assertThrows(ServiceUnavailableException.class, () -> await(channelFuture));
assertEquals(e.getMessage(), "Unable to establish connection in " + timeoutMillis + "ms");
}
}
- private ChannelConnectorImpl newConnector(AuthTokenManager authTokenManager) throws Exception {
+ private ChannelConnectorImpl newConnector(AuthTokenManager authTokenManager)
+ throws NoSuchAlgorithmException, KeyManagementException {
return newConnector(authTokenManager, Integer.MAX_VALUE);
}
private ChannelConnectorImpl newConnector(AuthTokenManager authTokenManager, int connectTimeoutMillis)
- throws Exception {
+ throws NoSuchAlgorithmException, KeyManagementException {
return newConnector(authTokenManager, trustAllCertificates(), connectTimeoutMillis);
}
@@ -227,7 +261,7 @@ private ChannelConnectorImpl newConnector(
BoltAgentUtil.VALUE);
}
- private static SecurityPlan trustAllCertificates() throws GeneralSecurityException {
- return SecurityPlanImpl.forAllCertificates(false, RevocationCheckingStrategy.NO_CHECKS);
+ private static SecurityPlan trustAllCertificates() throws NoSuchAlgorithmException, KeyManagementException {
+ return SecurityPlanImpl.forAllCertificates(false, RevocationCheckingStrategy.NO_CHECKS, null, Logging.none());
}
}
diff --git a/driver/src/test/java/org/neo4j/driver/integration/ConnectionHandlingIT.java b/driver/src/test/java/org/neo4j/driver/integration/ConnectionHandlingIT.java
index 7998ab3730..daee0f71cf 100644
--- a/driver/src/test/java/org/neo4j/driver/integration/ConnectionHandlingIT.java
+++ b/driver/src/test/java/org/neo4j/driver/integration/ConnectionHandlingIT.java
@@ -90,6 +90,7 @@ void createDriver() {
driver = driverFactory.newInstance(
neo4j.uri(),
authTokenProvider,
+ null,
Config.builder().withFetchSize(1).build(),
SecurityPlanImpl.insecure(),
null,
diff --git a/driver/src/test/java/org/neo4j/driver/integration/ConnectionPoolIT.java b/driver/src/test/java/org/neo4j/driver/integration/ConnectionPoolIT.java
index f759d19680..421a1121d1 100644
--- a/driver/src/test/java/org/neo4j/driver/integration/ConnectionPoolIT.java
+++ b/driver/src/test/java/org/neo4j/driver/integration/ConnectionPoolIT.java
@@ -92,7 +92,7 @@ void shouldDisposeChannelsBasedOnMaxLifetime() throws Exception {
.withMaxConnectionLifetime(maxConnLifetimeHours, TimeUnit.HOURS)
.build();
driver = driverFactory.newInstance(
- neo4j.uri(), neo4j.authTokenManager(), config, SecurityPlanImpl.insecure(), null, null);
+ neo4j.uri(), neo4j.authTokenManager(), null, config, SecurityPlanImpl.insecure(), null, null);
// force driver create channel and return it to the pool
startAndCloseTransactions(driver, 1);
diff --git a/driver/src/test/java/org/neo4j/driver/integration/ErrorIT.java b/driver/src/test/java/org/neo4j/driver/integration/ErrorIT.java
index 99c44fb16d..74824eb639 100644
--- a/driver/src/test/java/org/neo4j/driver/integration/ErrorIT.java
+++ b/driver/src/test/java/org/neo4j/driver/integration/ErrorIT.java
@@ -255,8 +255,8 @@ private Throwable testChannelErrorHandling(Consumer messag
var config = Config.builder().withLogging(DEV_NULL_LOGGING).build();
Throwable queryError = null;
- try (var driver =
- driverFactory.newInstance(uri, authTokenProvider, config, SecurityPlanImpl.insecure(), null, null)) {
+ try (var driver = driverFactory.newInstance(
+ uri, authTokenProvider, null, config, SecurityPlanImpl.insecure(), null, null)) {
driver.verifyConnectivity();
try (var session = driver.session()) {
messageFormatSetup.accept(driverFactory.getFailingMessageFormat());
diff --git a/driver/src/test/java/org/neo4j/driver/integration/ServerKilledIT.java b/driver/src/test/java/org/neo4j/driver/integration/ServerKilledIT.java
index a733a29973..bed0fc5542 100644
--- a/driver/src/test/java/org/neo4j/driver/integration/ServerKilledIT.java
+++ b/driver/src/test/java/org/neo4j/driver/integration/ServerKilledIT.java
@@ -123,6 +123,6 @@ private static void acquireAndReleaseConnections(int count, Driver driver) {
private Driver createDriver(Clock clock, Config config) {
DriverFactory factory = new DriverFactoryWithClock(clock);
return factory.newInstance(
- neo4j.uri(), neo4j.authTokenManager(), config, SecurityPlanImpl.insecure(), null, null);
+ neo4j.uri(), neo4j.authTokenManager(), null, config, SecurityPlanImpl.insecure(), null, null);
}
}
diff --git a/driver/src/test/java/org/neo4j/driver/integration/SessionBoltV3IT.java b/driver/src/test/java/org/neo4j/driver/integration/SessionBoltV3IT.java
index 1da068cf9f..8e4bc8990e 100644
--- a/driver/src/test/java/org/neo4j/driver/integration/SessionBoltV3IT.java
+++ b/driver/src/test/java/org/neo4j/driver/integration/SessionBoltV3IT.java
@@ -265,7 +265,13 @@ void shouldSendGoodbyeWhenClosingDriver() {
var driverFactory = new MessageRecordingDriverFactory();
try (var otherDriver = driverFactory.newInstance(
- driver.uri(), driver.authTokenManager(), defaultConfig(), SecurityPlanImpl.insecure(), null, null)) {
+ driver.uri(),
+ driver.authTokenManager(),
+ null,
+ defaultConfig(),
+ SecurityPlanImpl.insecure(),
+ null,
+ null)) {
List sessions = new ArrayList<>();
List txs = new ArrayList<>();
for (var i = 0; i < txCount; i++) {
diff --git a/driver/src/test/java/org/neo4j/driver/integration/SessionIT.java b/driver/src/test/java/org/neo4j/driver/integration/SessionIT.java
index c62b452166..a181bb9e0f 100644
--- a/driver/src/test/java/org/neo4j/driver/integration/SessionIT.java
+++ b/driver/src/test/java/org/neo4j/driver/integration/SessionIT.java
@@ -128,7 +128,7 @@ void shouldKnowSessionIsClosed() {
@Test
void shouldHandleNullConfig() {
// Given
- driver = GraphDatabase.driver(neo4j.uri(), neo4j.authTokenManager(), null);
+ driver = GraphDatabase.driver(neo4j.uri(), neo4j.authTokenManager(), (Config) null);
var session = driver.session();
// When
@@ -1317,7 +1317,13 @@ private Driver newDriverWithoutRetries() {
private Driver newDriverWithFixedRetries(int maxRetriesCount) {
DriverFactory driverFactory = new DriverFactoryWithFixedRetryLogic(maxRetriesCount);
return driverFactory.newInstance(
- neo4j.uri(), neo4j.authTokenManager(), noLoggingConfig(), SecurityPlanImpl.insecure(), null, null);
+ neo4j.uri(),
+ neo4j.authTokenManager(),
+ null,
+ noLoggingConfig(),
+ SecurityPlanImpl.insecure(),
+ null,
+ null);
}
private Driver newDriverWithLimitedRetries(int maxTxRetryTime) {
diff --git a/driver/src/test/java/org/neo4j/driver/integration/SharedEventLoopIT.java b/driver/src/test/java/org/neo4j/driver/integration/SharedEventLoopIT.java
index 894435ae82..56776464e8 100644
--- a/driver/src/test/java/org/neo4j/driver/integration/SharedEventLoopIT.java
+++ b/driver/src/test/java/org/neo4j/driver/integration/SharedEventLoopIT.java
@@ -80,6 +80,7 @@ private Driver createDriver(EventLoopGroup eventLoopGroup) {
return driverFactory.newInstance(
neo4j.uri(),
neo4j.authTokenManager(),
+ null,
Config.defaultConfig(),
SecurityPlanImpl.insecure(),
eventLoopGroup,
diff --git a/driver/src/test/java/org/neo4j/driver/integration/TransactionIT.java b/driver/src/test/java/org/neo4j/driver/integration/TransactionIT.java
index 0ac24d02c6..095a473b48 100644
--- a/driver/src/test/java/org/neo4j/driver/integration/TransactionIT.java
+++ b/driver/src/test/java/org/neo4j/driver/integration/TransactionIT.java
@@ -352,7 +352,7 @@ void shouldThrowWhenConnectionKilledDuringTransaction() {
var config = Config.builder().withLogging(DEV_NULL_LOGGING).build();
try (var driver = factory.newInstance(
- session.uri(), session.authTokenManager(), config, SecurityPlanImpl.insecure(), null, null)) {
+ session.uri(), session.authTokenManager(), null, config, SecurityPlanImpl.insecure(), null, null)) {
var e = assertThrows(ServiceUnavailableException.class, () -> {
try (var session1 = driver.session();
var tx = session1.beginTransaction()) {
diff --git a/driver/src/test/java/org/neo4j/driver/integration/UnmanagedTransactionIT.java b/driver/src/test/java/org/neo4j/driver/integration/UnmanagedTransactionIT.java
index 233d47c525..0b3a06f0d2 100644
--- a/driver/src/test/java/org/neo4j/driver/integration/UnmanagedTransactionIT.java
+++ b/driver/src/test/java/org/neo4j/driver/integration/UnmanagedTransactionIT.java
@@ -207,7 +207,7 @@ private void testCommitAndRollbackFailurePropagation(boolean commit) {
var config = Config.builder().withLogging(DEV_NULL_LOGGING).build();
try (var driver = driverFactory.newInstance(
- neo4j.uri(), neo4j.authTokenManager(), config, SecurityPlanImpl.insecure(), null, null)) {
+ neo4j.uri(), neo4j.authTokenManager(), null, config, SecurityPlanImpl.insecure(), null, null)) {
var session = ((InternalDriver) driver).newSession(SessionConfig.defaultConfig(), null);
{
var tx = beginTransaction(session);
diff --git a/driver/src/test/java/org/neo4j/driver/internal/CustomSecurityPlanTest.java b/driver/src/test/java/org/neo4j/driver/internal/CustomSecurityPlanTest.java
index 6598ee96b4..f0f49f13e1 100644
--- a/driver/src/test/java/org/neo4j/driver/internal/CustomSecurityPlanTest.java
+++ b/driver/src/test/java/org/neo4j/driver/internal/CustomSecurityPlanTest.java
@@ -45,6 +45,7 @@ void testCustomSecurityPlanUsed() {
driverFactory.newInstance(
URI.create("neo4j://somewhere:1234"),
new StaticAuthTokenManager(AuthTokens.none()),
+ null,
Config.defaultConfig(),
securityPlan,
null,
diff --git a/driver/src/test/java/org/neo4j/driver/internal/DriverFactoryTest.java b/driver/src/test/java/org/neo4j/driver/internal/DriverFactoryTest.java
index fc64256a87..33c5f6c13a 100644
--- a/driver/src/test/java/org/neo4j/driver/internal/DriverFactoryTest.java
+++ b/driver/src/test/java/org/neo4j/driver/internal/DriverFactoryTest.java
@@ -211,6 +211,7 @@ void shouldUseBuiltInRediscoveryByDefault() {
var driver = driverFactory.newInstance(
URI.create("neo4j://localhost:7687"),
new StaticAuthTokenManager(AuthTokens.none()),
+ null,
Config.defaultConfig(),
null,
null,
@@ -237,6 +238,7 @@ void shouldUseSuppliedRediscovery() {
var driver = driverFactory.newInstance(
URI.create("neo4j://localhost:7687"),
new StaticAuthTokenManager(AuthTokens.none()),
+ null,
Config.defaultConfig(),
null,
null,
@@ -256,7 +258,7 @@ private Driver createDriver(String uri, DriverFactory driverFactory) {
private Driver createDriver(String uri, DriverFactory driverFactory, Config config) {
var auth = AuthTokens.none();
- return driverFactory.newInstance(URI.create(uri), new StaticAuthTokenManager(auth), config);
+ return driverFactory.newInstance(URI.create(uri), new StaticAuthTokenManager(auth), null, config);
}
private static ConnectionPool connectionPoolMock() {
diff --git a/driver/src/test/java/org/neo4j/driver/internal/InternalRotatingClientCertificateManagerTests.java b/driver/src/test/java/org/neo4j/driver/internal/InternalRotatingClientCertificateManagerTests.java
new file mode 100644
index 0000000000..0b531333d2
--- /dev/null
+++ b/driver/src/test/java/org/neo4j/driver/internal/InternalRotatingClientCertificateManagerTests.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+
+import java.io.File;
+import org.junit.jupiter.api.Test;
+import org.neo4j.driver.ClientCertificates;
+
+class InternalRotatingClientCertificateManagerTests {
+ InternalRotatingClientCertificateManager manager;
+
+ @Test
+ void shouldThrowOnNullInitialCertificate() {
+ assertThrows(NullPointerException.class, () -> new InternalRotatingClientCertificateManager(null));
+ }
+
+ @Test
+ void shouldThrowOnNullUpdatedCertificate() {
+ var file = mock(File.class);
+ var certificate = ClientCertificates.of(file, file);
+ var manager = new InternalRotatingClientCertificateManager(certificate);
+
+ assertThrows(NullPointerException.class, () -> manager.rotate(null));
+ }
+
+ @Test
+ void shouldSupplyNullOnSubsequentCalls() {
+ var file = mock(File.class);
+ var certificate = ClientCertificates.of(file, file);
+ var manager = new InternalRotatingClientCertificateManager(certificate);
+ var actualCertificate =
+ manager.getClientCertificate().toCompletableFuture().join();
+
+ for (var i = 0; i < 5; i++) {
+ assertNull(manager.getClientCertificate().toCompletableFuture().join());
+ }
+
+ assertEquals(certificate, actualCertificate);
+ }
+
+ @Test
+ void shouldUpdateCertificate() {
+ var file = mock(File.class);
+ var certificate = ClientCertificates.of(file, file);
+ var manager = new InternalRotatingClientCertificateManager(certificate);
+ var actualCertificate =
+ manager.getClientCertificate().toCompletableFuture().join();
+ manager.getClientCertificate().toCompletableFuture().join();
+
+ manager.rotate(certificate);
+
+ var updatedCertificate =
+ manager.getClientCertificate().toCompletableFuture().join();
+ assertEquals(certificate, actualCertificate);
+ assertEquals(certificate, updatedCertificate);
+ }
+}
diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/connection/NettyChannelInitializerTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/connection/NettyChannelInitializerTest.java
index 76120cc3eb..e12c328185 100644
--- a/driver/src/test/java/org/neo4j/driver/internal/async/connection/NettyChannelInitializerTest.java
+++ b/driver/src/test/java/org/neo4j/driver/internal/async/connection/NettyChannelInitializerTest.java
@@ -34,12 +34,14 @@
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.ssl.SslHandler;
-import java.security.GeneralSecurityException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import javax.net.ssl.SNIHostName;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.neo4j.driver.AuthTokens;
+import org.neo4j.driver.Logging;
import org.neo4j.driver.RevocationCheckingStrategy;
import org.neo4j.driver.internal.BoltServerAddress;
import org.neo4j.driver.internal.security.SecurityPlan;
@@ -56,7 +58,7 @@ void tearDown() {
}
@Test
- void shouldAddSslHandlerWhenRequiresEncryption() throws Exception {
+ void shouldAddSslHandlerWhenRequiresEncryption() throws NoSuchAlgorithmException, KeyManagementException {
var security = trustAllCertificates();
var initializer = newInitializer(security);
@@ -76,7 +78,7 @@ void shouldNotAddSslHandlerWhenDoesNotRequireEncryption() {
}
@Test
- void shouldAddSslHandlerWithHandshakeTimeout() throws Exception {
+ void shouldAddSslHandlerWithHandshakeTimeout() throws NoSuchAlgorithmException, KeyManagementException {
var timeoutMillis = 424242;
var security = trustAllCertificates();
var initializer = newInitializer(security, timeoutMillis);
@@ -104,13 +106,15 @@ void shouldUpdateChannelAttributes() {
}
@Test
- void shouldIncludeSniHostName() throws Exception {
+ void shouldIncludeSniHostName() throws NoSuchAlgorithmException, KeyManagementException {
var address = new BoltServerAddress("database.neo4j.com", 8989);
+ var securityPlan = trustAllCertificates();
var initializer = new NettyChannelInitializer(
address,
- trustAllCertificates(),
+ securityPlan,
10000,
new StaticAuthTokenManager(AuthTokens.none()),
+ securityPlan.sslContext().toCompletableFuture().join(),
Clock.systemUTC(),
DEV_NULL_LOGGING);
@@ -126,18 +130,20 @@ void shouldIncludeSniHostName() throws Exception {
}
@Test
- void shouldEnableHostnameVerificationWhenConfigured() throws Exception {
+ void shouldEnableHostnameVerificationWhenConfigured() throws NoSuchAlgorithmException, KeyManagementException {
testHostnameVerificationSetting(true, "HTTPS");
}
@Test
- void shouldNotEnableHostnameVerificationWhenNotConfigured() throws Exception {
+ void shouldNotEnableHostnameVerificationWhenNotConfigured()
+ throws NoSuchAlgorithmException, KeyManagementException {
testHostnameVerificationSetting(false, null);
}
- private void testHostnameVerificationSetting(boolean enabled, String expectedValue) throws Exception {
- var initializer =
- newInitializer(SecurityPlanImpl.forAllCertificates(enabled, RevocationCheckingStrategy.NO_CHECKS));
+ private void testHostnameVerificationSetting(boolean enabled, String expectedValue)
+ throws NoSuchAlgorithmException, KeyManagementException {
+ var initializer = newInitializer(SecurityPlanImpl.forAllCertificates(
+ enabled, RevocationCheckingStrategy.NO_CHECKS, null, Logging.none()));
initializer.initChannel(channel);
@@ -162,11 +168,12 @@ private static NettyChannelInitializer newInitializer(
securityPlan,
connectTimeoutMillis,
new StaticAuthTokenManager(AuthTokens.none()),
+ securityPlan.sslContext().toCompletableFuture().join(),
clock,
DEV_NULL_LOGGING);
}
- private static SecurityPlan trustAllCertificates() throws GeneralSecurityException {
- return SecurityPlanImpl.forAllCertificates(false, RevocationCheckingStrategy.NO_CHECKS);
+ private static SecurityPlan trustAllCertificates() throws NoSuchAlgorithmException, KeyManagementException {
+ return SecurityPlanImpl.forAllCertificates(false, RevocationCheckingStrategy.NO_CHECKS, null, Logging.none());
}
}
diff --git a/driver/src/test/java/org/neo4j/driver/internal/security/SSLContextManagerTests.java b/driver/src/test/java/org/neo4j/driver/internal/security/SSLContextManagerTests.java
new file mode 100644
index 0000000000..7b4fc69b11
--- /dev/null
+++ b/driver/src/test/java/org/neo4j/driver/internal/security/SSLContextManagerTests.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+
+import java.io.File;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import org.junit.jupiter.api.Test;
+import org.neo4j.driver.ClientCertificateManager;
+import org.neo4j.driver.ClientCertificates;
+import org.neo4j.driver.Logger;
+import org.neo4j.driver.Logging;
+import org.neo4j.driver.exceptions.ClientException;
+import org.neo4j.driver.internal.InternalClientCertificate;
+
+class SSLContextManagerTests {
+ private SSLContextManager manager;
+
+ @Test
+ void shouldErrorOnInitialNullCertificate() throws NoSuchAlgorithmException, KeyManagementException {
+ // GIVEN
+ var clientManager = mock(ClientCertificateManager.class);
+ given(clientManager.getClientCertificate()).willReturn(CompletableFuture.completedStage(null));
+ var contextSupplier = mock(SecurityPlan.SSLContextSupplier.class);
+ var logging = mock(Logging.class);
+ var logger = mock(Logger.class);
+ given(logging.getLog(any(Class.class))).willReturn(logger);
+ manager = new SSLContextManager(clientManager, contextSupplier, logging);
+ Consumer> verify = contextFuture -> {
+ assertTrue(contextFuture.isCompletedExceptionally());
+ var completionException = assertThrows(CompletionException.class, contextFuture::join);
+ assertInstanceOf(ClientException.class, completionException.getCause());
+ };
+
+ for (var i = 0; i < 5; i++) {
+ // WHEN
+ var contextFuture = manager.getSSLContext().toCompletableFuture();
+ // THEN
+ verify.accept(contextFuture);
+ }
+ then(contextSupplier).shouldHaveNoInteractions();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ void shouldAllowNullCertificate() throws NoSuchAlgorithmException, KeyManagementException {
+ // GIVEN
+ var file = mock(File.class);
+ var certificate = ClientCertificates.of(file, file);
+ var clientManager = mock(ClientCertificateManager.class);
+ given(clientManager.getClientCertificate())
+ .willReturn(CompletableFuture.completedStage(certificate), CompletableFuture.completedStage(null));
+ var context = mock(SSLContext.class);
+ var contextSupplier = mock(SecurityPlan.SSLContextSupplier.class);
+ var keyManagers = new KeyManager[0];
+ given(contextSupplier.get(keyManagers)).willReturn(context);
+ var logging = mock(Logging.class);
+ var logger = mock(Logger.class);
+ given(logging.getLog(any(Class.class))).willReturn(logger);
+ manager = new ExtendedSSLContextManager(clientManager, contextSupplier, logging, ignored -> new KeyManager[0]);
+ var context1 = manager.getSSLContext().toCompletableFuture().join();
+
+ // WHEN
+ var context2 = manager.getSSLContext().toCompletableFuture().join();
+
+ // THEN
+ assertEquals(context1, context2);
+ then(clientManager).should(times(2)).getClientCertificate();
+ then(contextSupplier).should().get(keyManagers);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ void shouldErrorOnCreatingKeyManagers() throws NoSuchAlgorithmException, KeyManagementException {
+ // GIVEN
+ var file = mock(File.class);
+ var certificate = (InternalClientCertificate) ClientCertificates.of(file, file);
+ var clientManager = mock(ClientCertificateManager.class);
+ given(clientManager.getClientCertificate())
+ .willReturn(CompletableFuture.completedStage(certificate), CompletableFuture.completedStage(null));
+ var contextSupplier = mock(SecurityPlan.SSLContextSupplier.class);
+ var logging = mock(Logging.class);
+ var logger = mock(Logger.class);
+ given(logging.getLog(any(Class.class))).willReturn(logger);
+ Function keyManagersFunction = mock(Function.class);
+ var exception = new RuntimeException();
+ given(keyManagersFunction.apply(certificate)).willThrow(exception);
+ manager = new ExtendedSSLContextManager(clientManager, contextSupplier, logging, keyManagersFunction);
+ Consumer> verify = contextFuture -> {
+ assertTrue(contextFuture.isCompletedExceptionally());
+ var completionException = assertThrows(CompletionException.class, contextFuture::join);
+ assertInstanceOf(ClientException.class, completionException.getCause());
+ assertEquals(exception, completionException.getCause().getCause());
+ };
+
+ for (var i = 0; i < 2; i++) {
+ // WHEN
+ var contextFuture = manager.getSSLContext().toCompletableFuture();
+ // THEN
+ verify.accept(contextFuture);
+ }
+ then(clientManager).should(times(2)).getClientCertificate();
+ then(keyManagersFunction).should().apply(certificate);
+ then(contextSupplier).shouldHaveNoInteractions();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ void shouldReplaceErrorWithValidContext() throws NoSuchAlgorithmException, KeyManagementException {
+ // GIVEN
+ var file = mock(File.class);
+ var certificate = (InternalClientCertificate) ClientCertificates.of(file, file);
+ var clientManager = mock(ClientCertificateManager.class);
+ given(clientManager.getClientCertificate()).willReturn(CompletableFuture.completedStage(certificate));
+ var context = mock(SSLContext.class);
+ var contextSupplier = mock(SecurityPlan.SSLContextSupplier.class);
+ given(contextSupplier.get(any())).willReturn(context);
+ var logging = mock(Logging.class);
+ var logger = mock(Logger.class);
+ given(logging.getLog(any(Class.class))).willReturn(logger);
+ Function keyManagersFunction = mock(Function.class);
+ var exception = new RuntimeException();
+ var callNum = new AtomicInteger();
+ var keyManagers = new KeyManager[0];
+ given(keyManagersFunction.apply(certificate)).willAnswer(invocation -> {
+ if (callNum.get() > 0) {
+ return keyManagers;
+ } else {
+ callNum.getAndIncrement();
+ throw exception;
+ }
+ });
+ manager = new ExtendedSSLContextManager(clientManager, contextSupplier, logging, keyManagersFunction);
+ var failedContextFuture = manager.getSSLContext().toCompletableFuture();
+ assertTrue(failedContextFuture.isCompletedExceptionally());
+
+ // WHEN
+ var actualContext = manager.getSSLContext().toCompletableFuture().join();
+
+ // THEN
+ assertEquals(context, actualContext);
+ then(clientManager).should(times(2)).getClientCertificate();
+ then(keyManagersFunction).should(times(2)).apply(certificate);
+ then(contextSupplier).should().get(keyManagers);
+ }
+
+ @Test
+ void shouldAcceptNullCertificateManager() throws NoSuchAlgorithmException, KeyManagementException {
+ // GIVEN
+ var context = mock(SSLContext.class);
+ var contextSupplier = mock(SecurityPlan.SSLContextSupplier.class);
+ var keyManagers = new KeyManager[0];
+ given(contextSupplier.get(keyManagers)).willReturn(context);
+ var logging = mock(Logging.class);
+ var logger = mock(Logger.class);
+ given(logging.getLog(any(Class.class))).willReturn(logger);
+ manager = new SSLContextManager(null, contextSupplier, logging);
+
+ var actualContext = manager.getSSLContext().toCompletableFuture().join();
+
+ assertEquals(context, actualContext);
+ then(contextSupplier).should().get(keyManagers);
+ }
+
+ private static class ExtendedSSLContextManager extends SSLContextManager {
+ private final Function keyManagersFunction;
+
+ public ExtendedSSLContextManager(
+ ClientCertificateManager clientCertificateManager,
+ SecurityPlan.SSLContextSupplier sslContextSupplier,
+ Logging logging,
+ Function keyManagersFunction)
+ throws NoSuchAlgorithmException, KeyManagementException {
+ super(clientCertificateManager, sslContextSupplier, logging);
+ this.keyManagersFunction = keyManagersFunction;
+ }
+
+ @Override
+ protected KeyManager[] createKeyManagers(InternalClientCertificate clientCertificate) {
+ return keyManagersFunction.apply(clientCertificate);
+ }
+ }
+}
diff --git a/driver/src/test/java/org/neo4j/driver/internal/security/SecurityPlans.java b/driver/src/test/java/org/neo4j/driver/internal/security/SecurityPlans.java
index 849ece030e..39d4f4c4f9 100644
--- a/driver/src/test/java/org/neo4j/driver/internal/security/SecurityPlans.java
+++ b/driver/src/test/java/org/neo4j/driver/internal/security/SecurityPlans.java
@@ -28,6 +28,7 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.neo4j.driver.Config;
+import org.neo4j.driver.Logging;
import org.neo4j.driver.exceptions.ClientException;
import org.neo4j.driver.internal.SecuritySettings;
@@ -53,7 +54,7 @@ private static Stream allSecureSchemes() {
void testEncryptionSchemeEnablesEncryption(String scheme) {
var securitySettings = new SecuritySettings.SecuritySettingsBuilder().build();
- var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme);
+ var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme, null, Logging.none());
assertTrue(securityPlan.requiresEncryption());
}
@@ -63,7 +64,7 @@ void testEncryptionSchemeEnablesEncryption(String scheme) {
void testSystemCertCompatibleConfiguration(String scheme) {
var securitySettings = new SecuritySettings.SecuritySettingsBuilder().build();
- var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme);
+ var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme, null, Logging.none());
assertTrue(securityPlan.requiresEncryption());
assertTrue(securityPlan.requiresHostnameVerification());
@@ -75,7 +76,7 @@ void testSystemCertCompatibleConfiguration(String scheme) {
void testSelfSignedCertConfigDisablesHostnameVerification(String scheme) {
var securitySettings = new SecuritySettings.SecuritySettingsBuilder().build();
- var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme);
+ var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme, null, Logging.none());
assertTrue(securityPlan.requiresEncryption());
assertFalse(securityPlan.requiresHostnameVerification());
@@ -87,7 +88,9 @@ void testThrowsOnUserCustomizedEncryption(String scheme) {
var securitySettings =
new SecuritySettings.SecuritySettingsBuilder().withEncryption().build();
- var ex = assertThrows(ClientException.class, () -> SecurityPlans.createSecurityPlan(securitySettings, scheme));
+ var ex = assertThrows(
+ ClientException.class,
+ () -> SecurityPlans.createSecurityPlan(securitySettings, scheme, null, Logging.none()));
assertTrue(ex.getMessage()
.contains(String.format(
@@ -101,7 +104,9 @@ void testThrowsOnUserCustomizedTrustConfiguration(String scheme) {
.withTrustStrategy(Config.TrustStrategy.trustAllCertificates())
.build();
- var ex = assertThrows(ClientException.class, () -> SecurityPlans.createSecurityPlan(securitySettings, scheme));
+ var ex = assertThrows(
+ ClientException.class,
+ () -> SecurityPlans.createSecurityPlan(securitySettings, scheme, null, Logging.none()));
assertTrue(ex.getMessage()
.contains(String.format(
@@ -116,7 +121,9 @@ void testThrowsOnUserCustomizedTrustConfigurationAndEncryption(String scheme) {
.withEncryption()
.build();
- var ex = assertThrows(ClientException.class, () -> SecurityPlans.createSecurityPlan(securitySettings, scheme));
+ var ex = assertThrows(
+ ClientException.class,
+ () -> SecurityPlans.createSecurityPlan(securitySettings, scheme, null, Logging.none()));
assertTrue(ex.getMessage()
.contains(String.format(
@@ -128,7 +135,7 @@ void testThrowsOnUserCustomizedTrustConfigurationAndEncryption(String scheme) {
void testNoEncryption(String scheme) {
var securitySettings = new SecuritySettings.SecuritySettingsBuilder().build();
- var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme);
+ var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme, null, Logging.none());
assertFalse(securityPlan.requiresEncryption());
}
@@ -139,7 +146,7 @@ void testConfiguredEncryption(String scheme) {
var securitySettings =
new SecuritySettings.SecuritySettingsBuilder().withEncryption().build();
- var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme);
+ var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme, null, Logging.none());
assertTrue(securityPlan.requiresEncryption());
}
@@ -152,7 +159,7 @@ void testConfiguredAllCertificates(String scheme) {
.withTrustStrategy(Config.TrustStrategy.trustAllCertificates())
.build();
- var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme);
+ var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme, null, Logging.none());
assertTrue(securityPlan.requiresEncryption());
}
@@ -166,7 +173,7 @@ void testConfigureStrictRevocationChecking(String scheme) {
.withEncryption()
.build();
- var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme);
+ var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme, null, Logging.none());
assertEquals(STRICT, securityPlan.revocationCheckingStrategy());
}
@@ -180,7 +187,7 @@ void testConfigureVerifyIfPresentRevocationChecking(String scheme) {
.withEncryption()
.build();
- var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme);
+ var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme, null, Logging.none());
assertEquals(VERIFY_IF_PRESENT, securityPlan.revocationCheckingStrategy());
}
@@ -193,7 +200,7 @@ void testRevocationCheckingDisabledByDefault(String scheme) {
.withEncryption()
.build();
- var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme);
+ var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, scheme, null, Logging.none());
assertEquals(NO_CHECKS, securityPlan.revocationCheckingStrategy());
}
diff --git a/driver/src/test/java/org/neo4j/driver/internal/util/io/ChannelTrackingConnector.java b/driver/src/test/java/org/neo4j/driver/internal/util/io/ChannelTrackingConnector.java
index 00440cbda1..e1bbe1f0ea 100644
--- a/driver/src/test/java/org/neo4j/driver/internal/util/io/ChannelTrackingConnector.java
+++ b/driver/src/test/java/org/neo4j/driver/internal/util/io/ChannelTrackingConnector.java
@@ -20,6 +20,7 @@
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import java.util.List;
+import java.util.function.Function;
import org.neo4j.driver.internal.BoltServerAddress;
import org.neo4j.driver.internal.async.connection.ChannelConnector;
@@ -33,9 +34,13 @@ public ChannelTrackingConnector(ChannelConnector realConnector, List ch
}
@Override
- public ChannelFuture connect(BoltServerAddress address, Bootstrap bootstrap) {
- var channelFuture = realConnector.connect(address, bootstrap);
- channels.add(channelFuture.channel());
- return channelFuture;
+ public ChannelFuture connect(
+ BoltServerAddress address,
+ Bootstrap bootstrap,
+ Function channelFutureExtensionMapper) {
+ return realConnector.connect(address, bootstrap, channelFuture -> {
+ channels.add(channelFuture.channel());
+ return channelFutureExtensionMapper.apply(channelFuture);
+ });
}
}
diff --git a/driver/src/test/java/org/neo4j/driver/org/neo4j/driver/ClientCertificatesTests.java b/driver/src/test/java/org/neo4j/driver/org/neo4j/driver/ClientCertificatesTests.java
new file mode 100644
index 0000000000..4a57b56152
--- /dev/null
+++ b/driver/src/test/java/org/neo4j/driver/org/neo4j/driver/ClientCertificatesTests.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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.org.neo4j.driver;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+
+import java.io.File;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.neo4j.driver.ClientCertificates;
+
+class ClientCertificatesTests {
+ @ParameterizedTest
+ @MethodSource("shouldThrowOnNullArgs")
+ void shouldThrowOnNull(File certificate, File key) {
+ assertThrows(NullPointerException.class, () -> ClientCertificates.of(certificate, key));
+ }
+
+ static Stream shouldThrowOnNullArgs() {
+ return Stream.of(
+ Arguments.of(null, mock(File.class)), Arguments.of(mock(File.class), null), Arguments.of(null, null));
+ }
+
+ @ParameterizedTest
+ @MethodSource("shouldThrowOnNullWithPassword")
+ void shouldThrowOnNullWithPassword(File certificate, File key, String password) {
+ assertThrows(NullPointerException.class, () -> ClientCertificates.of(certificate, key, password));
+ }
+
+ static Stream shouldThrowOnNullWithPassword() {
+ return Stream.of(
+ Arguments.of(null, mock(File.class), "password"),
+ Arguments.of(mock(File.class), null, "password"),
+ Arguments.of(null, null, "password"));
+ }
+
+ @Test
+ void shouldAcceptNullPassword() {
+ var clientCertificate = ClientCertificates.of(mock(File.class), mock(File.class), null);
+
+ assertNotNull(clientCertificate);
+ }
+}
diff --git a/examples/pom.xml b/examples/pom.xml
index 060db236e8..add6b4cea6 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -6,7 +6,7 @@
org.neo4j.driver
neo4j-java-driver-parent
- 5.18-SNAPSHOT
+ 5.19-SNAPSHOT
org.neo4j.doc.driver
diff --git a/pom.xml b/pom.xml
index b3b473fb9f..072fd84512 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.neo4j.driver
neo4j-java-driver-parent
- 5.18-SNAPSHOT
+ 5.19-SNAPSHOT
pom
Neo4j Java Driver Project
diff --git a/testkit-backend/pom.xml b/testkit-backend/pom.xml
index 9da5ffb90d..ab73cd2c09 100644
--- a/testkit-backend/pom.xml
+++ b/testkit-backend/pom.xml
@@ -7,7 +7,7 @@
neo4j-java-driver-parent
org.neo4j.driver
- 5.18-SNAPSHOT
+ 5.19-SNAPSHOT
testkit-backend
diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/TestkitState.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/TestkitState.java
index 3822823c56..d5ab468f63 100644
--- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/TestkitState.java
+++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/TestkitState.java
@@ -43,6 +43,7 @@
import neo4j.org.testkit.backend.messages.responses.TestkitResponse;
import org.neo4j.driver.AuthTokenManager;
import org.neo4j.driver.BookmarkManager;
+import org.neo4j.driver.ClientCertificateManager;
import org.neo4j.driver.Logging;
import org.neo4j.driver.internal.cluster.RoutingTableRegistry;
import reactor.core.publisher.Mono;
@@ -54,6 +55,8 @@ public class TestkitState {
private static final String RESULT_NOT_FOUND_MESSAGE = "Could not find result";
private static final String BOOKMARK_MANAGER_NOT_FOUND_MESSAGE = "Could not find bookmark manager";
private static final String AUTH_PROVIDER_NOT_FOUND_MESSAGE = "Could not find authentication provider";
+ private static final String CLIENT_CERTIFICATE_PROVIDER_NOT_FOUND_MESSAGE =
+ "Could not find client certificate provider";
private final Map driverIdToDriverHolder = new HashMap<>();
@@ -79,6 +82,7 @@ public class TestkitState {
private final Map bookmarkManagerIdToBookmarkManager = new HashMap<>();
private final Logging logging;
private final Map authProviderIdToAuthProvider = new HashMap<>();
+ private final Map managerIdToClientCertificateManager = new HashMap<>();
@Getter
private final Map errors = new HashMap<>();
@@ -260,6 +264,20 @@ public void removeAuthProvider(String id) {
}
}
+ public void addClientCertificateManager(String id, ClientCertificateManager manager) {
+ managerIdToClientCertificateManager.put(id, manager);
+ }
+
+ public ClientCertificateManager getClientCertificateManager(String id) {
+ return get(id, managerIdToClientCertificateManager, CLIENT_CERTIFICATE_PROVIDER_NOT_FOUND_MESSAGE);
+ }
+
+ public void removeClientCertificateManager(String id) {
+ if (managerIdToClientCertificateManager.remove(id) == null) {
+ throw new RuntimeException(CLIENT_CERTIFICATE_PROVIDER_NOT_FOUND_MESSAGE);
+ }
+ }
+
private String add(T value, Map idToT) {
var id = newId();
idToT.put(id, value);
diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificate.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificate.java
new file mode 100644
index 0000000000..c44be03e11
--- /dev/null
+++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificate.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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 neo4j.org.testkit.backend.messages.requests;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "name")
+public class ClientCertificate {
+ private Data data;
+
+ @Getter
+ @Setter
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class Data {
+ String certfile;
+ String keyfile;
+ String password;
+ }
+}
diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderClose.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderClose.java
new file mode 100644
index 0000000000..4526a3a705
--- /dev/null
+++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderClose.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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 neo4j.org.testkit.backend.messages.requests;
+
+import lombok.Getter;
+import lombok.Setter;
+import neo4j.org.testkit.backend.TestkitState;
+import neo4j.org.testkit.backend.messages.responses.ClientCertificateProvider;
+import neo4j.org.testkit.backend.messages.responses.TestkitResponse;
+
+@Setter
+@Getter
+public class ClientCertificateProviderClose extends AbstractBasicTestkitRequest {
+ private AuthTokenManagerCloseBody data;
+
+ @Override
+ protected TestkitResponse processAndCreateResponse(TestkitState testkitState) {
+ testkitState.removeClientCertificateManager(data.getId());
+ return ClientCertificateProvider.builder()
+ .data(ClientCertificateProvider.ClientCertificateProviderBody.builder()
+ .id(data.getId())
+ .build())
+ .build();
+ }
+
+ @Setter
+ @Getter
+ public static class AuthTokenManagerCloseBody {
+ private String id;
+ }
+}
diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderCompleted.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderCompleted.java
new file mode 100644
index 0000000000..638901d684
--- /dev/null
+++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderCompleted.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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 neo4j.org.testkit.backend.messages.requests;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+public class ClientCertificateProviderCompleted implements TestkitCallbackResult {
+ private ClientCertificateProviderCompletedBody data;
+
+ @Override
+ public String getCallbackId() {
+ return data.getRequestId();
+ }
+
+ @Setter
+ @Getter
+ public static class ClientCertificateProviderCompletedBody {
+ private String requestId;
+ private ClientCertificate clientCertificate;
+ private boolean hasUpdate;
+ }
+}
diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java
index a95f6b8c23..3fea70b749 100644
--- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java
+++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java
@@ -68,7 +68,8 @@ public class GetFeatures implements TestkitRequest {
"Feature:API:Session:AuthConfig",
"Feature:Auth:Managed",
"Feature:API:Driver.SupportsSessionAuth",
- "Feature:API:RetryableExceptions"));
+ "Feature:API:RetryableExceptions",
+ "Feature:API:SSLClientCertificate"));
private static final Set SYNC_FEATURES = new HashSet<>(Arrays.asList(
"Feature:Bolt:3.0",
diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewClientCertificateProvider.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewClientCertificateProvider.java
new file mode 100644
index 0000000000..f65e3b9d94
--- /dev/null
+++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewClientCertificateProvider.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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 neo4j.org.testkit.backend.messages.requests;
+
+import java.nio.file.Paths;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import lombok.Getter;
+import lombok.Setter;
+import neo4j.org.testkit.backend.TestkitState;
+import neo4j.org.testkit.backend.messages.responses.ClientCertificateProvider;
+import neo4j.org.testkit.backend.messages.responses.ClientCertificateProviderRequest;
+import neo4j.org.testkit.backend.messages.responses.TestkitCallback;
+import neo4j.org.testkit.backend.messages.responses.TestkitResponse;
+import org.neo4j.driver.ClientCertificate;
+import org.neo4j.driver.ClientCertificateManager;
+import org.neo4j.driver.ClientCertificates;
+
+@Setter
+@Getter
+public class NewClientCertificateProvider extends AbstractBasicTestkitRequest {
+ private NewClientCertificateProviderBody data;
+
+ @Override
+ protected TestkitResponse processAndCreateResponse(TestkitState testkitState) {
+ var id = testkitState.newId();
+ testkitState.addClientCertificateManager(id, new TestkitClientCertificateManager(id, testkitState));
+ return ClientCertificateProvider.builder()
+ .data(ClientCertificateProvider.ClientCertificateProviderBody.builder()
+ .id(id)
+ .build())
+ .build();
+ }
+
+ private record TestkitClientCertificateManager(String id, TestkitState testkitState)
+ implements ClientCertificateManager {
+ @Override
+ public CompletionStage getClientCertificate() {
+ var callbackId = testkitState.newId();
+
+ var callback = ClientCertificateProviderRequest.builder()
+ .data(ClientCertificateProviderRequest.ClientCertificateProviderRequestBody.builder()
+ .clientCertificateProviderId(id)
+ .build())
+ .build();
+
+ ClientCertificate clientCertificate = null;
+ var callbackStage = dispatchTestkitCallback(testkitState, callback);
+ try {
+ var response = callbackStage.toCompletableFuture().get();
+ if (response instanceof ClientCertificateProviderCompleted clientCertificateComplete) {
+ var data = clientCertificateComplete.getData();
+ var certificateData = data.getClientCertificate().getData();
+ var hasUpdate = data.isHasUpdate();
+ if (hasUpdate) {
+ clientCertificate = ClientCertificates.of(
+ Paths.get(certificateData.getCertfile()).toFile(),
+ Paths.get(certificateData.getKeyfile()).toFile(),
+ certificateData.getPassword());
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected failure during Testkit callback", e);
+ }
+ return CompletableFuture.completedStage(clientCertificate);
+ }
+
+ private CompletionStage dispatchTestkitCallback(
+ TestkitState testkitState, TestkitCallback response) {
+ var future = new CompletableFuture();
+ testkitState.getCallbackIdToFuture().put(response.getCallbackId(), future);
+ testkitState.getResponseWriter().accept(response);
+ return future;
+ }
+ }
+
+ @Setter
+ @Getter
+ public static class NewClientCertificateProviderBody {}
+}
diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewDriver.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewDriver.java
index 781f44a5c0..bbeb178a3b 100644
--- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewDriver.java
+++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewDriver.java
@@ -45,6 +45,9 @@
import neo4j.org.testkit.backend.messages.responses.TestkitCallback;
import neo4j.org.testkit.backend.messages.responses.TestkitResponse;
import org.neo4j.driver.AuthTokenManager;
+import org.neo4j.driver.ClientCertificateManager;
+import org.neo4j.driver.ClientCertificateManagers;
+import org.neo4j.driver.ClientCertificates;
import org.neo4j.driver.Config;
import org.neo4j.driver.NotificationConfig;
import org.neo4j.driver.internal.BoltServerAddress;
@@ -101,6 +104,16 @@ public TestkitResponse process(TestkitState testkitState) {
configBuilder.withNotificationConfig(
toNotificationConfig(data.notificationsMinSeverity, data.notificationsDisabledCategories));
configBuilder.withDriverMetrics();
+ var clientCertificateManager = Optional.ofNullable(data.getClientCertificateProviderId())
+ .map(testkitState::getClientCertificateManager)
+ .or(() -> Optional.ofNullable(data.getClientCertificate())
+ .map(ClientCertificate::getData)
+ .map(certificateData -> ClientCertificates.of(
+ Paths.get(certificateData.getCertfile()).toFile(),
+ Paths.get(certificateData.getKeyfile()).toFile(),
+ certificateData.getPassword()))
+ .map(ClientCertificateManagers::rotating))
+ .orElse(null);
configBuilder.withLogging(testkitState.getLogging());
org.neo4j.driver.Driver driver;
var config = configBuilder.build();
@@ -108,6 +121,7 @@ public TestkitResponse process(TestkitState testkitState) {
driver = driver(
URI.create(data.uri),
authTokenManager,
+ clientCertificateManager,
config,
domainNameResolver,
configureSecuritySettingsBuilder(),
@@ -203,15 +217,17 @@ private CompletionStage dispatchTestkitCallback(
private org.neo4j.driver.Driver driver(
URI uri,
AuthTokenManager authTokenManager,
+ ClientCertificateManager clientCertificateManager,
Config config,
DomainNameResolver domainNameResolver,
SecuritySettings.SecuritySettingsBuilder securitySettingsBuilder,
TestkitState testkitState,
String driverId) {
var securitySettings = securitySettingsBuilder.build();
- var securityPlan = SecurityPlans.createSecurityPlan(securitySettings, uri.getScheme());
+ var securityPlan = SecurityPlans.createSecurityPlan(
+ securitySettings, uri.getScheme(), clientCertificateManager, config.logging());
return new DriverFactoryWithDomainNameResolver(domainNameResolver, testkitState, driverId)
- .newInstance(uri, authTokenManager, config, securityPlan, null, null);
+ .newInstance(uri, authTokenManager, clientCertificateManager, config, securityPlan, null, null);
}
private Optional handleExceptionAsErrorResponse(TestkitState testkitState, RuntimeException e) {
@@ -293,6 +309,8 @@ public static class NewDriverBody {
private boolean encrypted;
private List trustedCertificates;
private Boolean telemetryDisabled;
+ private ClientCertificate clientCertificate;
+ private String clientCertificateProviderId;
}
@RequiredArgsConstructor
diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/TestkitRequest.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/TestkitRequest.java
index 9a7ea00bc3..237840feb7 100644
--- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/TestkitRequest.java
+++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/TestkitRequest.java
@@ -73,7 +73,10 @@
@JsonSubTypes.Type(FakeTimeTick.class),
@JsonSubTypes.Type(FakeTimeUninstall.class),
@JsonSubTypes.Type(CheckSessionAuthSupport.class),
- @JsonSubTypes.Type(VerifyAuthentication.class)
+ @JsonSubTypes.Type(VerifyAuthentication.class),
+ @JsonSubTypes.Type(NewClientCertificateProvider.class),
+ @JsonSubTypes.Type(ClientCertificateProviderCompleted.class),
+ @JsonSubTypes.Type(ClientCertificateProviderClose.class)
})
public interface TestkitRequest {
TestkitResponse process(TestkitState testkitState);
diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProvider.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProvider.java
new file mode 100644
index 0000000000..fca91060bb
--- /dev/null
+++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProvider.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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 neo4j.org.testkit.backend.messages.responses;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class ClientCertificateProvider implements TestkitResponse {
+ private ClientCertificateProviderBody data;
+
+ @Override
+ public String testkitName() {
+ return "ClientCertificateProvider";
+ }
+
+ @Getter
+ @Builder
+ public static class ClientCertificateProviderBody {
+ private String id;
+ }
+}
diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProviderRequest.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProviderRequest.java
new file mode 100644
index 0000000000..d00d79d6db
--- /dev/null
+++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProviderRequest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [https://neo4j.com]
+ *
+ * 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 neo4j.org.testkit.backend.messages.responses;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class ClientCertificateProviderRequest implements TestkitCallback {
+ private ClientCertificateProviderRequestBody data;
+
+ @Override
+ public String getCallbackId() {
+ return data.getId();
+ }
+
+ @Override
+ public String testkitName() {
+ return "ClientCertificateProviderRequest";
+ }
+
+ @Getter
+ @Builder
+ public static class ClientCertificateProviderRequestBody {
+ private String id;
+ private String clientCertificateProviderId;
+ }
+}
diff --git a/testkit-tests/pom.xml b/testkit-tests/pom.xml
index e00827f881..55a8d3280c 100644
--- a/testkit-tests/pom.xml
+++ b/testkit-tests/pom.xml
@@ -6,7 +6,7 @@
org.neo4j.driver
neo4j-java-driver-parent
- 5.18-SNAPSHOT
+ 5.19-SNAPSHOT
..