diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index a34f710b8e..a1b8ad9bb5 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -636,8 +636,6 @@ functions: export PROJECT_DIRECTORY="$(pwd)" export NODE_LTS_VERSION=${NODE_LTS_VERSION} export DRIVERS_TOOLS="${DRIVERS_TOOLS}" - export SSL_CA_FILE="${SSL_CA_FILE}" - export SSL_KEY_FILE="${SSL_KEY_FILE}" export MONGODB_URI="${MONGODB_URI}" bash ${PROJECT_DIRECTORY}/.evergreen/run-tls-tests.sh diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 058475c86d..ee2f6ff1f3 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -589,8 +589,6 @@ functions: export PROJECT_DIRECTORY="$(pwd)" export NODE_LTS_VERSION=${NODE_LTS_VERSION} export DRIVERS_TOOLS="${DRIVERS_TOOLS}" - export SSL_CA_FILE="${SSL_CA_FILE}" - export SSL_KEY_FILE="${SSL_KEY_FILE}" export MONGODB_URI="${MONGODB_URI}" bash ${PROJECT_DIRECTORY}/.evergreen/run-tls-tests.sh diff --git a/.evergreen/run-tls-tests.sh b/.evergreen/run-tls-tests.sh index f5f30af716..88b21a82e4 100644 --- a/.evergreen/run-tls-tests.sh +++ b/.evergreen/run-tls-tests.sh @@ -4,7 +4,8 @@ set -o errexit # Exit the script with error if any of the commands fail source "${PROJECT_DIRECTORY}/.evergreen/init-node-and-npm-env.sh" -export SSL_KEY_FILE="$DRIVERS_TOOLS/.evergreen/x509gen/client.pem" -export SSL_CA_FILE="$DRIVERS_TOOLS/.evergreen/x509gen/ca.pem" +export TLS_KEY_FILE="$DRIVERS_TOOLS/.evergreen/x509gen/client.pem" +export TLS_CA_FILE="$DRIVERS_TOOLS/.evergreen/x509gen/ca.pem" +export TLS_CRL_FILE="$DRIVERS_TOOLS/.evergreen/x509gen/crl.pem" npm run check:tls diff --git a/src/connection_string.ts b/src/connection_string.ts index bac2a13207..54dec09765 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -1095,6 +1095,9 @@ export const OPTIONS = { tlsCAFile: { type: 'string' }, + tlsCRLFile: { + type: 'string' + }, tlsCertificateKeyFile: { type: 'string' }, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 1e0971cb8c..64a3305eda 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -117,6 +117,8 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC tlsCertificateKeyFilePassword?: string; /** Specifies the location of a local .pem file that contains the root certificate chain from the Certificate Authority. This file is used to validate the certificate presented by the mongod/mongos instance. */ tlsCAFile?: string; + /** Specifies the location of a local CRL .pem file that contains the client revokation list. */ + tlsCRLFile?: string; /** Bypasses validation of the certificates presented by the mongod/mongos instance */ tlsAllowInvalidCertificates?: boolean; /** Disables hostname validation of the certificate presented by the mongod/mongos instance. */ @@ -437,6 +439,9 @@ export class MongoClient extends TypedEventEmitter { if (typeof options.tlsCAFile === 'string') { options.ca ??= await fs.readFile(options.tlsCAFile); } + if (typeof options.tlsCRLFile === 'string') { + options.crl ??= await fs.readFile(options.tlsCRLFile); + } if (typeof options.tlsCertificateKeyFile === 'string') { if (!options.key || !options.cert) { const contents = await fs.readFile(options.tlsCertificateKeyFile); @@ -790,7 +795,7 @@ export interface MongoOptions * | nodejs native option | driver spec equivalent option name | driver option type | * |:----------------------|:----------------------------------------------|:-------------------| * | `ca` | `tlsCAFile` | `string` | - * | `crl` | N/A | `string` | + * | `crl` | `tlsCRLFile` | `string` | * | `cert` | `tlsCertificateKeyFile` | `string` | * | `key` | `tlsCertificateKeyFile` | `string` | * | `passphrase` | `tlsCertificateKeyFilePassword` | `string` | @@ -805,17 +810,17 @@ export interface MongoOptions * to a no-op and `rejectUnauthorized` to the inverse value of `tlsAllowInvalidCertificates`. If * `tlsAllowInvalidCertificates` is not set, then `rejectUnauthorized` will be set to `true`. * - * ### Note on `tlsCAFile` and `tlsCertificateKeyFile` + * ### Note on `tlsCAFile`, `tlsCertificateKeyFile` and `tlsCRLFile` * - * The files specified by the paths passed in to the `tlsCAFile` and `tlsCertificateKeyFile` fields - * are read lazily on the first call to `MongoClient.connect`. Once these files have been read and - * the `ca`, `cert` and `key` fields are populated, they will not be read again on subsequent calls to + * The files specified by the paths passed in to the `tlsCAFile`, `tlsCertificateKeyFile` and `tlsCRLFile` + * fields are read lazily on the first call to `MongoClient.connect`. Once these files have been read and + * the `ca`, `cert`, `crl` and `key` fields are populated, they will not be read again on subsequent calls to * `MongoClient.connect`. As a result, until the first call to `MongoClient.connect`, the `ca`, - * `cert` and `key` fields will be undefined. + * `cert`, `crl` and `key` fields will be undefined. */ tls: boolean; - tlsCAFile?: string; + tlsCRLFile?: string; tlsCertificateKeyFile?: string; /** @internal */ diff --git a/test/manual/tls_support.test.ts b/test/manual/tls_support.test.ts index f50c43a48d..85b42468b7 100644 --- a/test/manual/tls_support.test.ts +++ b/test/manual/tls_support.test.ts @@ -8,18 +8,19 @@ import { MongoServerSelectionError } from '../mongodb'; -const REQUIRED_ENV = ['MONGODB_URI', 'SSL_KEY_FILE', 'SSL_CA_FILE']; +const REQUIRED_ENV = ['MONGODB_URI', 'TLS_KEY_FILE', 'TLS_CA_FILE', 'TLS_CRL_FILE']; describe('TLS Support', function () { for (const key of REQUIRED_ENV) { if (process.env[key] == null) { - throw new Error(`skipping SSL tests, ${key} environment variable is not defined`); + throw new Error(`skipping TLS tests, ${key} environment variable is not defined`); } } const CONNECTION_STRING = process.env.MONGODB_URI as string; - const TLS_CERT_KEY_FILE = process.env.SSL_KEY_FILE as string; - const TLS_CA_FILE = process.env.SSL_CA_FILE as string; + const TLS_CERT_KEY_FILE = process.env.TLS_KEY_FILE as string; + const TLS_CA_FILE = process.env.TLS_CA_FILE as string; + const TLS_CRL_FILE = process.env.TLS_CRL_FILE as string; const tlsSettings = { tls: true, tlsCertificateKeyFile: TLS_CERT_KEY_FILE, @@ -42,41 +43,79 @@ describe('TLS Support', function () { context('when tls filepaths are provided', () => { let client: MongoClient; + afterEach(async () => { - if (client) await client.close(); + await client?.close(); }); context('when tls filepaths have length > 0', () => { - beforeEach(async () => { - client = new MongoClient(CONNECTION_STRING, tlsSettings); - }); + context('when connection will succeed', () => { + beforeEach(async () => { + client = new MongoClient(CONNECTION_STRING, tlsSettings); + }); + + it('should read in files async at connect time', async () => { + expect(client.options).property('tlsCAFile', TLS_CA_FILE); + expect(client.options).property('tlsCertificateKeyFile', TLS_CERT_KEY_FILE); + expect(client.options).not.have.property('ca'); + expect(client.options).not.have.property('key'); + expect(client.options).not.have.property('cert'); + + await client.connect(); + + expect(client.options).property('ca').to.exist; + expect(client.options).property('key').to.exist; + expect(client.options).property('cert').to.exist; + }); + + context('when client has been opened and closed more than once', function () { + it('should only read files once', async () => { + await client.connect(); + await client.close(); - it('should read in files async at connect time', async () => { - expect(client.options).property('tlsCAFile', TLS_CA_FILE); - expect(client.options).property('tlsCertificateKeyFile', TLS_CERT_KEY_FILE); - expect(client.options).not.have.property('ca'); - expect(client.options).not.have.property('key'); - expect(client.options).not.have.property('cert'); + const caFileAccessTime = (await fs.stat(TLS_CA_FILE)).atime; + const certKeyFileAccessTime = (await fs.stat(TLS_CERT_KEY_FILE)).atime; - await client.connect(); + await client.connect(); - expect(client.options).property('ca').to.exist; - expect(client.options).property('key').to.exist; - expect(client.options).property('cert').to.exist; + expect((await fs.stat(TLS_CA_FILE)).atime).to.deep.equal(caFileAccessTime); + expect((await fs.stat(TLS_CERT_KEY_FILE)).atime).to.deep.equal(certKeyFileAccessTime); + }); + }); }); - context('when client has been opened and closed more than once', function () { - it('should only read files once', async () => { - await client.connect(); - await client.close(); + context('when the connection will fail', () => { + beforeEach(async () => { + client = new MongoClient(CONNECTION_STRING, { + tls: true, + tlsCRLFile: TLS_CRL_FILE, + serverSelectionTimeoutMS: 2000, + connectTimeoutMS: 2000 + }); + }); - const caFileAccessTime = (await fs.stat(TLS_CA_FILE)).atime; - const certKeyFileAccessTime = (await fs.stat(TLS_CERT_KEY_FILE)).atime; + it('should read in files async at connect time', async () => { + expect(client.options).property('tlsCRLFile', TLS_CRL_FILE); + expect(client.options).not.have.property('crl'); - await client.connect(); + const err = await client.connect().catch(e => e); + + expect(err).to.be.instanceof(Error); + expect(client.options).property('crl').to.exist; + }); - expect((await fs.stat(TLS_CA_FILE)).atime).to.deep.equal(caFileAccessTime); - expect((await fs.stat(TLS_CERT_KEY_FILE)).atime).to.deep.equal(certKeyFileAccessTime); + context('when client has been opened and closed more than once', function () { + it('should only read files once', async () => { + await client.connect().catch(e => e); + await client.close(); + + const crlFileAccessTime = (await fs.stat(TLS_CRL_FILE)).atime; + + const err = await client.connect().catch(e => e); + + expect(err).to.be.instanceof(Error); + expect((await fs.stat(TLS_CRL_FILE)).atime).to.deep.equal(crlFileAccessTime); + }); }); }); }); @@ -114,6 +153,29 @@ describe('TLS Support', function () { }); }); + context('when providing tlsCRLFile', () => { + context('when the file will revoke the certificate', () => { + let client: MongoClient; + beforeEach(() => { + client = new MongoClient(CONNECTION_STRING, { + tls: true, + tlsCAFile: TLS_CA_FILE, + tlsCRLFile: TLS_CRL_FILE, + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000 + }); + }); + afterEach(async () => { + await client?.close(); + }); + + it('throws a MongoServerSelectionError', async () => { + const err = await client.connect().catch(e => e); + expect(err).to.be.instanceOf(MongoServerSelectionError); + }); + }); + }); + context('when tlsCertificateKeyFile is provided, but tlsCAFile is missing', () => { let client: MongoClient; beforeEach(() => { diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index 98866db767..01baa184e1 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -438,6 +438,13 @@ describe('Connection String', function () { }); }); + context('when providing tlsCRLFile', function () { + it('sets the tlsCRLFile option', function () { + const options = parseOptions('mongodb://localhost/?tls=true&tlsCRLFile=path/to/crl.pem'); + expect(options.tlsCRLFile).to.equal('path/to/crl.pem'); + }); + }); + context('when both tls and ssl options are provided', function () { context('when the options are provided in the URI', function () { context('when the options are equal', function () { diff --git a/test/unit/mongo_client.test.js b/test/unit/mongo_client.test.js index c43a8a17ba..ad18d944a8 100644 --- a/test/unit/mongo_client.test.js +++ b/test/unit/mongo_client.test.js @@ -35,6 +35,7 @@ describe('MongoOptions', function () { const options = parseOptions('mongodb://localhost:27017/?ssl=true', { tlsCertificateKeyFile: filename, tlsCAFile: filename, + tlsCRLFile: filename, tlsCertificateKeyFilePassword: 'tlsCertificateKeyFilePassword' }); fs.unlinkSync(filename); @@ -61,6 +62,7 @@ describe('MongoOptions', function () { expect(options).to.not.have.property('cert'); expect(options).to.have.property('tlsCertificateKeyFile', filename); expect(options).to.have.property('tlsCAFile', filename); + expect(options).to.have.property('tlsCRLFile', filename); expect(options).has.property('passphrase', 'tlsCertificateKeyFilePassword'); expect(options).has.property('tls', true); });