From 95c2524d986b9718fe54d52f1c205905e852d1ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 21 Feb 2024 12:33:28 +0100 Subject: [PATCH 01/23] Introduce Client Certificate configuration --- packages/core/src/client-certificate.ts | 142 ++++++++++++++++++ packages/core/src/types.ts | 12 ++ .../lib/core/client-certificate.ts | 142 ++++++++++++++++++ packages/neo4j-driver-deno/lib/core/types.ts | 12 ++ 4 files changed, 308 insertions(+) create mode 100644 packages/core/src/client-certificate.ts create mode 100644 packages/neo4j-driver-deno/lib/core/client-certificate.ts diff --git a/packages/core/src/client-certificate.ts b/packages/core/src/client-certificate.ts new file mode 100644 index 000000000..85aeecd60 --- /dev/null +++ b/packages/core/src/client-certificate.ts @@ -0,0 +1,142 @@ +/** + * 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. + */ + +/** + * Holds the Client TLS certificate information. + * + * Browser instances of the driver should configure the certificate + * in the system. + * + * @interface + */ +export default class ClientCertificate { + public readonly certfile: string + public readonly keyfile: string + public readonly password?: string + + private constructor () { + /** + * The path to client certificate file. + * + * @type {string} + */ + this.certfile = '' + + /** + * The path to the key file. + * + * @type {string} + */ + this.keyfile = '' + + /** + * The certificate's password. + * + * @type {string|undefined} + */ + this.password = undefined + } +} + +/** + * Provides a client certificate to the driver for mutual TLS. + * + * The driver will call {@link ClientCertificateProvider#hasUpdate()} to check if the client wants to update the certificate. + * If so, it will call {@link ClientCertificateProvider#getCertificate()} to get the new certificate. + * + * The certificate is only used as a second factor for authentication authenticating the client. + * The DMBS user still needs to authenticate with an authentication token. + * + * All implementations of this interface must be thread-safe and non-blocking for caller threads. + * For instance, IO operations must not be done on the calling thread. + * + * Note that the work done in the methods of this interface count towards the connectionAcquisition. + * Should fetching the certificate be particularly slow, it might be necessary to increase the timeout. + * + * @interface + */ +export class ClientCertificateProvider { + /** + * Indicates whether the client wants the driver to update the certificate. + * + * @returns {Promise|boolean} true if the client wants the driver to update the certificate + */ + + hasUpdate (): boolean | Promise { + throw new Error('Not Implemented') + } + + /** + * Returns the certificate to use for new connections. + * + * Will be called by the driver after {@link ClientCertificateProvider#hasUpdate()} returned true + * or when the driver establishes the first connection. + * + * @returns {Promise|ClientCertificate} the certificate to use for new connections + */ + getClientCertificate (): ClientCertificate | Promise { + throw new Error('Not Implemented') + } +} + +/** + * Interface for {@link ClientCertificateProvider} which provides update certificate function. + * @interface + */ +export class RotatingClientCertificateProvider extends ClientCertificateProvider { + /** + * Updates the certificate stored in the provider. + * + * To be called by user-code when a new client certificate is available. + * + * @param {ClientCertificate} certificate - the new certificate + */ + updateCertificate (certificate: ClientCertificate): void { + throw new Error('Not implemented') + } +} + +/** + * Defines the object which holds the common {@link ClientCertificateProviders} used in the Driver + */ +class ClientCertificateProviders { + /** + * + * @param {object} param0 - The params + * @param {ClientCertificate} param0.initialCertificate - The certificated used by the driver until {@link RotatingClientCertificateProvider#updateCertificate} get called. + * + * @returns {RotatingClientCertificateProvider} The rotating client certificate provider + */ + rotating ({ initialCertificate }: { initialCertificate: ClientCertificate }): RotatingClientCertificateProvider { + throw new Error('Not implemented') + } +} + +/** + * Holds the common {@link ClientCertificateProviders} used in the Driver. + */ +const clientCertificateProviders: ClientCertificateProviders = new ClientCertificateProviders() + +Object.freeze(clientCertificateProviders) + +export { + clientCertificateProviders +} + +export type { + ClientCertificateProviders +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 05e07d134..d9a117ac4 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import ClientCertificate, { ClientCertificateProvider } from './client-certificate' import NotificationFilter from './notification-filter' /** @@ -80,6 +81,7 @@ export class Config { resolver?: (address: string) => string[] | Promise userAgent?: string telemetryDisabled?: boolean + clientCertificate?: ClientCertificate | ClientCertificateProvider /** * @constructor @@ -340,6 +342,16 @@ export class Config { * @type {boolean} */ this.telemetryDisabled = false + + /** + * Client Certificate used for mutual TLS. + * + * A {@link ClientCertificateProvider} can be configure for scenarios + * where the {@link ClientCertificate} might change over time. + * + * @type {ClientCertificate|ClientCertificateProvider|undefined} + */ + this.clientCertificate = undefined } } diff --git a/packages/neo4j-driver-deno/lib/core/client-certificate.ts b/packages/neo4j-driver-deno/lib/core/client-certificate.ts new file mode 100644 index 000000000..85aeecd60 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/client-certificate.ts @@ -0,0 +1,142 @@ +/** + * 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. + */ + +/** + * Holds the Client TLS certificate information. + * + * Browser instances of the driver should configure the certificate + * in the system. + * + * @interface + */ +export default class ClientCertificate { + public readonly certfile: string + public readonly keyfile: string + public readonly password?: string + + private constructor () { + /** + * The path to client certificate file. + * + * @type {string} + */ + this.certfile = '' + + /** + * The path to the key file. + * + * @type {string} + */ + this.keyfile = '' + + /** + * The certificate's password. + * + * @type {string|undefined} + */ + this.password = undefined + } +} + +/** + * Provides a client certificate to the driver for mutual TLS. + * + * The driver will call {@link ClientCertificateProvider#hasUpdate()} to check if the client wants to update the certificate. + * If so, it will call {@link ClientCertificateProvider#getCertificate()} to get the new certificate. + * + * The certificate is only used as a second factor for authentication authenticating the client. + * The DMBS user still needs to authenticate with an authentication token. + * + * All implementations of this interface must be thread-safe and non-blocking for caller threads. + * For instance, IO operations must not be done on the calling thread. + * + * Note that the work done in the methods of this interface count towards the connectionAcquisition. + * Should fetching the certificate be particularly slow, it might be necessary to increase the timeout. + * + * @interface + */ +export class ClientCertificateProvider { + /** + * Indicates whether the client wants the driver to update the certificate. + * + * @returns {Promise|boolean} true if the client wants the driver to update the certificate + */ + + hasUpdate (): boolean | Promise { + throw new Error('Not Implemented') + } + + /** + * Returns the certificate to use for new connections. + * + * Will be called by the driver after {@link ClientCertificateProvider#hasUpdate()} returned true + * or when the driver establishes the first connection. + * + * @returns {Promise|ClientCertificate} the certificate to use for new connections + */ + getClientCertificate (): ClientCertificate | Promise { + throw new Error('Not Implemented') + } +} + +/** + * Interface for {@link ClientCertificateProvider} which provides update certificate function. + * @interface + */ +export class RotatingClientCertificateProvider extends ClientCertificateProvider { + /** + * Updates the certificate stored in the provider. + * + * To be called by user-code when a new client certificate is available. + * + * @param {ClientCertificate} certificate - the new certificate + */ + updateCertificate (certificate: ClientCertificate): void { + throw new Error('Not implemented') + } +} + +/** + * Defines the object which holds the common {@link ClientCertificateProviders} used in the Driver + */ +class ClientCertificateProviders { + /** + * + * @param {object} param0 - The params + * @param {ClientCertificate} param0.initialCertificate - The certificated used by the driver until {@link RotatingClientCertificateProvider#updateCertificate} get called. + * + * @returns {RotatingClientCertificateProvider} The rotating client certificate provider + */ + rotating ({ initialCertificate }: { initialCertificate: ClientCertificate }): RotatingClientCertificateProvider { + throw new Error('Not implemented') + } +} + +/** + * Holds the common {@link ClientCertificateProviders} used in the Driver. + */ +const clientCertificateProviders: ClientCertificateProviders = new ClientCertificateProviders() + +Object.freeze(clientCertificateProviders) + +export { + clientCertificateProviders +} + +export type { + ClientCertificateProviders +} diff --git a/packages/neo4j-driver-deno/lib/core/types.ts b/packages/neo4j-driver-deno/lib/core/types.ts index f8cc7a735..de6e69307 100644 --- a/packages/neo4j-driver-deno/lib/core/types.ts +++ b/packages/neo4j-driver-deno/lib/core/types.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import ClientCertificate, { ClientCertificateProvider } from './client-certificate.ts' import NotificationFilter from './notification-filter.ts' /** @@ -80,6 +81,7 @@ export class Config { resolver?: (address: string) => string[] | Promise userAgent?: string telemetryDisabled?: boolean + clientCertificate?: ClientCertificate | ClientCertificateProvider /** * @constructor @@ -340,6 +342,16 @@ export class Config { * @type {boolean} */ this.telemetryDisabled = false + + /** + * Client Certificate used for mutual TLS. + * + * A {@link ClientCertificateProvider} can be configure for scenarios + * where the {@link ClientCertificate} might change over time. + * + * @type {ClientCertificate|ClientCertificateProvider|undefined} + */ + this.clientCertificate = undefined } } From e4379d19e496431879ddbeb496406c96e8eef35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 21 Feb 2024 15:39:59 +0100 Subject: [PATCH 02/23] Exporting types --- packages/core/src/index.ts | 13 ++++++++++--- packages/neo4j-driver-deno/lib/core/index.ts | 13 ++++++++++--- packages/neo4j-driver-deno/lib/mod.ts | 19 +++++++++++++++---- packages/neo4j-driver-lite/src/index.ts | 19 +++++++++++++++---- packages/neo4j-driver/src/index.js | 9 ++++++--- packages/neo4j-driver/types/index.d.ts | 15 +++++++++++++-- 6 files changed, 69 insertions(+), 19 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7a2580d11..5e74bafca 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -91,6 +91,7 @@ import { Config } from './types' import * as types from './types' import * as json from './json' import resultTransformers, { ResultTransformer } from './result-transformers' +import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider } from './client-certificate' import * as internal from './internal' // todo: removed afterwards /** @@ -169,7 +170,8 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + clientCertificateProviders } export { @@ -238,7 +240,8 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + clientCertificateProviders } export type { @@ -263,7 +266,11 @@ export type { NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, - NotificationFilterMinimumSeverityLevel + NotificationFilterMinimumSeverityLevel, + ClientCertificate, + ClientCertificateProvider, + ClientCertificateProviders, + RotatingClientCertificateProvider } export default forExport diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index d9f4fb95f..db54d0b06 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -91,6 +91,7 @@ import { Config } from './types.ts' import * as types from './types.ts' import * as json from './json.ts' import resultTransformers, { ResultTransformer } from './result-transformers.ts' +import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider } from './client-certificate.ts' import * as internal from './internal/index.ts' /** @@ -169,7 +170,8 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + clientCertificateProviders } export { @@ -238,7 +240,8 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + clientCertificateProviders } export type { @@ -263,7 +266,11 @@ export type { NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, - NotificationFilterMinimumSeverityLevel + NotificationFilterMinimumSeverityLevel, + ClientCertificate, + ClientCertificateProvider, + ClientCertificateProviders, + RotatingClientCertificateProvider } export default forExport diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index fd65f7308..333e25ea7 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -97,7 +97,12 @@ import { Transaction, TransactionPromise, types as coreTypes, - UnboundRelationship + UnboundRelationship, + ClientCertificate, + ClientCertificateProvider, + ClientCertificateProviders, + RotatingClientCertificateProvider, + clientCertificateProviders } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts import { DirectConnectionProvider, RoutingConnectionProvider } from './bolt-connection/index.js' @@ -424,7 +429,8 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + clientCertificateProviders } export { @@ -491,7 +497,8 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + clientCertificateProviders } export type { QueryResult, @@ -516,6 +523,10 @@ export type { NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, - NotificationFilterMinimumSeverityLevel + NotificationFilterMinimumSeverityLevel, + ClientCertificate, + ClientCertificateProvider, + ClientCertificateProviders, + RotatingClientCertificateProvider } export default forExport diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index 7dfeeb1fa..7508796a8 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -97,7 +97,12 @@ import { Transaction, TransactionPromise, types as coreTypes, - UnboundRelationship + UnboundRelationship, + ClientCertificate, + ClientCertificateProvider, + ClientCertificateProviders, + RotatingClientCertificateProvider, + clientCertificateProviders } from 'neo4j-driver-core' import { DirectConnectionProvider, RoutingConnectionProvider } from 'neo4j-driver-bolt-connection' @@ -423,7 +428,8 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + clientCertificateProviders } export { @@ -490,7 +496,8 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + clientCertificateProviders } export type { QueryResult, @@ -515,6 +522,10 @@ export type { NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, - NotificationFilterMinimumSeverityLevel + NotificationFilterMinimumSeverityLevel, + ClientCertificate, + ClientCertificateProvider, + ClientCertificateProviders, + RotatingClientCertificateProvider } export default forExport diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index b2686f690..6abae01b9 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -73,7 +73,8 @@ import { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - staticAuthTokenManager + staticAuthTokenManager, + clientCertificateProviders } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -394,7 +395,8 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + clientCertificateProviders } export { @@ -462,6 +464,7 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + clientCertificateProviders } export default forExport diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index 25ce8748b..18c9a5cbf 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -87,6 +87,11 @@ import { notificationFilterMinimumSeverityLevel, AuthTokenManager, AuthTokenAndExpiration, + ClientCertificate, + ClientCertificateProvider, + ClientCertificateProviders, + RotatingClientCertificateProvider, + clientCertificateProviders, types as coreTypes } from 'neo4j-driver-core' import { @@ -283,6 +288,7 @@ declare const forExport: { notificationFilterDisabledCategory: typeof notificationFilterDisabledCategory notificationFilterMinimumSeverityLevel: typeof notificationFilterMinimumSeverityLevel logging: typeof logging + clientCertificateProviders: typeof clientCertificateProviders } export { @@ -358,7 +364,8 @@ export { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - logging + logging, + clientCertificateProviders } export type { @@ -376,7 +383,11 @@ export type { NotificationFilterDisabledCategory, NotificationFilterMinimumSeverityLevel, AuthTokenManager, - AuthTokenAndExpiration + AuthTokenAndExpiration, + ClientCertificate, + ClientCertificateProvider, + ClientCertificateProviders, + RotatingClientCertificateProvider } export default forExport From 16275592eaec270571821305218047b16ac2bdb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 21 Feb 2024 17:42:31 +0100 Subject: [PATCH 03/23] Implementing ClientCertificateProviders.rotating() --- packages/core/src/client-certificate.ts | 65 +++++++-- packages/core/test/client-certificate.test.ts | 129 ++++++++++++++++++ 2 files changed, 179 insertions(+), 15 deletions(-) create mode 100644 packages/core/test/client-certificate.test.ts diff --git a/packages/core/src/client-certificate.ts b/packages/core/src/client-certificate.ts index 85aeecd60..13b9bd633 100644 --- a/packages/core/src/client-certificate.ts +++ b/packages/core/src/client-certificate.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import * as json from './json' + /** * Holds the Client TLS certificate information. * @@ -30,24 +32,24 @@ export default class ClientCertificate { private constructor () { /** - * The path to client certificate file. - * - * @type {string} - */ + * The path to client certificate file. + * + * @type {string} + */ this.certfile = '' /** - * The path to the key file. - * - * @type {string} - */ + * The path to the key file. + * + * @type {string} + */ this.keyfile = '' /** - * The certificate's password. - * - * @type {string|undefined} - */ + * The certificate's password. + * + * @type {string|undefined} + */ this.password = undefined } } @@ -74,8 +76,7 @@ export class ClientCertificateProvider { * Indicates whether the client wants the driver to update the certificate. * * @returns {Promise|boolean} true if the client wants the driver to update the certificate - */ - + */ hasUpdate (): boolean | Promise { throw new Error('Not Implemented') } @@ -122,7 +123,12 @@ class ClientCertificateProviders { * @returns {RotatingClientCertificateProvider} The rotating client certificate provider */ rotating ({ initialCertificate }: { initialCertificate: ClientCertificate }): RotatingClientCertificateProvider { - throw new Error('Not implemented') + if (initialCertificate == null || typeof initialCertificate !== 'object') { + throw new TypeError(`initialCertificate should be ClientCertificate, but got ${json.stringify(initialCertificate)}`) + } + + const certificate = { ...initialCertificate } + return new InternalRotatingClientCertificateProvider(certificate) } } @@ -140,3 +146,32 @@ export { export type { ClientCertificateProviders } + +/** + * Internal implementation + * + * @private + */ +class InternalRotatingClientCertificateProvider { + constructor ( + private _certificate: ClientCertificate, + private _updated: boolean = false) { + } + + hasUpdate (): boolean | Promise { + try { + return this._updated + } finally { + this._updated = false + } + } + + getClientCertificate (): ClientCertificate | Promise { + return this._certificate + } + + updateCertificate (certificate: ClientCertificate): void { + this._certificate = { ...certificate } + this._updated = true + } +} diff --git a/packages/core/test/client-certificate.test.ts b/packages/core/test/client-certificate.test.ts new file mode 100644 index 000000000..ec15dcc56 --- /dev/null +++ b/packages/core/test/client-certificate.test.ts @@ -0,0 +1,129 @@ +/** + * 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. + */ + +import { clientCertificateProviders } from '../src/client-certificate' + +describe('clientCertificateProviders', () => { + describe('.rotating()', () => { + describe.each([ + undefined, + null, + {}, + { initialCertificate: null }, + { + someOtherProperty: { + certfile: 'other_file', + keyfile: 'some_file', + password: 'pass' + } + } + ])('when invalid configuration (%o)', (config) => { + it('should thrown TypeError', () => { + // @ts-expect-error + expect(() => clientCertificateProviders.rotating(config)) + .toThrow(TypeError) + }) + }) + + describe.each([ + { + initialCertificate: { + certfile: 'other_file', + keyfile: 'some_file', + password: 'pass' + } + }, + { + initialCertificate: { + certfile: 'other_file', + keyfile: 'some_file' + } + } + ])('when valid configuration (%o)', (config) => { + it('should return a RotatingClientCertificateProvider', () => { + const provider = clientCertificateProviders.rotating(config) + + expect(provider).toBeDefined() + expect(provider.getClientCertificate).toBeInstanceOf(Function) + expect(provider.hasUpdate).toBeInstanceOf(Function) + expect(provider.updateCertificate).toBeInstanceOf(Function) + }) + + it('should getClientCertificate return a copy of initialCertificate until certificate is not update', async () => { + const provider = clientCertificateProviders.rotating(config) + + for (let i = 0; i < 100; i++) { + const certificate = await provider.getClientCertificate() + expect(certificate).toEqual(config.initialCertificate) + expect(certificate).not.toBe(config.initialCertificate) + } + + provider.updateCertificate({ + certfile: 'new_cert_file', + keyfile: 'new_key_file', + password: 'new_pass_word' + }) + + const certificate = await provider.getClientCertificate() + expect(certificate).not.toEqual(config.initialCertificate) + expect(certificate).not.toBe(config.initialCertificate) + }) + + it('should updateCertificate change certificate for a new one', async () => { + const provider = clientCertificateProviders.rotating(config) + + await expect(Promise.resolve(provider.getClientCertificate())).resolves.toEqual(config.initialCertificate) + + for (let i = 0; i < 100; i++) { + const certificate = { + certfile: `new_cert_file${i}`, + keyfile: `new_key_file${i}`, + password: i % 2 === 0 ? `new_pass_word${i}` : undefined + } + + provider.updateCertificate(certificate) + + await expect(Promise.resolve(provider.getClientCertificate())).resolves.toEqual(certificate) + await expect(Promise.resolve(provider.getClientCertificate())).resolves.toEqual(certificate) + await expect(Promise.resolve(provider.getClientCertificate())).resolves.toEqual(certificate) + } + }) + + it('should hasUpdate Return false, unless updateCertificate() was called since the last call of hasUpdate', async () => { + const provider = clientCertificateProviders.rotating(config) + + await expect(Promise.resolve(provider.hasUpdate())).resolves.toBe(false) + + const certificate = { + certfile: 'new_cert_file', + keyfile: 'new_key_file' + } + provider.updateCertificate(certificate) + + await expect(Promise.resolve(provider.hasUpdate())).resolves.toBe(true) + await expect(Promise.resolve(provider.hasUpdate())).resolves.toBe(false) + + await expect(Promise.resolve(provider.getClientCertificate())).resolves.toEqual(certificate) + provider.updateCertificate(certificate) + await expect(Promise.resolve(provider.getClientCertificate())).resolves.toEqual(certificate) + + await expect(Promise.resolve(provider.hasUpdate())).resolves.toBe(true) + await expect(Promise.resolve(provider.hasUpdate())).resolves.toBe(false) + }) + }) + }) +}) From 36c9b2b844d71f7cb6e77bec8ef7a6a5bf8ef753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 21 Feb 2024 17:50:38 +0100 Subject: [PATCH 04/23] Implement ClientCertificateProviders.routing --- packages/core/src/client-certificate.ts | 70 +++++------ .../lib/core/client-certificate.ts | 119 +++++++++++------- 2 files changed, 112 insertions(+), 77 deletions(-) diff --git a/packages/core/src/client-certificate.ts b/packages/core/src/client-certificate.ts index 13b9bd633..6952180f3 100644 --- a/packages/core/src/client-certificate.ts +++ b/packages/core/src/client-certificate.ts @@ -32,24 +32,24 @@ export default class ClientCertificate { private constructor () { /** - * The path to client certificate file. - * - * @type {string} - */ + * The path to client certificate file. + * + * @type {string} + */ this.certfile = '' /** - * The path to the key file. - * - * @type {string} - */ + * The path to the key file. + * + * @type {string} + */ this.keyfile = '' /** - * The certificate's password. - * - * @type {string|undefined} - */ + * The certificate's password. + * + * @type {string|undefined} + */ this.password = undefined } } @@ -73,22 +73,22 @@ export default class ClientCertificate { */ export class ClientCertificateProvider { /** - * Indicates whether the client wants the driver to update the certificate. - * - * @returns {Promise|boolean} true if the client wants the driver to update the certificate - */ + * Indicates whether the client wants the driver to update the certificate. + * + * @returns {Promise|boolean} true if the client wants the driver to update the certificate + */ hasUpdate (): boolean | Promise { throw new Error('Not Implemented') } /** - * Returns the certificate to use for new connections. - * - * Will be called by the driver after {@link ClientCertificateProvider#hasUpdate()} returned true - * or when the driver establishes the first connection. - * - * @returns {Promise|ClientCertificate} the certificate to use for new connections - */ + * Returns the certificate to use for new connections. + * + * Will be called by the driver after {@link ClientCertificateProvider#hasUpdate()} returned true + * or when the driver establishes the first connection. + * + * @returns {Promise|ClientCertificate} the certificate to use for new connections + */ getClientCertificate (): ClientCertificate | Promise { throw new Error('Not Implemented') } @@ -100,12 +100,12 @@ export class ClientCertificateProvider { */ export class RotatingClientCertificateProvider extends ClientCertificateProvider { /** - * Updates the certificate stored in the provider. - * - * To be called by user-code when a new client certificate is available. - * - * @param {ClientCertificate} certificate - the new certificate - */ + * Updates the certificate stored in the provider. + * + * To be called by user-code when a new client certificate is available. + * + * @param {ClientCertificate} certificate - the new certificate + */ updateCertificate (certificate: ClientCertificate): void { throw new Error('Not implemented') } @@ -116,12 +116,12 @@ export class RotatingClientCertificateProvider extends ClientCertificateProvider */ class ClientCertificateProviders { /** - * - * @param {object} param0 - The params - * @param {ClientCertificate} param0.initialCertificate - The certificated used by the driver until {@link RotatingClientCertificateProvider#updateCertificate} get called. - * - * @returns {RotatingClientCertificateProvider} The rotating client certificate provider - */ + * + * @param {object} param0 - The params + * @param {ClientCertificate} param0.initialCertificate - The certificated used by the driver until {@link RotatingClientCertificateProvider#updateCertificate} get called. + * + * @returns {RotatingClientCertificateProvider} The rotating client certificate provider + */ rotating ({ initialCertificate }: { initialCertificate: ClientCertificate }): RotatingClientCertificateProvider { if (initialCertificate == null || typeof initialCertificate !== 'object') { throw new TypeError(`initialCertificate should be ClientCertificate, but got ${json.stringify(initialCertificate)}`) diff --git a/packages/neo4j-driver-deno/lib/core/client-certificate.ts b/packages/neo4j-driver-deno/lib/core/client-certificate.ts index 85aeecd60..950241463 100644 --- a/packages/neo4j-driver-deno/lib/core/client-certificate.ts +++ b/packages/neo4j-driver-deno/lib/core/client-certificate.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import * as json from './json.ts' + /** * Holds the Client TLS certificate information. * @@ -28,26 +30,26 @@ export default class ClientCertificate { public readonly keyfile: string public readonly password?: string - private constructor () { + private constructor() { /** - * The path to client certificate file. - * - * @type {string} - */ + * The path to client certificate file. + * + * @type {string} + */ this.certfile = '' /** - * The path to the key file. - * - * @type {string} - */ + * The path to the key file. + * + * @type {string} + */ this.keyfile = '' /** - * The certificate's password. - * - * @type {string|undefined} - */ + * The certificate's password. + * + * @type {string|undefined} + */ this.password = undefined } } @@ -71,24 +73,23 @@ export default class ClientCertificate { */ export class ClientCertificateProvider { /** - * Indicates whether the client wants the driver to update the certificate. - * - * @returns {Promise|boolean} true if the client wants the driver to update the certificate - */ - - hasUpdate (): boolean | Promise { + * Indicates whether the client wants the driver to update the certificate. + * + * @returns {Promise|boolean} true if the client wants the driver to update the certificate + */ + hasUpdate(): boolean | Promise { throw new Error('Not Implemented') } /** - * Returns the certificate to use for new connections. - * - * Will be called by the driver after {@link ClientCertificateProvider#hasUpdate()} returned true - * or when the driver establishes the first connection. - * - * @returns {Promise|ClientCertificate} the certificate to use for new connections - */ - getClientCertificate (): ClientCertificate | Promise { + * Returns the certificate to use for new connections. + * + * Will be called by the driver after {@link ClientCertificateProvider#hasUpdate()} returned true + * or when the driver establishes the first connection. + * + * @returns {Promise|ClientCertificate} the certificate to use for new connections + */ + getClientCertificate(): ClientCertificate | Promise { throw new Error('Not Implemented') } } @@ -99,13 +100,13 @@ export class ClientCertificateProvider { */ export class RotatingClientCertificateProvider extends ClientCertificateProvider { /** - * Updates the certificate stored in the provider. - * - * To be called by user-code when a new client certificate is available. - * - * @param {ClientCertificate} certificate - the new certificate - */ - updateCertificate (certificate: ClientCertificate): void { + * Updates the certificate stored in the provider. + * + * To be called by user-code when a new client certificate is available. + * + * @param {ClientCertificate} certificate - the new certificate + */ + updateCertificate(certificate: ClientCertificate): void { throw new Error('Not implemented') } } @@ -115,14 +116,19 @@ export class RotatingClientCertificateProvider extends ClientCertificateProvider */ class ClientCertificateProviders { /** - * - * @param {object} param0 - The params - * @param {ClientCertificate} param0.initialCertificate - The certificated used by the driver until {@link RotatingClientCertificateProvider#updateCertificate} get called. - * - * @returns {RotatingClientCertificateProvider} The rotating client certificate provider - */ - rotating ({ initialCertificate }: { initialCertificate: ClientCertificate }): RotatingClientCertificateProvider { - throw new Error('Not implemented') + * + * @param {object} param0 - The params + * @param {ClientCertificate} param0.initialCertificate - The certificated used by the driver until {@link RotatingClientCertificateProvider#updateCertificate} get called. + * + * @returns {RotatingClientCertificateProvider} The rotating client certificate provider + */ + rotating({ initialCertificate }: { initialCertificate: ClientCertificate }): RotatingClientCertificateProvider { + if (initialCertificate == null || typeof initialCertificate !== 'object') { + throw new TypeError(`initialCertificate should be ClientCertificate, but got ${json.stringify(initialCertificate)}`) + } + + const certificate = { ...initialCertificate } + return new InternalRotatingClientCertificateProvider(certificate) } } @@ -140,3 +146,32 @@ export { export type { ClientCertificateProviders } + +/** + * Internal implementation + * + * @private + */ +class InternalRotatingClientCertificateProvider { + constructor( + private _certificate: ClientCertificate, + private _updated: boolean = false) { + } + + hasUpdate(): boolean | Promise { + try { + return this._updated + } finally { + this._updated = false + } + } + + getClientCertificate(): ClientCertificate | Promise { + return this._certificate + } + + updateCertificate(certificate: ClientCertificate): void { + this._certificate = { ...certificate } + this._updated = true + } +} From 9dbed03f346571b6c56903bf0e180768cfd27539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 21 Feb 2024 18:29:52 +0100 Subject: [PATCH 05/23] Add function to convert ClientCertificate to ClientCertificateProvider --- packages/core/src/client-certificate.ts | 22 ++++++++++ packages/core/test/client-certificate.test.ts | 33 ++++++++++++++- .../lib/core/client-certificate.ts | 40 ++++++++++++++----- 3 files changed, 85 insertions(+), 10 deletions(-) diff --git a/packages/core/src/client-certificate.ts b/packages/core/src/client-certificate.ts index 6952180f3..5123412a8 100644 --- a/packages/core/src/client-certificate.ts +++ b/packages/core/src/client-certificate.ts @@ -147,6 +147,28 @@ export type { ClientCertificateProviders } +export function resolveCertificateProvider (input: unknown): ClientCertificateProvider | undefined { + if (input == null) { + return undefined + } + + if (typeof input === 'object' && 'hasUpdate' in input && 'getClientCertificate' in input && + typeof input.getClientCertificate === 'function' && typeof input.hasUpdate === 'function') { + return input as ClientCertificateProvider + } + + if (typeof input === 'object' && 'certfile' in input && 'keyfile' in input && + typeof input.certfile === 'string' && typeof input.keyfile === 'string') { + const certificate = { ...input } as unknown as ClientCertificate + return { + getClientCertificate: () => certificate, + hasUpdate: () => false + } + } + + throw new TypeError(`clientCertificate should be configured with ClientCertificate or ClientCertificateProvider, but got ${json.stringify(input)}`) +} + /** * Internal implementation * diff --git a/packages/core/test/client-certificate.test.ts b/packages/core/test/client-certificate.test.ts index ec15dcc56..e8e9149eb 100644 --- a/packages/core/test/client-certificate.test.ts +++ b/packages/core/test/client-certificate.test.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { clientCertificateProviders } from '../src/client-certificate' +import { clientCertificateProviders, resolveCertificateProvider } from '../src/client-certificate' describe('clientCertificateProviders', () => { describe('.rotating()', () => { @@ -127,3 +127,34 @@ describe('clientCertificateProviders', () => { }) }) }) + +describe('resolveCertificateProvider', () => { + const rotatingProvider = clientCertificateProviders.rotating({ initialCertificate: { certfile: 'certfile', keyfile: 'keyfile' } }) + + it.each([ + [undefined, undefined], + [undefined, null], + [rotatingProvider, rotatingProvider] + ])('should return %o when called with %o', (expectedResult, input) => { + expect(resolveCertificateProvider(input)).toBe(expectedResult) + }) + + it('should a static provider when configured with ClientCertificate ', async () => { + const certificate = { certfile: 'certfile', keyfile: 'keyfile' } + + const maybeProvider = resolveCertificateProvider(certificate) + + expect(maybeProvider).toBeDefined() + + expect(maybeProvider?.getClientCertificate).toBeInstanceOf(Function) + expect(maybeProvider?.hasUpdate).toBeInstanceOf(Function) + // @ts-expect-error + expect(maybeProvider?.updateCertificate).toBeUndefined() + + for (let i = 0; i < 100; i++) { + await expect(Promise.resolve(maybeProvider?.getClientCertificate())).resolves.toEqual(certificate) + await expect(Promise.resolve(maybeProvider?.getClientCertificate())).resolves.not.toBe(certificate) + await expect(Promise.resolve(maybeProvider?.hasUpdate())).resolves.toBe(false) + } + }) +}) diff --git a/packages/neo4j-driver-deno/lib/core/client-certificate.ts b/packages/neo4j-driver-deno/lib/core/client-certificate.ts index 950241463..0f49ce6a4 100644 --- a/packages/neo4j-driver-deno/lib/core/client-certificate.ts +++ b/packages/neo4j-driver-deno/lib/core/client-certificate.ts @@ -30,7 +30,7 @@ export default class ClientCertificate { public readonly keyfile: string public readonly password?: string - private constructor() { + private constructor () { /** * The path to client certificate file. * @@ -77,7 +77,7 @@ export class ClientCertificateProvider { * * @returns {Promise|boolean} true if the client wants the driver to update the certificate */ - hasUpdate(): boolean | Promise { + hasUpdate (): boolean | Promise { throw new Error('Not Implemented') } @@ -89,7 +89,7 @@ export class ClientCertificateProvider { * * @returns {Promise|ClientCertificate} the certificate to use for new connections */ - getClientCertificate(): ClientCertificate | Promise { + getClientCertificate (): ClientCertificate | Promise { throw new Error('Not Implemented') } } @@ -106,7 +106,7 @@ export class RotatingClientCertificateProvider extends ClientCertificateProvider * * @param {ClientCertificate} certificate - the new certificate */ - updateCertificate(certificate: ClientCertificate): void { + updateCertificate (certificate: ClientCertificate): void { throw new Error('Not implemented') } } @@ -122,7 +122,7 @@ class ClientCertificateProviders { * * @returns {RotatingClientCertificateProvider} The rotating client certificate provider */ - rotating({ initialCertificate }: { initialCertificate: ClientCertificate }): RotatingClientCertificateProvider { + rotating ({ initialCertificate }: { initialCertificate: ClientCertificate }): RotatingClientCertificateProvider { if (initialCertificate == null || typeof initialCertificate !== 'object') { throw new TypeError(`initialCertificate should be ClientCertificate, but got ${json.stringify(initialCertificate)}`) } @@ -147,18 +147,40 @@ export type { ClientCertificateProviders } +export function resolveCertificateProvider (input: unknown): ClientCertificateProvider | undefined { + if (input == null) { + return undefined + } + + if (typeof input === 'object' && 'hasUpdate' in input && 'getClientCertificate' in input + && typeof input.getClientCertificate === 'function' && typeof input.hasUpdate === 'function') { + return input as ClientCertificateProvider + } + + if (typeof input === 'object' && 'certfile' in input && 'keyfile' in input && + typeof input.certfile === 'string' && typeof input.keyfile === 'string') { + const certificate = { ...input } as unknown as ClientCertificate + return { + getClientCertificate: () => certificate, + hasUpdate: () => false + } + } + + throw new TypeError(`clientCertificate should be configured with ClientCertificate or ClientCertificateProvider, but got ${json.stringify(input)}`) +} + /** * Internal implementation * * @private */ class InternalRotatingClientCertificateProvider { - constructor( + constructor ( private _certificate: ClientCertificate, private _updated: boolean = false) { } - hasUpdate(): boolean | Promise { + hasUpdate (): boolean | Promise { try { return this._updated } finally { @@ -166,11 +188,11 @@ class InternalRotatingClientCertificateProvider { } } - getClientCertificate(): ClientCertificate | Promise { + getClientCertificate (): ClientCertificate | Promise { return this._certificate } - updateCertificate(certificate: ClientCertificate): void { + updateCertificate (certificate: ClientCertificate): void { this._certificate = { ...certificate } this._updated = true } From 69ba1c3b56e83c07c5e265097137b1e904a604b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 22 Feb 2024 12:41:46 +0100 Subject: [PATCH 06/23] Improve validations --- packages/core/src/client-certificate.ts | 35 ++++++- packages/core/test/client-certificate.test.ts | 93 +++++++++++++++++-- .../lib/core/client-certificate.ts | 39 +++++++- 3 files changed, 153 insertions(+), 14 deletions(-) diff --git a/packages/core/src/client-certificate.ts b/packages/core/src/client-certificate.ts index 5123412a8..385ddca08 100644 --- a/packages/core/src/client-certificate.ts +++ b/packages/core/src/client-certificate.ts @@ -105,6 +105,7 @@ export class RotatingClientCertificateProvider extends ClientCertificateProvider * To be called by user-code when a new client certificate is available. * * @param {ClientCertificate} certificate - the new certificate + * @throws {TypeError} If initialCertificate is not a ClientCertificate. */ updateCertificate (certificate: ClientCertificate): void { throw new Error('Not implemented') @@ -121,9 +122,10 @@ class ClientCertificateProviders { * @param {ClientCertificate} param0.initialCertificate - The certificated used by the driver until {@link RotatingClientCertificateProvider#updateCertificate} get called. * * @returns {RotatingClientCertificateProvider} The rotating client certificate provider + * @throws {TypeError} If initialCertificate is not a ClientCertificate. */ rotating ({ initialCertificate }: { initialCertificate: ClientCertificate }): RotatingClientCertificateProvider { - if (initialCertificate == null || typeof initialCertificate !== 'object') { + if (initialCertificate == null || !isClientClientCertificate(initialCertificate)) { throw new TypeError(`initialCertificate should be ClientCertificate, but got ${json.stringify(initialCertificate)}`) } @@ -147,6 +149,16 @@ export type { ClientCertificateProviders } +/** + * Resolves ClientCertificate or ClientCertificateProvider to a ClientCertificateProvider + * + * Method validates the input. + * + * @private + * @param input + * @returns {ClientCertificateProvider?} A client certificate provider if provided a ClientCertificate or a ClientCertificateProvider + * @throws {TypeError} If input is not a ClientCertificate, ClientCertificateProvider, undefined or null. + */ export function resolveCertificateProvider (input: unknown): ClientCertificateProvider | undefined { if (input == null) { return undefined @@ -157,8 +169,7 @@ export function resolveCertificateProvider (input: unknown): ClientCertificatePr return input as ClientCertificateProvider } - if (typeof input === 'object' && 'certfile' in input && 'keyfile' in input && - typeof input.certfile === 'string' && typeof input.keyfile === 'string') { + if (isClientClientCertificate(input)) { const certificate = { ...input } as unknown as ClientCertificate return { getClientCertificate: () => certificate, @@ -169,6 +180,21 @@ export function resolveCertificateProvider (input: unknown): ClientCertificatePr throw new TypeError(`clientCertificate should be configured with ClientCertificate or ClientCertificateProvider, but got ${json.stringify(input)}`) } +/** + * Verify if object is a client certificate + * @private + * @param maybeClientCertificate - Maybe the certificate + * @returns {boolean} if maybeClientCertificate is a client certificate object + * + */ +function isClientClientCertificate (maybeClientCertificate: unknown): maybeClientCertificate is ClientCertificate { + return maybeClientCertificate != null && + typeof maybeClientCertificate === 'object' && + 'certfile' in maybeClientCertificate && typeof maybeClientCertificate.certfile === 'string' && + 'keyfile' in maybeClientCertificate && typeof maybeClientCertificate.keyfile === 'string' && + (!('password' in maybeClientCertificate) || maybeClientCertificate.password == null || typeof maybeClientCertificate.password === 'string') +} + /** * Internal implementation * @@ -193,6 +219,9 @@ class InternalRotatingClientCertificateProvider { } updateCertificate (certificate: ClientCertificate): void { + if (!isClientClientCertificate(certificate)) { + throw new TypeError(`certificate should be ClientCertificate, but got ${json.stringify(certificate)}`) + } this._certificate = { ...certificate } this._updated = true } diff --git a/packages/core/test/client-certificate.test.ts b/packages/core/test/client-certificate.test.ts index e8e9149eb..af3f095de 100644 --- a/packages/core/test/client-certificate.test.ts +++ b/packages/core/test/client-certificate.test.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { clientCertificateProviders, resolveCertificateProvider } from '../src/client-certificate' +import { ClientCertificateProvider, RotatingClientCertificateProvider, clientCertificateProviders, resolveCertificateProvider } from '../src/client-certificate' describe('clientCertificateProviders', () => { describe('.rotating()', () => { @@ -30,7 +30,8 @@ describe('clientCertificateProviders', () => { keyfile: 'some_file', password: 'pass' } - } + }, + ...invalidCertificates().map(initialCertificate => ({ initialCertificate })) ])('when invalid configuration (%o)', (config) => { it('should thrown TypeError', () => { // @ts-expect-error @@ -52,6 +53,19 @@ describe('clientCertificateProviders', () => { certfile: 'other_file', keyfile: 'some_file' } + }, + { + initialCertificate: { + get certfile () { return 'other_file' }, + get keyfile () { return 'some_file' }, + get password () { return 'pass' } + } + }, + { + initialCertificate: { + get certfile () { return 'other_file' }, + get keyfile () { return 'some_file' } + } } ])('when valid configuration (%o)', (config) => { it('should return a RotatingClientCertificateProvider', () => { @@ -103,6 +117,19 @@ describe('clientCertificateProviders', () => { } }) + it.each([ + ...invalidCertificates(), + null, + undefined + ])('should updateCertificate change certificate for a new one', async (invalidCertificate) => { + const provider = clientCertificateProviders.rotating(config) + + await expect(Promise.resolve(provider.getClientCertificate())).resolves.toEqual(config.initialCertificate) + + // @ts-expect-error + expect(() => provider.updateCertificate(invalidCertificate)).toThrow(TypeError) + }) + it('should hasUpdate Return false, unless updateCertificate() was called since the last call of hasUpdate', async () => { const provider = clientCertificateProviders.rotating(config) @@ -130,18 +157,42 @@ describe('clientCertificateProviders', () => { describe('resolveCertificateProvider', () => { const rotatingProvider = clientCertificateProviders.rotating({ initialCertificate: { certfile: 'certfile', keyfile: 'keyfile' } }) + const customProvider: ClientCertificateProvider = { + getClientCertificate () { + return { certfile: 'certfile', keyfile: 'keyfile' } + }, + hasUpdate () { + return false + } + } + + const customRotatingProvider: RotatingClientCertificateProvider = { + getClientCertificate () { + return { certfile: 'certfile', keyfile: 'keyfile' } + }, + hasUpdate () { + return false + }, + updateCertificate (certificate) { + } + } it.each([ [undefined, undefined], [undefined, null], - [rotatingProvider, rotatingProvider] + [rotatingProvider, rotatingProvider], + [customProvider, customProvider], + [customRotatingProvider, customRotatingProvider] ])('should return %o when called with %o', (expectedResult, input) => { expect(resolveCertificateProvider(input)).toBe(expectedResult) }) - it('should a static provider when configured with ClientCertificate ', async () => { - const certificate = { certfile: 'certfile', keyfile: 'keyfile' } - + it.each([ + { certfile: 'certfile', keyfile: 'keyfile' }, + { certfile: 'certfile', keyfile: 'keyfile', password: 'password' }, + { get certfile () { return 'the cert file' }, get keyfile () { return 'the key file' } }, + { get certfile () { return 'the cert file' }, get keyfile () { return 'the key file' }, get password () { return 'the password' } } + ])('should a static provider when configured with ClientCertificate ', async (certificate) => { const maybeProvider = resolveCertificateProvider(certificate) expect(maybeProvider).toBeDefined() @@ -157,4 +208,34 @@ describe('resolveCertificateProvider', () => { await expect(Promise.resolve(maybeProvider?.hasUpdate())).resolves.toBe(false) } }) + + it.each([ + ...invalidCertificates(), + { getClientCertificate () {} }, + { hasUpdate () {} }, + { updateCertificate () {} }, + { getClientCertificate () {}, hasUpdate: true }, + { getClientCertificate () {}, get hasUpdate () { return true } }, + { getClientCertificate: 'certificate', hasUpdate () {} } + ])('should thrown when object is not a ClientCertificate, ClientCertificateProvider or absent (%o)', (value) => { + expect(() => resolveCertificateProvider(value)).toThrow(TypeError) + }) }) + +function invalidCertificates (): any[] { + return [ + [], + ['certfile', 'file', 'keyfile', 'the key file'], + { certfile: 'file' }, + { keyfile: 'file' }, + { password: 'password_123' }, + { certfile: 123, keyfile: 'file' }, + { certfile: 'file', keyfile: 3.4 }, + { certfile: 3.5, keyfile: 3.4 }, + { certfile: 'sAED', keyfile: Symbol.asyncIterator }, + { certfile: '123', keyfile: 'file', password: 123 }, + { certfile () { return 'the cert file' }, get keyfile () { return 'the key file' } }, + { get certfile () { return 'the cert file' }, keyfile () { return 'the key file' } }, + { get certfile () { return 'the cert file' }, get keyfile () { return 'the key file' }, password () { return 'the password' } } + ] +} diff --git a/packages/neo4j-driver-deno/lib/core/client-certificate.ts b/packages/neo4j-driver-deno/lib/core/client-certificate.ts index 0f49ce6a4..9fcff6c4f 100644 --- a/packages/neo4j-driver-deno/lib/core/client-certificate.ts +++ b/packages/neo4j-driver-deno/lib/core/client-certificate.ts @@ -105,6 +105,7 @@ export class RotatingClientCertificateProvider extends ClientCertificateProvider * To be called by user-code when a new client certificate is available. * * @param {ClientCertificate} certificate - the new certificate + * @throws {TypeError} If initialCertificate is not a ClientCertificate. */ updateCertificate (certificate: ClientCertificate): void { throw new Error('Not implemented') @@ -121,9 +122,10 @@ class ClientCertificateProviders { * @param {ClientCertificate} param0.initialCertificate - The certificated used by the driver until {@link RotatingClientCertificateProvider#updateCertificate} get called. * * @returns {RotatingClientCertificateProvider} The rotating client certificate provider + * @throws {TypeError} If initialCertificate is not a ClientCertificate. */ rotating ({ initialCertificate }: { initialCertificate: ClientCertificate }): RotatingClientCertificateProvider { - if (initialCertificate == null || typeof initialCertificate !== 'object') { + if (initialCertificate == null || !isClientClientCertificate(initialCertificate)) { throw new TypeError(`initialCertificate should be ClientCertificate, but got ${json.stringify(initialCertificate)}`) } @@ -147,18 +149,27 @@ export type { ClientCertificateProviders } +/** + * Resolves ClientCertificate or ClientCertificateProvider to a ClientCertificateProvider + * + * Method validates the input. + * + * @private + * @param input + * @returns {ClientCertificateProvider?} A client certificate provider if provided a ClientCertificate or a ClientCertificateProvider + * @throws {TypeError} If input is not a ClientCertificate, ClientCertificateProvider, undefined or null. + */ export function resolveCertificateProvider (input: unknown): ClientCertificateProvider | undefined { if (input == null) { return undefined } - if (typeof input === 'object' && 'hasUpdate' in input && 'getClientCertificate' in input - && typeof input.getClientCertificate === 'function' && typeof input.hasUpdate === 'function') { + if (typeof input === 'object' && 'hasUpdate' in input && 'getClientCertificate' in input && + typeof input.getClientCertificate === 'function' && typeof input.hasUpdate === 'function') { return input as ClientCertificateProvider } - if (typeof input === 'object' && 'certfile' in input && 'keyfile' in input && - typeof input.certfile === 'string' && typeof input.keyfile === 'string') { + if (isClientClientCertificate(input)) { const certificate = { ...input } as unknown as ClientCertificate return { getClientCertificate: () => certificate, @@ -169,6 +180,21 @@ export function resolveCertificateProvider (input: unknown): ClientCertificatePr throw new TypeError(`clientCertificate should be configured with ClientCertificate or ClientCertificateProvider, but got ${json.stringify(input)}`) } +/** + * Verify if object is a client certificate + * @private + * @param maybeClientCertificate - Maybe the certificate + * @returns {boolean} if maybeClientCertificate is a client certificate object + * + */ +function isClientClientCertificate (maybeClientCertificate: unknown): maybeClientCertificate is ClientCertificate { + return maybeClientCertificate != null && + typeof maybeClientCertificate === 'object' && + 'certfile' in maybeClientCertificate && typeof maybeClientCertificate.certfile === 'string' && + 'keyfile' in maybeClientCertificate && typeof maybeClientCertificate.keyfile === 'string' && + (!('password' in maybeClientCertificate) || maybeClientCertificate.password == null || typeof maybeClientCertificate.password === 'string') +} + /** * Internal implementation * @@ -193,6 +219,9 @@ class InternalRotatingClientCertificateProvider { } updateCertificate (certificate: ClientCertificate): void { + if (!isClientClientCertificate(certificate)) { + throw new TypeError(`certificate should be ClientCertificate, but got ${json.stringify(certificate)}`) + } this._certificate = { ...certificate } this._updated = true } From cff410cba148cc60723fabaea83a8702b99612be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 22 Feb 2024 13:13:22 +0100 Subject: [PATCH 07/23] Push clientCertificate down the stack --- .../src/channel/channel-config.js | 4 +++- .../connection-provider-direct.js | 8 +------- .../connection-provider-pooled.js | 19 +++++++++++++++++-- .../src/connection/connection-channel.js | 5 ++++- packages/core/src/index.ts | 8 +++++--- packages/core/test/client-certificate.test.ts | 1 - .../bolt-connection/channel/channel-config.js | 4 +++- .../connection-provider-direct.js | 8 +------- .../connection-provider-pooled.js | 19 +++++++++++++++++-- .../connection/connection-channel.js | 5 ++++- packages/neo4j-driver-deno/lib/core/index.ts | 8 +++++--- packages/neo4j-driver-deno/lib/mod.ts | 4 +++- packages/neo4j-driver-lite/src/index.ts | 4 +++- packages/neo4j-driver/src/index.js | 4 +++- 14 files changed, 69 insertions(+), 32 deletions(-) diff --git a/packages/bolt-connection/src/channel/channel-config.js b/packages/bolt-connection/src/channel/channel-config.js index 3d9254b12..3f4e743e3 100644 --- a/packages/bolt-connection/src/channel/channel-config.js +++ b/packages/bolt-connection/src/channel/channel-config.js @@ -46,8 +46,9 @@ export default class ChannelConfig { * @param {ServerAddress} address the address for the channel to connect to. * @param {Object} driverConfig the driver config provided by the user when driver is created. * @param {string} connectionErrorCode the default error code to use on connection errors. + * @param {object} clientCertificate the client certificate */ - constructor (address, driverConfig, connectionErrorCode) { + constructor (address, driverConfig, connectionErrorCode, clientCertificate) { this.address = address this.encrypted = extractEncrypted(driverConfig) this.trust = extractTrust(driverConfig) @@ -55,6 +56,7 @@ export default class ChannelConfig { this.knownHostsPath = extractKnownHostsPath(driverConfig) this.connectionErrorCode = connectionErrorCode || SERVICE_UNAVAILABLE this.connectionTimeout = driverConfig.connectionTimeout + this.clientCertificate = clientCertificate } } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index 1e4ee5efa..565955a2e 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -17,7 +17,6 @@ import PooledConnectionProvider from './connection-provider-pooled' import { - createChannelConnection, DelegateConnection, ConnectionErrorHandler } from '../connection' @@ -75,12 +74,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { } async _hasProtocolVersion (versionPredicate) { - const connection = await createChannelConnection( - this._address, - this._config, - this._createConnectionErrorHandler(), - this._log - ) + const connection = await this._createChannelConnection(this._address) const protocolVersion = connection.protocol() ? connection.protocol().version diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index cb3875bf4..8f4aca0c2 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -40,18 +40,21 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._id = id this._config = config this._log = log + this._clientCertificate = null this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent, boltAgent }) this._livenessCheckProvider = new LivenessCheckProvider({ connectionLivenessCheckTimeout: config.connectionLivenessCheckTimeout }) this._userAgent = userAgent this._boltAgent = boltAgent this._createChannelConnection = createChannelConnectionHook || - (address => { + (async address => { + await this._updateClientCertificateWhenNeeded() return createChannelConnection( address, this._config, this._createConnectionErrorHandler(), - this._log + this._log, + await this._clientCertificate ) }) this._connectionPool = newPool({ @@ -75,6 +78,18 @@ export default class PooledConnectionProvider extends ConnectionProvider { return new ConnectionErrorHandler(SERVICE_UNAVAILABLE) } + async _updateClientCertificateWhenNeeded () { + if (this._config.clientCertificate == null) { + return + } + if (this._clientCertificate == null || await this._config.clientCertificate.hasUpdate()) { + this._clientCertificate = this._config.clientCertificate.getClientCertificate() + .then(clientCertificate => { + this._clientCertificate = clientCertificate + }) + } + } + /** * Create a new connection and initialize it. * @return {Promise} promise resolved with a new connection or rejected when failed to connect. diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index 73ec2a821..b056e3e1b 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -33,6 +33,7 @@ let idGenerator = 0 * @param {Object} config - the driver configuration. * @param {ConnectionErrorHandler} errorHandler - the error handler for connection errors. * @param {Logger} log - configured logger. + * @param {clientCertificate} clientCertificate - configured client certificate * @return {Connection} - new connection. */ export function createChannelConnection ( @@ -40,13 +41,15 @@ export function createChannelConnection ( config, errorHandler, log, + clientCertificate, serversideRouting = null, createChannel = channelConfig => new Channel(channelConfig) ) { const channelConfig = new ChannelConfig( address, config, - errorHandler.errorCode() + errorHandler.errorCode(), + clientCertificate ) const channel = createChannel(channelConfig) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5e74bafca..af9677287 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -91,7 +91,7 @@ import { Config } from './types' import * as types from './types' import * as json from './json' import resultTransformers, { ResultTransformer } from './result-transformers' -import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider } from './client-certificate' +import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider, resolveCertificateProvider } from './client-certificate' import * as internal from './internal' // todo: removed afterwards /** @@ -171,7 +171,8 @@ const forExport = { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + resolveCertificateProvider } export { @@ -241,7 +242,8 @@ export { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + resolveCertificateProvider } export type { diff --git a/packages/core/test/client-certificate.test.ts b/packages/core/test/client-certificate.test.ts index af3f095de..8cfd29968 100644 --- a/packages/core/test/client-certificate.test.ts +++ b/packages/core/test/client-certificate.test.ts @@ -126,7 +126,6 @@ describe('clientCertificateProviders', () => { await expect(Promise.resolve(provider.getClientCertificate())).resolves.toEqual(config.initialCertificate) - // @ts-expect-error expect(() => provider.updateCertificate(invalidCertificate)).toThrow(TypeError) }) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/channel-config.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/channel-config.js index 605ce2658..d06c80acd 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/channel/channel-config.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/channel-config.js @@ -46,8 +46,9 @@ export default class ChannelConfig { * @param {ServerAddress} address the address for the channel to connect to. * @param {Object} driverConfig the driver config provided by the user when driver is created. * @param {string} connectionErrorCode the default error code to use on connection errors. + * @param {object} clientCertificate the client certificate */ - constructor (address, driverConfig, connectionErrorCode) { + constructor (address, driverConfig, connectionErrorCode, clientCertificate) { this.address = address this.encrypted = extractEncrypted(driverConfig) this.trust = extractTrust(driverConfig) @@ -55,6 +56,7 @@ export default class ChannelConfig { this.knownHostsPath = extractKnownHostsPath(driverConfig) this.connectionErrorCode = connectionErrorCode || SERVICE_UNAVAILABLE this.connectionTimeout = driverConfig.connectionTimeout + this.clientCertificate = clientCertificate } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index c815830d7..94d864865 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -17,7 +17,6 @@ import PooledConnectionProvider from './connection-provider-pooled.js' import { - createChannelConnection, DelegateConnection, ConnectionErrorHandler } from '../connection/index.js' @@ -75,12 +74,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { } async _hasProtocolVersion (versionPredicate) { - const connection = await createChannelConnection( - this._address, - this._config, - this._createConnectionErrorHandler(), - this._log - ) + const connection = await this._createChannelConnection(this._address) const protocolVersion = connection.protocol() ? connection.protocol().version diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index 7f9a5ef17..eb369f959 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -40,18 +40,21 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._id = id this._config = config this._log = log + this._clientCertificate = null this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent, boltAgent }) this._livenessCheckProvider = new LivenessCheckProvider({ connectionLivenessCheckTimeout: config.connectionLivenessCheckTimeout }) this._userAgent = userAgent this._boltAgent = boltAgent this._createChannelConnection = createChannelConnectionHook || - (address => { + (async address => { + await this._updateClientCertificateWhenNeeded() return createChannelConnection( address, this._config, this._createConnectionErrorHandler(), - this._log + this._log, + await this._clientCertificate ) }) this._connectionPool = newPool({ @@ -75,6 +78,18 @@ export default class PooledConnectionProvider extends ConnectionProvider { return new ConnectionErrorHandler(SERVICE_UNAVAILABLE) } + async _updateClientCertificateWhenNeeded () { + if (this._config.clientCertificate == null) { + return + } + if (this._clientCertificate == null || await this._config.clientCertificate.hasUpdate()) { + this._clientCertificate = this._config.clientCertificate.getClientCertificate() + .then(clientCertificate => { + this._clientCertificate = clientCertificate + }) + } + } + /** * Create a new connection and initialize it. * @return {Promise} promise resolved with a new connection or rejected when failed to connect. diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index 5efd40da1..12ad7fc86 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -33,6 +33,7 @@ let idGenerator = 0 * @param {Object} config - the driver configuration. * @param {ConnectionErrorHandler} errorHandler - the error handler for connection errors. * @param {Logger} log - configured logger. + * @param {clientCertificate} clientCertificate - configured client certificate * @return {Connection} - new connection. */ export function createChannelConnection ( @@ -40,13 +41,15 @@ export function createChannelConnection ( config, errorHandler, log, + clientCertificate, serversideRouting = null, createChannel = channelConfig => new Channel(channelConfig) ) { const channelConfig = new ChannelConfig( address, config, - errorHandler.errorCode() + errorHandler.errorCode(), + clientCertificate ) const channel = createChannel(channelConfig) diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index db54d0b06..ad3dd1f76 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -91,7 +91,7 @@ import { Config } from './types.ts' import * as types from './types.ts' import * as json from './json.ts' import resultTransformers, { ResultTransformer } from './result-transformers.ts' -import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider } from './client-certificate.ts' +import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider, resolveCertificateProvider } from './client-certificate.ts' import * as internal from './internal/index.ts' /** @@ -171,7 +171,8 @@ const forExport = { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + resolveCertificateProvider } export { @@ -241,7 +242,8 @@ export { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + resolveCertificateProvider } export type { diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index 333e25ea7..b8e652221 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -102,7 +102,8 @@ import { ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider, - clientCertificateProviders + clientCertificateProviders, + resolveCertificateProvider } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts import { DirectConnectionProvider, RoutingConnectionProvider } from './bolt-connection/index.js' @@ -214,6 +215,7 @@ function driver ( } _config.encrypted = ENCRYPTION_ON _config.trust = trust + _config.clientCertificate = resolveCertificateProvider(config.clientCertificate) } const authTokenManager = createAuthManager(authToken) diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index 7508796a8..a9b440367 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -102,7 +102,8 @@ import { ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider, - clientCertificateProviders + clientCertificateProviders, + resolveCertificateProvider } from 'neo4j-driver-core' import { DirectConnectionProvider, RoutingConnectionProvider } from 'neo4j-driver-bolt-connection' @@ -213,6 +214,7 @@ function driver ( } _config.encrypted = ENCRYPTION_ON _config.trust = trust + _config.clientCertificate = resolveCertificateProvider(config.clientCertificate) } const authTokenManager = createAuthManager(authToken) diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 6abae01b9..e0b2b4aa4 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -74,7 +74,8 @@ import { notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, staticAuthTokenManager, - clientCertificateProviders + clientCertificateProviders, + resolveCertificateProvider } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -170,6 +171,7 @@ function driver (url, authToken, config = {}) { } config.encrypted = ENCRYPTION_ON config.trust = trust + config.clientCertificate = resolveCertificateProvider(config.clientCertificate) } const authTokenManager = createAuthManager(authToken) From 1c17032c6022f922a53272bdc2c9b92bf47dc26b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 22 Feb 2024 13:34:57 +0100 Subject: [PATCH 08/23] Configure client certificate in Node (missing test) --- .../src/channel/node/node-channel.js | 31 ++++++++++++++++--- .../channel/node/node-channel.js | 31 ++++++++++++++++--- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/packages/bolt-connection/src/channel/node/node-channel.js b/packages/bolt-connection/src/channel/node/node-channel.js index e5c0ee368..f8cab2bac 100644 --- a/packages/bolt-connection/src/channel/node/node-channel.js +++ b/packages/bolt-connection/src/channel/node/node-channel.js @@ -49,7 +49,10 @@ const TrustStrategy = { const tlsOpts = newTlsOptions( config.address.host(), - config.trustedCertificates.map(f => fs.readFileSync(f)) + config.trustedCertificates.map(f => fs.readFileSync(f)), + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.key) : undefined, + config.clientCertificate != null ? config.clientCertificate.password : undefined ) const socket = tls.connect( config.address.port(), @@ -79,7 +82,13 @@ const TrustStrategy = { return configureSocket(socket) }, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: function (config, onSuccess, onFailure) { - const tlsOpts = newTlsOptions(config.address.host()) + const tlsOpts = newTlsOptions( + config.address.host(), + undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.key) : undefined, + config.clientCertificate != null ? config.clientCertificate.password : undefined + ) const socket = tls.connect( config.address.port(), config.address.resolvedHost(), @@ -109,7 +118,13 @@ const TrustStrategy = { return configureSocket(socket) }, TRUST_ALL_CERTIFICATES: function (config, onSuccess, onFailure) { - const tlsOpts = newTlsOptions(config.address.host()) + const tlsOpts = newTlsOptions( + config.address.host(), + undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.key) : undefined, + config.clientCertificate != null ? config.clientCertificate.password : undefined + ) const socket = tls.connect( config.address.port(), config.address.resolvedHost(), @@ -198,13 +213,19 @@ function trustStrategyName (config) { * Create a new configuration options object for the {@code tls.connect()} call. * @param {string} hostname the target hostname. * @param {string|undefined} ca an optional CA. + * @param {string|undefined} cert an optional client cert. + * @param {string|undefined} key an optional client cert key. + * @param {string|undefined} passphrase an optional client cert passphrase * @return {Object} a new options object. */ -function newTlsOptions (hostname, ca = undefined) { +function newTlsOptions (hostname, ca = undefined, cert = undefined, key = undefined, passphrase = undefined) { return { rejectUnauthorized: false, // we manually check for this in the connect callback, to give a more helpful error to the user servername: hostname, // server name for the SNI (Server Name Indication) TLS extension - ca // optional CA useful for TRUST_CUSTOM_CA_SIGNED_CERTIFICATES trust mode + ca, // optional CA useful for TRUST_CUSTOM_CA_SIGNED_CERTIFICATES trust mode, + cert, + key, + passphrase } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js index acf8eeb33..755a1226c 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js @@ -49,7 +49,10 @@ const TrustStrategy = { const tlsOpts = newTlsOptions( config.address.host(), - config.trustedCertificates.map(f => fs.readFileSync(f)) + config.trustedCertificates.map(f => fs.readFileSync(f)), + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.key) : undefined, + config.clientCertificate != null ? config.clientCertificate.password : undefined ) const socket = tls.connect( config.address.port(), @@ -79,7 +82,13 @@ const TrustStrategy = { return configureSocket(socket) }, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: function (config, onSuccess, onFailure) { - const tlsOpts = newTlsOptions(config.address.host()) + const tlsOpts = newTlsOptions( + config.address.host(), + undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.key) : undefined, + config.clientCertificate != null ? config.clientCertificate.password : undefined + ) const socket = tls.connect( config.address.port(), config.address.resolvedHost(), @@ -109,7 +118,13 @@ const TrustStrategy = { return configureSocket(socket) }, TRUST_ALL_CERTIFICATES: function (config, onSuccess, onFailure) { - const tlsOpts = newTlsOptions(config.address.host()) + const tlsOpts = newTlsOptions( + config.address.host(), + undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.key) : undefined, + config.clientCertificate != null ? config.clientCertificate.password : undefined + ) const socket = tls.connect( config.address.port(), config.address.resolvedHost(), @@ -198,13 +213,19 @@ function trustStrategyName (config) { * Create a new configuration options object for the {@code tls.connect()} call. * @param {string} hostname the target hostname. * @param {string|undefined} ca an optional CA. + * @param {string|undefined} cert an optional client cert. + * @param {string|undefined} key an optional client cert key. + * @param {string|undefined} passphrase an optional client cert passphrase * @return {Object} a new options object. */ -function newTlsOptions (hostname, ca = undefined) { +function newTlsOptions (hostname, ca = undefined, cert = undefined, key = undefined, passphrase = undefined) { return { rejectUnauthorized: false, // we manually check for this in the connect callback, to give a more helpful error to the user servername: hostname, // server name for the SNI (Server Name Indication) TLS extension - ca // optional CA useful for TRUST_CUSTOM_CA_SIGNED_CERTIFICATES trust mode + ca, // optional CA useful for TRUST_CUSTOM_CA_SIGNED_CERTIFICATES trust mode, + cert, + key, + passphrase } } From f8daa0588f507a0124a406322333e6e15ed59313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 22 Feb 2024 14:57:09 +0100 Subject: [PATCH 09/23] Fix node mtls --- .../bolt-connection/src/channel/node/node-channel.js | 6 +++--- .../connection-provider/connection-provider-pooled.js | 6 +++++- .../connection-provider-routing.js | 11 ++++------- .../lib/bolt-connection/channel/node/node-channel.js | 6 +++--- .../connection-provider/connection-provider-pooled.js | 6 +++++- .../connection-provider-routing.js | 11 ++++------- 6 files changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/bolt-connection/src/channel/node/node-channel.js b/packages/bolt-connection/src/channel/node/node-channel.js index f8cab2bac..ada96d64f 100644 --- a/packages/bolt-connection/src/channel/node/node-channel.js +++ b/packages/bolt-connection/src/channel/node/node-channel.js @@ -51,7 +51,7 @@ const TrustStrategy = { config.address.host(), config.trustedCertificates.map(f => fs.readFileSync(f)), config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.key) : undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.keyfile) : undefined, config.clientCertificate != null ? config.clientCertificate.password : undefined ) const socket = tls.connect( @@ -86,7 +86,7 @@ const TrustStrategy = { config.address.host(), undefined, config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.key) : undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.keyfile) : undefined, config.clientCertificate != null ? config.clientCertificate.password : undefined ) const socket = tls.connect( @@ -122,7 +122,7 @@ const TrustStrategy = { config.address.host(), undefined, config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.key) : undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.keyfile) : undefined, config.clientCertificate != null ? config.clientCertificate.password : undefined ) const socket = tls.connect( diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 8f4aca0c2..931bb01c7 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -83,13 +83,17 @@ export default class PooledConnectionProvider extends ConnectionProvider { return } if (this._clientCertificate == null || await this._config.clientCertificate.hasUpdate()) { - this._clientCertificate = this._config.clientCertificate.getClientCertificate() + this._clientCertificate = this._getClientCertificate() .then(clientCertificate => { this._clientCertificate = clientCertificate }) } } + async _getClientCertificate () { + return this._config.clientCertificate.getClientCertificate() + } + /** * Create a new connection and initialize it. * @return {Promise} promise resolved with a new connection or rejected when failed to connect. diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index b3e567793..f859c0471 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -71,12 +71,14 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider routingTablePurgeDelay, newPool }) { - super({ id, config, log, userAgent, boltAgent, authTokenManager, newPool }, address => { + super({ id, config, log, userAgent, boltAgent, authTokenManager, newPool }, async address => { + await this._updateClientCertificateWhenNeeded() return createChannelConnection( address, this._config, this._createConnectionErrorHandler(), this._log, + await this._clientCertificate, this._routingContext ) }) @@ -212,12 +214,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider let lastError for (let i = 0; i < addresses.length; i++) { try { - const connection = await createChannelConnection( - addresses[i], - this._config, - this._createConnectionErrorHandler(), - this._log - ) + const connection = await this._createChannelConnection(addresses[i]) const protocolVersion = connection.protocol() ? connection.protocol().version : null diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js index 755a1226c..511726836 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js @@ -51,7 +51,7 @@ const TrustStrategy = { config.address.host(), config.trustedCertificates.map(f => fs.readFileSync(f)), config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.key) : undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.keyfile) : undefined, config.clientCertificate != null ? config.clientCertificate.password : undefined ) const socket = tls.connect( @@ -86,7 +86,7 @@ const TrustStrategy = { config.address.host(), undefined, config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.key) : undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.keyfile) : undefined, config.clientCertificate != null ? config.clientCertificate.password : undefined ) const socket = tls.connect( @@ -122,7 +122,7 @@ const TrustStrategy = { config.address.host(), undefined, config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.key) : undefined, + config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.keyfile) : undefined, config.clientCertificate != null ? config.clientCertificate.password : undefined ) const socket = tls.connect( diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index eb369f959..c92cfb1c9 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -83,13 +83,17 @@ export default class PooledConnectionProvider extends ConnectionProvider { return } if (this._clientCertificate == null || await this._config.clientCertificate.hasUpdate()) { - this._clientCertificate = this._config.clientCertificate.getClientCertificate() + this._clientCertificate = this._getClientCertificate() .then(clientCertificate => { this._clientCertificate = clientCertificate }) } } + async _getClientCertificate () { + return this._config.clientCertificate.getClientCertificate() + } + /** * Create a new connection and initialize it. * @return {Promise} promise resolved with a new connection or rejected when failed to connect. diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index 1500c75d0..1030dd246 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -71,12 +71,14 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider routingTablePurgeDelay, newPool }) { - super({ id, config, log, userAgent, boltAgent, authTokenManager, newPool }, address => { + super({ id, config, log, userAgent, boltAgent, authTokenManager, newPool }, async address => { + await this._updateClientCertificateWhenNeeded() return createChannelConnection( address, this._config, this._createConnectionErrorHandler(), this._log, + await this._clientCertificate, this._routingContext ) }) @@ -212,12 +214,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider let lastError for (let i = 0; i < addresses.length; i++) { try { - const connection = await createChannelConnection( - addresses[i], - this._config, - this._createConnectionErrorHandler(), - this._log - ) + const connection = await this._createChannelConnection(addresses[i]) const protocolVersion = connection.protocol() ? connection.protocol().version : null From ad4c02998d1e5e95dbc9b01fe4f9a70942507ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 22 Feb 2024 15:17:32 +0100 Subject: [PATCH 10/23] Deno does not support mtls --- .../bolt-connection/src/channel/deno/deno-channel.js | 11 +++++++++++ packages/neo4j-driver-deno/README.md | 2 ++ packages/neo4j-driver-deno/lib/README.md | 2 ++ .../lib/bolt-connection/channel/deno/deno-channel.js | 11 +++++++++++ 4 files changed, 26 insertions(+) diff --git a/packages/bolt-connection/src/channel/deno/deno-channel.js b/packages/bolt-connection/src/channel/deno/deno-channel.js index a80812e6d..7d6d9ec2b 100644 --- a/packages/bolt-connection/src/channel/deno/deno-channel.js +++ b/packages/bolt-connection/src/channel/deno/deno-channel.js @@ -239,6 +239,8 @@ const TrustStrategy = { ); } + assertNotClientCertificates(config) + const caCerts = await Promise.all( config.trustedCertificates.map(f => Deno.readTextFile(f)) ) @@ -250,6 +252,8 @@ const TrustStrategy = { }) }, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: function (config) { + assertNotClientCertificates(config) + return Deno.connectTls({ hostname: config.address.resolvedHost(), port: config.address.port() @@ -265,6 +269,13 @@ const TrustStrategy = { } } +async function assertNotClientCertificates (config) { + if (config.clientCertificate != null) { + throw newError('clientCertificates are not supported in DenoJS since the API does not ' + + 'support its configuration. See, https://deno.land/api@v1.29.0?s=Deno.ConnectTlsOptions.') + } +} + async function _connect (config) { if (!isEncrypted(config)) { return Deno.connect({ diff --git a/packages/neo4j-driver-deno/README.md b/packages/neo4j-driver-deno/README.md index 913e2a591..de721d15e 100644 --- a/packages/neo4j-driver-deno/README.md +++ b/packages/neo4j-driver-deno/README.md @@ -48,6 +48,8 @@ For Deno versions bellow `1.27.1`, you should use the flag `--allow-env` instead For using system certificates, the `DENO_TLS_CA_STORE` should be set to `"system"`. `TRUST_ALL_CERTIFICATES` should be handle by `--unsafely-ignore-certificate-errors` and not by driver configuration. See, https://deno.com/blog/v1.13#disable-tls-verification; +Client certificates are not support in this version of the driver since there is no support for this feature in the DenoJS API. See, https://deno.land/api@v1.29.0?s=Deno.ConnectTlsOptions. + ### Basic Example ```typescript diff --git a/packages/neo4j-driver-deno/lib/README.md b/packages/neo4j-driver-deno/lib/README.md index 913e2a591..de721d15e 100644 --- a/packages/neo4j-driver-deno/lib/README.md +++ b/packages/neo4j-driver-deno/lib/README.md @@ -48,6 +48,8 @@ For Deno versions bellow `1.27.1`, you should use the flag `--allow-env` instead For using system certificates, the `DENO_TLS_CA_STORE` should be set to `"system"`. `TRUST_ALL_CERTIFICATES` should be handle by `--unsafely-ignore-certificate-errors` and not by driver configuration. See, https://deno.com/blog/v1.13#disable-tls-verification; +Client certificates are not support in this version of the driver since there is no support for this feature in the DenoJS API. See, https://deno.land/api@v1.29.0?s=Deno.ConnectTlsOptions. + ### Basic Example ```typescript diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-channel.js index 75d2d8ee2..1fbd4766f 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-channel.js @@ -239,6 +239,8 @@ const TrustStrategy = { ); } + assertNotClientCertificates(config) + const caCerts = await Promise.all( config.trustedCertificates.map(f => Deno.readTextFile(f)) ) @@ -250,6 +252,8 @@ const TrustStrategy = { }) }, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: function (config) { + assertNotClientCertificates(config) + return Deno.connectTls({ hostname: config.address.resolvedHost(), port: config.address.port() @@ -265,6 +269,13 @@ const TrustStrategy = { } } +async function assertNotClientCertificates (config) { + if (config.clientCertificate != null) { + throw newError('clientCertificates are not supported in DenoJS since the API does not ' + + 'support its configuration. See, https://deno.land/api@v1.29.0?s=Deno.ConnectTlsOptions.') + } +} + async function _connect (config) { if (!isEncrypted(config)) { return Deno.connect({ From 12908f452d5e586f9319750774aa5923b16ec24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 22 Feb 2024 15:32:49 +0100 Subject: [PATCH 11/23] Fix connection config tests --- packages/neo4j-driver/test/internal/connection-channel.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/neo4j-driver/test/internal/connection-channel.test.js b/packages/neo4j-driver/test/internal/connection-channel.test.js index ea8acd912..8a8bd9e91 100644 --- a/packages/neo4j-driver/test/internal/connection-channel.test.js +++ b/packages/neo4j-driver/test/internal/connection-channel.test.js @@ -157,6 +157,7 @@ describe('#integration ChannelConnection', () => { new ConnectionErrorHandler(SERVICE_UNAVAILABLE), Logger.noOp(), null, + null, () => channel ) .then(c => { From 1b8e9716acdebb081b7a9bca669045700aa19924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 22 Feb 2024 17:15:12 +0100 Subject: [PATCH 12/23] Adjust test code --- packages/neo4j-driver-deno/test/neo4j.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/neo4j-driver-deno/test/neo4j.test.ts b/packages/neo4j-driver-deno/test/neo4j.test.ts index 1560ac602..e682dac20 100644 --- a/packages/neo4j-driver-deno/test/neo4j.test.ts +++ b/packages/neo4j-driver-deno/test/neo4j.test.ts @@ -64,9 +64,10 @@ Deno.test('session.beginTransaction should rollback the transaction if not commi // Deno will fail with resource leaks Deno.test('session.beginTransaction should noop if resource committed', async () => { await using driver = neo4j.driver(uri, authToken) + const name = "Must Be Conor" + try { await using session = driver.session() - const name = "Must Be Conor" { await using tx = session.beginTransaction() From 9ecd4ffe9804aff7d0e8eeb65f992ac97bfd5385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Fri, 23 Feb 2024 12:04:13 +0100 Subject: [PATCH 13/23] Enable the keyfile and cert to have multiple files --- packages/core/src/client-certificate.ts | 95 +++++++++++++++++-- packages/core/test/client-certificate.test.ts | 85 ++++++++++------- .../lib/core/client-certificate.ts | 95 +++++++++++++++++-- 3 files changed, 225 insertions(+), 50 deletions(-) diff --git a/packages/core/src/client-certificate.ts b/packages/core/src/client-certificate.ts index 385ddca08..8c78aa1cc 100644 --- a/packages/core/src/client-certificate.ts +++ b/packages/core/src/client-certificate.ts @@ -17,31 +17,46 @@ import * as json from './json' +type KeyFile = string | { path: string, password?: string } + /** * Holds the Client TLS certificate information. * * Browser instances of the driver should configure the certificate * in the system. * + * Files defined in the {@link ClientCertificate#certfile} + * and {@link ClientCertificate#keyfile} will read and loaded to + * memory to fill the fields `cert` and `key` in security context. + * * @interface + * @see https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions + */ +/** + * Represents KeyFile represented as file. + * + * @typedef {object} KeyFileObject + * @property {string} path - The path of the file + * @property {string?} password - the password of the key. If none, + * the password defined at {@link ClientCertificate} will be used. */ export default class ClientCertificate { - public readonly certfile: string - public readonly keyfile: string + public readonly certfile: string | string[] + public readonly keyfile: KeyFile | KeyFile[] public readonly password?: string private constructor () { /** * The path to client certificate file. * - * @type {string} + * @type {string | string} */ this.certfile = '' /** * The path to the key file. * - * @type {string} + * @type {string | string[] | KeyFileObject | KeyFileObject[] } */ this.keyfile = '' @@ -190,9 +205,75 @@ export function resolveCertificateProvider (input: unknown): ClientCertificatePr function isClientClientCertificate (maybeClientCertificate: unknown): maybeClientCertificate is ClientCertificate { return maybeClientCertificate != null && typeof maybeClientCertificate === 'object' && - 'certfile' in maybeClientCertificate && typeof maybeClientCertificate.certfile === 'string' && - 'keyfile' in maybeClientCertificate && typeof maybeClientCertificate.keyfile === 'string' && - (!('password' in maybeClientCertificate) || maybeClientCertificate.password == null || typeof maybeClientCertificate.password === 'string') + 'certfile' in maybeClientCertificate && isCertFile(maybeClientCertificate.certfile) && + 'keyfile' in maybeClientCertificate && isKeyFile(maybeClientCertificate.keyfile) && + isStringOrNotPresent('password', maybeClientCertificate) +} + +/** + * Check value is a cert file + * @private + * @param {any} value the value + * @returns {boolean} is a cert file + */ +function isCertFile (value: unknown): value is string | string [] { + return isString(value) || isArrayOf(value, isString) +} + +/** + * Check if the value is a keyfile. + * + * @private + * @param {any} maybeKeyFile might be a keyfile value + * @returns {boolean} the value is a KeyFile + */ +function isKeyFile (maybeKeyFile: unknown): maybeKeyFile is KeyFile { + function check (obj: unknown): obj is KeyFile { + return typeof obj === 'string' || + (obj != null && + typeof obj === 'object' && + 'path' in obj && typeof obj.path === 'string' && + isStringOrNotPresent('password', obj)) + } + + return check(maybeKeyFile) || isArrayOf(maybeKeyFile, check) +} + +/** + * Verify if value is string + * + * @private + * @param {any} value the value + * @returns {boolean} is string + */ +function isString (value: unknown): value is string { + return typeof value === 'string' +} + +/** + * Verifies if value is a array of type + * + * @private + * @param {any} value the value + * @param {function} isType the type checker + * @returns {boolean} value is array of type + */ +function isArrayOf (value: unknown, isType: (val: unknown) => val is T, allowEmpty: boolean = false): value is T[] { + return Array.isArray(value) && + (allowEmpty || value.length > 0) && + value.filter(isType).length === value.length +} + +/** + * Verify if valueName is present in the object and is a string, or not present at all. + * + * @private + * @param {string} valueName The value in the object + * @param {object} obj The object + * @returns {boolean} if the value is present in object as string or not present + */ +function isStringOrNotPresent (valueName: string, obj: Record): boolean { + return !(valueName in obj) || obj[valueName] == null || typeof obj[valueName] === 'string' } /** diff --git a/packages/core/test/client-certificate.test.ts b/packages/core/test/client-certificate.test.ts index 8cfd29968..5dd64d4d2 100644 --- a/packages/core/test/client-certificate.test.ts +++ b/packages/core/test/client-certificate.test.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { ClientCertificateProvider, RotatingClientCertificateProvider, clientCertificateProviders, resolveCertificateProvider } from '../src/client-certificate' +import ClientCertificate, { ClientCertificateProvider, RotatingClientCertificateProvider, clientCertificateProviders, resolveCertificateProvider } from '../src/client-certificate' describe('clientCertificateProviders', () => { describe('.rotating()', () => { @@ -40,34 +40,9 @@ describe('clientCertificateProviders', () => { }) }) - describe.each([ - { - initialCertificate: { - certfile: 'other_file', - keyfile: 'some_file', - password: 'pass' - } - }, - { - initialCertificate: { - certfile: 'other_file', - keyfile: 'some_file' - } - }, - { - initialCertificate: { - get certfile () { return 'other_file' }, - get keyfile () { return 'some_file' }, - get password () { return 'pass' } - } - }, - { - initialCertificate: { - get certfile () { return 'other_file' }, - get keyfile () { return 'some_file' } - } - } - ])('when valid configuration (%o)', (config) => { + describe.each(validCertificates() + .map(initialCertificate => ({ initialCertificate })) + )('when valid configuration (%o)', (config) => { it('should return a RotatingClientCertificateProvider', () => { const provider = clientCertificateProviders.rotating(config) @@ -186,12 +161,7 @@ describe('resolveCertificateProvider', () => { expect(resolveCertificateProvider(input)).toBe(expectedResult) }) - it.each([ - { certfile: 'certfile', keyfile: 'keyfile' }, - { certfile: 'certfile', keyfile: 'keyfile', password: 'password' }, - { get certfile () { return 'the cert file' }, get keyfile () { return 'the key file' } }, - { get certfile () { return 'the cert file' }, get keyfile () { return 'the key file' }, get password () { return 'the password' } } - ])('should a static provider when configured with ClientCertificate ', async (certificate) => { + it.each(validCertificates())('should a static provider when configured with ClientCertificate ', async (certificate) => { const maybeProvider = resolveCertificateProvider(certificate) expect(maybeProvider).toBeDefined() @@ -235,6 +205,49 @@ function invalidCertificates (): any[] { { certfile: '123', keyfile: 'file', password: 123 }, { certfile () { return 'the cert file' }, get keyfile () { return 'the key file' } }, { get certfile () { return 'the cert file' }, keyfile () { return 'the key file' } }, - { get certfile () { return 'the cert file' }, get keyfile () { return 'the key file' }, password () { return 'the password' } } + { get certfile () { return 'the cert file' }, get keyfile () { return 'the key file' }, password () { return 'the password' } }, + // key file as object + { certfile: 'certfile', keyfile: { } }, + { certfile: 'certfile', keyfile: { path: null, password: 142 } }, + { certfile: 'certfile', keyfile: { path: 1123 }, password: 'the password' }, + { certfile: 'certfile', keyfile: { path: 'the key path', password: 456 }, password: 'the password' }, + // key file as object and getter + { certfile: 'certfile', get keyfile () { return { path: 1919 } } }, + { certfile: 'certfile', get keyfile () { return { path: 'the key path', password: {} } } }, + { certfile: 'certfile', get keyfile () { return { path: { path: 'path' } } }, password: 'the password' }, + { certfile: 'certfile', get keyfile () { return { path: ['123'], password: 'password' } }, password: 'the password' }, + // multiple certificates + { certfile: ['certfile'], keyfile: [] }, + { certfile: [], keyfile: ['keyfile'], password: 'password' }, + { certfile: [1234], keyfile: ['keyfile'] }, + { certfile: ['certfile'], keyfile: [1234], password: 'password' }, + { certfile: ['certfile'], keyfile: [{ path: 1234 }] }, + { certfile: ['certfile'], keyfile: [{ path: 'the key path', password: 1234 }], password: 'password' } + ] +} + +function validCertificates (): ClientCertificate[] { + return [ + // strings + { certfile: 'certfile', keyfile: 'keyfile' }, + { certfile: 'certfile', keyfile: 'keyfile', password: 'password' }, + // string getters + { get certfile () { return 'the cert file' }, get keyfile () { return 'the key file' } }, + { get certfile () { return 'the cert file' }, get keyfile () { return 'the key file' }, get password () { return 'the password' } }, + // key file as object + { certfile: 'certfile', keyfile: { path: 'the key path' } }, + { certfile: 'certfile', keyfile: { path: 'the key path', password: 'password' } }, + { certfile: 'certfile', keyfile: { path: 'the key path' }, password: 'the password' }, + { certfile: 'certfile', keyfile: { path: 'the key path', password: 'password' }, password: 'the password' }, + // key file as object and getter + { certfile: 'certfile', get keyfile () { return { path: 'the key path' } } }, + { certfile: 'certfile', get keyfile () { return { path: 'the key path', password: 'password' } } }, + { certfile: 'certfile', get keyfile () { return { path: 'the key path' } }, password: 'the password' }, + { certfile: 'certfile', get keyfile () { return { path: 'the key path', password: 'password' } }, password: 'the password' }, + // multiple certificates + { certfile: ['certfile'], keyfile: ['keyfile'] }, + { certfile: ['certfile'], keyfile: ['keyfile'], password: 'password' }, + { certfile: ['certfile'], keyfile: [{ path: 'the key path' }] }, + { certfile: ['certfile'], keyfile: [{ path: 'the key path', password: 'password' }], password: 'password' } ] } diff --git a/packages/neo4j-driver-deno/lib/core/client-certificate.ts b/packages/neo4j-driver-deno/lib/core/client-certificate.ts index 9fcff6c4f..a3156825a 100644 --- a/packages/neo4j-driver-deno/lib/core/client-certificate.ts +++ b/packages/neo4j-driver-deno/lib/core/client-certificate.ts @@ -17,31 +17,46 @@ import * as json from './json.ts' +type KeyFile = string | { path: string, password?: string } + /** * Holds the Client TLS certificate information. * * Browser instances of the driver should configure the certificate * in the system. * + * Files defined in the {@link ClientCertificate#certfile} + * and {@link ClientCertificate#keyfile} will read and loaded to + * memory to fill the fields `cert` and `key` in security context. + * * @interface + * @see https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions + */ +/** + * Represents KeyFile represented as file. + * + * @typedef {object} KeyFileObject + * @property {string} path - The path of the file + * @property {string?} password - the password of the key. If none, + * the password defined at {@link ClientCertificate} will be used. */ export default class ClientCertificate { - public readonly certfile: string - public readonly keyfile: string + public readonly certfile: string | string[] + public readonly keyfile: KeyFile | KeyFile[] public readonly password?: string private constructor () { /** * The path to client certificate file. * - * @type {string} + * @type {string | string} */ this.certfile = '' /** * The path to the key file. * - * @type {string} + * @type {string | string[] | KeyFileObject | KeyFileObject[] } */ this.keyfile = '' @@ -190,9 +205,75 @@ export function resolveCertificateProvider (input: unknown): ClientCertificatePr function isClientClientCertificate (maybeClientCertificate: unknown): maybeClientCertificate is ClientCertificate { return maybeClientCertificate != null && typeof maybeClientCertificate === 'object' && - 'certfile' in maybeClientCertificate && typeof maybeClientCertificate.certfile === 'string' && - 'keyfile' in maybeClientCertificate && typeof maybeClientCertificate.keyfile === 'string' && - (!('password' in maybeClientCertificate) || maybeClientCertificate.password == null || typeof maybeClientCertificate.password === 'string') + 'certfile' in maybeClientCertificate && isCertFile(maybeClientCertificate.certfile) && + 'keyfile' in maybeClientCertificate && isKeyFile(maybeClientCertificate.keyfile) && + isStringOrNotPresent('password', maybeClientCertificate) +} + +/** + * Check value is a cert file + * @private + * @param {any} value the value + * @returns {boolean} is a cert file + */ +function isCertFile (value: unknown): value is string | string [] { + return isString(value) || isArrayOf(value, isString) +} + +/** + * Check if the value is a keyfile. + * + * @private + * @param {any} maybeKeyFile might be a keyfile value + * @returns {boolean} the value is a KeyFile + */ +function isKeyFile (maybeKeyFile: unknown): maybeKeyFile is KeyFile { + function check (obj: unknown): obj is KeyFile { + return typeof obj === 'string' || + (obj != null && + typeof obj === 'object' && + 'path' in obj && typeof obj.path === 'string' && + isStringOrNotPresent('password', obj)) + } + + return check(maybeKeyFile) || isArrayOf(maybeKeyFile, check) +} + +/** + * Verify if value is string + * + * @private + * @param {any} value the value + * @returns {boolean} is string + */ +function isString (value: unknown): value is string { + return typeof value === 'string' +} + +/** + * Verifies if value is a array of type + * + * @private + * @param {any} value the value + * @param {function} isType the type checker + * @returns {boolean} value is array of type + */ +function isArrayOf (value: unknown, isType: (val: unknown) => val is T, allowEmpty: boolean = false): value is T[] { + return Array.isArray(value) && + (allowEmpty || value.length > 0) && + value.filter(isType).length === value.length +} + +/** + * Verify if valueName is present in the object and is a string, or not present at all. + * + * @private + * @param {string} valueName The value in the object + * @param {object} obj The object + * @returns {boolean} if the value is present in object as string or not present + */ +function isStringOrNotPresent (valueName: string, obj: Record): boolean { + return !(valueName in obj) || obj[valueName] == null || typeof obj[valueName] === 'string' } /** From 0172cbdcfd2baddafc3ec6aca2a66b2d53242e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Fri, 23 Feb 2024 12:47:44 +0100 Subject: [PATCH 14/23] Docs adjustments --- packages/core/src/client-certificate.ts | 35 +++++++++++++++------- packages/core/src/connection-provider.ts | 3 ++ packages/core/src/connection.ts | 38 ++++++++++++++++++++++++ packages/core/src/driver.ts | 2 ++ 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/packages/core/src/client-certificate.ts b/packages/core/src/client-certificate.ts index 8c78aa1cc..d9ed7a09d 100644 --- a/packages/core/src/client-certificate.ts +++ b/packages/core/src/client-certificate.ts @@ -19,6 +19,14 @@ import * as json from './json' type KeyFile = string | { path: string, password?: string } +/** + * Represents KeyFile represented as file. + * + * @typedef {object} KeyFileObject + * @property {string} path - The path of the file + * @property {string|undefined} password - the password of the key. If none, + * the password defined at {@link ClientCertificate} will be used. + */ /** * Holds the Client TLS certificate information. * @@ -32,14 +40,6 @@ type KeyFile = string | { path: string, password?: string } * @interface * @see https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions */ -/** - * Represents KeyFile represented as file. - * - * @typedef {object} KeyFileObject - * @property {string} path - The path of the file - * @property {string?} password - the password of the key. If none, - * the password defined at {@link ClientCertificate} will be used. - */ export default class ClientCertificate { public readonly certfile: string | string[] public readonly keyfile: KeyFile | KeyFile[] @@ -49,14 +49,14 @@ export default class ClientCertificate { /** * The path to client certificate file. * - * @type {string | string} + * @type {string|string[]} */ this.certfile = '' /** * The path to the key file. * - * @type {string | string[] | KeyFileObject | KeyFileObject[] } + * @type {string|string[]|KeyFileObject|KeyFileObject[]} */ this.keyfile = '' @@ -200,7 +200,6 @@ export function resolveCertificateProvider (input: unknown): ClientCertificatePr * @private * @param maybeClientCertificate - Maybe the certificate * @returns {boolean} if maybeClientCertificate is a client certificate object - * */ function isClientClientCertificate (maybeClientCertificate: unknown): maybeClientCertificate is ClientCertificate { return maybeClientCertificate != null && @@ -287,6 +286,11 @@ class InternalRotatingClientCertificateProvider { private _updated: boolean = false) { } + /** + * + * @returns {boolean|Promise} + */ + hasUpdate (): boolean | Promise { try { return this._updated @@ -295,10 +299,19 @@ class InternalRotatingClientCertificateProvider { } } + /** + * + * @returns {ClientCertificate|Promise} + */ getClientCertificate (): ClientCertificate | Promise { return this._certificate } + /** + * + * @param certificate + * @returns {void} + */ updateCertificate (certificate: ClientCertificate): void { if (!isClientClientCertificate(certificate)) { throw new TypeError(`certificate should be ClientCertificate, but got ${json.stringify(certificate)}`) diff --git a/packages/core/src/connection-provider.ts b/packages/core/src/connection-provider.ts index 226358a1f..0cfacc1d4 100644 --- a/packages/core/src/connection-provider.ts +++ b/packages/core/src/connection-provider.ts @@ -28,6 +28,9 @@ import { AuthToken } from './types' * @interface */ class Releasable { + /** + * @returns {Promise} + */ release (): Promise { throw new Error('Not implemented') } diff --git a/packages/core/src/connection.ts b/packages/core/src/connection.ts index 568dc2128..ed2e72cfa 100644 --- a/packages/core/src/connection.ts +++ b/packages/core/src/connection.ts @@ -72,34 +72,72 @@ interface RunQueryConfig extends BeginTransactionConfig { * @interface */ class Connection { + /** + * + * @param config + * @returns {ResultStreamObserver} + */ beginTransaction (config: BeginTransactionConfig): ResultStreamObserver { throw new Error('Not implemented') } + /** + * + * @param query + * @param parameters + * @param config + * @returns {ResultStreamObserver} + */ run (query: string, parameters?: Record, config?: RunQueryConfig): ResultStreamObserver { throw new Error('Not implemented') } + /** + * + * @param config + * @returns {ResultStreamObserver} + */ commitTransaction (config: CommitTransactionConfig): ResultStreamObserver { throw new Error('Not implemented') } + /** + * + * @param config + * @returns {ResultStreamObserver} + */ rollbackTransaction (config: RollbackConnectionConfig): ResultStreamObserver { throw new Error('Not implemented') } + /** + * + * @returns {Promise} + */ resetAndFlush (): Promise { throw new Error('Not implemented') } + /** + * + * @returns {boolean} + */ isOpen (): boolean { throw new Error('Not implemented') } + /** + * + * @returns {number} + */ getProtocolVersion (): number { throw new Error('Not implemented') } + /** + * + * @returns {boolean} + */ hasOngoingObservableRequests (): boolean { throw new Error('Not implemented') } diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index d2476810e..e045204cb 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -906,6 +906,7 @@ function validateConfig (config: any, log: Logger): any { /** * @private + * @returns {void} */ function sanitizeConfig (config: any): void { config.maxConnectionLifetime = sanitizeIntValue( @@ -932,6 +933,7 @@ function sanitizeConfig (config: any): void { /** * @private + * @returns {number} */ function sanitizeIntValue (rawValue: any, defaultWhenAbsent: number): number { const sanitizedValue = parseInt(rawValue, 10) From b7d2ab0e305c3a955442485369c3f38ae39eda58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Fri, 23 Feb 2024 12:48:26 +0100 Subject: [PATCH 15/23] sync deno --- .../lib/core/client-certificate.ts | 35 +++++++++++------ .../lib/core/connection-provider.ts | 3 ++ .../neo4j-driver-deno/lib/core/connection.ts | 38 +++++++++++++++++++ packages/neo4j-driver-deno/lib/core/driver.ts | 2 + 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/client-certificate.ts b/packages/neo4j-driver-deno/lib/core/client-certificate.ts index a3156825a..b97194827 100644 --- a/packages/neo4j-driver-deno/lib/core/client-certificate.ts +++ b/packages/neo4j-driver-deno/lib/core/client-certificate.ts @@ -19,6 +19,14 @@ import * as json from './json.ts' type KeyFile = string | { path: string, password?: string } +/** + * Represents KeyFile represented as file. + * + * @typedef {object} KeyFileObject + * @property {string} path - The path of the file + * @property {string|undefined} password - the password of the key. If none, + * the password defined at {@link ClientCertificate} will be used. + */ /** * Holds the Client TLS certificate information. * @@ -32,14 +40,6 @@ type KeyFile = string | { path: string, password?: string } * @interface * @see https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions */ -/** - * Represents KeyFile represented as file. - * - * @typedef {object} KeyFileObject - * @property {string} path - The path of the file - * @property {string?} password - the password of the key. If none, - * the password defined at {@link ClientCertificate} will be used. - */ export default class ClientCertificate { public readonly certfile: string | string[] public readonly keyfile: KeyFile | KeyFile[] @@ -49,14 +49,14 @@ export default class ClientCertificate { /** * The path to client certificate file. * - * @type {string | string} + * @type {string|string[]} */ this.certfile = '' /** * The path to the key file. * - * @type {string | string[] | KeyFileObject | KeyFileObject[] } + * @type {string|string[]|KeyFileObject|KeyFileObject[]} */ this.keyfile = '' @@ -200,7 +200,6 @@ export function resolveCertificateProvider (input: unknown): ClientCertificatePr * @private * @param maybeClientCertificate - Maybe the certificate * @returns {boolean} if maybeClientCertificate is a client certificate object - * */ function isClientClientCertificate (maybeClientCertificate: unknown): maybeClientCertificate is ClientCertificate { return maybeClientCertificate != null && @@ -287,6 +286,11 @@ class InternalRotatingClientCertificateProvider { private _updated: boolean = false) { } + /** + * + * @returns {boolean|Promise} + */ + hasUpdate (): boolean | Promise { try { return this._updated @@ -295,10 +299,19 @@ class InternalRotatingClientCertificateProvider { } } + /** + * + * @returns {ClientCertificate|Promise} + */ getClientCertificate (): ClientCertificate | Promise { return this._certificate } + /** + * + * @param certificate + * @returns {void} + */ updateCertificate (certificate: ClientCertificate): void { if (!isClientClientCertificate(certificate)) { throw new TypeError(`certificate should be ClientCertificate, but got ${json.stringify(certificate)}`) diff --git a/packages/neo4j-driver-deno/lib/core/connection-provider.ts b/packages/neo4j-driver-deno/lib/core/connection-provider.ts index ab4a7dc96..977aeeada 100644 --- a/packages/neo4j-driver-deno/lib/core/connection-provider.ts +++ b/packages/neo4j-driver-deno/lib/core/connection-provider.ts @@ -28,6 +28,9 @@ import { AuthToken } from './types.ts' * @interface */ class Releasable { + /** + * @returns {Promise} + */ release (): Promise { throw new Error('Not implemented') } diff --git a/packages/neo4j-driver-deno/lib/core/connection.ts b/packages/neo4j-driver-deno/lib/core/connection.ts index dfe08e7bc..53cdbdb9d 100644 --- a/packages/neo4j-driver-deno/lib/core/connection.ts +++ b/packages/neo4j-driver-deno/lib/core/connection.ts @@ -72,34 +72,72 @@ interface RunQueryConfig extends BeginTransactionConfig { * @interface */ class Connection { + /** + * + * @param config + * @returns {ResultStreamObserver} + */ beginTransaction (config: BeginTransactionConfig): ResultStreamObserver { throw new Error('Not implemented') } + /** + * + * @param query + * @param parameters + * @param config + * @returns {ResultStreamObserver} + */ run (query: string, parameters?: Record, config?: RunQueryConfig): ResultStreamObserver { throw new Error('Not implemented') } + /** + * + * @param config + * @returns {ResultStreamObserver} + */ commitTransaction (config: CommitTransactionConfig): ResultStreamObserver { throw new Error('Not implemented') } + /** + * + * @param config + * @returns {ResultStreamObserver} + */ rollbackTransaction (config: RollbackConnectionConfig): ResultStreamObserver { throw new Error('Not implemented') } + /** + * + * @returns {Promise} + */ resetAndFlush (): Promise { throw new Error('Not implemented') } + /** + * + * @returns {boolean} + */ isOpen (): boolean { throw new Error('Not implemented') } + /** + * + * @returns {number} + */ getProtocolVersion (): number { throw new Error('Not implemented') } + /** + * + * @returns {boolean} + */ hasOngoingObservableRequests (): boolean { throw new Error('Not implemented') } diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 9ba6e07cf..3f7a81b36 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -906,6 +906,7 @@ function validateConfig (config: any, log: Logger): any { /** * @private + * @returns {void} */ function sanitizeConfig (config: any): void { config.maxConnectionLifetime = sanitizeIntValue( @@ -932,6 +933,7 @@ function sanitizeConfig (config: any): void { /** * @private + * @returns {number} */ function sanitizeIntValue (rawValue: any, defaultWhenAbsent: number): number { const sanitizedValue = parseInt(rawValue, 10) From 35b2ee3f5f8bb58a320aa514fe8cd8b73c5640eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Fri, 23 Feb 2024 13:18:59 +0100 Subject: [PATCH 16/23] Loading files --- .../browser-client-certificates-loader.js | 21 ++++++ .../src/channel/browser/index.js | 3 +- .../deno/deno-client-certificates-loader.js | 21 ++++++ .../bolt-connection/src/channel/deno/index.js | 3 +- .../bolt-connection/src/channel/node/index.js | 3 +- .../src/channel/node/node-channel.js | 18 ++--- .../node/node-client-certificates-loader.js | 65 +++++++++++++++++++ .../connection-provider-pooled.js | 2 + .../browser-client-certificates-loader.js | 21 ++++++ .../bolt-connection/channel/browser/index.js | 3 +- .../deno/deno-client-certificates-loader.js | 21 ++++++ .../lib/bolt-connection/channel/deno/index.js | 3 +- .../lib/bolt-connection/channel/node/index.js | 3 +- .../channel/node/node-channel.js | 18 ++--- .../node/node-client-certificates-loader.js | 65 +++++++++++++++++++ .../connection-provider-pooled.js | 2 + 16 files changed, 240 insertions(+), 32 deletions(-) create mode 100644 packages/bolt-connection/src/channel/browser/browser-client-certificates-loader.js create mode 100644 packages/bolt-connection/src/channel/deno/deno-client-certificates-loader.js create mode 100644 packages/bolt-connection/src/channel/node/node-client-certificates-loader.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-client-certificates-loader.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-client-certificates-loader.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-client-certificates-loader.js diff --git a/packages/bolt-connection/src/channel/browser/browser-client-certificates-loader.js b/packages/bolt-connection/src/channel/browser/browser-client-certificates-loader.js new file mode 100644 index 000000000..2bc1d5619 --- /dev/null +++ b/packages/bolt-connection/src/channel/browser/browser-client-certificates-loader.js @@ -0,0 +1,21 @@ +/** + * 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. + */ +export default { + async load (clientCertificate) { + return clientCertificate + } +} diff --git a/packages/bolt-connection/src/channel/browser/index.js b/packages/bolt-connection/src/channel/browser/index.js index 25fc2aeb4..e27fec4a3 100644 --- a/packages/bolt-connection/src/channel/browser/index.js +++ b/packages/bolt-connection/src/channel/browser/index.js @@ -17,7 +17,7 @@ import WebSocketChannel from './browser-channel' import BrowserHosNameResolver from './browser-host-name-resolver' - +import BrowserClientCertificatesLoader from './browser-client-certificates-loader' /* 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 */ export const Channel = WebSocketChannel export const HostNameResolver = BrowserHosNameResolver +export const ClientCertificatesLoader = BrowserClientCertificatesLoader diff --git a/packages/bolt-connection/src/channel/deno/deno-client-certificates-loader.js b/packages/bolt-connection/src/channel/deno/deno-client-certificates-loader.js new file mode 100644 index 000000000..2bc1d5619 --- /dev/null +++ b/packages/bolt-connection/src/channel/deno/deno-client-certificates-loader.js @@ -0,0 +1,21 @@ +/** + * 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. + */ +export default { + async load (clientCertificate) { + return clientCertificate + } +} diff --git a/packages/bolt-connection/src/channel/deno/index.js b/packages/bolt-connection/src/channel/deno/index.js index e1746391a..b9b5585a0 100644 --- a/packages/bolt-connection/src/channel/deno/index.js +++ b/packages/bolt-connection/src/channel/deno/index.js @@ -17,7 +17,7 @@ import DenoChannel from './deno-channel' import DenoHostNameResolver from './deno-host-name-resolver' - +import DenoClientCertificatesLoader from './deno-client-certificates-loader' /* This module exports a set of components to be used in deno environment. @@ -30,3 +30,4 @@ import DenoHostNameResolver from './deno-host-name-resolver' */ export const Channel = DenoChannel export const HostNameResolver = DenoHostNameResolver +export const ClientCertificatesLoader = DenoClientCertificatesLoader diff --git a/packages/bolt-connection/src/channel/node/index.js b/packages/bolt-connection/src/channel/node/index.js index 1923d6b59..e8d61dcb4 100644 --- a/packages/bolt-connection/src/channel/node/index.js +++ b/packages/bolt-connection/src/channel/node/index.js @@ -17,7 +17,7 @@ import NodeChannel from './node-channel' import NodeHostNameResolver from './node-host-name-resolver' - +import NodeClientCertificatesLoader from './node-client-certificates-loader' /* 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 export const Channel = NodeChannel export const HostNameResolver = NodeHostNameResolver +export const ClientCertificatesLoader = NodeClientCertificatesLoader diff --git a/packages/bolt-connection/src/channel/node/node-channel.js b/packages/bolt-connection/src/channel/node/node-channel.js index ada96d64f..f682bb150 100644 --- a/packages/bolt-connection/src/channel/node/node-channel.js +++ b/packages/bolt-connection/src/channel/node/node-channel.js @@ -50,9 +50,7 @@ const TrustStrategy = { const tlsOpts = newTlsOptions( config.address.host(), config.trustedCertificates.map(f => fs.readFileSync(f)), - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.keyfile) : undefined, - config.clientCertificate != null ? config.clientCertificate.password : undefined + config.clientCertificate ) const socket = tls.connect( config.address.port(), @@ -85,9 +83,7 @@ const TrustStrategy = { const tlsOpts = newTlsOptions( config.address.host(), undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.keyfile) : undefined, - config.clientCertificate != null ? config.clientCertificate.password : undefined + config.clientCertificate ) const socket = tls.connect( config.address.port(), @@ -121,9 +117,7 @@ const TrustStrategy = { const tlsOpts = newTlsOptions( config.address.host(), undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.keyfile) : undefined, - config.clientCertificate != null ? config.clientCertificate.password : undefined + config.clientCertificate ) const socket = tls.connect( config.address.port(), @@ -218,14 +212,12 @@ function trustStrategyName (config) { * @param {string|undefined} passphrase an optional client cert passphrase * @return {Object} a new options object. */ -function newTlsOptions (hostname, ca = undefined, cert = undefined, key = undefined, passphrase = undefined) { +function newTlsOptions (hostname, ca = undefined, clientCertificate = undefined) { return { rejectUnauthorized: false, // we manually check for this in the connect callback, to give a more helpful error to the user servername: hostname, // server name for the SNI (Server Name Indication) TLS extension ca, // optional CA useful for TRUST_CUSTOM_CA_SIGNED_CERTIFICATES trust mode, - cert, - key, - passphrase + ...clientCertificate } } diff --git a/packages/bolt-connection/src/channel/node/node-client-certificates-loader.js b/packages/bolt-connection/src/channel/node/node-client-certificates-loader.js new file mode 100644 index 000000000..17381d0d3 --- /dev/null +++ b/packages/bolt-connection/src/channel/node/node-client-certificates-loader.js @@ -0,0 +1,65 @@ +/** + * 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. + */ + +import fs from 'fs' + +function readFile (file) { + return new Promise((resolve, reject) => fs.readFile(file, (err, data) => { + if (err) { + return reject(err) + } + return resolve(data) + })) +} + +function loadCert (fileOrFiles) { + if (Array.isArray(fileOrFiles)) { + return Promise.all(fileOrFiles.map(loadCert)) + } + return readFile(fileOrFiles) +} + +function loadKey (fileOrFiles) { + if (Array.isArray(fileOrFiles)) { + return Promise.all(fileOrFiles.map(loadKey)) + } + + if (typeof fileOrFiles === 'string') { + return readFile(fileOrFiles) + } + + return readFile(fileOrFiles.path) + .then(pem => ({ + pem, + passphrase: fileOrFiles.password + })) +} + +export default { + async load (clientCertificate) { + const certPromise = loadCert(clientCertificate.certfile) + const keyPromise = loadKey(clientCertificate.keyfile) + + const [cert, key] = await Promise.all([certPromise, keyPromise]) + + return { + cert, + key, + passphrase: clientCertificate.password + } + } +} diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 931bb01c7..08a3e917a 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -21,6 +21,7 @@ import { error, ConnectionProvider, ServerInfo, newError } from 'neo4j-driver-co import AuthenticationProvider from './authentication-provider' import { object } from '../lang' import LivenessCheckProvider from './liveness-check-provider' +import { ClientCertificatesLoader } from '../channel' const { SERVICE_UNAVAILABLE } = error const AUTHENTICATION_ERRORS = [ @@ -84,6 +85,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { } if (this._clientCertificate == null || await this._config.clientCertificate.hasUpdate()) { this._clientCertificate = this._getClientCertificate() + .then(ClientCertificatesLoader.load) .then(clientCertificate => { this._clientCertificate = clientCertificate }) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-client-certificates-loader.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-client-certificates-loader.js new file mode 100644 index 000000000..2bc1d5619 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-client-certificates-loader.js @@ -0,0 +1,21 @@ +/** + * 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. + */ +export default { + async load (clientCertificate) { + return clientCertificate + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/index.js index c6359740b..32ac0d984 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/index.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/index.js @@ -17,7 +17,7 @@ import WebSocketChannel from './browser-channel.js' import BrowserHosNameResolver from './browser-host-name-resolver.js' - +import BrowserClientCertificatesLoader from './browser-client-certificates-loader.js' /* 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 */ export const Channel = WebSocketChannel export const HostNameResolver = BrowserHosNameResolver +export const ClientCertificatesLoader = BrowserClientCertificatesLoader diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-client-certificates-loader.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-client-certificates-loader.js new file mode 100644 index 000000000..2bc1d5619 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-client-certificates-loader.js @@ -0,0 +1,21 @@ +/** + * 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. + */ +export default { + async load (clientCertificate) { + return clientCertificate + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/index.js index cc1e00925..b39fb3fd5 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/index.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/index.js @@ -17,7 +17,7 @@ import DenoChannel from './deno-channel.js' import DenoHostNameResolver from './deno-host-name-resolver.js' - +import DenoClientCertificatesLoader from './deno-client-certificates-loader.js' /* This module exports a set of components to be used in deno environment. @@ -30,3 +30,4 @@ import DenoHostNameResolver from './deno-host-name-resolver.js' */ export const Channel = DenoChannel export const HostNameResolver = DenoHostNameResolver +export const ClientCertificatesLoader = DenoClientCertificatesLoader diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/index.js index 4ca485e5a..72506cf33 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/index.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/index.js @@ -17,7 +17,7 @@ import NodeChannel from './node-channel.js' import NodeHostNameResolver from './node-host-name-resolver.js' - +import NodeClientCertificatesLoader from './node-client-certificates-loader.js' /* 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 export const Channel = NodeChannel export const HostNameResolver = NodeHostNameResolver +export const ClientCertificatesLoader = NodeClientCertificatesLoader diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js index 511726836..a3faa8e0c 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js @@ -50,9 +50,7 @@ const TrustStrategy = { const tlsOpts = newTlsOptions( config.address.host(), config.trustedCertificates.map(f => fs.readFileSync(f)), - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.keyfile) : undefined, - config.clientCertificate != null ? config.clientCertificate.password : undefined + config.clientCertificate ) const socket = tls.connect( config.address.port(), @@ -85,9 +83,7 @@ const TrustStrategy = { const tlsOpts = newTlsOptions( config.address.host(), undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.keyfile) : undefined, - config.clientCertificate != null ? config.clientCertificate.password : undefined + config.clientCertificate ) const socket = tls.connect( config.address.port(), @@ -121,9 +117,7 @@ const TrustStrategy = { const tlsOpts = newTlsOptions( config.address.host(), undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.certfile) : undefined, - config.clientCertificate != null ? fs.readFileSync(config.clientCertificate.keyfile) : undefined, - config.clientCertificate != null ? config.clientCertificate.password : undefined + config.clientCertificate ) const socket = tls.connect( config.address.port(), @@ -218,14 +212,12 @@ function trustStrategyName (config) { * @param {string|undefined} passphrase an optional client cert passphrase * @return {Object} a new options object. */ -function newTlsOptions (hostname, ca = undefined, cert = undefined, key = undefined, passphrase = undefined) { +function newTlsOptions (hostname, ca = undefined, clientCertificate = undefined) { return { rejectUnauthorized: false, // we manually check for this in the connect callback, to give a more helpful error to the user servername: hostname, // server name for the SNI (Server Name Indication) TLS extension ca, // optional CA useful for TRUST_CUSTOM_CA_SIGNED_CERTIFICATES trust mode, - cert, - key, - passphrase + ...clientCertificate } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-client-certificates-loader.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-client-certificates-loader.js new file mode 100644 index 000000000..17381d0d3 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-client-certificates-loader.js @@ -0,0 +1,65 @@ +/** + * 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. + */ + +import fs from 'fs' + +function readFile (file) { + return new Promise((resolve, reject) => fs.readFile(file, (err, data) => { + if (err) { + return reject(err) + } + return resolve(data) + })) +} + +function loadCert (fileOrFiles) { + if (Array.isArray(fileOrFiles)) { + return Promise.all(fileOrFiles.map(loadCert)) + } + return readFile(fileOrFiles) +} + +function loadKey (fileOrFiles) { + if (Array.isArray(fileOrFiles)) { + return Promise.all(fileOrFiles.map(loadKey)) + } + + if (typeof fileOrFiles === 'string') { + return readFile(fileOrFiles) + } + + return readFile(fileOrFiles.path) + .then(pem => ({ + pem, + passphrase: fileOrFiles.password + })) +} + +export default { + async load (clientCertificate) { + const certPromise = loadCert(clientCertificate.certfile) + const keyPromise = loadKey(clientCertificate.keyfile) + + const [cert, key] = await Promise.all([certPromise, keyPromise]) + + return { + cert, + key, + passphrase: clientCertificate.password + } + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index c92cfb1c9..b469a9a1f 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -21,6 +21,7 @@ import { error, ConnectionProvider, ServerInfo, newError } from '../../core/inde import AuthenticationProvider from './authentication-provider.js' import { object } from '../lang/index.js' import LivenessCheckProvider from './liveness-check-provider.js' +import { ClientCertificatesLoader } from '../channel/index.js' const { SERVICE_UNAVAILABLE } = error const AUTHENTICATION_ERRORS = [ @@ -84,6 +85,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { } if (this._clientCertificate == null || await this._config.clientCertificate.hasUpdate()) { this._clientCertificate = this._getClientCertificate() + .then(ClientCertificatesLoader.load) .then(clientCertificate => { this._clientCertificate = clientCertificate }) From 4371b88b9441da16a5aa2842b0b0ad2559731f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Fri, 23 Feb 2024 16:25:04 +0100 Subject: [PATCH 17/23] Test Client Certificate holder --- .../client-certificate-holder.js | 43 +++ .../connection-provider-pooled.js | 19 +- .../connection-provider-routing.js | 3 +- .../client-certificate-holder.test.js | 313 ++++++++++++++++++ .../client-certificate-holder.js | 43 +++ .../connection-provider-pooled.js | 19 +- .../connection-provider-routing.js | 3 +- .../testkit-backend/src/request-handlers.js | 4 + 8 files changed, 411 insertions(+), 36 deletions(-) create mode 100644 packages/bolt-connection/src/connection-provider/client-certificate-holder.js create mode 100644 packages/bolt-connection/test/connection-provider/client-certificate-holder.test.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/client-certificate-holder.js diff --git a/packages/bolt-connection/src/connection-provider/client-certificate-holder.js b/packages/bolt-connection/src/connection-provider/client-certificate-holder.js new file mode 100644 index 000000000..672e2be5d --- /dev/null +++ b/packages/bolt-connection/src/connection-provider/client-certificate-holder.js @@ -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. + */ +import { ClientCertificatesLoader } from '../channel' + +export default class ClientCertificateHolder { + constructor ({ clientCertificateProvider, loader }) { + this._clientCertificateProvider = clientCertificateProvider + this._loader = loader || ClientCertificatesLoader + this._clientCertificate = null + } + + async getClientCertificate () { + if (this._clientCertificateProvider != null && + (this._clientCertificate == null || await this._clientCertificateProvider.hasUpdate())) { + this._clientCertificate = Promise.resolve(this._clientCertificateProvider.getClientCertificate()) + .then(this._loader.load) + .then(clientCertificate => { + this._clientCertificate = clientCertificate + return this._clientCertificate + }) + .catch(error => { + this._clientCertificate = null + throw error + }) + } + + return this._clientCertificate + } +} diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 08a3e917a..3618b49e0 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -21,7 +21,7 @@ import { error, ConnectionProvider, ServerInfo, newError } from 'neo4j-driver-co import AuthenticationProvider from './authentication-provider' import { object } from '../lang' import LivenessCheckProvider from './liveness-check-provider' -import { ClientCertificatesLoader } from '../channel' +import ClientCertificateHolder from './client-certificate-holder' const { SERVICE_UNAVAILABLE } = error const AUTHENTICATION_ERRORS = [ @@ -41,7 +41,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._id = id this._config = config this._log = log - this._clientCertificate = null + this._clientCertificateHolder = new ClientCertificateHolder({ clientCertificateProvider: this._config.clientCertificate }) this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent, boltAgent }) this._livenessCheckProvider = new LivenessCheckProvider({ connectionLivenessCheckTimeout: config.connectionLivenessCheckTimeout }) this._userAgent = userAgent @@ -55,7 +55,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._config, this._createConnectionErrorHandler(), this._log, - await this._clientCertificate + await this._clientCertificateHolder.getClientCertificate() ) }) this._connectionPool = newPool({ @@ -79,19 +79,6 @@ export default class PooledConnectionProvider extends ConnectionProvider { return new ConnectionErrorHandler(SERVICE_UNAVAILABLE) } - async _updateClientCertificateWhenNeeded () { - if (this._config.clientCertificate == null) { - return - } - if (this._clientCertificate == null || await this._config.clientCertificate.hasUpdate()) { - this._clientCertificate = this._getClientCertificate() - .then(ClientCertificatesLoader.load) - .then(clientCertificate => { - this._clientCertificate = clientCertificate - }) - } - } - async _getClientCertificate () { return this._config.clientCertificate.getClientCertificate() } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index f859c0471..8653b64ca 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -72,13 +72,12 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider newPool }) { super({ id, config, log, userAgent, boltAgent, authTokenManager, newPool }, async address => { - await this._updateClientCertificateWhenNeeded() return createChannelConnection( address, this._config, this._createConnectionErrorHandler(), this._log, - await this._clientCertificate, + await this._clientCertificateHolder.getClientCertificate(), this._routingContext ) }) diff --git a/packages/bolt-connection/test/connection-provider/client-certificate-holder.test.js b/packages/bolt-connection/test/connection-provider/client-certificate-holder.test.js new file mode 100644 index 000000000..aa9f8d28f --- /dev/null +++ b/packages/bolt-connection/test/connection-provider/client-certificate-holder.test.js @@ -0,0 +1,313 @@ +/** + * 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. + */ +import { clientCertificateProviders, newError } from 'neo4j-driver-core' +import ClientCertificateHolder from '../../src/connection-provider/client-certificate-holder' + +describe('ClientCertificateHolder', () => { + describe('.getClientCertificate()', () => { + describe('when provider is not set', () => { + it('should resolve as null when provider is not set', async () => { + const config = extendsDefaultConfigWith() + const holder = new ClientCertificateHolder(config) + + await expect(holder.getClientCertificate()).resolves.toBe(null) + expect(config.loader.load).not.toHaveBeenCalled() + }) + }) + + describe('when provider is set', () => { + it('should load and resolve the loaded certificate in the first call', async () => { + const initialCertificate = { + keyfile: 'the file', + certfile: 'cert file' + } + const clientCertificateProvider = clientCertificateProviders.rotating({ + initialCertificate + }) + + const config = extendsDefaultConfigWith({ clientCertificateProvider }) + const holder = new ClientCertificateHolder(config) + + await expect(holder.getClientCertificate()).resolves.toEqual({ + ...initialCertificate, + loaded: true + }) + expect(config.loader.load).toHaveBeenCalledWith(initialCertificate) + }) + + it('should resolve the previous certificate if certificate was not updated', async () => { + const initialCertificate = { + keyfile: 'the file', + certfile: 'cert file' + } + const clientCertificateProvider = clientCertificateProviders.rotating({ + initialCertificate + }) + + const config = extendsDefaultConfigWith({ clientCertificateProvider }) + const holder = new ClientCertificateHolder(config) + + await expect(holder.getClientCertificate()).resolves.toEqual({ + ...initialCertificate, + loaded: true + }) + expect(config.loader.load).toHaveBeenCalledWith(initialCertificate) + + await expect(holder.getClientCertificate()).resolves.toEqual({ + ...initialCertificate, + loaded: true + }) + expect(config.loader.load).toHaveBeenCalledTimes(1) + }) + + it('should update certificate when certificate get updated', async () => { + const initialCertificate = { + keyfile: 'the file', + certfile: 'cert file' + } + const newCertificate = { + keyfile: 'the new file', + certfile: 'new cert file' + } + + const clientCertificateProvider = clientCertificateProviders.rotating({ + initialCertificate + }) + + const config = extendsDefaultConfigWith({ clientCertificateProvider }) + const holder = new ClientCertificateHolder(config) + + await expect(holder.getClientCertificate()).resolves.toEqual({ + ...initialCertificate, + loaded: true + }) + expect(config.loader.load).toHaveBeenCalledWith(initialCertificate) + + clientCertificateProvider.updateCertificate(newCertificate) + + await expect(holder.getClientCertificate()).resolves.toEqual({ + ...newCertificate, + loaded: true + }) + expect(config.loader.load).toHaveBeenCalledTimes(2) + + await expect(holder.getClientCertificate()).resolves.toEqual({ + ...newCertificate, + loaded: true + }) + expect(config.loader.load).toHaveBeenCalledTimes(2) + }) + + it('should return same promise when multiple requests are depending on same loaded certificate', async () => { + const initialCertificate = { + keyfile: 'the file', + certfile: 'cert file' + } + + const clientCertificateProvider = clientCertificateProviders.rotating({ + initialCertificate + }) + + const promiseStates = [] + + clientCertificateProvider.getClientCertificate = jest.fn(() => { + const promiseState = {} + const promise = new Promise((resolve, reject) => { + promiseState.resolve = resolve + promiseState.reject = reject + }) + + promiseStates.push(promiseState) + return promise + }) + + const config = extendsDefaultConfigWith({ clientCertificateProvider }) + const holder = new ClientCertificateHolder(config) + + const certPromises = [ + holder.getClientCertificate(), + holder.getClientCertificate(), + holder.getClientCertificate() + ] + + expect(promiseStates.length).toBe(1) + promiseStates.forEach(promiseState => promiseState.resolve(initialCertificate)) + + for (let i = 0; i < certPromises.length - 1; i++) { + await expect(certPromises[i]).resolves.toEqual({ + ...initialCertificate, + loaded: true + }) + } + + expect(config.loader.load).toHaveBeenCalledTimes(1) + }) + + it('should throws when getting certificates fail', async () => { + const initialCertificate = { + keyfile: 'the file', + certfile: 'cert file' + } + const newCertificate = { + keyfile: 'the new file', + certfile: 'new cert file' + } + const clientCertificateProvider = clientCertificateProviders.rotating({ + initialCertificate + }) + + const config = extendsDefaultConfigWith({ clientCertificateProvider }) + const holder = new ClientCertificateHolder(config) + + await expect(holder.getClientCertificate()).resolves.toEqual({ + ...initialCertificate, + loaded: true + }) + expect(config.loader.load).toHaveBeenCalledWith(initialCertificate) + + clientCertificateProvider.updateCertificate(newCertificate) + + const expectedError = newError('Error') + clientCertificateProvider.getClientCertificate = jest.fn(() => Promise.reject(expectedError)) + + await expect(holder.getClientCertificate()).rejects.toEqual(expectedError) + + expect(config.loader.load).toHaveBeenCalledTimes(1) + }) + + it('should recover from getting certificates failures', async () => { + const initialCertificate = { + keyfile: 'the file', + certfile: 'cert file' + } + const newCertificate = { + keyfile: 'the new file', + certfile: 'new cert file' + } + const clientCertificateProvider = clientCertificateProviders.rotating({ + initialCertificate + }) + + const config = extendsDefaultConfigWith({ clientCertificateProvider }) + const holder = new ClientCertificateHolder(config) + + await expect(holder.getClientCertificate()).resolves.toEqual({ + ...initialCertificate, + loaded: true + }) + expect(config.loader.load).toHaveBeenCalledWith(initialCertificate) + + clientCertificateProvider.updateCertificate(newCertificate) + + const expectedError = newError('Error') + clientCertificateProvider.getClientCertificate = jest.fn(() => Promise.reject(expectedError)) + + await expect(holder.getClientCertificate()).rejects.toEqual(expectedError) + expect(config.loader.load).toHaveBeenCalledTimes(1) + + clientCertificateProvider.getClientCertificate = jest.fn(() => Promise.resolve(newCertificate)) + + await expect(holder.getClientCertificate()).resolves.toEqual({ + ...newCertificate, + loaded: true + }) + expect(config.loader.load).toHaveBeenCalledWith(newCertificate) + }) + + it('should throws when loading certificates fail', async () => { + const initialCertificate = { + keyfile: 'the file', + certfile: 'cert file' + } + const newCertificate = { + keyfile: 'the new file', + certfile: 'new cert file' + } + const clientCertificateProvider = clientCertificateProviders.rotating({ + initialCertificate + }) + + const config = extendsDefaultConfigWith({ clientCertificateProvider }) + const holder = new ClientCertificateHolder(config) + + await expect(holder.getClientCertificate()).resolves.toEqual({ + ...initialCertificate, + loaded: true + }) + expect(config.loader.load).toHaveBeenCalledWith(initialCertificate) + + clientCertificateProvider.updateCertificate(newCertificate) + + const expectedError = newError('Error') + config.loader.load.mockReturnValueOnce(Promise.reject(expectedError)) + + await expect(holder.getClientCertificate()).rejects.toEqual(expectedError) + + expect(config.loader.load).toHaveBeenCalledTimes(2) + + await expect(holder.getClientCertificate()).resolves.toEqual({ + ...newCertificate, + loaded: true + }) + expect(config.loader.load).toHaveBeenCalledWith(newCertificate) + expect(config.loader.load).toHaveBeenCalledTimes(3) + }) + + it('should recover from loading certificates fail', async () => { + const initialCertificate = { + keyfile: 'the file', + certfile: 'cert file' + } + const newCertificate = { + keyfile: 'the new file', + certfile: 'new cert file' + } + const clientCertificateProvider = clientCertificateProviders.rotating({ + initialCertificate + }) + + const config = extendsDefaultConfigWith({ clientCertificateProvider }) + const holder = new ClientCertificateHolder(config) + + await expect(holder.getClientCertificate()).resolves.toEqual({ + ...initialCertificate, + loaded: true + }) + expect(config.loader.load).toHaveBeenCalledWith(initialCertificate) + + clientCertificateProvider.updateCertificate(newCertificate) + + const expectedError = newError('Error') + config.loader.load.mockReturnValueOnce(Promise.reject(expectedError)) + + await expect(holder.getClientCertificate()).rejects.toEqual(expectedError) + + expect(config.loader.load).toHaveBeenCalledTimes(2) + }) + }) + }) +}) + +function extendsDefaultConfigWith (params) { + return { + clientCertificateProvider: null, + loader: { + load: jest.fn(async a => ({ ...a, loaded: true })) + }, + ...params + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/client-certificate-holder.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/client-certificate-holder.js new file mode 100644 index 000000000..0bd5796ea --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/client-certificate-holder.js @@ -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. + */ +import { ClientCertificatesLoader } from '../channel/index.js' + +export default class ClientCertificateHolder { + constructor ({ clientCertificateProvider, loader }) { + this._clientCertificateProvider = clientCertificateProvider + this._loader = loader || ClientCertificatesLoader + this._clientCertificate = null + } + + async getClientCertificate () { + if (this._clientCertificateProvider != null && + (this._clientCertificate == null || await this._clientCertificateProvider.hasUpdate())) { + this._clientCertificate = Promise.resolve(this._clientCertificateProvider.getClientCertificate()) + .then(this._loader.load) + .then(clientCertificate => { + this._clientCertificate = clientCertificate + return this._clientCertificate + }) + .catch(error => { + this._clientCertificate = null + throw error + }) + } + + return this._clientCertificate + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index b469a9a1f..0ae0bb6bd 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -21,7 +21,7 @@ import { error, ConnectionProvider, ServerInfo, newError } from '../../core/inde import AuthenticationProvider from './authentication-provider.js' import { object } from '../lang/index.js' import LivenessCheckProvider from './liveness-check-provider.js' -import { ClientCertificatesLoader } from '../channel/index.js' +import ClientCertificateHolder from './client-certificate-holder.js' const { SERVICE_UNAVAILABLE } = error const AUTHENTICATION_ERRORS = [ @@ -41,7 +41,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._id = id this._config = config this._log = log - this._clientCertificate = null + this._clientCertificateHolder = new ClientCertificateHolder({ clientCertificateProvider: this._config.clientCertificate }) this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent, boltAgent }) this._livenessCheckProvider = new LivenessCheckProvider({ connectionLivenessCheckTimeout: config.connectionLivenessCheckTimeout }) this._userAgent = userAgent @@ -55,7 +55,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._config, this._createConnectionErrorHandler(), this._log, - await this._clientCertificate + await this._clientCertificateHolder.getClientCertificate() ) }) this._connectionPool = newPool({ @@ -79,19 +79,6 @@ export default class PooledConnectionProvider extends ConnectionProvider { return new ConnectionErrorHandler(SERVICE_UNAVAILABLE) } - async _updateClientCertificateWhenNeeded () { - if (this._config.clientCertificate == null) { - return - } - if (this._clientCertificate == null || await this._config.clientCertificate.hasUpdate()) { - this._clientCertificate = this._getClientCertificate() - .then(ClientCertificatesLoader.load) - .then(clientCertificate => { - this._clientCertificate = clientCertificate - }) - } - } - async _getClientCertificate () { return this._config.clientCertificate.getClientCertificate() } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index 1030dd246..fbd1a8b4d 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -72,13 +72,12 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider newPool }) { super({ id, config, log, userAgent, boltAgent, authTokenManager, newPool }, async address => { - await this._updateClientCertificateWhenNeeded() return createChannelConnection( address, this._config, this._createConnectionErrorHandler(), this._log, - await this._clientCertificate, + await this._clientCertificateHolder.getClientCertificate(), this._routingContext ) }) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index c7198c506..abb01f13f 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -86,6 +86,10 @@ export function NewDriver ({ neo4j }, context, data, wire) { config.connectionLivenessCheckTimeout = data.livenessCheckTimeoutMs } + if ('clientCertificate' in data) { + config.clientCertificate = data.clientCertificate + } + let driver try { driver = neo4j.driver(uri, parsedAuthToken, config) From e4aa93c2008f1912341660b12d80e75bb5dd3436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 28 Feb 2024 15:22:16 +0100 Subject: [PATCH 18/23] Fix PooledProvider --- .../src/connection-provider/connection-provider-pooled.js | 1 - .../connection-provider/connection-provider-pooled.js | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 3618b49e0..a85809eff 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -49,7 +49,6 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._createChannelConnection = createChannelConnectionHook || (async address => { - await this._updateClientCertificateWhenNeeded() return createChannelConnection( address, this._config, diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index 0ae0bb6bd..04ca8bf66 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -49,7 +49,6 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._createChannelConnection = createChannelConnectionHook || (async address => { - await this._updateClientCertificateWhenNeeded() return createChannelConnection( address, this._config, From ea8985b630675d1750fc0a50b5d3f3dd10e2997b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 29 Feb 2024 14:40:15 +0100 Subject: [PATCH 19/23] Consolidating clientCertificate data type --- packages/testkit-backend/src/request-handlers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index abb01f13f..faa484726 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -87,7 +87,7 @@ export function NewDriver ({ neo4j }, context, data, wire) { } if ('clientCertificate' in data) { - config.clientCertificate = data.clientCertificate + config.clientCertificate = data.clientCertificate.data } let driver From be44fa3971e34ecfd5f3c76bc06dd8adb6d2a9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 29 Feb 2024 17:03:41 +0100 Subject: [PATCH 20/23] Add feature flag and skip tests where needed --- packages/testkit-backend/src/feature/common.js | 1 + packages/testkit-backend/src/request-handlers.js | 2 +- packages/testkit-backend/src/skipped-tests/deno.js | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index 1e420e447..772e1b267 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -7,6 +7,7 @@ const features = [ 'Feature:API:BookmarkManager', 'Feature:API:RetryableExceptions', 'Feature:API:Session:AuthConfig', + 'Feature:API:SSLClientCertificate', 'Feature:API:SSLConfig', 'Feature:API:SSLSchemes', 'Feature:API:Type.Temporal', diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index faa484726..e7391b60e 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -86,7 +86,7 @@ export function NewDriver ({ neo4j }, context, data, wire) { config.connectionLivenessCheckTimeout = data.livenessCheckTimeoutMs } - if ('clientCertificate' in data) { + if (data.clientCertificate != null) { config.clientCertificate = data.clientCertificate.data } diff --git a/packages/testkit-backend/src/skipped-tests/deno.js b/packages/testkit-backend/src/skipped-tests/deno.js index e2dd2eec2..90d3891f0 100644 --- a/packages/testkit-backend/src/skipped-tests/deno.js +++ b/packages/testkit-backend/src/skipped-tests/deno.js @@ -8,6 +8,9 @@ const skippedTests = [ ifEndsWith('test_untrusted_ca_correct_hostname'), ifEndsWith('test_1_1') ), + skip('DenoJS does not support client certificates', + ifStartsWith('tls.test_client_certificate.') + ), skip('Trust All is not available as configuration', ifStartsWith('tls.test_self_signed_scheme.TestTrustAllCertsConfig.'), ifStartsWith('tls.test_self_signed_scheme.TestSelfSignedScheme.') From 3611fc35ceeaaca6ab1c4f74bd96fcccf619a235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 29 Feb 2024 17:13:51 +0100 Subject: [PATCH 21/23] Set feature as preview --- packages/core/src/client-certificate.ts | 12 ++++++++++++ packages/core/src/types.ts | 2 ++ .../neo4j-driver-deno/lib/core/client-certificate.ts | 12 ++++++++++++ packages/neo4j-driver-deno/lib/core/types.ts | 2 ++ 4 files changed, 28 insertions(+) diff --git a/packages/core/src/client-certificate.ts b/packages/core/src/client-certificate.ts index d9ed7a09d..d003bda8c 100644 --- a/packages/core/src/client-certificate.ts +++ b/packages/core/src/client-certificate.ts @@ -39,6 +39,8 @@ type KeyFile = string | { path: string, password?: string } * * @interface * @see https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions + * @experimental Exposed as preview feature. + * @since 5.19 */ export default class ClientCertificate { public readonly certfile: string | string[] @@ -85,6 +87,8 @@ export default class ClientCertificate { * Should fetching the certificate be particularly slow, it might be necessary to increase the timeout. * * @interface + * @experimental Exposed as preview feature. + * @since 5.19 */ export class ClientCertificateProvider { /** @@ -112,6 +116,8 @@ export class ClientCertificateProvider { /** * Interface for {@link ClientCertificateProvider} which provides update certificate function. * @interface + * @experimental Exposed as preview feature. + * @since 5.19 */ export class RotatingClientCertificateProvider extends ClientCertificateProvider { /** @@ -129,6 +135,9 @@ export class RotatingClientCertificateProvider extends ClientCertificateProvider /** * Defines the object which holds the common {@link ClientCertificateProviders} used in the Driver + * + * @experimental Exposed as preview feature. + * @since 5.19 */ class ClientCertificateProviders { /** @@ -151,6 +160,9 @@ class ClientCertificateProviders { /** * Holds the common {@link ClientCertificateProviders} used in the Driver. + * + * @experimental Exposed as preview feature. + * @since 5.19 */ const clientCertificateProviders: ClientCertificateProviders = new ClientCertificateProviders() diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d9a117ac4..5e63b9ff1 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -350,6 +350,8 @@ export class Config { * where the {@link ClientCertificate} might change over time. * * @type {ClientCertificate|ClientCertificateProvider|undefined} + * @experimental Exposed as preview feature. + * @since 5.19 */ this.clientCertificate = undefined } diff --git a/packages/neo4j-driver-deno/lib/core/client-certificate.ts b/packages/neo4j-driver-deno/lib/core/client-certificate.ts index b97194827..35c5757fc 100644 --- a/packages/neo4j-driver-deno/lib/core/client-certificate.ts +++ b/packages/neo4j-driver-deno/lib/core/client-certificate.ts @@ -39,6 +39,8 @@ type KeyFile = string | { path: string, password?: string } * * @interface * @see https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions + * @experimental Exposed as preview feature. + * @since 5.19 */ export default class ClientCertificate { public readonly certfile: string | string[] @@ -85,6 +87,8 @@ export default class ClientCertificate { * Should fetching the certificate be particularly slow, it might be necessary to increase the timeout. * * @interface + * @experimental Exposed as preview feature. + * @since 5.19 */ export class ClientCertificateProvider { /** @@ -112,6 +116,8 @@ export class ClientCertificateProvider { /** * Interface for {@link ClientCertificateProvider} which provides update certificate function. * @interface + * @experimental Exposed as preview feature. + * @since 5.19 */ export class RotatingClientCertificateProvider extends ClientCertificateProvider { /** @@ -129,6 +135,9 @@ export class RotatingClientCertificateProvider extends ClientCertificateProvider /** * Defines the object which holds the common {@link ClientCertificateProviders} used in the Driver + * + * @experimental Exposed as preview feature. + * @since 5.19 */ class ClientCertificateProviders { /** @@ -151,6 +160,9 @@ class ClientCertificateProviders { /** * Holds the common {@link ClientCertificateProviders} used in the Driver. + * + * @experimental Exposed as preview feature. + * @since 5.19 */ const clientCertificateProviders: ClientCertificateProviders = new ClientCertificateProviders() diff --git a/packages/neo4j-driver-deno/lib/core/types.ts b/packages/neo4j-driver-deno/lib/core/types.ts index de6e69307..d493c400d 100644 --- a/packages/neo4j-driver-deno/lib/core/types.ts +++ b/packages/neo4j-driver-deno/lib/core/types.ts @@ -350,6 +350,8 @@ export class Config { * where the {@link ClientCertificate} might change over time. * * @type {ClientCertificate|ClientCertificateProvider|undefined} + * @experimental Exposed as preview feature. + * @since 5.19 */ this.clientCertificate = undefined } From 7b4836b556d1332f42e6b79ff439af1c8ab70e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Mon, 4 Mar 2024 17:30:12 +0100 Subject: [PATCH 22/23] Implement cert rotation in the backend --- packages/testkit-backend/src/context.js | 28 ++++++++++ .../src/request-handlers-rx.js | 3 + .../testkit-backend/src/request-handlers.js | 55 ++++++++++++++++++- packages/testkit-backend/src/responses.js | 8 +++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/context.js b/packages/testkit-backend/src/context.js index 4cdc99d74..019889125 100644 --- a/packages/testkit-backend/src/context.js +++ b/packages/testkit-backend/src/context.js @@ -19,6 +19,8 @@ export default class Context { this._basicAuthTokenProviderRequests = {} this._binder = binder this._environmentLogLevel = environmentLogLevel + this._clientCertificateProviders = {} + this._clientCertificateProviderRequests = {} } get binder () { @@ -226,6 +228,32 @@ export default class Context { delete this._basicAuthTokenProviderRequests[id] } + addClientCertificate (clientCertificateFactory) { + this._id++ + this._clientCertificateProviders[this._id] = clientCertificateFactory(this._id) + return this._id + } + + getClientCertificate (id) { + return this._clientCertificateProviders[id] + } + + removeClientCertificate (id) { + delete this._clientCertificateProviders[id] + } + + addClientCertificateProviderRequest (resolve, reject) { + return this._add(this._clientCertificateProviderRequests, { resolve, reject }) + } + + getClientCertificateProviderRequest (id) { + return this._clientCertificateProviderRequests[id] + } + + removeClientCertificateProviderRequest (id) { + delete this._clientCertificateProviderRequests[id] + } + _add (map, object) { this._id++ map[this._id] = object diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index c093932be..546820c03 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -33,6 +33,9 @@ export { BearerAuthTokenProviderCompleted, NewBasicAuthTokenManager, BasicAuthTokenProviderCompleted, + NewClientCertificateProvider, + ClientCertificateProviderClose, + ClientCertificateProviderCompleted, FakeTimeInstall, FakeTimeTick, FakeTimeUninstall diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index e7391b60e..11a833207 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -86,8 +86,15 @@ export function NewDriver ({ neo4j }, context, data, wire) { config.connectionLivenessCheckTimeout = data.livenessCheckTimeoutMs } - if (data.clientCertificate != null) { + if (data.clientCertificate != null && data.clientCertificateProviderId != null) { + throw new Error('Can not set clientCertificate and clientCertificateProviderId') + } if (data.clientCertificate != null) { config.clientCertificate = data.clientCertificate.data + } else if (data.clientCertificateProviderId != null) { + config.clientCertificate = context.getClientCertificate(data.clientCertificateProviderId) + if (config.clientCertificate == null) { + throw new Error('Invalid ClientCertificateProvider') + } } let driver @@ -617,6 +624,52 @@ export function BasicAuthTokenProviderCompleted (_, context, { requestId, auth } context.removeBasicAuthTokenProviderRequest(requestId) } +export function NewClientCertificateProvider (_, context, _data, wire) { + const id = context.addClientCertificate((id) => { + const state = { + clientCertificate: undefined, + hasUpdate: undefined + } + const requestCertificate = () => new Promise((resolve, reject) => { + const requestId = context.addClientCertificateProviderRequest(resolve, reject) + wire.writeResponse(responses.ClientCertificateProviderRequest({ + id: requestId, + clientCertificateProviderId: id + })) + }) + + return { + hasUpdate: async () => { + const { hasUpdate, clientCertificate } = await requestCertificate() + state.clientCertificate = clientCertificate + state.hasUpdate = hasUpdate + return hasUpdate + }, + getClientCertificate: async () => { + if (state.clientCertificate != null) { + return state.clientCertificate + } + const { clientCertificate } = await requestCertificate() + state.clientCertificate = clientCertificate + return clientCertificate + } + } + }) + + wire.writeResponse(responses.ClientCertificateProvider({ id })) +} + +export function ClientCertificateProviderClose (_, context, { id }, wire) { + context.removeClientCertificate(id) + wire.writeResponse(responses.ClientCertificateProvider({ id })) +} + +export function ClientCertificateProviderCompleted (_, context, { requestId, clientCertificate, hasUpdate }) { + const request = context.getClientCertificateProviderRequest(requestId) + request.resolve({ hasUpdate, clientCertificate: clientCertificate.data }) + context.removeClientCertificateProviderRequest(requestId) +} + export function GetRoutingTable (_, context, { driverId, database }, wire) { const driver = context.getDriver(driverId) const routingTable = diff --git a/packages/testkit-backend/src/responses.js b/packages/testkit-backend/src/responses.js index c6d67a3c4..b78324672 100644 --- a/packages/testkit-backend/src/responses.js +++ b/packages/testkit-backend/src/responses.js @@ -139,6 +139,14 @@ export function DriverIsAuthenticated ({ id, authenticated }) { return response('DriverIsAuthenticated', { id, authenticated }) } +export function ClientCertificateProvider ({ id }) { + return response('ClientCertificateProvider', { id }) +} + +export function ClientCertificateProviderRequest ({ id, clientCertificateProviderId }) { + return response('ClientCertificateProviderRequest', { id, clientCertificateProviderId }) +} + // Testkit controller messages export function RunTest () { return response('RunTest', null) From 60bce58edfdcb1d71d8d3511edd91375f043fb89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Tue, 5 Mar 2024 10:15:39 +0100 Subject: [PATCH 23/23] Small fix in the doc --- packages/core/src/client-certificate.ts | 2 +- packages/neo4j-driver-deno/lib/core/client-certificate.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/client-certificate.ts b/packages/core/src/client-certificate.ts index d003bda8c..d69bb8b03 100644 --- a/packages/core/src/client-certificate.ts +++ b/packages/core/src/client-certificate.ts @@ -63,7 +63,7 @@ export default class ClientCertificate { this.keyfile = '' /** - * The certificate's password. + * The key's password. * * @type {string|undefined} */ diff --git a/packages/neo4j-driver-deno/lib/core/client-certificate.ts b/packages/neo4j-driver-deno/lib/core/client-certificate.ts index 35c5757fc..7a338068b 100644 --- a/packages/neo4j-driver-deno/lib/core/client-certificate.ts +++ b/packages/neo4j-driver-deno/lib/core/client-certificate.ts @@ -63,7 +63,7 @@ export default class ClientCertificate { this.keyfile = '' /** - * The certificate's password. + * The key's password. * * @type {string|undefined} */ @@ -135,7 +135,7 @@ export class RotatingClientCertificateProvider extends ClientCertificateProvider /** * Defines the object which holds the common {@link ClientCertificateProviders} used in the Driver - * + * * @experimental Exposed as preview feature. * @since 5.19 */ @@ -160,7 +160,7 @@ class ClientCertificateProviders { /** * Holds the common {@link ClientCertificateProviders} used in the Driver. - * + * * @experimental Exposed as preview feature. * @since 5.19 */