diff --git a/src/client-side-encryption/state_machine.ts b/src/client-side-encryption/state_machine.ts index fd5a393ea8..c0dd776da6 100644 --- a/src/client-side-encryption/state_machine.ts +++ b/src/client-side-encryption/state_machine.ts @@ -108,10 +108,12 @@ declare module 'mongodb-client-encryption' { * - tlsInsecure * * These options are not included in the type, and are ignored if provided. + * + * Note that if a secureContext option is provided, all other TLS options will be ignored. */ export type ClientEncryptionTlsOptions = Pick< MongoClientOptions, - 'tlsCAFile' | 'tlsCertificateKeyFile' | 'tlsCertificateKeyFilePassword' + 'tlsCAFile' | 'tlsCertificateKeyFile' | 'tlsCertificateKeyFilePassword' | 'secureContext' >; /** @public */ @@ -526,15 +528,20 @@ export class StateMachine { tlsOptions: ClientEncryptionTlsOptions, options: tls.ConnectionOptions ): Promise { - if (tlsOptions.tlsCertificateKeyFile) { - const cert = await fs.readFile(tlsOptions.tlsCertificateKeyFile); - options.cert = options.key = cert; - } - if (tlsOptions.tlsCAFile) { - options.ca = await fs.readFile(tlsOptions.tlsCAFile); - } - if (tlsOptions.tlsCertificateKeyFilePassword) { - options.passphrase = tlsOptions.tlsCertificateKeyFilePassword; + // If a secureContext is provided, it takes precedence over the other options. + if (tlsOptions.secureContext) { + options.secureContext = tlsOptions.secureContext; + } else { + if (tlsOptions.tlsCertificateKeyFile) { + const cert = await fs.readFile(tlsOptions.tlsCertificateKeyFile); + options.cert = options.key = cert; + } + if (tlsOptions.tlsCAFile) { + options.ca = await fs.readFile(tlsOptions.tlsCAFile); + } + if (tlsOptions.tlsCertificateKeyFilePassword) { + options.passphrase = tlsOptions.tlsCertificateKeyFilePassword; + } } } diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.test.js b/test/integration/client-side-encryption/client_side_encryption.prose.test.ts similarity index 97% rename from test/integration/client-side-encryption/client_side_encryption.prose.test.js rename to test/integration/client-side-encryption/client_side_encryption.prose.test.ts index 7d3c265997..547687d107 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.test.js +++ b/test/integration/client-side-encryption/client_side_encryption.prose.test.ts @@ -1,25 +1,24 @@ -'use strict'; -const BSON = require('bson'); -const { expect } = require('chai'); -const fs = require('fs'); -const path = require('path'); - -const { dropCollection, APMEventCollector } = require('../shared'); - -const { EJSON } = BSON; -const { LEGACY_HELLO_COMMAND, MongoCryptError, MongoRuntimeError } = require('../../mongodb'); -const { MongoServerError, MongoServerSelectionError, MongoClient } = require('../../mongodb'); -const { getEncryptExtraOptions } = require('../../tools/utils'); - -const { - externalSchema -} = require('../../spec/client-side-encryption/external/external-schema.json'); -/* eslint-disable no-restricted-modules */ -const { ClientEncryption } = require('../../../src/client-side-encryption/client_encryption'); -const { getCSFLEKMSProviders } = require('../../csfle-kms-providers'); -const { AlpineTestConfiguration } = require('../../tools/runner/config'); - -const getKmsProviders = (localKey, kmipEndpoint, azureEndpoint, gcpEndpoint) => { +import { BSON, EJSON } from 'bson'; +import { expect } from 'chai'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; +import { getCSFLEKMSProviders } from '../../csfle-kms-providers'; +import { + LEGACY_HELLO_COMMAND, + MongoClient, + MongoCryptError, + MongoRuntimeError, + MongoServerError, + MongoServerSelectionError +} from '../../mongodb'; +import { AlpineTestConfiguration } from '../../tools/runner/config'; +import { getEncryptExtraOptions } from '../../tools/utils'; +import { APMEventCollector, dropCollection } from '../shared'; + +export const getKmsProviders = (localKey, kmipEndpoint, azureEndpoint, gcpEndpoint) => { const result = getCSFLEKMSProviders(); if (localKey) { result.local = { key: localKey }; @@ -39,6 +38,7 @@ const getKmsProviders = (localKey, kmipEndpoint, azureEndpoint, gcpEndpoint) => return result; }; +// eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => {}; const metadata = { requires: { @@ -55,6 +55,24 @@ const eeMetadata = { } }; +async function loadExternal(file) { + return EJSON.parse( + await fs.readFile( + path.resolve(__dirname, '../../spec/client-side-encryption/external', file), + 'utf8' + ) + ); +} + +async function loadLimits(file) { + return EJSON.parse( + await fs.readFile( + path.resolve(__dirname, '../../spec/client-side-encryption/limits', file), + 'utf8' + ) + ); +} + // Tests for the ClientEncryption type are not included as part of the YAML tests. // In the prose tests LOCAL_MASTERKEY refers to the following base64: @@ -63,6 +81,9 @@ const eeMetadata = { // Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk describe('Client Side Encryption Prose Tests', metadata, function () { + let externalKey; + let externalSchema; + const dataDbName = 'db'; const dataCollName = 'coll'; const dataNamespace = `${dataDbName}.${dataCollName}`; @@ -75,6 +96,11 @@ describe('Client Side Encryption Prose Tests', metadata, function () { 'base64' ); + before(async function () { + externalKey = await loadExternal('external-key.json'); + externalSchema = await loadExternal('external-schema.json'); + }); + describe('Data key and double encryption', function () { // Data key and double encryption // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -350,18 +376,8 @@ describe('Client Side Encryption Prose Tests', metadata, function () { // and confirming that the externalClient is firing off keyVault requests during // encrypted operations describe('External Key Vault Test', function () { - function loadExternal(file) { - return EJSON.parse( - fs.readFileSync(path.resolve(__dirname, '../../spec/client-side-encryption/external', file)) - ); - } - - const externalKey = loadExternal('external-key.json'); - const externalSchema = loadExternal('external-schema.json'); - - beforeEach(function () { + beforeEach(async function () { this.client = this.configuration.newClient(); - // 1. Create a MongoClient without encryption enabled (referred to as ``client``). return ( this.client @@ -551,15 +567,15 @@ describe('Client Side Encryption Prose Tests', metadata, function () { }); describe('BSON size limits and batch splitting', function () { - function loadLimits(file) { - return EJSON.parse( - fs.readFileSync(path.resolve(__dirname, '../../spec/client-side-encryption/limits', file)) - ); - } - - const limitsSchema = loadLimits('limits-schema.json'); - const limitsKey = loadLimits('limits-key.json'); - const limitsDoc = loadLimits('limits-doc.json'); + let limitsSchema; + let limitsKey; + let limitsDoc; + + before(async function () { + limitsSchema = await loadLimits('limits-schema.json'); + limitsKey = await loadLimits('limits-key.json'); + limitsDoc = await loadLimits('limits-doc.json'); + }); let hasRunFirstTimeSetup = false; @@ -826,9 +842,9 @@ describe('Client Side Encryption Prose Tests', metadata, function () { describe('Corpus Test', function () { it('runs in a separate suite', () => { - expect(() => - fs.statSync(path.resolve(__dirname, './client_side_encryption.prose.06.corpus.test.ts')) - ).not.to.throw(); + expect(async () => { + await fs.stat(path.resolve(__dirname, './client_side_encryption.prose.06.corpus.test.ts')); + }).not.to.throw(); }); }); @@ -1691,6 +1707,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { context( 'Case 5: `tlsDisableOCSPEndpointCheck` is permitted', metadata, + // eslint-disable-next-line @typescript-eslint/no-empty-function function () {} ).skipReason = 'TODO(NODE-4840): Node does not support any OCSP options'; @@ -1911,12 +1928,12 @@ describe('Client Side Encryption Prose Tests', metadata, function () { beforeEach(async function () { // Load the file encryptedFields.json as encryptedFields. encryptedFields = EJSON.parse( - await fs.promises.readFile(path.join(data, 'encryptedFields.json')), + await fs.readFile(path.join(data, 'encryptedFields.json'), 'utf8'), { relaxed: false } ); // Load the file key1-document.json as key1Document. key1Document = EJSON.parse( - await fs.promises.readFile(path.join(data, 'keys', 'key1-document.json')), + await fs.readFile(path.join(data, 'keys', 'key1-document.json'), 'utf8'), { relaxed: false } ); // Read the "_id" field of key1Document as key1ID. @@ -2312,15 +2329,13 @@ describe('Client Side Encryption Prose Tests', metadata, function () { kmip: {}, local: undefined }; - /** @type {import('../../mongodb').MongoClient} */ let client1; - /** @type {import('../../mongodb').MongoClient} */ let client2; describe('Case 1: Rewrap with separate ClientEncryption', function () { /** - * Run the following test case for each pair of KMS providers (referred to as ``srcProvider`` and ``dstProvider``). - * Include pairs where ``srcProvider`` equals ``dstProvider``. + * Run the following test case for each pair of KMS providers (referred to as `srcProvider` and `dstProvider`). + * Include pairs where `srcProvider` equals `dstProvider`. */ function* generateTestCombinations() { const providers = Object.keys(masterKeys); diff --git a/test/integration/client-side-encryption/driver.test.ts b/test/integration/client-side-encryption/driver.test.ts index 5bdf86bf84..69a9b93011 100644 --- a/test/integration/client-side-encryption/driver.test.ts +++ b/test/integration/client-side-encryption/driver.test.ts @@ -1,8 +1,10 @@ import { UUID } from 'bson'; import { expect } from 'chai'; import * as crypto from 'crypto'; +import * as fs from 'fs/promises'; import * as sinon from 'sinon'; import { setTimeout } from 'timers/promises'; +import * as tls from 'tls'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption'; @@ -240,7 +242,6 @@ describe('Client Side Encryption Functional', function () { Object.freeze(['1', 1] as const), Object.freeze(['0', 1] as const) ]); - // @ts-expect-error: Our findOne API does not accept readonly input await collection.findOne({}, { sort }); const findEvent = events.find(event => !!event.command.find); expect(findEvent).to.have.property('commandName', 'find'); @@ -256,7 +257,6 @@ describe('Client Side Encryption Functional', function () { Object.freeze(['1', 1] as const), Object.freeze(['0', 1] as const) ]); - // @ts-expect-error: Our findOneAndUpdate API does not accept readonly input await collection.findOneAndUpdate({}, { $setOnInsert: { a: 1 } }, { sort }); const findAndModifyEvent = events.find(event => !!event.command.findAndModify); expect(findAndModifyEvent).to.have.property('commandName', 'findAndModify'); @@ -1242,4 +1242,143 @@ describe('CSOT', function () { ); }); }); + + describe('TLS Authentication with Client Encryption and Auto Encryption', function () { + context('when providing node specific secureContext TLS option', function () { + const dataDbName = 'db'; + const dataCollName = 'coll'; + const dataNamespace = `${dataDbName}.${dataCollName}`; + const keyVaultDbName = 'keyvault'; + const keyVaultCollName = 'datakeys'; + const keyVaultNamespace = `${keyVaultDbName}.${keyVaultCollName}`; + const masterKey = { + region: 'us-east-1', + key: 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0' + }; + const schemaMap = { + [dataNamespace]: { + bsonType: 'object', + properties: { + encrypted_placeholder: { + encrypt: { + keyId: '/placeholder', + bsonType: 'string', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random' + } + } + } + } + }; + let secureContextOptions; + + beforeEach(async function () { + const caFile = await fs.readFile(process.env.CSFLE_TLS_CA_FILE); + const certFile = await fs.readFile(process.env.CSFLE_TLS_CLIENT_CERT_FILE); + secureContextOptions = { + ca: caFile, + key: certFile, + cert: certFile + }; + }); + + context('when no driver specific TLS options are provided', function () { + let client; + let clientEncryption; + const options = { + keyVaultNamespace, + kmsProviders: { aws: getCSFLEKMSProviders().aws }, + tlsOptions: { + aws: { + secureContext: tls.createSecureContext(secureContextOptions) + } + }, + extraOptions: getEncryptExtraOptions() + }; + + beforeEach(async function () { + client = this.configuration.newClient({}, { autoEncryption: { ...options, schemaMap } }); + clientEncryption = new ClientEncryption(client, options); + await client.connect(); + }); + + afterEach(async function () { + await client.db(keyVaultDbName).collection(keyVaultCollName).deleteMany(); + await client.close(); + }); + + it('successfully connects with TLS', metadata, async function () { + // Use client encryption to create a data key. If this succeeds, then TLS worked. + const awsDatakeyId = await clientEncryption.createDataKey('aws', { + masterKey, + keyAltNames: ['aws_altname'] + }); + expect(awsDatakeyId).to.have.property('sub_type', 4); + // Use the client to get the data key. If this succeeds, then the TLS connection + // for auto encryption worked. + const results = await client + .db(keyVaultDbName) + .collection(keyVaultCollName) + .find({ _id: awsDatakeyId }) + .toArray(); + expect(results) + .to.have.a.lengthOf(1) + .and.to.have.nested.property('0.masterKey.provider', 'aws'); + }); + }); + + context('when driver specific TLS options are provided', function () { + let client; + let clientEncryption; + // Note we set tlsCAFile and tlsCertificateKeyFile to 'nofilename' to also + // test that the driver does not attempt to read these files in this case. + const options = { + keyVaultNamespace, + kmsProviders: { aws: getCSFLEKMSProviders().aws }, + tlsOptions: { + aws: { + secureContext: tls.createSecureContext(secureContextOptions), + tlsCAFile: 'nofilename', + tlsCertificateKeyFile: 'nofilename', + tlsCertificateKeyFilePassword: 'invalid' + } + }, + extraOptions: getEncryptExtraOptions() + }; + + beforeEach(async function () { + client = this.configuration.newClient({}, { autoEncryption: { ...options, schemaMap } }); + clientEncryption = new ClientEncryption(client, options); + await client.connect(); + }); + + afterEach(async function () { + await client.db(keyVaultDbName).collection(keyVaultCollName).deleteMany(); + await client.close(); + }); + + it( + 'successfully connects with TLS without attempting to parse the driver specific options', + metadata, + async function () { + // Use client encryption to create a data key. If this succeeds, then TLS worked. + const awsDatakeyId = await clientEncryption.createDataKey('aws', { + masterKey, + keyAltNames: ['aws_altname'] + }); + expect(awsDatakeyId).to.have.property('sub_type', 4); + // Use the client to get the data key. If this succeeds, then the TLS connection + // for auto encryption worked. + const results = await client + .db(keyVaultDbName) + .collection(keyVaultCollName) + .find({ _id: awsDatakeyId }) + .toArray(); + expect(results) + .to.have.a.lengthOf(1) + .and.to.have.nested.property('0.masterKey.provider', 'aws'); + } + ); + }); + }); + }); });