Skip to content

Commit 1467075

Browse files
authored
feat(shell-api): add KMIP support MONGOSH-1013 (#1235)
Add support for passing through `tlsOptions` for FLE specifically. This was previously not possible, and is necessary for KMIP support, since KMIP uses TLS with a client key/certificate for authentication.
1 parent eb6ffa7 commit 1467075

26 files changed

+64
-8
lines changed

packages/cli-repl/test/e2e-tls.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { promisify } from 'util';
88
import rimraf from 'rimraf';
99

1010
function getCertPath(filename: string): string {
11-
return path.join(__dirname, 'fixtures', 'certificates', filename);
11+
return path.join(__dirname, '..', '..', '..', 'testing', 'certificates', filename);
1212
}
1313
const CA_CERT = getCertPath('ca.crt');
1414
const NON_CA_CERT = getCertPath('non-ca.crt');

packages/service-provider-core/src/all-fle-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type {
1010
ClientEncryptionEncryptCallback,
1111
ClientEncryptionEncryptOptions,
1212
ClientEncryptionOptions,
13+
ClientEncryptionTlsOptions,
1314
KMSProviders
1415
} from 'mongodb-client-encryption';
1516

packages/shell-api/src/field-level-encryption.spec.ts

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { MongoshInvalidInputError } from '@mongosh/errors';
2-
import { bson, ClientEncryption as FLEClientEncryption, ServiceProvider } from '@mongosh/service-provider-core';
2+
import { bson, ClientEncryption as FLEClientEncryption, ClientEncryptionTlsOptions, ServiceProvider } from '@mongosh/service-provider-core';
33
import { expect } from 'chai';
44
import { EventEmitter } from 'events';
5+
import { promises as fs } from 'fs';
6+
import path from 'path';
7+
import { Duplex } from 'stream';
58
import sinon, { StubbedInstance, stubInterface } from 'ts-sinon';
69
import Database from './database';
710
import { signatures, toShellResult } from './decorators';
@@ -47,6 +50,10 @@ const ALGO = 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic';
4750

4851
const RAW_CLIENT = { client: 1 } as any;
4952

53+
function getCertPath(filename: string): string {
54+
return path.join(__dirname, '..', '..', '..', 'testing', 'certificates', filename);
55+
}
56+
5057
describe('Field Level Encryption', () => {
5158
let sp: StubbedInstance<ServiceProvider>;
5259
let mongo: Mongo;
@@ -482,6 +489,17 @@ describe('Field Level Encryption', () => {
482489

483490
connections = [];
484491
sinon.replace(require('tls'), 'connect', sinon.fake((options, onConnect) => {
492+
if (options.host === 'kmip.example.com') {
493+
// KMIP is not http(s)-based, we don't implement strong fakes for it
494+
// and instead only verify that a connection has occurred.
495+
connections.push({ options });
496+
process.nextTick(onConnect);
497+
const conn = new Duplex({
498+
read() { setImmediate(() => this.destroy(new Error('mock connection broken'))); },
499+
write(chunk, enc, cb) { cb(); }
500+
});
501+
return conn;
502+
}
485503
if (!fakeAWSHandlers.some(handler => handler.host.test(options.host))) {
486504
throw new Error(`Unexpected TLS connection to ${options.host}`);
487505
}
@@ -498,7 +516,7 @@ describe('Field Level Encryption', () => {
498516
sinon.restore();
499517
});
500518

501-
const kms: [keyof KMSProvider, KMSProvider[keyof KMSProvider]][] = [
519+
const kms: [keyof KMSProvider, KMSProvider[keyof KMSProvider] & { tlsOptions?: ClientEncryptionTlsOptions }][] = [
502520
['local', {
503521
key: new bson.Binary(Buffer.from('kh4Gv2N8qopZQMQYMEtww/AkPsIrXNmEMxTrs3tUoTQZbZu4msdRUaR8U5fXD7A7QXYHcEvuu4WctJLoT+NvvV3eeIg3MD+K8H9SR794m/safgRHdIfy6PD+rFpvmFbY', 'base64'), 0)
504522
}],
@@ -528,15 +546,25 @@ lt6waE7I2uSPqIC20LcCIQDJQYIHQII+3YaPqyhGgqMexuuuGx+lDKD6/Fu/JwPb
528546
5QIhAKthiYcYKlL9h8bjDsQhZDUACPasjzdsDEdq8inDyLOFAiEAmCr/tZwA3qeA
529547
ZoBzI10DGPIuoKXBd3nk/eBxPkaxlEECIQCNymjsoI7GldtujVnr1qT+3yedLfHK
530548
srDVjIT3LsvTqw==`
549+
}],
550+
['kmip', {
551+
endpoint: 'kmip.example.com:123',
552+
tlsOptions: {
553+
tlsCertificateKeyFile: getCertPath('client.bundle.encrypted.pem'),
554+
tlsCertificateKeyFilePassword: 'p4ssw0rd',
555+
tlsCAFile: getCertPath('ca.crt')
556+
}
531557
}]
532558
];
533-
for (const [ kmsName, kmsOptions ] of kms) {
559+
for (const [ kmsName, kmsAndTlsOptions ] of kms) {
534560
// eslint-disable-next-line no-loop-func
535561
it(`provides ClientEncryption for kms=${kmsName}`, async() => {
562+
const kmsOptions = { ...kmsAndTlsOptions, tlsOptions: undefined };
536563
const mongo = new Mongo(instanceState, uri, {
537564
keyVaultNamespace: `${dbname}.__keyVault`,
538565
kmsProviders: { [kmsName]: kmsOptions } as any,
539-
explicitEncryptionOnly: true
566+
explicitEncryptionOnly: true,
567+
tlsOptions: { [kmsName]: kmsAndTlsOptions.tlsOptions ?? undefined }
540568
}, serviceProvider);
541569
await mongo.connect();
542570
instanceState.mongos.push(mongo);
@@ -567,6 +595,28 @@ srDVjIT3LsvTqw==`
567595
keyName: 'foobar'
568596
});
569597
break;
598+
case 'kmip':
599+
try {
600+
await keyVault.createKey('kmip', undefined);
601+
} catch (err) {
602+
// See above, we don't attempt to successfully encrypt/decrypt
603+
// when using KMIP
604+
expect(err.message).to.include('KMS request failed');
605+
expect(connections).to.deep.equal([{
606+
options: {
607+
host: 'kmip.example.com',
608+
servername: 'kmip.example.com',
609+
port: 123,
610+
passphrase: 'p4ssw0rd',
611+
ca: await fs.readFile(getCertPath('ca.crt')),
612+
cert: await fs.readFile(getCertPath('client.bundle.encrypted.pem')),
613+
key: await fs.readFile(getCertPath('client.bundle.encrypted.pem'))
614+
}
615+
}]);
616+
return;
617+
}
618+
expect.fail('missed exception');
619+
break;
570620
default:
571621
throw new Error(`unreachable ${kmsName}`);
572622
}
@@ -584,12 +634,12 @@ srDVjIT3LsvTqw==`
584634
expect(encrypted.sub_type).to.equal(6); // Encrypted
585635
expect(decrypted).to.deep.equal(plaintextValue);
586636

587-
if ((kmsOptions as any).sessionToken) { // as any -> NODE-3107
637+
if ('sessionToken' in kmsOptions) {
588638
expect(
589639
connections.map(
590640
conn => conn.requests.map(
591641
req => req.headers['x-amz-security-token'])).flat())
592-
.to.include((kmsOptions as any).sessionToken);
642+
.to.include(kmsOptions.sessionToken);
593643
}
594644
});
595645
}

packages/shell-api/src/field-level-encryption.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
ClientEncryptionDataKeyProvider,
1313
ClientEncryptionOptions,
1414
ClientEncryptionEncryptOptions,
15+
ClientEncryptionTlsOptions,
1516
KMSProviders,
1617
ReplPlatform,
1718
AWSEncryptionKeyOptions,
@@ -41,6 +42,7 @@ export interface ClientSideFieldLevelEncryptionOptions {
4142
schemaMap?: Document,
4243
bypassAutoEncryption?: boolean;
4344
explicitEncryptionOnly?: boolean;
45+
tlsOptions?: { [k in keyof ClientSideFieldLevelEncryptionKmsProvider]?: ClientEncryptionTlsOptions };
4446
}
4547

4648
@shellApiClassDefault

packages/shell-api/src/helpers.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,7 @@ export function assertCLI(platform: ReplPlatform, features: string): void {
659659
export function processFLEOptions(fleOptions: ClientSideFieldLevelEncryptionOptions): AutoEncryptionOptions {
660660
assertKeysDefined(fleOptions, ['keyVaultNamespace', 'kmsProviders']);
661661
Object.keys(fleOptions).forEach(k => {
662-
if (['keyVaultClient', 'keyVaultNamespace', 'kmsProviders', 'schemaMap', 'bypassAutoEncryption'].indexOf(k) === -1) {
662+
if (['keyVaultClient', 'keyVaultNamespace', 'kmsProviders', 'schemaMap', 'bypassAutoEncryption', 'tlsOptions'].indexOf(k) === -1) {
663663
throw new MongoshInvalidInputError(`Unrecognized FLE Client Option ${k}`);
664664
}
665665
});
@@ -691,6 +691,9 @@ export function processFLEOptions(fleOptions: ClientSideFieldLevelEncryptionOpti
691691
if (fleOptions.bypassAutoEncryption !== undefined) {
692692
autoEncryption.bypassAutoEncryption = fleOptions.bypassAutoEncryption;
693693
}
694+
if (fleOptions.tlsOptions !== undefined) {
695+
autoEncryption.tlsOptions = fleOptions.tlsOptions;
696+
}
694697
return autoEncryption;
695698
}
696699

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)