Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 2 additions & 6 deletions bindings/node/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,7 @@ export interface KMSProviders {
* - tlsDisableOCSPEndpointCheck
* - tlsDisableCertificateRevocationCheck
*/
export interface ClientEncryptionTLSOptions {
/**
* Enables or disables TLS/SSL for the connection.
*/
tls?: boolean;
export interface ClientEncryptionTlsOptions {
/**
* Specifies the location of a local .pem file that contains
* either the client's TLS/SSL certificate and key or only the
Expand Down Expand Up @@ -203,7 +199,7 @@ export interface ClientEncryptionOptions {
/**
* TLS options for kms providers to use.
*/
tlsOptions?: ClientEncryptionTLSOptions;
tlsOptions?: { [kms in keyof KMSProviders]?: ClientEncryptionTLSOptions };
}

/**
Expand Down
15 changes: 13 additions & 2 deletions bindings/node/lib/autoEncrypter.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ module.exports = function(modules) {
this._keyVaultClient = options.keyVaultClient || client;
this._metaDataClient = options.metadataClient || client;
this._proxyOptions = options.proxyOptions || {};
this._tlsOptions = options.tlsOptions || {};

const mongoCryptOptions = {};
if (options.schemaMap) {
Expand Down Expand Up @@ -213,7 +214,12 @@ module.exports = function(modules) {
context.ns = ns;
context.document = cmd;

const stateMachine = new StateMachine({ bson, ...options, proxyOptions: this._proxyOptions });
const stateMachine = new StateMachine({
bson,
...options,
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions
});
stateMachine.execute(this, context, callback);
}

Expand Down Expand Up @@ -244,7 +250,12 @@ module.exports = function(modules) {
// TODO: should this be an accessor from the addon?
context.id = this._contextCounter++;

const stateMachine = new StateMachine({ bson, ...options, proxyOptions: this._proxyOptions });
const stateMachine = new StateMachine({
bson,
...options,
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions
});
stateMachine.execute(this, context, callback);
}
}
Expand Down
20 changes: 17 additions & 3 deletions bindings/node/lib/clientEncryption.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ module.exports = function(modules) {
* @param {MongoClient} client The client used for encryption
* @param {object} options Additional settings
* @param {string} options.keyVaultNamespace The namespace of the key vault, used to store encryption keys
* @param {object} options.tlsOptions An object that maps KMS provider names to TLS options.
* @param {MongoClient} [options.keyVaultClient] A `MongoClient` used to fetch keys from a key vault. Defaults to `client`
* @param {KMSProviders} [options.kmsProviders] options for specific KMS providers to use
*
Expand Down Expand Up @@ -66,6 +67,7 @@ module.exports = function(modules) {
this._client = client;
this._bson = options.bson || client.topology.bson;
this._proxyOptions = options.proxyOptions;
this._tlsOptions = options.tlsOptions;

if (options.keyVaultNamespace == null) {
throw new TypeError('Missing required option `keyVaultNamespace`');
Expand Down Expand Up @@ -199,7 +201,11 @@ module.exports = function(modules) {

const dataKeyBson = bson.serialize(dataKey);
const context = this._mongoCrypt.makeDataKeyContext(dataKeyBson, { keyAltNames });
const stateMachine = new StateMachine({ bson, proxyOptions: this._proxyOptions });
const stateMachine = new StateMachine({
bson,
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions
});

return promiseOrCallback(callback, cb => {
stateMachine.execute(this, context, (err, dataKey) => {
Expand Down Expand Up @@ -291,7 +297,11 @@ module.exports = function(modules) {
contextOptions.keyAltName = bson.serialize({ keyAltName });
}

const stateMachine = new StateMachine({ bson, proxyOptions: this._proxyOptions });
const stateMachine = new StateMachine({
bson,
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions
});
const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions);

return promiseOrCallback(callback, cb => {
Expand Down Expand Up @@ -336,7 +346,11 @@ module.exports = function(modules) {
const valueBuffer = bson.serialize({ v: value });
const context = this._mongoCrypt.makeExplicitDecryptionContext(valueBuffer);

const stateMachine = new StateMachine({ bson, proxyOptions: this._proxyOptions });
const stateMachine = new StateMachine({
bson,
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions
});

return promiseOrCallback(callback, cb => {
stateMachine.execute(this, context, (err, result) => {
Expand Down
58 changes: 58 additions & 0 deletions bindings/node/lib/stateMachine.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module.exports = function(modules) {
const tls = require('tls');
const net = require('net');
const path = require('path');
const fs = require ('fs');
const { once } = require('events');
const { SocksClient } = require('socks');

Expand Down Expand Up @@ -38,6 +40,14 @@ module.exports = function(modules) {
[MONGOCRYPT_CTX_DONE, 'MONGOCRYPT_CTX_DONE']
]);

const INSECURE_TLS_OPTIONS = [
'tlsInsecure',
'tlsAllowInvalidCertificates',
'tlsAllowInvalidHostnames',
'tlsDisableOCSPEndpointCheck',
'tlsDisableCertificateRevocationCheck'
];

/**
* @ignore
* @callback StateMachine~executeCallback
Expand Down Expand Up @@ -283,6 +293,16 @@ module.exports = function(modules) {
}
}

const tlsOptions = this.options.tlsOptions;
if (tlsOptions) {
const kmsProvider = request.kmsProvider;
const providerTlsOptions = tlsOptions[kmsProvider];
if (providerTlsOptions) {
const error = this.validateTlsOptions(kmsProvider, providerTlsOptions);
if (error) reject(error);
this.setTlsOptions(providerTlsOptions, options);
}
}
socket = tls.connect(options, () => {
socket.write(message);
});
Expand All @@ -305,6 +325,44 @@ module.exports = function(modules) {
});
}

/**
* @ignore
* Validates the provided TLS options are secure.
*
* @param {string} kmsProvider The KMS provider name.
* @param {ClientEncryptionTLSOptions} tlsOptions The client TLS options for the provider.
*
* @returns {Error} If any option is invalid.
*/
validateTlsOptions(kmsProvider, tlsOptions) {
const tlsOptionNames = Object.keys(tlsOptions);
for (const option of INSECURE_TLS_OPTIONS) {
if (tlsOptionNames.includes(option)) {
return new MongoCryptError(`Insecure TLS options prohibited for ${kmsProvider}: ${option}`);
}
}
}

/**
* @ignore
* Sets only the valid secure TLS options.
*
* @param {ClientEncryptionTLSOptions} tlsOptions The client TLS options for the provider.
* @param {Object} options The existing connection options.
*/
setTlsOptions(tlsOptions, options) {
if (tlsOptions.tlsCertificateKeyFile) {
const cert = fs.readFileSync(tlsOptions.tlsCertificateKeyFile);
options.cert = options.key = cert;
}
if (tlsOptions.tlsCAFile) {
options.ca = fs.readFileSync(tlsOptions.tlsCAFile);
}
if (tlsOptions.tlsCertificateKeyFilePassword) {
options.passphrase = tlsOptions.tlsCertificateKeyFilePassword;
}
}

/**
* @ignore
* Fetches collection info for a provided namespace, when libmongocrypt
Expand Down
2 changes: 1 addition & 1 deletion bindings/node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

169 changes: 140 additions & 29 deletions bindings/node/test/stateMachine.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const BSON = require('bson');
const { EventEmitter, once } = require('events');
const net = require('net');
const tls = require('tls');
const fs = require('fs');
const expect = require('chai').expect;
const sinon = require('sinon');
const mongodb = require('mongodb');
Expand Down Expand Up @@ -52,40 +53,150 @@ describe('StateMachine', function() {
this.sinon = sinon.createSandbox();
});

beforeEach(function() {
this.fakeSocket = undefined;
this.sinon.stub(tls, 'connect').callsFake((options, callback) => {
this.fakeSocket = new MockSocket(callback);
return this.fakeSocket;
context('when handling standard kms requests', function() {
beforeEach(function() {
this.fakeSocket = undefined;
this.sinon.stub(tls, 'connect').callsFake((options, callback) => {
this.fakeSocket = new MockSocket(callback);
return this.fakeSocket;
});
});
});

it('should only resolve once bytesNeeded drops to zero', function(done) {
const stateMachine = new StateMachine({ bson: BSON });
const request = new MockRequest(Buffer.from('foobar'), 500);
let status = 'pending';
stateMachine
.kmsRequest(request)
.then(
() => (status = 'resolved'),
() => (status = 'rejected')
)
.catch(() => {});

this.fakeSocket.emit('connect');
setTimeout(() => {
expect(status).to.equal('pending');
expect(request.bytesNeeded).to.equal(500);
expect(request.kmsProvider).to.equal('aws');
this.fakeSocket.emit('data', Buffer.alloc(300));
it('should only resolve once bytesNeeded drops to zero', function(done) {
const stateMachine = new StateMachine({ bson: BSON });
const request = new MockRequest(Buffer.from('foobar'), 500);
let status = 'pending';
stateMachine
.kmsRequest(request)
.then(
() => (status = 'resolved'),
() => (status = 'rejected')
)
.catch(() => {});

this.fakeSocket.emit('connect');
setTimeout(() => {
expect(status).to.equal('pending');
expect(request.bytesNeeded).to.equal(200);
this.fakeSocket.emit('data', Buffer.alloc(200));
expect(request.bytesNeeded).to.equal(500);
expect(request.kmsProvider).to.equal('aws');
this.fakeSocket.emit('data', Buffer.alloc(300));
setTimeout(() => {
expect(status).to.equal('resolved');
expect(request.bytesNeeded).to.equal(0);
done();
expect(status).to.equal('pending');
expect(request.bytesNeeded).to.equal(200);
this.fakeSocket.emit('data', Buffer.alloc(200));
setTimeout(() => {
expect(status).to.equal('resolved');
expect(request.bytesNeeded).to.equal(0);
done();
});
});
});
});
});

context('when tls options are provided', function() {
context('when the options are insecure', function() {
[
'tlsInsecure',
'tlsAllowInvalidCertificates',
'tlsAllowInvalidHostnames',
'tlsDisableOCSPEndpointCheck',
'tlsDisableCertificateRevocationCheck'
].forEach(function(option) {
context(`when the option is ${option}`, function() {
const stateMachine = new StateMachine({
bson: BSON,
tlsOptions: { aws: { [option]: true }}
});
const request = new MockRequest(Buffer.from('foobar'), 500);

it('rejects with the validation error', function(done) {
stateMachine
.kmsRequest(request)
.catch((err) => {
expect(err.message).to.equal(`Insecure TLS options prohibited for aws: ${option}`);
done();
});
});
});
});
});

context('when the options are secure', function() {
context('when providing tlsCertificateKeyFile', function() {
const stateMachine = new StateMachine({
bson: BSON,
tlsOptions: { aws: { tlsCertificateKeyFile: 'test.pem' }}
});
const request = new MockRequest(Buffer.from('foobar'), -1);
const buffer = Buffer.from('foobar');
let connectOptions;

it('sets the cert and key options in the tls connect options', function(done) {
this.sinon.stub(fs, 'readFileSync').callsFake((fileName) => {
expect(fileName).to.equal('test.pem');
return buffer;
});
this.sinon.stub(tls, 'connect').callsFake((options, callback) => {
connectOptions = options;
this.fakeSocket = new MockSocket(callback);
return this.fakeSocket;
});
stateMachine.kmsRequest(request).then(function() {
expect(connectOptions.cert).to.equal(buffer);
expect(connectOptions.key).to.equal(buffer);
done();
});
this.fakeSocket.emit('data', Buffer.alloc(0));
});
});

context('when providing tlsCAFile', function() {
const stateMachine = new StateMachine({
bson: BSON,
tlsOptions: { aws: { tlsCAFile: 'test.pem' }}
});
const request = new MockRequest(Buffer.from('foobar'), -1);
const buffer = Buffer.from('foobar');
let connectOptions;

it('sets the ca options in the tls connect options', function(done) {
this.sinon.stub(fs, 'readFileSync').callsFake((fileName) => {
expect(fileName).to.equal('test.pem');
return buffer;
});
this.sinon.stub(tls, 'connect').callsFake((options, callback) => {
connectOptions = options;
this.fakeSocket = new MockSocket(callback);
return this.fakeSocket;
});
stateMachine.kmsRequest(request).then(function() {
expect(connectOptions.ca).to.equal(buffer);
done();
});
this.fakeSocket.emit('data', Buffer.alloc(0));
});
});

context('when providing tlsCertificateKeyFilePassword', function() {
const stateMachine = new StateMachine({
bson: BSON,
tlsOptions: { aws: { tlsCertificateKeyFilePassword: 'test' }}
});
const request = new MockRequest(Buffer.from('foobar'), -1);
let connectOptions;

it('sets the passphrase option in the tls connect options', function(done) {
this.sinon.stub(tls, 'connect').callsFake((options, callback) => {
connectOptions = options;
this.fakeSocket = new MockSocket(callback);
return this.fakeSocket;
});
stateMachine.kmsRequest(request).then(function() {
expect(connectOptions.passphrase).to.equal('test');
done();
});
this.fakeSocket.emit('data', Buffer.alloc(0));
});
});
});
Expand Down