Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/client-side-encryption/state_machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ declare module 'mongodb-client-encryption' {
*/
export type ClientEncryptionTlsOptions = Pick<
MongoClientOptions,
'tlsCAFile' | 'tlsCertificateKeyFile' | 'tlsCertificateKeyFilePassword'
'tlsCAFile' | 'tlsCertificateKeyFile' | 'tlsCertificateKeyFilePassword' | 'secureContext'
>;

/** @public */
Expand Down Expand Up @@ -521,6 +521,10 @@ export class StateMachine {
tlsOptions: ClientEncryptionTlsOptions,
options: tls.ConnectionOptions
): Promise<void> {
// If a secureContext is provided, ensure it is set.
if (tlsOptions.secureContext) {
options.secureContext = tlsOptions.secureContext;
}
if (tlsOptions.tlsCertificateKeyFile) {
const cert = await fs.readFile(tlsOptions.tlsCertificateKeyFile);
options.cert = options.key = cert;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 };
Expand All @@ -39,6 +38,7 @@ const getKmsProviders = (localKey, kmipEndpoint, azureEndpoint, gcpEndpoint) =>
return result;
};

// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
/** @type { MongoDBMetadataUI } */
const metadata = {
Expand All @@ -56,6 +56,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:
Expand All @@ -64,6 +82,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}`;
Expand All @@ -76,6 +97,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
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -351,18 +377,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
Expand Down Expand Up @@ -552,15 +568,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;

Expand Down Expand Up @@ -827,9 +843,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();
});
});

Expand Down Expand Up @@ -1687,6 +1703,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';

Expand Down Expand Up @@ -1907,12 +1924,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.
Expand Down Expand Up @@ -2308,15 +2325,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);
Expand Down
138 changes: 136 additions & 2 deletions test/integration/client-side-encryption/driver.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -1242,4 +1242,138 @@ 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 with a secure context', 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: process.env.CSFLE_TLS_CA_FILE,
tlsCertificateKeyFile: process.env.CSFLE_TLS_CLIENT_CERT_FILE
}
},
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');
});
});
});
});
});