Skip to content

Commit

Permalink
feat(NODE-3777): set tls options per kms provider (#235)
Browse files Browse the repository at this point in the history
  • Loading branch information
durran committed Jan 20, 2022
1 parent dd55a31 commit 5920d84
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 41 deletions.
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

0 comments on commit 5920d84

Please sign in to comment.