Skip to content

Commit 552ea6c

Browse files
authored
Introduce Client Certificate configuration (#1183)
⚠️ This API is released as preview. ⚠️ This feature is NodeJS only. Browser should configure the client certificate in the system certificates. The ClientCertificate is a mechanism to support mutual TLS as a second factor for authentication. The driver's certificate will not be used to authenticate a user on the DBMS, but only to authenticate the driver as a trusted client to the DBMS. Another authentication mechanism is still required to authenticate the user. The configuration is done by using the driver configuration, the property name is `clientCertificate` and this a object with the following properties: * `certfile`: The path to client certificate file. This is can configured as list of paths. * `keyfile`: The path to key file. This can also be configured as a list of paths, an object contain `path` and `password` or a list of objects contain `path` and `password`. * `password` (optional): The client key password. See [node documentation](https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions) for understanding how password, certs and keys work together. Configuration example: ```javascript import neo4j from 'neo4j-driver' const driver = neo4j.driver('neo4j+s://myhost:7687', MY_CREDENTIALS, { clientCertificate: { certfile: '/path/to/cert/file.cert', keyfile: '/path/to/cert/file.pem', password: 'the_key_password' // optional } }) // then use your driver as usual. ``` ### Client Certificate Provider and Certificate Rotation In case of the certificate needs to be changed, the driver offers an api for change client certificates without creating a new driver instance. The change is done by inform an `ClientCertificateProvider` instead of a `ClientCertificate` in the driver configuration. `ClientCertificateProvider` is a public interface which can be implemented by user. However, the driver offers an implementation of this interface for working with certificate rotation scenarios. ```javascript import neo4j from 'neo4j-driver' const initialClientCertificate: { certfile: '/path/to/cert/file.cert', keyfile: '/path/to/cert/file.pem', password: 'the_key_password' // optional } const clientCertificateProvider = neo4j.clientCertificateProviders.rotating({ initialCertificate: initialClientCertificate }) const driver = neo4j.driver('neo4j+s://myhost:7687', MY_CREDENTIALS, { clientCertificate: clientCertificateProvider }) // use the driver as usual // then you have new certificate which will replace the old one clientCertificateProvider.updateCertificate({ certfile: '/path/to/cert/new_file.cert', keyfile: '/path/to/cert/new_file.pem', password: 'the_new_key_password' // optional }) // New connections will be created using the new certificate. // however, older connections will not be closed if they still working. ``` ⚠️ This feature is NodeJS only. Browser should configure the client certificate in the system certificates. ⚠️ This API is released as preview.
1 parent 6f72b3d commit 552ea6c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1944
-72
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [https://neo4j.com]
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
export default {
18+
async load (clientCertificate) {
19+
return clientCertificate
20+
}
21+
}

packages/bolt-connection/src/channel/browser/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import WebSocketChannel from './browser-channel'
1919
import BrowserHosNameResolver from './browser-host-name-resolver'
20-
20+
import BrowserClientCertificatesLoader from './browser-client-certificates-loader'
2121
/*
2222
2323
This module exports a set of components to be used in browser environment.
@@ -30,3 +30,4 @@ NOTE: exports in this module should have exactly the same names/structure as exp
3030
*/
3131
export const Channel = WebSocketChannel
3232
export const HostNameResolver = BrowserHosNameResolver
33+
export const ClientCertificatesLoader = BrowserClientCertificatesLoader

packages/bolt-connection/src/channel/channel-config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,17 @@ export default class ChannelConfig {
4646
* @param {ServerAddress} address the address for the channel to connect to.
4747
* @param {Object} driverConfig the driver config provided by the user when driver is created.
4848
* @param {string} connectionErrorCode the default error code to use on connection errors.
49+
* @param {object} clientCertificate the client certificate
4950
*/
50-
constructor (address, driverConfig, connectionErrorCode) {
51+
constructor (address, driverConfig, connectionErrorCode, clientCertificate) {
5152
this.address = address
5253
this.encrypted = extractEncrypted(driverConfig)
5354
this.trust = extractTrust(driverConfig)
5455
this.trustedCertificates = extractTrustedCertificates(driverConfig)
5556
this.knownHostsPath = extractKnownHostsPath(driverConfig)
5657
this.connectionErrorCode = connectionErrorCode || SERVICE_UNAVAILABLE
5758
this.connectionTimeout = driverConfig.connectionTimeout
59+
this.clientCertificate = clientCertificate
5860
}
5961
}
6062

packages/bolt-connection/src/channel/deno/deno-channel.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ const TrustStrategy = {
239239
);
240240
}
241241

242+
assertNotClientCertificates(config)
243+
242244
const caCerts = await Promise.all(
243245
config.trustedCertificates.map(f => Deno.readTextFile(f))
244246
)
@@ -250,6 +252,8 @@ const TrustStrategy = {
250252
})
251253
},
252254
TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: function (config) {
255+
assertNotClientCertificates(config)
256+
253257
return Deno.connectTls({
254258
hostname: config.address.resolvedHost(),
255259
port: config.address.port()
@@ -265,6 +269,13 @@ const TrustStrategy = {
265269
}
266270
}
267271

272+
async function assertNotClientCertificates (config) {
273+
if (config.clientCertificate != null) {
274+
throw newError('clientCertificates are not supported in DenoJS since the API does not ' +
275+
'support its configuration. See, https://deno.land/[email protected]?s=Deno.ConnectTlsOptions.')
276+
}
277+
}
278+
268279
async function _connect (config) {
269280
if (!isEncrypted(config)) {
270281
return Deno.connect({
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [https://neo4j.com]
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
export default {
18+
async load (clientCertificate) {
19+
return clientCertificate
20+
}
21+
}

packages/bolt-connection/src/channel/deno/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import DenoChannel from './deno-channel'
1919
import DenoHostNameResolver from './deno-host-name-resolver'
20-
20+
import DenoClientCertificatesLoader from './deno-client-certificates-loader'
2121
/*
2222
2323
This module exports a set of components to be used in deno environment.
@@ -30,3 +30,4 @@ import DenoHostNameResolver from './deno-host-name-resolver'
3030
*/
3131
export const Channel = DenoChannel
3232
export const HostNameResolver = DenoHostNameResolver
33+
export const ClientCertificatesLoader = DenoClientCertificatesLoader

packages/bolt-connection/src/channel/node/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import NodeChannel from './node-channel'
1919
import NodeHostNameResolver from './node-host-name-resolver'
20-
20+
import NodeClientCertificatesLoader from './node-client-certificates-loader'
2121
/*
2222
2323
This module exports a set of components to be used in NodeJS environment.
@@ -31,3 +31,4 @@ NOTE: exports in this module should have exactly the same names/structure as exp
3131

3232
export const Channel = NodeChannel
3333
export const HostNameResolver = NodeHostNameResolver
34+
export const ClientCertificatesLoader = NodeClientCertificatesLoader

packages/bolt-connection/src/channel/node/node-channel.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ const TrustStrategy = {
4949

5050
const tlsOpts = newTlsOptions(
5151
config.address.host(),
52-
config.trustedCertificates.map(f => fs.readFileSync(f))
52+
config.trustedCertificates.map(f => fs.readFileSync(f)),
53+
config.clientCertificate
5354
)
5455
const socket = tls.connect(
5556
config.address.port(),
@@ -79,7 +80,11 @@ const TrustStrategy = {
7980
return configureSocket(socket)
8081
},
8182
TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: function (config, onSuccess, onFailure) {
82-
const tlsOpts = newTlsOptions(config.address.host())
83+
const tlsOpts = newTlsOptions(
84+
config.address.host(),
85+
undefined,
86+
config.clientCertificate
87+
)
8388
const socket = tls.connect(
8489
config.address.port(),
8590
config.address.resolvedHost(),
@@ -109,7 +114,11 @@ const TrustStrategy = {
109114
return configureSocket(socket)
110115
},
111116
TRUST_ALL_CERTIFICATES: function (config, onSuccess, onFailure) {
112-
const tlsOpts = newTlsOptions(config.address.host())
117+
const tlsOpts = newTlsOptions(
118+
config.address.host(),
119+
undefined,
120+
config.clientCertificate
121+
)
113122
const socket = tls.connect(
114123
config.address.port(),
115124
config.address.resolvedHost(),
@@ -198,13 +207,17 @@ function trustStrategyName (config) {
198207
* Create a new configuration options object for the {@code tls.connect()} call.
199208
* @param {string} hostname the target hostname.
200209
* @param {string|undefined} ca an optional CA.
210+
* @param {string|undefined} cert an optional client cert.
211+
* @param {string|undefined} key an optional client cert key.
212+
* @param {string|undefined} passphrase an optional client cert passphrase
201213
* @return {Object} a new options object.
202214
*/
203-
function newTlsOptions (hostname, ca = undefined) {
215+
function newTlsOptions (hostname, ca = undefined, clientCertificate = undefined) {
204216
return {
205217
rejectUnauthorized: false, // we manually check for this in the connect callback, to give a more helpful error to the user
206218
servername: hostname, // server name for the SNI (Server Name Indication) TLS extension
207-
ca // optional CA useful for TRUST_CUSTOM_CA_SIGNED_CERTIFICATES trust mode
219+
ca, // optional CA useful for TRUST_CUSTOM_CA_SIGNED_CERTIFICATES trust mode,
220+
...clientCertificate
208221
}
209222
}
210223

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [https://neo4j.com]
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import fs from 'fs'
19+
20+
function readFile (file) {
21+
return new Promise((resolve, reject) => fs.readFile(file, (err, data) => {
22+
if (err) {
23+
return reject(err)
24+
}
25+
return resolve(data)
26+
}))
27+
}
28+
29+
function loadCert (fileOrFiles) {
30+
if (Array.isArray(fileOrFiles)) {
31+
return Promise.all(fileOrFiles.map(loadCert))
32+
}
33+
return readFile(fileOrFiles)
34+
}
35+
36+
function loadKey (fileOrFiles) {
37+
if (Array.isArray(fileOrFiles)) {
38+
return Promise.all(fileOrFiles.map(loadKey))
39+
}
40+
41+
if (typeof fileOrFiles === 'string') {
42+
return readFile(fileOrFiles)
43+
}
44+
45+
return readFile(fileOrFiles.path)
46+
.then(pem => ({
47+
pem,
48+
passphrase: fileOrFiles.password
49+
}))
50+
}
51+
52+
export default {
53+
async load (clientCertificate) {
54+
const certPromise = loadCert(clientCertificate.certfile)
55+
const keyPromise = loadKey(clientCertificate.keyfile)
56+
57+
const [cert, key] = await Promise.all([certPromise, keyPromise])
58+
59+
return {
60+
cert,
61+
key,
62+
passphrase: clientCertificate.password
63+
}
64+
}
65+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [https://neo4j.com]
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
import { ClientCertificatesLoader } from '../channel'
18+
19+
export default class ClientCertificateHolder {
20+
constructor ({ clientCertificateProvider, loader }) {
21+
this._clientCertificateProvider = clientCertificateProvider
22+
this._loader = loader || ClientCertificatesLoader
23+
this._clientCertificate = null
24+
}
25+
26+
async getClientCertificate () {
27+
if (this._clientCertificateProvider != null &&
28+
(this._clientCertificate == null || await this._clientCertificateProvider.hasUpdate())) {
29+
this._clientCertificate = Promise.resolve(this._clientCertificateProvider.getClientCertificate())
30+
.then(this._loader.load)
31+
.then(clientCertificate => {
32+
this._clientCertificate = clientCertificate
33+
return this._clientCertificate
34+
})
35+
.catch(error => {
36+
this._clientCertificate = null
37+
throw error
38+
})
39+
}
40+
41+
return this._clientCertificate
42+
}
43+
}

packages/bolt-connection/src/connection-provider/connection-provider-direct.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import PooledConnectionProvider from './connection-provider-pooled'
1919
import {
20-
createChannelConnection,
2120
DelegateConnection,
2221
ConnectionErrorHandler
2322
} from '../connection'
@@ -75,12 +74,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider {
7574
}
7675

7776
async _hasProtocolVersion (versionPredicate) {
78-
const connection = await createChannelConnection(
79-
this._address,
80-
this._config,
81-
this._createConnectionErrorHandler(),
82-
this._log
83-
)
77+
const connection = await this._createChannelConnection(this._address)
8478

8579
const protocolVersion = connection.protocol()
8680
? connection.protocol().version

packages/bolt-connection/src/connection-provider/connection-provider-pooled.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { error, ConnectionProvider, ServerInfo, newError } from 'neo4j-driver-co
2121
import AuthenticationProvider from './authentication-provider'
2222
import { object } from '../lang'
2323
import LivenessCheckProvider from './liveness-check-provider'
24+
import ClientCertificateHolder from './client-certificate-holder'
2425

2526
const { SERVICE_UNAVAILABLE } = error
2627
const AUTHENTICATION_ERRORS = [
@@ -40,18 +41,20 @@ export default class PooledConnectionProvider extends ConnectionProvider {
4041
this._id = id
4142
this._config = config
4243
this._log = log
44+
this._clientCertificateHolder = new ClientCertificateHolder({ clientCertificateProvider: this._config.clientCertificate })
4345
this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent, boltAgent })
4446
this._livenessCheckProvider = new LivenessCheckProvider({ connectionLivenessCheckTimeout: config.connectionLivenessCheckTimeout })
4547
this._userAgent = userAgent
4648
this._boltAgent = boltAgent
4749
this._createChannelConnection =
4850
createChannelConnectionHook ||
49-
(address => {
51+
(async address => {
5052
return createChannelConnection(
5153
address,
5254
this._config,
5355
this._createConnectionErrorHandler(),
54-
this._log
56+
this._log,
57+
await this._clientCertificateHolder.getClientCertificate()
5558
)
5659
})
5760
this._connectionPool = newPool({
@@ -75,6 +78,10 @@ export default class PooledConnectionProvider extends ConnectionProvider {
7578
return new ConnectionErrorHandler(SERVICE_UNAVAILABLE)
7679
}
7780

81+
async _getClientCertificate () {
82+
return this._config.clientCertificate.getClientCertificate()
83+
}
84+
7885
/**
7986
* Create a new connection and initialize it.
8087
* @return {Promise<Connection>} promise resolved with a new connection or rejected when failed to connect.

packages/bolt-connection/src/connection-provider/connection-provider-routing.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,13 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
7171
routingTablePurgeDelay,
7272
newPool
7373
}) {
74-
super({ id, config, log, userAgent, boltAgent, authTokenManager, newPool }, address => {
74+
super({ id, config, log, userAgent, boltAgent, authTokenManager, newPool }, async address => {
7575
return createChannelConnection(
7676
address,
7777
this._config,
7878
this._createConnectionErrorHandler(),
7979
this._log,
80+
await this._clientCertificateHolder.getClientCertificate(),
8081
this._routingContext
8182
)
8283
})
@@ -212,12 +213,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
212213
let lastError
213214
for (let i = 0; i < addresses.length; i++) {
214215
try {
215-
const connection = await createChannelConnection(
216-
addresses[i],
217-
this._config,
218-
this._createConnectionErrorHandler(),
219-
this._log
220-
)
216+
const connection = await this._createChannelConnection(addresses[i])
221217
const protocolVersion = connection.protocol()
222218
? connection.protocol().version
223219
: null

0 commit comments

Comments
 (0)