From dda40b930498b70bb5546f857b27744039a5649d Mon Sep 17 00:00:00 2001 From: Vladislav Vaintroub Date: Mon, 28 Aug 2017 18:28:07 +0000 Subject: [PATCH] AWS Key Management : Introduce "mock" variable, available in debug build. If this variable is set, skip actual AWS calls, and fake/mock both generation and encryption of the keys. The advantage of having a mock mode is that more aws_key_management tests can be enabled on buildbot. --- client/mysqltest.cc | 18 ++ .../mariabackup/xb_aws_key_management.opt | 2 +- .../mariabackup/xb_aws_key_management.result | 5 + .../mariabackup/xb_aws_key_management.test | 6 +- plugin/aws_key_management/CMakeLists.txt | 1 + .../aws_key_management_plugin.cc | 202 ++++++++++++++---- 6 files changed, 189 insertions(+), 45 deletions(-) diff --git a/client/mysqltest.cc b/client/mysqltest.cc index 99e4e4dfc66..155d0c4c092 100644 --- a/client/mysqltest.cc +++ b/client/mysqltest.cc @@ -4024,7 +4024,25 @@ static int rmtree(const char *dir) strxnmov(path, sizeof(path), dir, sep, file->name, NULL); if (!MY_S_ISDIR(file->mystat->st_mode)) + { err= my_delete(path, 0); +#ifdef _WIN32 + /* + On Windows, check and possible reset readonly attribute. + my_delete(), or DeleteFile does not remove theses files. + */ + if (err) + { + DWORD attr= GetFileAttributes(path); + if (attr != INVALID_FILE_ATTRIBUTES && + (attr & FILE_ATTRIBUTE_READONLY)) + { + SetFileAttributes(path, attr &~ FILE_ATTRIBUTE_READONLY); + err= my_delete(path, 0); + } + } +#endif + } else err= rmtree(path); diff --git a/mysql-test/suite/mariabackup/xb_aws_key_management.opt b/mysql-test/suite/mariabackup/xb_aws_key_management.opt index 62d4f864073..fceff3be28d 100644 --- a/mysql-test/suite/mariabackup/xb_aws_key_management.opt +++ b/mysql-test/suite/mariabackup/xb_aws_key_management.opt @@ -1,3 +1,3 @@ --plugin-load-add=$AWS_KEY_MANAGEMENT_SO --loose-aws-key-management ---loose-aws-key-management-master-key-id=$AWS_KEY_MANAGEMENT_MASTER_KEY_ID +--loose-aws-key-management-mock=ON diff --git a/mysql-test/suite/mariabackup/xb_aws_key_management.result b/mysql-test/suite/mariabackup/xb_aws_key_management.result index ccad423f631..6efc76a8be3 100644 --- a/mysql-test/suite/mariabackup/xb_aws_key_management.result +++ b/mysql-test/suite/mariabackup/xb_aws_key_management.result @@ -1,4 +1,7 @@ CREATE TABLE t(c VARCHAR(10)) ENGINE INNODB encrypted=yes; +Warnings: +Note 1105 AWS KMS plugin: generated encrypted datakey for key id=1, version=1 +Note 1105 AWS KMS plugin: loaded key 1, version 1, key length 128 bit INSERT INTO t VALUES('foobar1'); # xtrabackup backup # shutdown server @@ -8,4 +11,6 @@ INSERT INTO t VALUES('foobar1'); SELECT * from t; c foobar1 +Warnings: +Note 1105 AWS KMS plugin: loaded key 1, version 1, key length 128 bit DROP TABLE t; diff --git a/mysql-test/suite/mariabackup/xb_aws_key_management.test b/mysql-test/suite/mariabackup/xb_aws_key_management.test index ca01be607c7..149e422c058 100644 --- a/mysql-test/suite/mariabackup/xb_aws_key_management.test +++ b/mysql-test/suite/mariabackup/xb_aws_key_management.test @@ -1,13 +1,9 @@ +--source include/have_debug.inc if (`SELECT COUNT(*)=0 FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME = 'aws_key_management' AND PLUGIN_STATUS='ACTIVE'`) { --skip needs aws_key_management plugin plugin } -if (`SELECT @@aws_key_management_master_key_id=''`) -{ - --skip Test requires AWS_KEY_MANAGEMEMENT_MASTER_KEY_ID env. variable -} - CREATE TABLE t(c VARCHAR(10)) ENGINE INNODB encrypted=yes; INSERT INTO t VALUES('foobar1'); echo # xtrabackup backup; diff --git a/plugin/aws_key_management/CMakeLists.txt b/plugin/aws_key_management/CMakeLists.txt index 00e788ffbd5..9221b54ef7e 100644 --- a/plugin/aws_key_management/CMakeLists.txt +++ b/plugin/aws_key_management/CMakeLists.txt @@ -20,6 +20,7 @@ MACRO(SKIP_AWS_PLUGIN msg) ENDIF() RETURN() ENDMACRO() +SET(CMAKE_CXX_STANDARD 11) MYSQL_ADD_PLUGIN(aws_key_management aws_key_management_plugin.cc DISABLED COMPONENT aws-key-management) diff --git a/plugin/aws_key_management/aws_key_management_plugin.cc b/plugin/aws_key_management/aws_key_management_plugin.cc index 365c5caf198..60ca6fd1ff3 100644 --- a/plugin/aws_key_management/aws_key_management_plugin.cc +++ b/plugin/aws_key_management/aws_key_management_plugin.cc @@ -82,9 +82,20 @@ static unsigned long log_level; static int rotate_key; static int request_timeout; +#ifndef DBUG_OFF +#define WITH_AWS_MOCK 1 +#else +#define WITH_AWS_MOCK 0 +#endif + +#if WITH_AWS_MOCK +static char mock; +#endif + + /* AWS functionality*/ -static int aws_decrypt_key(const char *path, KEY_INFO *info); -static int aws_generate_datakey(uint key_id, uint version); +static int read_and_decrypt_key(const char *path, KEY_INFO *info); +static int generate_and_save_datakey(uint key_id, uint version); static int extract_id_and_version(const char *name, uint *id, uint *ver); static unsigned int get_latest_key_version(unsigned int key_id); @@ -95,6 +106,35 @@ static std::mutex mtx; static Aws::KMS::KMSClient *client; +#if WITH_AWS_MOCK +/* + Mock routines to test plugin without actual AWS KMS interaction + we only need to mock 2 functions - generating encrypted key, and decrypt + + This mock functions do no-op encryption, i.e encrypt and decrypt of + a buffer return the buffer itself. +*/ + +/* + Generate random "encrypted" key. We do not encrypt anything in mock mode. +*/ +static int mock_generate_encrypted_key(Aws::Utils::ByteBuffer *result) +{ + size_t len = key_spec == 0?16 : 32; + *result = Aws::Utils::ByteBuffer(len); + my_random_bytes(result->GetUnderlyingData(), (int)len); + return 0; +} + + +static int mock_decrypt(Aws::Utils::ByteBuffer input, Aws::Utils::ByteBuffer* output, Aws::String *errmsg) +{ + /* We do not encrypt or decrypt in mock mode.*/ + *output = input; + return 0; +} +#endif + /* Redirect AWS trace to error log */ class MySQLLogSystem : public Aws::Utils::Logging::FormattedLogSystem { @@ -163,13 +203,7 @@ static vector traverse_current_directory() Aws::SDKOptions sdkOptions; -/* - Plugin initialization. - - Create KMS client and scan datadir to find out which keys and versions - are present. -*/ -static int plugin_init(void *p) +static int aws_init() { #ifdef HAVE_YASSL @@ -198,7 +232,29 @@ static int plugin_init(void *p) my_printf_error(ER_UNKNOWN_ERROR, "Can not initialize KMS client", ME_ERROR_LOG | ME_WARNING); return -1; } - + return 0; +} + +static int init() +{ +#if WITH_AWS_MOCK + if(mock) + return 0; +#endif + return aws_init(); +} + +/* + Plugin initialization. + + Create KMS client and scan datadir to find out which keys and versions + are present. +*/ +static int plugin_init(void *p) +{ + if (init()) + return -1; + vector files= traverse_current_directory(); for (size_t i=0; i < files.size(); i++) { @@ -214,14 +270,29 @@ static int plugin_init(void *p) } +static void aws_shutdown() +{ + delete client; + ShutdownAWSLogging(); + Aws::ShutdownAPI(sdkOptions); +} + + +static void shutdown() +{ +#if WITH_AWS_MOCK + if(mock) + return; +#endif + aws_shutdown(); +} + + static int plugin_deinit(void *p) { latest_version_cache.clear(); key_info_cache.clear(); - delete client; - ShutdownAWSLogging(); - - Aws::ShutdownAPI(sdkOptions); + shutdown(); return 0; } @@ -251,7 +322,7 @@ static int load_key(KEY_INFO *info) char path[256]; format_keyfile_name(path, sizeof(path), info->key_id, info->key_version); - ret= aws_decrypt_key(path, info); + ret= read_and_decrypt_key(path, info); if (ret) info->load_failed= true; @@ -317,14 +388,7 @@ static unsigned int get_latest_key_version_nolock(unsigned int key_id) else // (ver == 0) { /* Generate a new key, version 1 */ - if (!master_key_id[0]) - { - my_printf_error(ER_UNKNOWN_ERROR, - "Can't generate encryption key %u, because 'aws_key_management_master_key_id' parameter is not set", - MYF(0), key_id); - return(ENCRYPTION_KEY_VERSION_INVALID); - } - if (aws_generate_datakey(key_id, 1) != 0) + if (generate_and_save_datakey(key_id, 1) != 0) return(ENCRYPTION_KEY_VERSION_INVALID); info.key_id= key_id; info.key_version= 1; @@ -336,11 +400,35 @@ static unsigned int get_latest_key_version_nolock(unsigned int key_id) return(info.key_version); } +/* Decrypt Byte buffer with AWS. */ +static int aws_decrypt(Aws::Utils::ByteBuffer input, Aws::Utils::ByteBuffer* output, Aws::String *errmsg) +{ + DecryptRequest request; + request.SetCiphertextBlob(input); + DecryptOutcome outcome = client->Decrypt(request); + if (!outcome.IsSuccess()) + { + *errmsg = outcome.GetError().GetMessage(); + return -1; + } + *output= outcome.GetResult().GetPlaintext(); + return 0; +} + + +static int decrypt(Aws::Utils::ByteBuffer input, Aws::Utils::ByteBuffer* output, Aws::String *errmsg) +{ +#if WITH_AWS_MOCK + if(mock) + return mock_decrypt(input,output, errmsg); +#endif + return aws_decrypt(input, output, errmsg); +} /* Decrypt a file with KMS */ -static int aws_decrypt_key(const char *path, KEY_INFO *info) +static int read_and_decrypt_key(const char *path, KEY_INFO *info) { /* Read file content into memory */ @@ -361,20 +449,21 @@ static int aws_decrypt_key(const char *path, KEY_INFO *info) ifs.read(&contents[0], pos); /* Decrypt data the with AWS */ - DecryptRequest request; - Aws::Utils::ByteBuffer byteBuffer((unsigned char *)contents.data(), pos); - request.SetCiphertextBlob(byteBuffer); - DecryptOutcome outcome = client->Decrypt(request); - if (!outcome.IsSuccess()) + + Aws::Utils::ByteBuffer input((unsigned char *)contents.data(), pos); + Aws::Utils::ByteBuffer plaintext; + Aws::String errmsg; + + if (decrypt(input, &plaintext, &errmsg)) { - my_printf_error(ER_UNKNOWN_ERROR, "AWS KMS plugin: Decrypt failed for %s : %s", ME_ERROR_LOG, path, - outcome.GetError().GetMessage().c_str()); - return(-1); + my_printf_error(ER_UNKNOWN_ERROR, "AWS KMS plugin: Decrypt failed for %s : %s", ME_ERROR_LOG, path, + errmsg.c_str()); + return -1; } - Aws::Utils::ByteBuffer plaintext = outcome.GetResult().GetPlaintext(); + size_t len = plaintext.GetLength(); - if (len > (int)sizeof(info->data)) + if (len > sizeof(info->data)) { my_printf_error(ER_UNKNOWN_ERROR, "AWS KMS plugin: encoding key too large for %s", ME_ERROR_LOG, path); return(ENCRYPTION_KEY_BUFFER_TOO_SMALL); @@ -385,9 +474,15 @@ static int aws_decrypt_key(const char *path, KEY_INFO *info) } -/* Generate a new datakey and store it a file */ -static int aws_generate_datakey(uint keyid, uint version) +int aws_generate_encrypted_key(Aws::Utils::ByteBuffer *result) { + if (!master_key_id[0]) + { + my_printf_error(ER_UNKNOWN_ERROR, + "Can't generate encryption key, because 'aws_key_management_master_key_id' parameter is not set", + MYF(0)); + return(-1); + } GenerateDataKeyWithoutPlaintextRequest request; request.SetKeyId(master_key_id); request.SetKeySpec(DataKeySpecMapper::GetDataKeySpecForName(key_spec_names[key_spec])); @@ -401,11 +496,30 @@ static int aws_generate_datakey(uint keyid, uint version) outcome.GetError().GetMessage().c_str()); return(-1); } + *result = outcome.GetResult().GetCiphertextBlob(); + return 0; +} + + +static int generate_encrypted_key(Aws::Utils::ByteBuffer *output) +{ +#if WITH_AWS_MOCK + if(mock) + return mock_generate_encrypted_key(output); +#endif + return aws_generate_encrypted_key(output); +} + +/* Generate a new datakey and store it a file */ +static int generate_and_save_datakey(uint keyid, uint version) +{ + Aws::Utils::ByteBuffer byteBuffer; + + if (generate_encrypted_key(&byteBuffer)) + return -1; string out; char filename[20]; - Aws::Utils::ByteBuffer byteBuffer = outcome.GetResult().GetCiphertextBlob(); - format_keyfile_name(filename, sizeof(filename), keyid, version); int fd= open(filename, O_WRONLY |O_CREAT|O_BINARY, IF_WIN(_S_IREAD, S_IRUSR| S_IRGRP| S_IROTH)); if (fd < 0) @@ -438,7 +552,7 @@ static int rotate_single_key(uint key_id) my_printf_error(ER_UNKNOWN_ERROR, "key %u does not exist", MYF(ME_JUST_WARNING), key_id); return -1; } - else if (aws_generate_datakey(key_id, ver + 1)) + else if (generate_and_save_datakey(key_id, ver + 1)) { my_printf_error(ER_UNKNOWN_ERROR, "Could not generate datakey for key id= %u, ver= %u", MYF(ME_JUST_WARNING), key_id, ver); @@ -594,6 +708,13 @@ static MYSQL_SYSVAR_STR(region, region, "AWS region. For example us-east-1, or eu-central-1. If no value provided, SDK default is used.", NULL, NULL, ""); +#if WITH_AWS_MOCK +static MYSQL_SYSVAR_BOOL(mock, mock, + PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY, + "Mock AWS KMS calls (for testing).", + NULL, NULL, 0); +#endif + static struct st_mysql_sys_var* settings[]= { MYSQL_SYSVAR(master_key_id), MYSQL_SYSVAR(key_spec), @@ -601,6 +722,9 @@ static struct st_mysql_sys_var* settings[]= { MYSQL_SYSVAR(log_level), MYSQL_SYSVAR(request_timeout), MYSQL_SYSVAR(region), +#if WITH_AWS_MOCK + MYSQL_SYSVAR(mock), +#endif NULL };