summaryrefslogtreecommitdiff
path: root/jstests/client_encrypt
diff options
context:
space:
mode:
authorMark Benvenuto <mark.benvenuto@mongodb.com>2019-06-18 00:19:33 -0400
committerMark Benvenuto <mark.benvenuto@mongodb.com>2019-06-18 00:19:33 -0400
commitc436b8090417baf847143e97f5d221285b1898e1 (patch)
treebb5369a793c8ec6646b20e92b1a0f85b7979682f /jstests/client_encrypt
parent5eda33f9fa40a1a17f9f63f904a8c147700d648c (diff)
downloadmongo-c436b8090417baf847143e97f5d221285b1898e1.tar.gz
SERVER-41644 Expose explicit encryption helpers in community shell
Diffstat (limited to 'jstests/client_encrypt')
-rw-r--r--jstests/client_encrypt/fle_aws_faults.js142
-rw-r--r--jstests/client_encrypt/fle_command_line_encryption.js41
-rw-r--r--jstests/client_encrypt/fle_encrypt_decrypt_shell.js112
-rw-r--r--jstests/client_encrypt/fle_key_faults.js94
-rw-r--r--jstests/client_encrypt/fle_keys.js75
-rw-r--r--jstests/client_encrypt/fle_valid_fle_options.js68
-rw-r--r--jstests/client_encrypt/lib/fle_command_line_explicit_encryption.js84
-rw-r--r--jstests/client_encrypt/lib/kms_http_common.py21
-rw-r--r--jstests/client_encrypt/lib/kms_http_control.py52
-rwxr-xr-xjstests/client_encrypt/lib/kms_http_server.py298
-rw-r--r--jstests/client_encrypt/lib/mock_kms.js161
11 files changed, 1148 insertions, 0 deletions
diff --git a/jstests/client_encrypt/fle_aws_faults.js b/jstests/client_encrypt/fle_aws_faults.js
new file mode 100644
index 00000000000..bee9586ca43
--- /dev/null
+++ b/jstests/client_encrypt/fle_aws_faults.js
@@ -0,0 +1,142 @@
+/**
+ * Verify the AWS KMS implementation can handle a buggy KMS.
+ */
+
+load("jstests/client_encrypt/lib/mock_kms.js");
+load('jstests/ssl/libs/ssl_helpers.js');
+
+(function() {
+ "use strict";
+
+ const x509_options = {sslMode: "requireSSL", sslPEMKeyFile: SERVER_CERT, sslCAFile: CA_CERT};
+
+ const randomAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Random";
+
+ const conn = MongoRunner.runMongod(x509_options);
+ const test = conn.getDB("test");
+ const collection = test.coll;
+
+ function runKMS(mock_kms, func) {
+ mock_kms.start();
+
+ const awsKMS = {
+ accessKeyId: "access",
+ secretAccessKey: "secret",
+ url: mock_kms.getURL(),
+ };
+
+ const clientSideFLEOptions = {
+ kmsProviders: {
+ aws: awsKMS,
+ },
+ keyVaultNamespace: "test.coll",
+ schemaMap: {}
+ };
+
+ const shell = Mongo(conn.host, clientSideFLEOptions);
+ const cleanCacheShell = Mongo(conn.host, clientSideFLEOptions);
+
+ collection.drop();
+
+ func(shell, cleanCacheShell);
+
+ mock_kms.stop();
+ }
+
+ function testBadEncryptResult(fault) {
+ const mock_kms = new MockKMSServer(fault, false);
+
+ runKMS(mock_kms, (shell) => {
+ const keyVault = shell.getKeyVault();
+
+ assert.throws(() => keyVault.createKey(
+ "aws", "arn:aws:kms:us-east-1:fake:fake:fake", ["mongoKey"]));
+ assert.eq(keyVault.getKeys("mongoKey").toArray().length, 0);
+ });
+ }
+
+ testBadEncryptResult(FAULT_ENCRYPT);
+ testBadEncryptResult(FAULT_ENCRYPT_WRONG_FIELDS);
+ testBadEncryptResult(FAULT_ENCRYPT_BAD_BASE64);
+
+ function testBadEncryptError() {
+ const mock_kms = new MockKMSServer(FAULT_ENCRYPT_CORRECT_FORMAT, false);
+
+ runKMS(mock_kms, (shell) => {
+ const keyVault = shell.getKeyVault();
+
+ let error =
+ assert.throws(() => keyVault.createKey(
+ "aws", "arn:aws:kms:us-east-1:fake:fake:fake", ["mongoKey"]));
+ assert.commandFailedWithCode(error, [51224]);
+ assert.eq(
+ error,
+ "Error: AWS KMS failed to encrypt: NotFoundException : Error encrypting message");
+ });
+ }
+
+ testBadEncryptError();
+
+ function testBadDecryptResult(fault) {
+ const mock_kms = new MockKMSServer(fault, false);
+
+ runKMS(mock_kms, (shell) => {
+ const keyVault = shell.getKeyVault();
+ assert.writeOK(
+ keyVault.createKey("aws", "arn:aws:kms:us-east-1:fake:fake:fake", ["mongoKey"]));
+ const keyId = keyVault.getKeys("mongoKey").toArray()[0]._id;
+ const str = "mongo";
+ assert.throws(() => {
+ const encStr = shell.encrypt(keyId, str, randomAlgorithm);
+ });
+ });
+ }
+
+ testBadDecryptResult(FAULT_DECRYPT);
+
+ function testBadDecryptKeyResult(fault) {
+ const mock_kms = new MockKMSServer(fault, true);
+
+ runKMS(mock_kms, (shell, cleanCacheShell) => {
+ const keyVault = shell.getKeyVault();
+
+ assert.writeOK(
+ keyVault.createKey("aws", "arn:aws:kms:us-east-1:fake:fake:fake", ["mongoKey"]));
+ const keyId = keyVault.getKeys("mongoKey").toArray()[0]._id;
+ const str = "mongo";
+ const encStr = shell.encrypt(keyId, str, randomAlgorithm);
+
+ mock_kms.enableFaults();
+
+ assert.throws(() => {
+ var str = cleanCacheShell.decrypt(encStr);
+ });
+
+ });
+ }
+
+ testBadDecryptKeyResult(FAULT_DECRYPT_WRONG_KEY);
+
+ function testBadDecryptError() {
+ const mock_kms = new MockKMSServer(FAULT_DECRYPT_CORRECT_FORMAT, false);
+
+ runKMS(mock_kms, (shell) => {
+ const keyVault = shell.getKeyVault();
+ assert.writeOK(
+ keyVault.createKey("aws", "arn:aws:kms:us-east-1:fake:fake:fake", ["mongoKey"]));
+ const keyId = keyVault.getKeys("mongoKey").toArray()[0]._id;
+ const str = "mongo";
+ let error = assert.throws(() => {
+ const encStr = shell.encrypt(keyId, str, randomAlgorithm);
+ });
+ assert.commandFailedWithCode(error, [51225]);
+ assert.eq(
+ error,
+ "Error: AWS KMS failed to decrypt: NotFoundException : Error decrypting message");
+ });
+ }
+
+ testBadDecryptError();
+
+ MongoRunner.stopMongod(conn);
+}()); \ No newline at end of file
diff --git a/jstests/client_encrypt/fle_command_line_encryption.js b/jstests/client_encrypt/fle_command_line_encryption.js
new file mode 100644
index 00000000000..9113f9f2d74
--- /dev/null
+++ b/jstests/client_encrypt/fle_command_line_encryption.js
@@ -0,0 +1,41 @@
+/*
+ * This file tests an encrypted shell started using command line parameters.
+ *
+ */
+load('jstests/ssl/libs/ssl_helpers.js');
+
+(function() {
+
+ const x509_options = {sslMode: "requireSSL", sslPEMKeyFile: SERVER_CERT, sslCAFile: CA_CERT};
+ const conn = MongoRunner.runMongod(x509_options);
+
+ const shellOpts = [
+ "mongo",
+ "--host",
+ conn.host,
+ "--port",
+ conn.port,
+ "--tls",
+ "--sslPEMKeyFile",
+ CLIENT_CERT,
+ "--sslCAFile",
+ CA_CERT,
+ "--tlsAllowInvalidHostnames",
+ "--awsAccessKeyId",
+ "access",
+ "--awsSecretAccessKey",
+ "secret",
+ "--keyVaultNamespace",
+ "test.coll",
+ "--kmsURL",
+ "https://localhost:8000",
+ ];
+
+ const testFiles = [
+ "jstests/client_encrypt/lib/fle_command_line_explicit_encryption.js",
+ ];
+
+ for (const file of testFiles) {
+ runMongoProgram(...shellOpts, file);
+ }
+}()); \ No newline at end of file
diff --git a/jstests/client_encrypt/fle_encrypt_decrypt_shell.js b/jstests/client_encrypt/fle_encrypt_decrypt_shell.js
new file mode 100644
index 00000000000..79ffb87cc19
--- /dev/null
+++ b/jstests/client_encrypt/fle_encrypt_decrypt_shell.js
@@ -0,0 +1,112 @@
+/**
+ * Check the functionality of encrypt and decrypt functions in KeyStore.js
+ */
+load("jstests/client_encrypt/lib/mock_kms.js");
+load('jstests/ssl/libs/ssl_helpers.js');
+
+(function() {
+ "use strict";
+
+ const mock_kms = new MockKMSServer();
+ mock_kms.start();
+
+ const x509_options = {sslMode: "requireSSL", sslPEMKeyFile: SERVER_CERT, sslCAFile: CA_CERT};
+
+ const conn = MongoRunner.runMongod(x509_options);
+ const test = conn.getDB("test");
+ const collection = test.coll;
+
+ const awsKMS = {
+ accessKeyId: "access",
+ secretAccessKey: "secret",
+ url: mock_kms.getURL(),
+ };
+
+ let localKMS = {
+ key: BinData(
+ 0,
+ "/i8ytmWQuCe1zt3bIuVa4taPGKhqasVp0/0yI4Iy0ixQPNmeDF1J5qPUbBYoueVUJHMqj350eRTwztAWXuBdSQ=="),
+ };
+
+ const clientSideFLEOptions = {
+ kmsProviders: {
+ aws: awsKMS,
+ local: localKMS,
+ },
+ keyVaultNamespace: "test.coll",
+ schemaMap: {}
+ };
+
+ const kmsTypes = ["aws", "local"];
+
+ const randomAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Random";
+ const deterministicAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic";
+ const encryptionAlgorithms = [randomAlgorithm, deterministicAlgorithm];
+
+ const passTestCases = [
+ "mongo",
+ NumberLong(13),
+ NumberInt(23),
+ UUID(),
+ ISODate(),
+ new Date('December 17, 1995 03:24:00'),
+ BinData(2, '1234'),
+ new Timestamp(1, 2),
+ new ObjectId(),
+ new DBPointer("mongo", new ObjectId()),
+ /test/
+ ];
+
+ const failDeterministic = [
+ true,
+ false,
+ 12,
+ NumberDecimal(0.1234),
+ ["this is an array"],
+ {"value": "mongo"},
+ Code("function() { return true; }")
+ ];
+
+ const failTestCases = [null, undefined, MinKey(), MaxKey(), DBRef("test", "test", "test")];
+
+ const shell = Mongo(conn.host, clientSideFLEOptions);
+ const keyVault = shell.getKeyVault();
+
+ // Testing for every combination of (kmsType, algorithm, javascriptVariable)
+ for (const kmsType of kmsTypes) {
+ for (const encryptionAlgorithm of encryptionAlgorithms) {
+ collection.drop();
+
+ assert.writeOK(
+ keyVault.createKey(kmsType, "arn:aws:kms:us-east-1:fake:fake:fake", ['mongoKey']));
+ const keyId = keyVault.getKeyByAltName("mongoKey").toArray()[0]._id;
+
+ let pass;
+ let fail;
+ if (encryptionAlgorithm === randomAlgorithm) {
+ pass = [...passTestCases, ...failDeterministic];
+ fail = failTestCases;
+ } else if (encryptionAlgorithm === deterministicAlgorithm) {
+ pass = passTestCases;
+ fail = [...failTestCases, ...failDeterministic];
+ }
+
+ for (const passTestCase of pass) {
+ const encPassTestCase = shell.encrypt(keyId, passTestCase, encryptionAlgorithm);
+ assert.eq(passTestCase, shell.decrypt(encPassTestCase));
+
+ if (encryptionAlgorithm === deterministicAlgorithm) {
+ assert.eq(encPassTestCase,
+ shell.encrypt(keyId, passTestCase, encryptionAlgorithm));
+ }
+ }
+
+ for (const failTestCase of fail) {
+ assert.throws(shell.encrypt, [keyId, failTestCase, encryptionAlgorithm]);
+ }
+ }
+ }
+
+ MongoRunner.stopMongod(conn);
+ mock_kms.stop();
+}()); \ No newline at end of file
diff --git a/jstests/client_encrypt/fle_key_faults.js b/jstests/client_encrypt/fle_key_faults.js
new file mode 100644
index 00000000000..5f2fdcab08a
--- /dev/null
+++ b/jstests/client_encrypt/fle_key_faults.js
@@ -0,0 +1,94 @@
+/**
+ * Verify the KMS support handles a buggy Key Store
+ */
+
+load("jstests/client_encrypt/lib/mock_kms.js");
+load('jstests/ssl/libs/ssl_helpers.js');
+
+(function() {
+ "use strict";
+
+ const mock_kms = new MockKMSServer();
+ mock_kms.start();
+
+ const x509_options = {sslMode: "requireSSL", sslPEMKeyFile: SERVER_CERT, sslCAFile: CA_CERT};
+
+ const conn = MongoRunner.runMongod(x509_options);
+ const test = conn.getDB("test");
+ const collection = test.coll;
+
+ const awsKMS = {
+ accessKeyId: "access",
+ secretAccessKey: "secret",
+ url: mock_kms.getURL(),
+ };
+
+ var localKMS = {
+ key: BinData(
+ 0,
+ "/i8ytmWQuCe1zt3bIuVa4taPGKhqasVp0/0yI4Iy0ixQPNmeDF1J5qPUbBYoueVUJHMqj350eRTwztAWXuBdSQ=="),
+ };
+
+ const clientSideFLEOptions = {
+ kmsProviders: {
+ aws: awsKMS,
+ local: localKMS,
+ },
+ keyVaultNamespace: "test.coll",
+ schemaMap: {}
+ };
+
+ function testFault(kmsType, func) {
+ collection.drop();
+
+ const shell = Mongo(conn.host, clientSideFLEOptions);
+ const keyVault = shell.getKeyVault();
+
+ assert.writeOK(
+ keyVault.createKey(kmsType, "arn:aws:kms:us-east-1:fake:fake:fake", ['mongoKey']));
+ const keyId = keyVault.getKeyByAltName("mongoKey").toArray()[0]._id;
+
+ func(keyId, shell);
+ }
+
+ function testFaults(func) {
+ const kmsTypes = ["aws", "local"];
+
+ for (const kmsType of kmsTypes) {
+ testFault(kmsType, func);
+ }
+ }
+
+ // Negative - drop the key vault collection
+ testFaults((keyId, shell) => {
+ collection.drop();
+
+ const str = "mongo";
+ assert.throws(() => {
+ const encStr = shell.encrypt(keyId, str);
+ });
+ });
+
+ // Negative - delete the keys
+ testFaults((keyId, shell) => {
+ collection.deleteMany({});
+
+ const str = "mongo";
+ assert.throws(() => {
+ const encStr = shell.encrypt(keyId, str);
+ });
+ });
+
+ // Negative - corrupt the master key with an unkown provider
+ testFaults((keyId, shell) => {
+ collection.updateMany({}, {$set: {"masterKey.provider": "fake"}});
+
+ const str = "mongo";
+ assert.throws(() => {
+ const encStr = shell.encrypt(keyId, str);
+ });
+ });
+
+ MongoRunner.stopMongod(conn);
+ mock_kms.stop();
+}()); \ No newline at end of file
diff --git a/jstests/client_encrypt/fle_keys.js b/jstests/client_encrypt/fle_keys.js
new file mode 100644
index 00000000000..875615ac9a8
--- /dev/null
+++ b/jstests/client_encrypt/fle_keys.js
@@ -0,0 +1,75 @@
+/**
+ * Check functionality of KeyVault.js
+ */
+
+load("jstests/client_encrypt/lib/mock_kms.js");
+load('jstests/ssl/libs/ssl_helpers.js');
+
+(function() {
+ "use strict";
+
+ const mock_kms = new MockKMSServer();
+ mock_kms.start();
+
+ const x509_options = {sslMode: "requireSSL", sslPEMKeyFile: SERVER_CERT, sslCAFile: CA_CERT};
+
+ const conn = MongoRunner.runMongod(x509_options);
+ const test = conn.getDB("test");
+ const collection = test.coll;
+
+ const awsKMS = {
+ accessKeyId: "access",
+ secretAccessKey: "secret",
+ url: mock_kms.getURL(),
+ };
+
+ const clientSideFLEOptions = {
+ kmsProviders: {
+ aws: awsKMS,
+ },
+ keyVaultNamespace: "test.coll",
+ schemaMap: {}
+ };
+
+ const conn_str = "mongodb://" + conn.host + "/?ssl=true";
+ const shell = Mongo(conn_str, clientSideFLEOptions);
+ const keyVault = shell.getKeyVault();
+
+ var key = keyVault.createKey("aws", "arn:aws:kms:us-east-1:fake:fake:fake", ['mongoKey']);
+ assert.eq(1, keyVault.getKeys().itcount());
+
+ var result = keyVault.createKey("aws", "arn:aws:kms:us-east-4:fake:fake:fake", {});
+ assert.eq("TypeError: key alternate names must be of Array type.", result);
+
+ result = keyVault.createKey("aws", "arn:aws:kms:us-east-5:fake:fake:fake", [1]);
+ assert.eq("TypeError: items in key alternate names must be of String type.", result);
+
+ assert.eq(1, keyVault.getKeyByAltName("mongoKey").itcount());
+
+ var keyId = keyVault.getKeyByAltName("mongoKey").toArray()[0]._id;
+
+ keyVault.addKeyAlternateName(keyId, "mongoKey2");
+
+ assert.eq(1, keyVault.getKeyByAltName("mongoKey2").itcount());
+ assert.eq(2, keyVault.getKey(keyId).toArray()[0].keyAltNames.length);
+ assert.eq(1, keyVault.getKeys().itcount());
+
+ result = keyVault.addKeyAlternateName(keyId, [2]);
+ assert.eq("TypeError: key alternate name cannot be object or array type.", result);
+
+ keyVault.removeKeyAlternateName(keyId, "mongoKey2");
+ assert.eq(1, keyVault.getKey(keyId).toArray()[0].keyAltNames.length);
+
+ result = keyVault.deleteKey(keyId);
+ assert.eq(0, keyVault.getKey(keyId).itcount());
+ assert.eq(0, keyVault.getKeys().itcount());
+
+ assert.writeOK(keyVault.createKey("aws", "arn:aws:kms:us-east-1:fake:fake:fake1"));
+ assert.writeOK(keyVault.createKey("aws", "arn:aws:kms:us-east-2:fake:fake:fake2"));
+ assert.writeOK(keyVault.createKey("aws", "arn:aws:kms:us-east-3:fake:fake:fake3"));
+
+ assert.eq(3, keyVault.getKeys().itcount());
+
+ MongoRunner.stopMongod(conn);
+ mock_kms.stop();
+}()); \ No newline at end of file
diff --git a/jstests/client_encrypt/fle_valid_fle_options.js b/jstests/client_encrypt/fle_valid_fle_options.js
new file mode 100644
index 00000000000..2189501ad00
--- /dev/null
+++ b/jstests/client_encrypt/fle_valid_fle_options.js
@@ -0,0 +1,68 @@
+
+load("jstests/client_encrypt/lib/mock_kms.js");
+load('jstests/ssl/libs/ssl_helpers.js');
+
+(function() {
+ "use strict";
+
+ const mock_kms = new MockKMSServer();
+ mock_kms.start();
+
+ const randomAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Random";
+ const deterministicAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic";
+
+ const x509_options =
+ {sslMode: "requireSSL", sslPEMKeyFile: SERVER_CERT, sslCAFile: CA_CERT, vvvvv: ""};
+
+ const conn = MongoRunner.runMongod(x509_options);
+ const unencryptedDatabase = conn.getDB("test");
+ const collection = unencryptedDatabase.keystore;
+
+ const awsKMS = {
+ accessKeyId: "access",
+ secretAccessKey: "secret",
+ url: mock_kms.getURL(),
+ };
+
+ const clientSideFLEOptionsFail = [
+ {
+ kmsProviders: {
+ aws: awsKMS,
+ },
+ schemaMap: {},
+ },
+ {
+ kmsProviders: {
+ aws: awsKMS,
+ },
+ keyVaultNamespace: "test.keystore",
+ },
+ {
+ keyVaultNamespace: "test.keystore",
+ schemaMap: {},
+ },
+ ];
+
+ clientSideFLEOptionsFail.forEach(element => {
+ assert.throws(Mongo, [conn.host, element]);
+ });
+
+ const clientSideFLEOptionsPass = [
+ {
+ kmsProviders: {
+ aws: awsKMS,
+ },
+ keyVaultNamespace: "test.keystore",
+ schemaMap: {},
+ },
+ ];
+
+ clientSideFLEOptionsPass.forEach(element => {
+ assert.doesNotThrow(() => {
+ Mongo(conn.host, element);
+ });
+ });
+
+ MongoRunner.stopMongod(conn);
+ mock_kms.stop();
+}());
diff --git a/jstests/client_encrypt/lib/fle_command_line_explicit_encryption.js b/jstests/client_encrypt/lib/fle_command_line_explicit_encryption.js
new file mode 100644
index 00000000000..0ca10b2057c
--- /dev/null
+++ b/jstests/client_encrypt/lib/fle_command_line_explicit_encryption.js
@@ -0,0 +1,84 @@
+/**
+* Check the functionality of encrypt and decrypt functions in KeyVault.js. This test is run by
+* jstests/fle/fle_command_line_encryption.js.
+*/
+
+load("jstests/client_encrypt/lib/mock_kms.js");
+
+(function() {
+ "use strict";
+
+ const mock_kms = new MockKMSServer();
+ mock_kms.start();
+
+ const shell = Mongo();
+ const keyVault = shell.getKeyVault();
+
+ const test = shell.getDB("test");
+ const collection = test.coll;
+
+ const randomAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Random";
+ const deterministicAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic";
+ const encryptionAlgorithms = [randomAlgorithm, deterministicAlgorithm];
+
+ const passTestCases = [
+ "mongo",
+ NumberLong(13),
+ NumberInt(23),
+ UUID(),
+ ISODate(),
+ new Date('December 17, 1995 03:24:00'),
+ BinData(2, '1234'),
+ new Timestamp(1, 2),
+ new ObjectId(),
+ new DBPointer("mongo", new ObjectId()),
+ /test/
+ ];
+
+ const failDeterministic = [
+ true,
+ false,
+ 12,
+ NumberDecimal(0.1234),
+ ["this is an array"],
+ {"value": "mongo"},
+ Code("function() { return true; }")
+ ];
+
+ const failTestCases = [null, undefined, MinKey(), MaxKey(), DBRef("test", "test", "test")];
+
+ // Testing for every combination of (algorithm, javascriptVariable)
+ for (const encryptionAlgorithm of encryptionAlgorithms) {
+ collection.drop();
+
+ assert.writeOK(
+ keyVault.createKey("aws", "arn:aws:kms:us-east-1:fake:fake:fake", ['mongoKey']));
+ const keyId = keyVault.getKeyByAltName("mongoKey").toArray()[0]._id;
+
+ let pass;
+ let fail;
+ if (encryptionAlgorithm === randomAlgorithm) {
+ pass = [...passTestCases, ...failDeterministic];
+ fail = failTestCases;
+ } else if (encryptionAlgorithm === deterministicAlgorithm) {
+ pass = passTestCases;
+ fail = [...failTestCases, ...failDeterministic];
+ }
+
+ for (const passTestCase of pass) {
+ const encPassTestCase = shell.encrypt(keyId, passTestCase, encryptionAlgorithm);
+ assert.eq(passTestCase, shell.decrypt(encPassTestCase));
+
+ if (encryptionAlgorithm == deterministicAlgorithm) {
+ assert.eq(encPassTestCase, shell.encrypt(keyId, passTestCase, encryptionAlgorithm));
+ }
+ }
+
+ for (const failTestCase of fail) {
+ assert.throws(shell.encrypt, [keyId, failTestCase, encryptionAlgorithm]);
+ }
+ }
+
+ mock_kms.stop();
+ print("Test completed with no errors.");
+}()); \ No newline at end of file
diff --git a/jstests/client_encrypt/lib/kms_http_common.py b/jstests/client_encrypt/lib/kms_http_common.py
new file mode 100644
index 00000000000..aaef6a8ad69
--- /dev/null
+++ b/jstests/client_encrypt/lib/kms_http_common.py
@@ -0,0 +1,21 @@
+"""Common code for mock kms http endpoint."""
+import json
+
+URL_PATH_STATS = "/stats"
+URL_DISABLE_FAULTS = "/disable_faults"
+URL_ENABLE_FAULTS = "/enable_faults"
+
+class Stats:
+ """Stats class shared between client and server."""
+
+ def __init__(self):
+ self.encrypt_calls = 0
+ self.decrypt_calls = 0
+ self.fault_calls = 0
+
+ def __repr__(self):
+ return json.dumps({
+ 'decrypts': self.decrypt_calls,
+ 'encrypts': self.encrypt_calls,
+ 'faults': self.fault_calls,
+ })
diff --git a/jstests/client_encrypt/lib/kms_http_control.py b/jstests/client_encrypt/lib/kms_http_control.py
new file mode 100644
index 00000000000..2f62780fb77
--- /dev/null
+++ b/jstests/client_encrypt/lib/kms_http_control.py
@@ -0,0 +1,52 @@
+#! /usr/bin/env python3
+"""
+Python script to interact with mock AWS KMS HTTP server.
+"""
+
+import argparse
+import json
+import logging
+import sys
+import urllib.request
+import ssl
+
+import kms_http_common
+
+def main():
+ """Main entry point."""
+ parser = argparse.ArgumentParser(description='MongoDB Mock AWS KMS Endpoint.')
+
+ parser.add_argument('-p', '--port', type=int, default=8000, help="Port to listen on")
+
+ parser.add_argument('-v', '--verbose', action='count', help="Enable verbose tracing")
+
+ parser.add_argument('--ca_file', type=str, required=True, help="TLS CA PEM file")
+
+ parser.add_argument('--query', type=str, help="Query endpoint <name>")
+
+ args = parser.parse_args()
+ if args.verbose:
+ logging.basicConfig(level=logging.DEBUG)
+
+ url_str = "https://localhost:" + str(args.port)
+ if args.query == "stats":
+ url_str += kms_http_common.URL_PATH_STATS
+ elif args.query == "disable_faults":
+ url_str += kms_http_common.URL_DISABLE_FAULTS
+ elif args.query == "enable_faults":
+ url_str += kms_http_common.URL_ENABLE_FAULTS
+ else:
+ print("Unknown query type")
+ sys.exit(1)
+
+ context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=args.ca_file)
+
+ with urllib.request.urlopen(url_str, context=context) as f:
+ print(f.read().decode('utf-8'))
+
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+
+ main()
diff --git a/jstests/client_encrypt/lib/kms_http_server.py b/jstests/client_encrypt/lib/kms_http_server.py
new file mode 100755
index 00000000000..e2414bd8574
--- /dev/null
+++ b/jstests/client_encrypt/lib/kms_http_server.py
@@ -0,0 +1,298 @@
+#! /usr/bin/env python3
+"""Mock AWS KMS Endpoint."""
+
+import argparse
+import collections
+import base64
+import http.server
+import json
+import logging
+import socketserver
+import sys
+import urllib.parse
+import ssl
+
+import kms_http_common
+
+SECRET_PREFIX = "00SECRET"
+
+# Pass this data out of band instead of storing it in AwsKmsHandler since the
+# BaseHTTPRequestHandler does not call the methods as object methods but as class methods. This
+# means there is not self.
+stats = kms_http_common.Stats()
+disable_faults = False
+fault_type = None
+
+"""Fault which causes encrypt to return 500."""
+FAULT_ENCRYPT = "fault_encrypt"
+
+"""Fault which causes encrypt to return an error that contains a type and message"""
+FAULT_ENCRYPT_CORRECT_FORMAT = "fault_encrypt_correct_format"
+
+"""Fault which causes encrypt to return wrong fields in JSON."""
+FAULT_ENCRYPT_WRONG_FIELDS = "fault_encrypt_wrong_fields"
+
+"""Fault which causes encrypt to return bad BASE64."""
+FAULT_ENCRYPT_BAD_BASE64 = "fault_encrypt_bad_base64"
+
+"""Fault which causes decrypt to return 500."""
+FAULT_DECRYPT = "fault_decrypt"
+
+"""Fault which causes decrypt to return an error that contains a type and message"""
+FAULT_DECRYPT_CORRECT_FORMAT = "fault_decrypt_correct_format"
+
+"""Fault which causes decrypt to return wrong key."""
+FAULT_DECRYPT_WRONG_KEY = "fault_decrypt_wrong_key"
+
+
+# List of supported fault types
+SUPPORTED_FAULT_TYPES = [
+ FAULT_ENCRYPT,
+ FAULT_ENCRYPT_CORRECT_FORMAT,
+ FAULT_ENCRYPT_WRONG_FIELDS,
+ FAULT_ENCRYPT_BAD_BASE64,
+ FAULT_DECRYPT,
+ FAULT_DECRYPT_CORRECT_FORMAT,
+ FAULT_DECRYPT_WRONG_KEY,
+]
+
+class AwsKmsHandler(http.server.BaseHTTPRequestHandler):
+ """
+ Handle requests from AWS KMS Monitoring and test commands
+ """
+ protocol_version = "HTTP/1.1"
+
+ def do_GET(self):
+ """Serve a Test GET request."""
+ parts = urllib.parse.urlsplit(self.path)
+ path = parts[2]
+
+ if path == kms_http_common.URL_PATH_STATS:
+ self._do_stats()
+ elif path == kms_http_common.URL_DISABLE_FAULTS:
+ self._do_disable_faults()
+ elif path == kms_http_common.URL_ENABLE_FAULTS:
+ self._do_enable_faults()
+ else:
+ self.send_response(http.HTTPStatus.NOT_FOUND)
+ self.end_headers()
+ self.wfile.write("Unknown URL".encode())
+
+ def do_POST(self):
+ """Serve a POST request."""
+ parts = urllib.parse.urlsplit(self.path)
+ path = parts[2]
+
+ if path == "/":
+ self._do_post()
+ else:
+ self.send_response(http.HTTPStatus.NOT_FOUND)
+ self.end_headers()
+ self.wfile.write("Unknown URL".encode())
+
+ def _send_reply(self, data, status=http.HTTPStatus.OK):
+ print("Sending Response: " + data.decode())
+
+ self.send_response(status)
+ self.send_header("content-type", "application/octet-stream")
+ self.send_header("Content-Length", str(len(data)))
+ self.end_headers()
+
+ self.wfile.write(data)
+
+ def _do_post(self):
+ global stats
+ clen = int(self.headers.get('content-length'))
+
+ raw_input = self.rfile.read(clen)
+
+ print("RAW INPUT: " + str(raw_input))
+
+ # X-Amz-Target: TrentService.Encrypt
+ aws_operation = self.headers['X-Amz-Target']
+
+ if aws_operation == "TrentService.Encrypt":
+ stats.encrypt_calls += 1
+ self._do_encrypt(raw_input)
+ elif aws_operation == "TrentService.Decrypt":
+ stats.decrypt_calls += 1
+ self._do_decrypt(raw_input)
+ else:
+ data = "Unknown AWS Operation"
+ self._send_reply(data.encode("utf-8"))
+
+ def _do_encrypt(self, raw_input):
+ request = json.loads(raw_input)
+
+ print(request)
+
+ plaintext = request["Plaintext"]
+ keyid = request["KeyId"]
+
+ ciphertext = SECRET_PREFIX.encode() + plaintext.encode()
+ ciphertext = base64.b64encode(ciphertext).decode()
+
+ if fault_type and fault_type.startswith(FAULT_ENCRYPT) and not disable_faults:
+ return self._do_encrypt_faults(ciphertext)
+
+ response = {
+ "CiphertextBlob" : ciphertext,
+ "KeyId" : keyid,
+ }
+
+ self._send_reply(json.dumps(response).encode('utf-8'))
+
+ def _do_encrypt_faults(self, raw_ciphertext):
+ stats.fault_calls += 1
+
+ if fault_type == FAULT_ENCRYPT:
+ self._send_reply("Internal Error of some sort.".encode(), http.HTTPStatus.INTERNAL_SERVER_ERROR)
+ return
+ elif fault_type == FAULT_ENCRYPT_WRONG_FIELDS:
+ response = {
+ "SomeBlob" : raw_ciphertext,
+ "KeyId" : "foo",
+ }
+
+ self._send_reply(json.dumps(response).encode('utf-8'))
+ return
+ elif fault_type == FAULT_ENCRYPT_BAD_BASE64:
+ response = {
+ "CiphertextBlob" : "foo",
+ "KeyId" : "foo",
+ }
+
+ self._send_reply(json.dumps(response).encode('utf-8'))
+ return
+ elif fault_type == FAULT_ENCRYPT_CORRECT_FORMAT:
+ response = {
+ "__type" : "NotFoundException",
+ "message" : "Error encrypting message",
+ }
+
+ self._send_reply(json.dumps(response).encode('utf-8'))
+ return
+
+ raise ValueError("Unknown Fault Type: " + fault_type)
+
+ def _do_decrypt(self, raw_input):
+ request = json.loads(raw_input)
+ blob = base64.b64decode(request["CiphertextBlob"]).decode()
+
+ print("FOUND SECRET: " + blob)
+
+ # our "encrypted" values start with the word SECRET_PREFIX otherwise they did not come from us
+ if not blob.startswith(SECRET_PREFIX):
+ raise ValueError()
+
+ blob = blob[len(SECRET_PREFIX):]
+
+ if fault_type and fault_type.startswith(FAULT_DECRYPT) and not disable_faults:
+ return self._do_decrypt_faults(blob)
+
+ response = {
+ "Plaintext" : blob,
+ "KeyId" : "Not a clue",
+ }
+
+ self._send_reply(json.dumps(response).encode('utf-8'))
+
+ def _do_decrypt_faults(self, blob):
+ stats.fault_calls += 1
+
+ if fault_type == FAULT_DECRYPT:
+ self._send_reply("Internal Error of some sort.".encode(), http.HTTPStatus.INTERNAL_SERVER_ERROR)
+ return
+ elif fault_type == FAULT_DECRYPT_WRONG_KEY:
+ response = {
+ "Plaintext" : "ta7DXE7J0OiCRw03dYMJSeb8nVF5qxTmZ9zWmjuX4zW/SOorSCaY8VMTWG+cRInMx/rr/+QeVw2WjU2IpOSvMg==",
+ "KeyId" : "Not a clue",
+ }
+
+ self._send_reply(json.dumps(response).encode('utf-8'))
+ return
+ elif fault_type == FAULT_DECRYPT_CORRECT_FORMAT:
+ response = {
+ "__type" : "NotFoundException",
+ "message" : "Error decrypting message",
+ }
+
+ self._send_reply(json.dumps(response).encode('utf-8'))
+ return
+
+ raise ValueError("Unknown Fault Type: " + fault_type)
+
+ def _send_header(self):
+ self.send_response(http.HTTPStatus.OK)
+ self.send_header("content-type", "application/octet-stream")
+ self.end_headers()
+
+ def _do_stats(self):
+ self._send_header()
+
+ self.wfile.write(str(stats).encode('utf-8'))
+
+ def _do_disable_faults(self):
+ global disable_faults
+ disable_faults = True
+ self._send_header()
+
+ def _do_enable_faults(self):
+ global disable_faults
+ disable_faults = False
+ self._send_header()
+
+def run(port, cert_file, ca_file, server_class=http.server.HTTPServer, handler_class=AwsKmsHandler):
+ """Run web server."""
+ server_address = ('', port)
+
+ httpd = server_class(server_address, handler_class)
+
+ httpd.socket = ssl.wrap_socket (httpd.socket,
+ certfile=cert_file,
+ ca_certs=ca_file, server_side=True)
+
+ print("Mock KMS Web Server Listening on %s" % (str(server_address)))
+
+ httpd.serve_forever()
+
+
+def main():
+ """Main Method."""
+ global fault_type
+ global disable_faults
+
+ parser = argparse.ArgumentParser(description='MongoDB Mock AWS KMS Endpoint.')
+
+ parser.add_argument('-p', '--port', type=int, default=8000, help="Port to listen on")
+
+ parser.add_argument('-v', '--verbose', action='count', help="Enable verbose tracing")
+
+ parser.add_argument('--fault', type=str, help="Type of fault to inject")
+
+ parser.add_argument('--disable-faults', action='store_true', help="Disable faults on startup")
+
+ parser.add_argument('--ca_file', type=str, required=True, help="TLS CA PEM file")
+
+ parser.add_argument('--cert_file', type=str, required=True, help="TLS Server PEM file")
+
+ args = parser.parse_args()
+ if args.verbose:
+ logging.basicConfig(level=logging.DEBUG)
+
+ if args.fault:
+ if args.fault not in SUPPORTED_FAULT_TYPES:
+ print("Unsupported fault type %s, supports types are %s" % (args.fault, SUPPORTED_FAULT_TYPES))
+ sys.exit(1)
+
+ fault_type = args.fault
+
+ if args.disable_faults:
+ disable_faults = True
+
+ run(args.port, args.cert_file, args.ca_file)
+
+
+if __name__ == '__main__':
+
+ main()
diff --git a/jstests/client_encrypt/lib/mock_kms.js b/jstests/client_encrypt/lib/mock_kms.js
new file mode 100644
index 00000000000..a7f34c37312
--- /dev/null
+++ b/jstests/client_encrypt/lib/mock_kms.js
@@ -0,0 +1,161 @@
+/**
+ * Starts a mock KMS Server to test
+ * FLE encryption and decryption.
+ */
+
+// These faults must match the list of faults in kms_http_server.py, see the
+// SUPPORTED_FAULT_TYPES list in kms_http_server.py
+const FAULT_ENCRYPT = "fault_encrypt";
+const FAULT_ENCRYPT_CORRECT_FORMAT = "fault_encrypt_correct_format";
+const FAULT_ENCRYPT_WRONG_FIELDS = "fault_encrypt_wrong_fields";
+const FAULT_ENCRYPT_BAD_BASE64 = "fault_encrypt_bad_base64";
+const FAULT_DECRYPT = "fault_decrypt";
+const FAULT_DECRYPT_CORRECT_FORMAT = "fault_decrypt_correct_format";
+const FAULT_DECRYPT_WRONG_KEY = "fault_decrypt_wrong_key";
+
+const DISABLE_FAULTS = "disable_faults";
+const ENABLE_FAULTS = "enable_faults";
+
+class MockKMSServer {
+ /**
+ * Create a new webserver.
+ *
+ * @param {string} fault_type
+ * @param {bool} disableFaultsOnStartup optionally disable fault on startup
+ */
+ constructor(fault_type, disableFaultsOnStartup) {
+ this.python = "python3";
+ this.disableFaultsOnStartup = disableFaultsOnStartup || false;
+ this.fault_type = fault_type;
+
+ if (_isWindows()) {
+ this.python = "python.exe";
+ }
+
+ print("Using python interpreter: " + this.python);
+
+ this.ca_file = "jstests/libs/ca.pem";
+ this.server_cert_file = "jstests/libs/server.pem";
+ this.web_server_py = "jstests/client_encrypt/lib/kms_http_server.py";
+ this.control_py = "jstests/client_encrypt/lib/kms_http_control.py";
+ this.port = -1;
+ }
+
+ /**
+ * Start a web server
+ */
+ start() {
+ this.port = allocatePort();
+ print("Mock Web server is listening on port: " + this.port);
+
+ let args = [
+ this.python,
+ "-u",
+ this.web_server_py,
+ "--port=" + this.port,
+ "--ca_file=" + this.ca_file,
+ "--cert_file=" + this.server_cert_file
+ ];
+ if (this.fault_type) {
+ args.push("--fault=" + this.fault_type);
+ if (this.disableFaultsOnStartup) {
+ args.push("--disable-faults");
+ }
+ }
+
+ this.pid = _startMongoProgram({args: args});
+ assert(checkProgram(this.pid));
+
+ assert.soon(function() {
+ return rawMongoProgramOutput().search("Mock KMS Web Server Listening") !== -1;
+ });
+ sleep(1000);
+ print("Mock KMS Server successfully started");
+ }
+
+ _runCommand(cmd) {
+ let ret = 0;
+ if (_isWindows()) {
+ ret = runProgram('cmd.exe', '/c', cmd);
+ } else {
+ ret = runProgram('/bin/sh', '-c', cmd);
+ }
+
+ assert.eq(ret, 0);
+ }
+
+ /**
+ * Query the HTTP server.
+ *
+ * @param {string} query type
+ *
+ * @return {object} Object representation of JSON from the server.
+ */
+ query(query) {
+ const out_file = "out_" + this.port + ".txt";
+ const python_command = this.python + " -u " + this.control_py + " --port=" + this.port +
+ " --ca_file=" + this.ca_file + " --query=" + query + " > " + out_file;
+
+ this._runCommand(python_command);
+
+ const result = cat(out_file);
+
+ try {
+ return JSON.parse(result);
+ } catch (e) {
+ jsTestLog("Failed to parse: " + result + "\n" + result);
+ throw e;
+ }
+ }
+
+ /**
+ * Control the HTTP server.
+ *
+ * @param {string} query type
+ */
+ control(query) {
+ const python_command = this.python + " -u " + this.control_py + " --port=" + this.port +
+ " --ca_file=" + this.ca_file + " --query=" + query;
+
+ this._runCommand(python_command);
+ }
+
+ /**
+ * Disable Faults
+ */
+ disableFaults() {
+ this.control(DISABLE_FAULTS);
+ }
+
+ /**
+ * Enable Faults
+ */
+ enableFaults() {
+ this.control(ENABLE_FAULTS);
+ }
+
+ /**
+ * Query the stats page for the HTTP server.
+ *
+ * @return {object} Object representation of JSON from the server.
+ */
+ queryStats() {
+ return this.query("stats");
+ }
+
+ /**
+ * Get the URL.
+ *
+ * @return {string} url of http server
+ */
+ getURL() {
+ return "https://localhost:" + this.port;
+ }
+
+ /**
+ * Stop the web server
+ */
+ stop() {
+ stopMongoProgramByPid(this.pid);
+ }
+}