/* * Copyright 2019-present MongoDB, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "mongocrypt-cache-key-private.h" #include "mongocrypt-crypto-private.h" #include "mongocrypt.h" #include "test-mongocrypt-assert-match-bson.h" #include "test-mongocrypt.h" typedef struct { _mongocrypt_buffer_t bson; _mongocrypt_key_doc_t *parsed; _mongocrypt_buffer_t kms_reply; _mongocrypt_buffer_t uuid; _mongocrypt_buffer_t marking; } gen_key_t; /* The JSON spec tests refer to key ids by a shorthand integer. * This function maps that integer to a UUID buffer. */ void lookup_key_id(uint32_t index, _mongocrypt_buffer_t *buf) { const char *key_ids[] = {"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"}; BSON_ASSERT(index < 3); _mongocrypt_buffer_copy_from_hex(buf, key_ids[index]); buf->subtype = BSON_SUBTYPE_UUID; BSON_ASSERT(_mongocrypt_buffer_is_uuid(buf)); } /* Generate a realistic key document given a @key_description, which contains an * _id * and possible keyAltNames. key_out and key_doc_out are NULLable outputs. */ void gen_key(_mongocrypt_tester_t *tester, bson_t *key_description, bson_t *key_out, _mongocrypt_key_doc_t *key_doc_out) { bson_iter_t iter; _mongocrypt_buffer_t key_material; _mongocrypt_buffer_t key_id; bson_t key, masterkey; bool local_kms; bson_init(&key); BSON_APPEND_INT32(&key, "status", 1); BSON_APPEND_DATE_TIME(&key, "updateDate", 1234567890); BSON_APPEND_DATE_TIME(&key, "creationDate", 1234567890); BSON_APPEND_DOCUMENT_BEGIN(&key, "masterKey", &masterkey); local_kms = bson_iter_init_find(&iter, key_description, "local") && bson_iter_as_bool(&iter); if (local_kms) { BSON_APPEND_UTF8(&masterkey, "provider", "local"); } else { BSON_APPEND_UTF8(&masterkey, "provider", "aws"); BSON_APPEND_UTF8(&masterkey, "region", "us-east-1"); BSON_APPEND_UTF8(&masterkey, "key", "arn:aws:kms:us-east-1:579766882180:key/" "89fcc2c4-08b0-4bd9-9f25-e30687b580d0"); } bson_append_document_end(&key, &masterkey); BSON_ASSERT(bson_iter_init_find(&iter, key_description, "_id")); lookup_key_id(bson_iter_int32(&iter), &key_id); BSON_ASSERT(_mongocrypt_buffer_append(&key_id, &key, "_id", 3)); if (bson_iter_init_find(&iter, key_description, "keyAltNames")) { bson_t key_alt_name_bson; int counter = 0; BSON_APPEND_ARRAY_BEGIN(&key, "keyAltNames", &key_alt_name_bson); for (bson_iter_recurse(&iter, &iter); bson_iter_next(&iter);) { char *field; field = bson_strdup_printf("%d", counter); BSON_APPEND_UTF8(&key_alt_name_bson, field, bson_iter_utf8(&iter, NULL)); counter++; bson_free(field); } bson_append_array_end(&key, &key_alt_name_bson); } /* Append a keyMaterial that is decryptable by the local KMS masterkey. For * AWS it gets ignored since it is dictated by KMS response. */ _mongocrypt_buffer_copy_from_hex(&key_material, "75bdbbaec862a8ae09aa16f6c67c0ae117dd15bf49b8a7947bac6de5a" "610178a3adad4bbe5bec1e30c55378f7d80d0fd5152d46e954aa32528" "69901e03cf7938434fdf7e5bf27f0ec1c85c4c5a92e38b7e3f7ce686d" "7985102c85905da220a27ee01202de25b6831e64974baffb35b7c30c5" "941dfb37b04fff6871d7208e4cde8d1bff0cd69a70dcb613dc27cfe84" "7d7544b6d0d8b4f6c9a5b6fb9c1565c43ef"); key_material.subtype = BSON_SUBTYPE_BINARY; BSON_ASSERT(_mongocrypt_buffer_append(&key_material, &key, "keyMaterial", -1)); if (key_out) { bson_copy_to(&key, key_out); } if (key_doc_out) { mongocrypt_status_t *status; status = mongocrypt_status_new(); ASSERT_OK_STATUS(_mongocrypt_key_parse_owned(&key, key_doc_out, status), status); mongocrypt_status_destroy(status); } bson_destroy(&key); _mongocrypt_buffer_cleanup(&key_id); _mongocrypt_buffer_cleanup(&key_material); } /* Append a realistic subtype 6 marking to a BSON document given a description * of the requested key, which may contain either an _id or single keyAltName. */ static void _append_marking(bson_t *appendee, const char *key, const bson_t *request) { /* Create a marking with a fixed value "v": 1 */ bson_t marking_bson; _mongocrypt_buffer_t marking_buf; bson_iter_t iter; bson_init(&marking_bson); if (bson_iter_init_find(&iter, request, "_id")) { _mongocrypt_buffer_t key_id; lookup_key_id(bson_iter_int32(&iter), &key_id); BSON_ASSERT(_mongocrypt_buffer_append(&key_id, &marking_bson, "ki", 2)); _mongocrypt_buffer_cleanup(&key_id); } else { /* If no _id, then keyAltName must be specified. */ BSON_ASSERT(bson_iter_init_find(&iter, request, "keyAltName")); BSON_APPEND_UTF8(&marking_bson, "ka", bson_iter_utf8(&iter, NULL)); } /* Append an arbitrary value and algorithm. It won't be checked in the tests. */ BSON_APPEND_INT32(&marking_bson, "v", 123); BSON_APPEND_INT32(&marking_bson, "a", MONGOCRYPT_ENCRYPTION_ALGORITHM_DETERMINISTIC); /* Append the prefix 0 byte, per the marking binary format. */ _mongocrypt_buffer_init(&marking_buf); _mongocrypt_buffer_resize(&marking_buf, marking_bson.len + 1); marking_buf.data[0] = 0; memcpy(marking_buf.data + 1, bson_get_data(&marking_bson), marking_bson.len); marking_buf.subtype = 6; BSON_ASSERT(_mongocrypt_buffer_append(&marking_buf, appendee, key, -1)); bson_destroy(&marking_bson); _mongocrypt_buffer_cleanup(&marking_buf); } /* Manually add a cache entry given a description (_id and possible keyAltNames) */ static void _add_to_cache(_mongocrypt_tester_t *tester, mongocrypt_ctx_t *ctx, bson_t *cache_entry) { bson_iter_t iter; _mongocrypt_buffer_t key_id; _mongocrypt_key_alt_name_t *key_alt_names = NULL; _mongocrypt_cache_key_attr_t *cache_key_attr; _mongocrypt_cache_key_value_t *cache_key_value; _mongocrypt_key_doc_t *key_doc; _mongocrypt_buffer_t key_material_placeholder; mongocrypt_status_t *status; status = mongocrypt_status_new(); BSON_ASSERT(bson_iter_init_find(&iter, cache_entry, "_id")); lookup_key_id(bson_iter_int32(&iter), &key_id); if (bson_iter_init_find(&iter, cache_entry, "keyAltNames")) { ASSERT_OK_STATUS(_mongocrypt_key_alt_name_from_iter(&iter, &key_alt_names, status), status); } cache_key_attr = _mongocrypt_cache_key_attr_new(&key_id, key_alt_names); /* TODO: consider improving these tests by identifying the decrypted and * encrypted key material. That is a little tricky, since it will require * parsing the KMS request to know which decrypted key material to respond * with. */ _mongocrypt_buffer_init(&key_material_placeholder); _mongocrypt_buffer_resize(&key_material_placeholder, MONGOCRYPT_KEY_LEN); memset(key_material_placeholder.data, 0, MONGOCRYPT_KEY_LEN); key_doc = _mongocrypt_key_new(); gen_key(tester, cache_entry, NULL, key_doc); cache_key_value = _mongocrypt_cache_key_value_new(key_doc, &key_material_placeholder); ASSERT_OK_STATUS(_mongocrypt_cache_add_copy(&ctx->crypt->cache_key, cache_key_attr, cache_key_value, status), status); _mongocrypt_key_destroy(key_doc); _mongocrypt_buffer_cleanup(&key_material_placeholder); _mongocrypt_cache_key_attr_destroy(cache_key_attr); _mongocrypt_cache_key_value_destroy(cache_key_value); _mongocrypt_key_alt_name_destroy_all(key_alt_names); _mongocrypt_buffer_cleanup(&key_id); mongocrypt_status_destroy(status); } /* Match a single cache entry against an expectation document (containing _id * and * possible list of keyAltNames) */ static bool _match_one_cache_entry(_mongocrypt_cache_pair_t *pair, bson_t *expected_entry) { bson_iter_t iter; _mongocrypt_buffer_t key_id; _mongocrypt_key_alt_name_t *key_alt_names = NULL; _mongocrypt_cache_key_attr_t *attr; mongocrypt_status_t *status; bool matched = false; _mongocrypt_buffer_init(&key_id); attr = pair->attr; status = mongocrypt_status_new(); bson_iter_init_find(&iter, expected_entry, "_id"); lookup_key_id(bson_iter_int32(&iter), &key_id); if (0 != _mongocrypt_buffer_cmp(&key_id, &attr->id)) { goto done; } if (bson_iter_init_find(&iter, expected_entry, "keyAltNames")) { ASSERT_OK_STATUS(_mongocrypt_key_alt_name_from_iter(&iter, &key_alt_names, status), status); } if (!_mongocrypt_key_alt_name_unique_list_equal(key_alt_names, attr->alt_names)) { printf("failed to match key alt names\n"); goto done; } matched = true; done: _mongocrypt_buffer_cleanup(&key_id); _mongocrypt_key_alt_name_destroy_all(key_alt_names); mongocrypt_status_destroy(status); return matched; } /* Find exactly one cache entry matching expected_entry. * TODO: Instead of reaching inside the cache to make these checks, I think a * better approach would be to have the cache dump as BSON, then check * against that BSON with bson matching functions. Right now, if/when we modify * the cache structure, we'll need to update this test logic. * Alternatively, another solution would be to move part of this logic inside * the key cache and call it from the test. Maybe then the method would look * like a "count" method instead of a "return true iff there is a single match" * method. We could also use a "count" method to assert no matches, etc. */ static void _match_cache_entry(_mongocrypt_tester_t *tester, mongocrypt_ctx_t *ctx, bson_t *expected_entry) { _mongocrypt_cache_pair_t *pair; bool matched = false; pair = ctx->crypt->cache_key.pair; while (pair) { if (_match_one_cache_entry(pair, expected_entry)) { if (matched) { printf("double matched entry: %s\n", bson_as_relaxed_extended_json(expected_entry, NULL)); BSON_ASSERT(false); } matched = true; } pair = pair->next; } if (!matched) { printf("could not match entry: %s\n", bson_as_relaxed_extended_json(expected_entry, NULL)); BSON_ASSERT(false); } } static void _run_one_test(_mongocrypt_tester_t *tester, mongocrypt_ctx_t *ctx, bson_t *test) { bson_iter_t iter; bson_t mongocryptd_reply, tmp; _mongocrypt_buffer_t buf; int32_t counter; mongocrypt_status_t *status; if (bson_iter_init_find(&iter, test, "description")) { printf("- %s\n", bson_iter_utf8(&iter, NULL)); } if (bson_iter_init_find(&iter, test, "skipReason")) { printf(" - skipping: %s\n", bson_iter_utf8(&iter, NULL)); return; } status = mongocrypt_status_new(); /* Set up cache */ if (bson_iter_init_find(&iter, test, "cached")) { for (bson_iter_recurse(&iter, &iter); bson_iter_next(&iter);) { bson_t cache_entry; bson_iter_bson(&iter, &cache_entry); _add_to_cache(tester, ctx, &cache_entry); } } /* Supply the requests for keys through the mongocryptd reply */ BSON_ASSERT(bson_iter_init_find(&iter, test, "requests")); counter = 0; bson_init(&mongocryptd_reply); BSON_APPEND_DOCUMENT_BEGIN(&mongocryptd_reply, "result", &tmp); for (bson_iter_recurse(&iter, &iter); bson_iter_next(&iter);) { bson_t request; char *field; bson_iter_bson(&iter, &request); field = bson_strdup_printf("%d", counter); _append_marking(&tmp, field, &request); bson_free(field); counter++; } bson_append_document_end(&mongocryptd_reply, &tmp); _mongocrypt_tester_run_ctx_to(tester, ctx, MONGOCRYPT_CTX_NEED_MONGO_MARKINGS); _mongocrypt_buffer_from_bson(&buf, &mongocryptd_reply); ASSERT_OK(mongocrypt_ctx_mongo_feed(ctx, _mongocrypt_buffer_as_binary(&buf)), ctx); BSON_ASSERT(mongocrypt_ctx_mongo_done(ctx)); /* If we're expected to supply keys back, do so. */ if (bson_iter_init_find(&iter, test, "replies")) { BSON_ASSERT(mongocrypt_ctx_state(ctx) == MONGOCRYPT_CTX_NEED_MONGO_KEYS); for (bson_iter_recurse(&iter, &iter); bson_iter_next(&iter);) { bson_t key; bson_t key_description; bson_iter_bson(&iter, &key_description); gen_key(tester, &key_description, &key, NULL); _mongocrypt_buffer_from_bson(&buf, &key); /* If expectations expect failure, fall through. */ if (!mongocrypt_ctx_mongo_feed(ctx, _mongocrypt_buffer_as_binary(&buf))) { bson_destroy(&key); break; } bson_destroy(&key); } /* We might have failed at this point. If so, do not continue so we keep * original error message. */ if (mongocrypt_ctx_status(ctx, status)) { (void)mongocrypt_ctx_mongo_done(ctx); } } /* Check expectations */ if (bson_iter_init_find(&iter, test, "expect")) { bson_t expectations; mongocrypt_ctx_status(ctx, status); bson_iter_bson(&iter, &expectations); if (bson_iter_init_find(&iter, &expectations, "errmsg")) { _mongocrypt_tester_run_ctx_to(tester, ctx, MONGOCRYPT_CTX_ERROR); ASSERT_FAILS_STATUS(mongocrypt_status_ok(status), status, bson_iter_utf8(&iter, NULL)); } else { _mongocrypt_tester_run_ctx_to(tester, ctx, MONGOCRYPT_CTX_DONE); ASSERT_OK_STATUS(mongocrypt_status_ok(status), status); } if (bson_iter_init_find(&iter, &expectations, "cached")) { uint32_t count = 0; for (bson_iter_recurse(&iter, &iter); bson_iter_next(&iter);) { bson_t entry_description; bson_iter_bson(&iter, &entry_description); _match_cache_entry(tester, ctx, &entry_description); count++; } BSON_ASSERT(count == _mongocrypt_cache_num_entries(&ctx->crypt->cache_key)); } } bson_destroy(&mongocryptd_reply); mongocrypt_status_destroy(status); } /* Run declarative JSON tests, like driver spec tests. */ static void _test_key_cache(_mongocrypt_tester_t *tester) { mongocrypt_t *crypt; mongocrypt_ctx_t *ctx; bson_t test_file; bson_iter_t iter; _load_json_as_bson("./test/data/cache-tests.json", &test_file); for (bson_iter_init(&iter, &test_file); bson_iter_next(&iter);) { bson_t test; crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT); ctx = mongocrypt_ctx_new(crypt); bson_iter_bson(&iter, &test); BSON_ASSERT(mongocrypt_ctx_encrypt_init(ctx, "test", -1, TEST_BSON("{'insert': 'coll'}"))); _run_one_test(tester, ctx, &test); bson_destroy(&test); mongocrypt_ctx_destroy(ctx); mongocrypt_destroy(crypt); } bson_destroy(&test_file); } void _mongocrypt_tester_install_key_cache(_mongocrypt_tester_t *tester) { INSTALL_TEST(_test_key_cache); }