Skip to content

MONGOCRYPT-432 Allow keyAltName in encryptedFieldsMap#1091

Open
mdb-ad wants to merge 43 commits intomongodb:masterfrom
mdb-ad:keyaltname
Open

MONGOCRYPT-432 Allow keyAltName in encryptedFieldsMap#1091
mdb-ad wants to merge 43 commits intomongodb:masterfrom
mdb-ad:keyaltname

Conversation

@mdb-ad
Copy link
Contributor

@mdb-ad mdb-ad commented Oct 24, 2025

Background

Adds keyAltName to keyId client-side translation to libmongocrypt that allows users to specify human-readable keyAltName strings instead of binary key IDs.

Implementation

Whenever encryptionInformation is appended to at outgoing command, libmongocrypt looks through the encrypted fields for keyAltName fields and translates them to keyId.

Testing

Tested on the C driver with spec test: https://spruce.mongodb.com/version/697803dbc0c964000764d2a4/

@mdb-ad mdb-ad marked this pull request as ready for review February 6, 2026 22:21
@mdb-ad mdb-ad requested a review from a team as a code owner February 6, 2026 22:21
@mdb-ad mdb-ad requested review from vector-of-bool and removed request for vector-of-bool February 6, 2026 22:21
@mdb-ad mdb-ad requested a review from kevinAlbs February 9, 2026 18:43
@mdb-ad mdb-ad requested a review from kevinAlbs February 25, 2026 20:53
bool is_compact = false;
if (ctx->type == _MONGOCRYPT_TYPE_ENCRYPT) {
_mongocrypt_ctx_encrypt_t *ectx = (_mongocrypt_ctx_encrypt_t *)ctx;
if (ectx->cmd_name && 0 == strcmp(ectx->cmd_name, "compactStructuredEncryptionData")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does "cleanupStructuredEncryptedData" also need to be checked?


bool is_compact = false;
if (ctx->type == _MONGOCRYPT_TYPE_ENCRYPT) {
_mongocrypt_ctx_encrypt_t *ectx = (_mongocrypt_ctx_encrypt_t *)ctx;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than downcasting to _mongocrypt_ctx_encrypt_t, consider overriding the vtable function for mongo_done_keys here. This behavior is specific to an auto-encryption context. This may permit moving need_keys_for_encryptedFields to _mongocrypt_ctx_encrypt_t.

if (bson_iter_init_find(&field_iter, field, "keyId")) {
has_keyid = true;
}
if (bson_iter_init_find(&field_iter, field, "keyAltName")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scope also notes:

A new error message will be added for duplicate keyAltNames.

Suggest adding a test for duplicate keyAltNames. I expect this validation still needs to be added.

CLIENT_ERR("unable to parse uuid key from 'fields.keyId'");
return false;
if (has_keyid) {
BSON_ASSERT(bson_iter_init_find(&field_iter, field, "keyId"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than call bson_iter_init_find again, consider replacing field_iter with separate keyid_iter and keyaltname_iter:

bson_iter_t keyid_iter;
bson_iter_t keyaltname_iter;
if (bson_iter_init_find(&keyid_iter, field, "keyId")) {
    has_keyid = true;
}
if (bson_iter_init_find(&keyaltname_iter, field, "keyAltName")) {
    has_keyaltname = true;
}

Comment on lines +646 to +649
char idx_str[32];
const char *idx_str_ptr;
const size_t ret = bson_uint32_to_string((uint32_t)idx, &idx_str_ptr, idx_str, sizeof idx_str);
BSON_ASSERT(ret > 0 && ret <= (int)sizeof idx_str);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
char idx_str[32];
const char *idx_str_ptr;
const size_t ret = bson_uint32_to_string((uint32_t)idx, &idx_str_ptr, idx_str, sizeof idx_str);
BSON_ASSERT(ret > 0 && ret <= (int)sizeof idx_str);
const char *idx_str_ptr = bson_iter_key(&arr_it);

I expect the output indexes are equal to the input indexes. So the string keys "0", "1", "2", ... can be copied directly.

/* Extract keyAltName (if present) and strdup it so caller can
* derive a keyId from it. */
bson_iter_t doc_it2;
if (bson_iter_init(&doc_it2, &elem_doc)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: suggest combining to simplify:

if (bson_iter_init_find(&doc_it2, &elem_doc, "keyAltName") && BSON_ITER_HOLDS_UTF8(&doc_it2)) {

_mongocrypt_key_broker_status(kb, status);
return -1;
}
bson_append_binary(&new_doc, "keyId", -1, key_id_out.subtype, key_id_out.data, key_id_out.len);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bson_append_binary can fail (returns a bool). libmongocrypt is inconsistent about error handling for BSON appending: sometimes asserts, sometimes ignores, sometimes reports.

Since mc-schema-broker.c consistently reports BSON errors, suggest changing this function to report BSON errors. A helper function to translate one field may help to implement the "goto fail" pattern. Here is a suggested refactor that including other suggestions:

// translate_field_keyAltName_to_keyId translates one field. Returns false on error.
static bool translate_field_keyAltName_to_keyId(const bson_t *old_doc,
                                                bson_t *new_doc,
                                                _mongocrypt_key_broker_t *kb,
                                                int *found_keyAltName,
                                                mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(old_doc);
    BSON_ASSERT_PARAM(new_doc);
    BSON_ASSERT_PARAM(found_keyAltName);
    BSON_ASSERT_PARAM(kb);

    _mongocrypt_buffer_t unused = {0}, key_id_out = {0};
    bson_value_t key_alt_name_v = {0};
    bool ret = false;

    // Copy excluding "keyAltName"
    bson_copy_to_excluding_noinit(old_doc, new_doc, "keyAltName", NULL);

    // Translate keyAltName (if present) to keyId
    bson_iter_t keyAltName_iter;
    if (bson_iter_init_find(&keyAltName_iter, old_doc, "keyAltName") && BSON_ITER_HOLDS_UTF8(&keyAltName_iter)) {
        _bson_value_from_string(bson_iter_utf8(&keyAltName_iter, NULL), &key_alt_name_v);
        if (!_mongocrypt_key_broker_decrypted_key_by_name(kb, &key_alt_name_v, &unused, &key_id_out)) {
            _mongocrypt_key_broker_status(kb, status);
            goto fail;
        }
        TRY_BSON_OR(bson_append_binary(new_doc, "keyId", -1, key_id_out.subtype, key_id_out.data, key_id_out.len)) {
            goto fail;
        }
    }

    ret = true;
fail:
    _mongocrypt_buffer_cleanup(&unused);
    _mongocrypt_buffer_cleanup(&key_id_out);
    bson_value_destroy(&key_alt_name_v);
    return ret;
}

int mc_translate_fields_keyAltName_to_keyId(const bson_t *fields_bson,
                                            _mongocrypt_key_broker_t *kb,
                                            bson_t *out,
                                            mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(fields_bson);
    BSON_ASSERT_PARAM(kb);
    BSON_ASSERT_PARAM(out);
    int found_keyAltName = 0;

    bson_iter_t arr_it;
    if (!bson_iter_init(&arr_it, fields_bson)) {
        CLIENT_ERR("failed to iterate 'fields' array");
        return -1;
    }

    while (bson_iter_next(&arr_it)) {
        const char *idx_str = bson_iter_key(&arr_it);

        if (BSON_ITER_HOLDS_DOCUMENT(&arr_it)) {
            bson_t elem_doc;
            if (!mc_iter_document_as_bson(&arr_it, &elem_doc, status)) {
                return -1;
            }
            bson_t new_doc = BSON_INITIALIZER;
            if (!translate_field_keyAltName_to_keyId(&elem_doc, &new_doc, kb, &found_keyAltName, status)) {
                return -1;
            }
            TRY_BSON_OR(bson_append_document(out, idx_str, -1, &new_doc)) {
                bson_destroy(&new_doc);
                return -1;
            }
        } else {
            /* Non-document elements: copy as-is. */
            if (!BSON_APPEND_VALUE(out, idx_str, bson_iter_value(&arr_it))) {
                CLIENT_ERR("failed to append field value");
                return -1;
            }
        }
    }

    return found_keyAltName;
}

bson_t new_doc;
char *keyAltName_dup = NULL;

/* Extract keyAltName (if present) and strdup it so caller can
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect calling bson_strdup can be avoided. Call bson_copy_to_excluding_noinit first, then check if a "keyAltName" field exists.

_mongocrypt_key_broker_status(&ctx->kb, ctx->status);
_mongocrypt_ctx_fail(ctx);
bson_value_destroy(&keyAltName);
return -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return -1;
return false;

}

if (0 == strcmp(ectx->cmd_name, "create")
&& _mongocrypt_buffer_empty(&ctx->crypt->opts.encrypted_field_config_map)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can translating "create" be done without adding to encryptedFieldsMap? I expect this may have impact that poses other questions. E.g. should this overwrite an existing entry in encryptedFieldsMap? The empty check means only the first one is stored. I expect "create" could be run multiple times on the same collection (e.g. if the first fails).

You may be able to add a function similar to _try_schema_from_create_or_collMod_cmd to obtain encryptedFields from the "create" command.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See this suggested change: b5ff740

This appears to pass tests. It reads "encryptedFields" from the create command without modifying encryptedFieldsMaps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants