diff options
author | Mark Benvenuto <mark.benvenuto@mongodb.com> | 2019-06-18 00:19:33 -0400 |
---|---|---|
committer | Mark Benvenuto <mark.benvenuto@mongodb.com> | 2019-06-18 00:19:33 -0400 |
commit | c436b8090417baf847143e97f5d221285b1898e1 (patch) | |
tree | bb5369a793c8ec6646b20e92b1a0f85b7979682f /jstests/client_encrypt | |
parent | 5eda33f9fa40a1a17f9f63f904a8c147700d648c (diff) | |
download | mongo-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.js | 142 | ||||
-rw-r--r-- | jstests/client_encrypt/fle_command_line_encryption.js | 41 | ||||
-rw-r--r-- | jstests/client_encrypt/fle_encrypt_decrypt_shell.js | 112 | ||||
-rw-r--r-- | jstests/client_encrypt/fle_key_faults.js | 94 | ||||
-rw-r--r-- | jstests/client_encrypt/fle_keys.js | 75 | ||||
-rw-r--r-- | jstests/client_encrypt/fle_valid_fle_options.js | 68 | ||||
-rw-r--r-- | jstests/client_encrypt/lib/fle_command_line_explicit_encryption.js | 84 | ||||
-rw-r--r-- | jstests/client_encrypt/lib/kms_http_common.py | 21 | ||||
-rw-r--r-- | jstests/client_encrypt/lib/kms_http_control.py | 52 | ||||
-rwxr-xr-x | jstests/client_encrypt/lib/kms_http_server.py | 298 | ||||
-rw-r--r-- | jstests/client_encrypt/lib/mock_kms.js | 161 |
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); + } +} |