Skip to content

Commit 15cf4a6

Browse files
committed
feat(NODE-4179): allow secureContext in KMS TLS options
1 parent bff57ed commit 15cf4a6

File tree

4 files changed

+217
-62
lines changed

4 files changed

+217
-62
lines changed

src/client-side-encryption/state_machine.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ declare module 'mongodb-client-encryption' {
111111
*/
112112
export type ClientEncryptionTlsOptions = Pick<
113113
MongoClientOptions,
114-
'tlsCAFile' | 'tlsCertificateKeyFile' | 'tlsCertificateKeyFilePassword'
114+
'tlsCAFile' | 'tlsCertificateKeyFile' | 'tlsCertificateKeyFilePassword' | 'secureContext'
115115
>;
116116

117117
/** @public */
@@ -526,15 +526,20 @@ export class StateMachine {
526526
tlsOptions: ClientEncryptionTlsOptions,
527527
options: tls.ConnectionOptions
528528
): Promise<void> {
529-
if (tlsOptions.tlsCertificateKeyFile) {
530-
const cert = await fs.readFile(tlsOptions.tlsCertificateKeyFile);
531-
options.cert = options.key = cert;
532-
}
533-
if (tlsOptions.tlsCAFile) {
534-
options.ca = await fs.readFile(tlsOptions.tlsCAFile);
535-
}
536-
if (tlsOptions.tlsCertificateKeyFilePassword) {
537-
options.passphrase = tlsOptions.tlsCertificateKeyFilePassword;
529+
// If a secureContext is provided, it takes precedence over the other options.
530+
if (tlsOptions.secureContext) {
531+
options.secureContext = tlsOptions.secureContext;
532+
} else {
533+
if (tlsOptions.tlsCertificateKeyFile) {
534+
const cert = await fs.readFile(tlsOptions.tlsCertificateKeyFile);
535+
options.cert = options.key = cert;
536+
}
537+
if (tlsOptions.tlsCAFile) {
538+
options.ca = await fs.readFile(tlsOptions.tlsCAFile);
539+
}
540+
if (tlsOptions.tlsCertificateKeyFilePassword) {
541+
options.passphrase = tlsOptions.tlsCertificateKeyFilePassword;
542+
}
538543
}
539544
}
540545

test/integration/client-side-encryption/client_side_encryption.prose.test.js renamed to test/integration/client-side-encryption/client_side_encryption.prose.test.ts

Lines changed: 66 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
1-
'use strict';
2-
const BSON = require('bson');
3-
const { expect } = require('chai');
4-
const fs = require('fs');
5-
const path = require('path');
6-
7-
const { dropCollection, APMEventCollector } = require('../shared');
8-
9-
const { EJSON } = BSON;
10-
const { LEGACY_HELLO_COMMAND, MongoCryptError, MongoRuntimeError } = require('../../mongodb');
11-
const { MongoServerError, MongoServerSelectionError, MongoClient } = require('../../mongodb');
12-
const { getEncryptExtraOptions } = require('../../tools/utils');
13-
14-
const {
15-
externalSchema
16-
} = require('../../spec/client-side-encryption/external/external-schema.json');
17-
/* eslint-disable no-restricted-modules */
18-
const { ClientEncryption } = require('../../../src/client-side-encryption/client_encryption');
19-
const { getCSFLEKMSProviders } = require('../../csfle-kms-providers');
20-
const { AlpineTestConfiguration } = require('../../tools/runner/config');
21-
22-
const getKmsProviders = (localKey, kmipEndpoint, azureEndpoint, gcpEndpoint) => {
1+
import { BSON, EJSON } from 'bson';
2+
import { expect } from 'chai';
3+
import * as fs from 'fs/promises';
4+
import * as path from 'path';
5+
6+
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
7+
import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption';
8+
import { getCSFLEKMSProviders } from '../../csfle-kms-providers';
9+
import {
10+
LEGACY_HELLO_COMMAND,
11+
MongoClient,
12+
MongoCryptError,
13+
MongoRuntimeError,
14+
MongoServerError,
15+
MongoServerSelectionError
16+
} from '../../mongodb';
17+
import { AlpineTestConfiguration } from '../../tools/runner/config';
18+
import { getEncryptExtraOptions } from '../../tools/utils';
19+
import { APMEventCollector, dropCollection } from '../shared';
20+
21+
export const getKmsProviders = (localKey, kmipEndpoint, azureEndpoint, gcpEndpoint) => {
2322
const result = getCSFLEKMSProviders();
2423
if (localKey) {
2524
result.local = { key: localKey };
@@ -39,6 +38,7 @@ const getKmsProviders = (localKey, kmipEndpoint, azureEndpoint, gcpEndpoint) =>
3938
return result;
4039
};
4140

41+
// eslint-disable-next-line @typescript-eslint/no-empty-function
4242
const noop = () => {};
4343
const metadata = {
4444
requires: {
@@ -55,6 +55,24 @@ const eeMetadata = {
5555
}
5656
};
5757

58+
async function loadExternal(file) {
59+
return EJSON.parse(
60+
await fs.readFile(
61+
path.resolve(__dirname, '../../spec/client-side-encryption/external', file),
62+
'utf8'
63+
)
64+
);
65+
}
66+
67+
async function loadLimits(file) {
68+
return EJSON.parse(
69+
await fs.readFile(
70+
path.resolve(__dirname, '../../spec/client-side-encryption/limits', file),
71+
'utf8'
72+
)
73+
);
74+
}
75+
5876
// Tests for the ClientEncryption type are not included as part of the YAML tests.
5977

6078
// In the prose tests LOCAL_MASTERKEY refers to the following base64:
@@ -63,6 +81,9 @@ const eeMetadata = {
6381

6482
// Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk
6583
describe('Client Side Encryption Prose Tests', metadata, function () {
84+
let externalKey;
85+
let externalSchema;
86+
6687
const dataDbName = 'db';
6788
const dataCollName = 'coll';
6889
const dataNamespace = `${dataDbName}.${dataCollName}`;
@@ -75,6 +96,11 @@ describe('Client Side Encryption Prose Tests', metadata, function () {
7596
'base64'
7697
);
7798

99+
before(async function () {
100+
externalKey = await loadExternal('external-key.json');
101+
externalSchema = await loadExternal('external-schema.json');
102+
});
103+
78104
describe('Data key and double encryption', function () {
79105
// Data key and double encryption
80106
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -350,18 +376,8 @@ describe('Client Side Encryption Prose Tests', metadata, function () {
350376
// and confirming that the externalClient is firing off keyVault requests during
351377
// encrypted operations
352378
describe('External Key Vault Test', function () {
353-
function loadExternal(file) {
354-
return EJSON.parse(
355-
fs.readFileSync(path.resolve(__dirname, '../../spec/client-side-encryption/external', file))
356-
);
357-
}
358-
359-
const externalKey = loadExternal('external-key.json');
360-
const externalSchema = loadExternal('external-schema.json');
361-
362-
beforeEach(function () {
379+
beforeEach(async function () {
363380
this.client = this.configuration.newClient();
364-
365381
// 1. Create a MongoClient without encryption enabled (referred to as ``client``).
366382
return (
367383
this.client
@@ -551,15 +567,15 @@ describe('Client Side Encryption Prose Tests', metadata, function () {
551567
});
552568

553569
describe('BSON size limits and batch splitting', function () {
554-
function loadLimits(file) {
555-
return EJSON.parse(
556-
fs.readFileSync(path.resolve(__dirname, '../../spec/client-side-encryption/limits', file))
557-
);
558-
}
559-
560-
const limitsSchema = loadLimits('limits-schema.json');
561-
const limitsKey = loadLimits('limits-key.json');
562-
const limitsDoc = loadLimits('limits-doc.json');
570+
let limitsSchema;
571+
let limitsKey;
572+
let limitsDoc;
573+
574+
before(async function () {
575+
limitsSchema = await loadLimits('limits-schema.json');
576+
limitsKey = await loadLimits('limits-key.json');
577+
limitsDoc = await loadLimits('limits-doc.json');
578+
});
563579

564580
let hasRunFirstTimeSetup = false;
565581

@@ -826,9 +842,9 @@ describe('Client Side Encryption Prose Tests', metadata, function () {
826842

827843
describe('Corpus Test', function () {
828844
it('runs in a separate suite', () => {
829-
expect(() =>
830-
fs.statSync(path.resolve(__dirname, './client_side_encryption.prose.06.corpus.test.ts'))
831-
).not.to.throw();
845+
expect(async () => {
846+
await fs.stat(path.resolve(__dirname, './client_side_encryption.prose.06.corpus.test.ts'));
847+
}).not.to.throw();
832848
});
833849
});
834850

@@ -1691,6 +1707,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () {
16911707
context(
16921708
'Case 5: `tlsDisableOCSPEndpointCheck` is permitted',
16931709
metadata,
1710+
// eslint-disable-next-line @typescript-eslint/no-empty-function
16941711
function () {}
16951712
).skipReason = 'TODO(NODE-4840): Node does not support any OCSP options';
16961713

@@ -1911,12 +1928,12 @@ describe('Client Side Encryption Prose Tests', metadata, function () {
19111928
beforeEach(async function () {
19121929
// Load the file encryptedFields.json as encryptedFields.
19131930
encryptedFields = EJSON.parse(
1914-
await fs.promises.readFile(path.join(data, 'encryptedFields.json')),
1931+
await fs.readFile(path.join(data, 'encryptedFields.json'), 'utf8'),
19151932
{ relaxed: false }
19161933
);
19171934
// Load the file key1-document.json as key1Document.
19181935
key1Document = EJSON.parse(
1919-
await fs.promises.readFile(path.join(data, 'keys', 'key1-document.json')),
1936+
await fs.readFile(path.join(data, 'keys', 'key1-document.json'), 'utf8'),
19201937
{ relaxed: false }
19211938
);
19221939
// Read the "_id" field of key1Document as key1ID.
@@ -2312,15 +2329,13 @@ describe('Client Side Encryption Prose Tests', metadata, function () {
23122329
kmip: {},
23132330
local: undefined
23142331
};
2315-
/** @type {import('../../mongodb').MongoClient} */
23162332
let client1;
2317-
/** @type {import('../../mongodb').MongoClient} */
23182333
let client2;
23192334

23202335
describe('Case 1: Rewrap with separate ClientEncryption', function () {
23212336
/**
2322-
* Run the following test case for each pair of KMS providers (referred to as ``srcProvider`` and ``dstProvider``).
2323-
* Include pairs where ``srcProvider`` equals ``dstProvider``.
2337+
* Run the following test case for each pair of KMS providers (referred to as `srcProvider` and `dstProvider`).
2338+
* Include pairs where `srcProvider` equals `dstProvider`.
23242339
*/
23252340
function* generateTestCombinations() {
23262341
const providers = Object.keys(masterKeys);

test/integration/client-side-encryption/driver.test.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { UUID } from 'bson';
22
import { expect } from 'chai';
33
import * as crypto from 'crypto';
4+
import * as fs from 'fs/promises';
45
import * as sinon from 'sinon';
56
import { setTimeout } from 'timers/promises';
7+
import * as tls from 'tls';
68

79
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
810
import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption';
@@ -1242,4 +1244,136 @@ describe('CSOT', function () {
12421244
);
12431245
});
12441246
});
1247+
1248+
context('when providing node specific TLS options', function () {
1249+
const dataDbName = 'db';
1250+
const dataCollName = 'coll';
1251+
const dataNamespace = `${dataDbName}.${dataCollName}`;
1252+
const keyVaultDbName = 'keyvault';
1253+
const keyVaultCollName = 'datakeys';
1254+
const keyVaultNamespace = `${keyVaultDbName}.${keyVaultCollName}`;
1255+
const masterKey = {
1256+
region: 'us-east-1',
1257+
key: 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0'
1258+
};
1259+
const schemaMap = {
1260+
[dataNamespace]: {
1261+
bsonType: 'object',
1262+
properties: {
1263+
encrypted_placeholder: {
1264+
encrypt: {
1265+
keyId: '/placeholder',
1266+
bsonType: 'string',
1267+
algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'
1268+
}
1269+
}
1270+
}
1271+
}
1272+
};
1273+
let secureContextOptions;
1274+
1275+
before(async function () {
1276+
const caFile = await fs.readFile(process.env.CSFLE_TLS_CA_FILE);
1277+
const certFile = await fs.readFile(process.env.CSFLE_TLS_CLIENT_CERT_FILE);
1278+
secureContextOptions = {
1279+
ca: caFile,
1280+
key: certFile,
1281+
cert: certFile
1282+
};
1283+
});
1284+
1285+
context('when no driver specific TLS options are provided', function () {
1286+
let client;
1287+
let clientEncryption;
1288+
const options = {
1289+
keyVaultNamespace,
1290+
kmsProviders: { aws: getCSFLEKMSProviders().aws },
1291+
tlsOptions: {
1292+
aws: {
1293+
secureContext: tls.createSecureContext(secureContextOptions)
1294+
}
1295+
},
1296+
extraOptions: getEncryptExtraOptions()
1297+
};
1298+
1299+
beforeEach(async function () {
1300+
client = this.configuration.newClient({}, { autoEncryption: { ...options, schemaMap } });
1301+
clientEncryption = new ClientEncryption(client, options);
1302+
await client.connect();
1303+
});
1304+
1305+
afterEach(async function () {
1306+
await client.db(keyVaultDbName).collection(keyVaultCollName).deleteMany();
1307+
await client.close();
1308+
});
1309+
1310+
it('succeeds to connect', async function () {
1311+
// Use client encryption to create a data key. If this succeeds, then TLS worked.
1312+
const awsDatakeyId = await clientEncryption.createDataKey('aws', {
1313+
masterKey,
1314+
keyAltNames: ['aws_altname']
1315+
});
1316+
expect(awsDatakeyId).to.have.property('sub_type', 4);
1317+
// Use the client to get the data key. If this succeeds, then the TLS connection
1318+
// for auto encryption worked.
1319+
const results = await client
1320+
.db(keyVaultDbName)
1321+
.collection(keyVaultCollName)
1322+
.find({ _id: awsDatakeyId })
1323+
.toArray();
1324+
expect(results)
1325+
.to.have.a.lengthOf(1)
1326+
.and.to.have.nested.property('0.masterKey.provider', 'aws');
1327+
});
1328+
});
1329+
1330+
context('when driver specific TLS options are provided', function () {
1331+
let client;
1332+
let clientEncryption;
1333+
// Note we set tlsCAFile and tlsCertificateKeyFile to 'nofilename' to also
1334+
// test that the driver does not attempt to read these files in this case.
1335+
const options = {
1336+
keyVaultNamespace,
1337+
kmsProviders: { aws: getCSFLEKMSProviders().aws },
1338+
tlsOptions: {
1339+
aws: {
1340+
secureContext: tls.createSecureContext(secureContextOptions),
1341+
tlsCAFile: 'nofilename',
1342+
tlsCertificateKeyFile: 'nofilename'
1343+
}
1344+
},
1345+
extraOptions: getEncryptExtraOptions()
1346+
};
1347+
1348+
beforeEach(async function () {
1349+
client = this.configuration.newClient({}, { autoEncryption: { ...options, schemaMap } });
1350+
clientEncryption = new ClientEncryption(client, options);
1351+
await client.connect();
1352+
});
1353+
1354+
afterEach(async function () {
1355+
await client.db(keyVaultDbName).collection(keyVaultCollName).deleteMany();
1356+
await client.close();
1357+
});
1358+
1359+
it('succeeds to connect', async function () {
1360+
// Use client encryption to create a data key. If this succeeds, then TLS worked.
1361+
const awsDatakeyId = await clientEncryption.createDataKey('aws', {
1362+
masterKey,
1363+
keyAltNames: ['aws_altname']
1364+
});
1365+
expect(awsDatakeyId).to.have.property('sub_type', 4);
1366+
// Use the client to get the data key. If this succeeds, then the TLS connection
1367+
// for auto encryption worked.
1368+
const results = await client
1369+
.db(keyVaultDbName)
1370+
.collection(keyVaultCollName)
1371+
.find({ _id: awsDatakeyId })
1372+
.toArray();
1373+
expect(results)
1374+
.to.have.a.lengthOf(1)
1375+
.and.to.have.nested.property('0.masterKey.provider', 'aws');
1376+
});
1377+
});
1378+
});
12451379
});

test/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"compilerOptions": {
44
"strict": false,
55
"allowJs": true,
6-
"checkJs": false
6+
"checkJs": false,
7+
"resolveJsonModule": true
78
},
89
"include": [
910
"../node_modules/@types/mocha/index.d.ts",

0 commit comments

Comments
 (0)