/*
 * 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 "kms_message/kms_kmip_request.h"
#include "mongocrypt-binary-private.h"
#include "mongocrypt-buffer-private.h"
#include "mongocrypt-crypto-private.h"
#include "mongocrypt-ctx-private.h"
#include "mongocrypt-endpoint-private.h"
#include "mongocrypt-kek-private.h"
#include "mongocrypt-kms-ctx-private.h"
#include "mongocrypt-log-private.h"
#include "mongocrypt-opts-private.h"
#include "mongocrypt-private.h"
#include "mongocrypt-status-private.h"
#include "mongocrypt-util-private.h"
#include "mongocrypt.h"
#include <bson/bson.h>
#include <kms_message/kms_azure_request.h>
#include <kms_message/kms_b64.h>
#include <kms_message/kms_gcp_request.h>

/* Sadly, Windows does not define SSIZE_MAX. It is defined in bson-compat.h,
 * but only since 1.22.x, so copy this from bson-compat.h for now. */
#ifndef SSIZE_MAX
#define SSIZE_MAX (ssize_t)((((size_t)0x01u) << (sizeof(ssize_t) * (size_t)CHAR_BIT - 1u)) - 1u)
#endif

typedef struct {
    mongocrypt_status_t *status;
    void *ctx;
} ctx_with_status_t;

/* Before we've read the Content-Length header in an HTTP response,
 * we don't know how many bytes we'll need. So return this value
 * in kms_ctx_bytes_needed until we are fed the Content-Length.
 */
#define DEFAULT_MAX_KMS_BYTE_REQUEST 1024
#define SHA256_LEN 32
#define DEFAULT_HTTPS_PORT "443"
#define DEFAULT_KMIP_PORT "5696"

static bool _sha256(void *ctx, const char *input, size_t len, unsigned char *hash_out) {
    BSON_ASSERT_PARAM(ctx);
    BSON_ASSERT_PARAM(input);
    BSON_ASSERT_PARAM(hash_out);

    bool ret;
    ctx_with_status_t *ctx_with_status = (ctx_with_status_t *)ctx;
    _mongocrypt_crypto_t *crypto;
    mongocrypt_binary_t *plaintext, *out;

    crypto = (_mongocrypt_crypto_t *)ctx_with_status->ctx;
    BSON_ASSERT(crypto);
    BSON_ASSERT(len <= UINT32_MAX);
    plaintext = mongocrypt_binary_new_from_data((uint8_t *)input, (uint32_t)len);
    out = mongocrypt_binary_new();

    out->data = hash_out;
    out->len = SHA256_LEN;

    ret = crypto->sha_256(crypto->ctx, plaintext, out, ctx_with_status->status);

    mongocrypt_binary_destroy(plaintext);
    mongocrypt_binary_destroy(out);
    return ret;
}

static bool
_sha256_hmac(void *ctx, const char *key_input, size_t key_len, const char *input, size_t len, unsigned char *hash_out) {
    BSON_ASSERT_PARAM(ctx);
    BSON_ASSERT_PARAM(key_input);
    BSON_ASSERT_PARAM(input);
    BSON_ASSERT_PARAM(hash_out);

    ctx_with_status_t *ctx_with_status = (ctx_with_status_t *)ctx;
    _mongocrypt_crypto_t *crypto;
    mongocrypt_binary_t *key, *plaintext, *out;
    bool ret;

    crypto = (_mongocrypt_crypto_t *)ctx_with_status->ctx;
    BSON_ASSERT(crypto);

    BSON_ASSERT(key_len <= UINT32_MAX);
    key = mongocrypt_binary_new_from_data((uint8_t *)key_input, (uint32_t)key_len);
    BSON_ASSERT(len <= UINT32_MAX);
    plaintext = mongocrypt_binary_new_from_data((uint8_t *)input, (uint32_t)len);
    out = mongocrypt_binary_new();

    out->data = hash_out;
    out->len = SHA256_LEN;

    ret = crypto->hmac_sha_256(crypto->ctx, key, plaintext, out, ctx_with_status->status);

    mongocrypt_binary_destroy(key);
    mongocrypt_binary_destroy(plaintext);
    mongocrypt_binary_destroy(out);
    return ret;
}

static void
_set_kms_crypto_hooks(_mongocrypt_crypto_t *crypto, ctx_with_status_t *ctx_with_status, kms_request_opt_t *opts) {
    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(ctx_with_status);
    BSON_ASSERT_PARAM(opts);

    if (crypto->hooks_enabled) {
        kms_request_opt_set_crypto_hooks(opts, _sha256, _sha256_hmac, ctx_with_status);
    }
}

static bool is_kms(_kms_request_type_t kms_type) {
    return kms_type == MONGOCRYPT_KMS_KMIP_REGISTER || kms_type == MONGOCRYPT_KMS_KMIP_ACTIVATE
        || kms_type == MONGOCRYPT_KMS_KMIP_GET || kms_type == MONGOCRYPT_KMS_KMIP_ENCRYPT
        || kms_type == MONGOCRYPT_KMS_KMIP_DECRYPT || kms_type == MONGOCRYPT_KMS_KMIP_CREATE;
}

static void
_init_common(mongocrypt_kms_ctx_t *kms, _mongocrypt_log_t *log, _kms_request_type_t kms_type, const char *kmsid) {
    BSON_ASSERT_PARAM(kms);
    BSON_ASSERT_PARAM(kmsid);

    kms->kmsid = bson_strdup(kmsid);

    if (is_kms(kms_type)) {
        kms->parser = kms_kmip_response_parser_new(NULL /* reserved */);
    } else {
        kms->parser = kms_response_parser_new();
    }
    kms->log = log;
    kms->status = mongocrypt_status_new();
    kms->req_type = kms_type;
    _mongocrypt_buffer_init(&kms->result);
    kms->sleep_usec = 0;
    kms->attempts = 0;
    kms->should_retry = false;
}

bool _mongocrypt_kms_ctx_init_aws_decrypt(mongocrypt_kms_ctx_t *kms,
                                          _mongocrypt_opts_kms_providers_t *kms_providers,
                                          _mongocrypt_key_doc_t *key,
                                          _mongocrypt_crypto_t *crypto,
                                          const char *kmsid,
                                          _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms);
    BSON_ASSERT_PARAM(key);
    BSON_ASSERT_PARAM(kms_providers);
    BSON_ASSERT_PARAM(crypto);

    kms_request_opt_t *opt;
    mongocrypt_status_t *status;
    ctx_with_status_t ctx_with_status;
    bool ret = false;

    _init_common(kms, log, MONGOCRYPT_KMS_AWS_DECRYPT, kmsid);
    status = kms->status;
    ctx_with_status.ctx = crypto;
    ctx_with_status.status = mongocrypt_status_new();

    if (!key->kek.kms_provider) {
        CLIENT_ERR("no kms provider specified on key");
        goto done;
    }

    if (MONGOCRYPT_KMS_PROVIDER_AWS != key->kek.kms_provider) {
        CLIENT_ERR("expected aws kms provider");
        goto done;
    }

    if (!key->kek.provider.aws.region) {
        CLIENT_ERR("no key region provided");
        goto done;
    }

    mc_kms_creds_t kc;
    if (!_mongocrypt_opts_kms_providers_lookup(kms_providers, key->kek.kmsid, &kc)) {
        CLIENT_ERR("KMS provider `%s` is not configured", key->kek.kmsid);
        goto done;
    }
    BSON_ASSERT(kc.type == MONGOCRYPT_KMS_PROVIDER_AWS);

    if (!kc.value.aws.access_key_id) {
        CLIENT_ERR("aws access key id not provided");
        goto done;
    }

    if (!kc.value.aws.secret_access_key) {
        CLIENT_ERR("aws secret access key not provided");
        goto done;
    }

    /* create the KMS request. */
    opt = kms_request_opt_new();
    BSON_ASSERT(opt);

    _set_kms_crypto_hooks(crypto, &ctx_with_status, opt);
    kms_request_opt_set_connection_close(opt, true);

    kms->req = kms_decrypt_request_new(key->key_material.data, key->key_material.len, opt);

    kms_request_opt_destroy(opt);
    if (!kms_request_set_service(kms->req, "kms")) {
        CLIENT_ERR("failed to set service: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto done;
    }

    if (kc.value.aws.session_token) {
        if (!kms_request_add_header_field(kms->req, "X-Amz-Security-Token", kc.value.aws.session_token)) {
            CLIENT_ERR("failed to set session token: %s", kms_request_get_error(kms->req));
            _mongocrypt_status_append(status, ctx_with_status.status);
            goto done;
        }
    }

    if (kms_request_get_error(kms->req)) {
        CLIENT_ERR("error constructing KMS message: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto done;
    }

    /* If an endpoint was set, override the default Host header. */
    if (key->kek.provider.aws.endpoint) {
        if (!kms_request_add_header_field(kms->req, "Host", key->kek.provider.aws.endpoint->host_and_port)) {
            CLIENT_ERR("error constructing KMS message: %s", kms_request_get_error(kms->req));
            _mongocrypt_status_append(status, ctx_with_status.status);
            goto done;
        }
    }

    if (!kms_request_set_region(kms->req, key->kek.provider.aws.region)) {
        CLIENT_ERR("failed to set region: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto done;
    }

    if (!kms_request_set_access_key_id(kms->req, kc.value.aws.access_key_id)) {
        CLIENT_ERR("failed to set aws access key id: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto done;
    }
    if (!kms_request_set_secret_key(kms->req, kc.value.aws.secret_access_key)) {
        CLIENT_ERR("failed to set aws secret access key: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto done;
    }

    _mongocrypt_buffer_init(&kms->msg);
    kms->msg.data = (uint8_t *)kms_request_get_signed(kms->req);
    if (!kms->msg.data) {
        CLIENT_ERR("failed to create KMS message: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto done;
    }
    kms->msg.len = (uint32_t)strlen((char *)kms->msg.data);
    kms->msg.owned = true;

    if (key->kek.provider.aws.endpoint) {
        kms->endpoint = bson_strdup(key->kek.provider.aws.endpoint->host_and_port);
    } else {
        /* construct the endpoint from AWS region. */
        kms->endpoint = bson_strdup_printf("kms.%s.amazonaws.com", key->kek.provider.aws.region);
    }
    _mongocrypt_apply_default_port(&kms->endpoint, DEFAULT_HTTPS_PORT);

    ret = true;
done:
    mongocrypt_status_destroy(ctx_with_status.status);

    return ret;
}

bool _mongocrypt_kms_ctx_init_aws_encrypt(mongocrypt_kms_ctx_t *kms,
                                          _mongocrypt_opts_kms_providers_t *kms_providers,
                                          _mongocrypt_ctx_opts_t *ctx_opts,
                                          _mongocrypt_buffer_t *plaintext_key_material,
                                          _mongocrypt_crypto_t *crypto,
                                          const char *kmsid,
                                          _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms);
    BSON_ASSERT_PARAM(ctx_opts);
    BSON_ASSERT_PARAM(kms_providers);
    BSON_ASSERT_PARAM(crypto);
    BSON_ASSERT_PARAM(plaintext_key_material);

    kms_request_opt_t *opt;
    mongocrypt_status_t *status;
    ctx_with_status_t ctx_with_status;
    bool ret = false;

    _init_common(kms, log, MONGOCRYPT_KMS_AWS_ENCRYPT, kmsid);
    status = kms->status;
    ctx_with_status.ctx = crypto;
    ctx_with_status.status = mongocrypt_status_new();

    if (MONGOCRYPT_KMS_PROVIDER_AWS != ctx_opts->kek.kms_provider) {
        CLIENT_ERR("expected aws kms provider");
        goto done;
    }

    if (!ctx_opts->kek.provider.aws.region) {
        CLIENT_ERR("no key region provided");
        goto done;
    }

    if (!ctx_opts->kek.provider.aws.cmk) {
        CLIENT_ERR("no aws cmk provided");
        goto done;
    }

    mc_kms_creds_t kc;
    if (!_mongocrypt_opts_kms_providers_lookup(kms_providers, ctx_opts->kek.kmsid, &kc)) {
        CLIENT_ERR("KMS provider `%s` is not configured", ctx_opts->kek.kmsid);
        goto done;
    }
    BSON_ASSERT(kc.type == MONGOCRYPT_KMS_PROVIDER_AWS);

    if (!kc.value.aws.access_key_id) {
        CLIENT_ERR("aws access key id not provided");
        goto done;
    }

    if (!kc.value.aws.secret_access_key) {
        CLIENT_ERR("aws secret access key not provided");
        goto done;
    }

    /* create the KMS request. */
    opt = kms_request_opt_new();
    BSON_ASSERT(opt);

    _set_kms_crypto_hooks(crypto, &ctx_with_status, opt);
    kms_request_opt_set_connection_close(opt, true);

    kms->req = kms_encrypt_request_new(plaintext_key_material->data,
                                       plaintext_key_material->len,
                                       ctx_opts->kek.provider.aws.cmk,
                                       opt);

    kms_request_opt_destroy(opt);
    if (!kms_request_set_service(kms->req, "kms")) {
        CLIENT_ERR("failed to set service: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto done;
    }

    if (kc.value.aws.session_token) {
        if (!kms_request_add_header_field(kms->req, "X-Amz-Security-Token", kc.value.aws.session_token)) {
            CLIENT_ERR("failed to set session token: %s", kms_request_get_error(kms->req));
            _mongocrypt_status_append(status, ctx_with_status.status);
            goto done;
        }
    }

    if (kms_request_get_error(kms->req)) {
        CLIENT_ERR("error constructing KMS message: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto done;
    }

    /* If an endpoint was set, override the default Host header. */
    if (ctx_opts->kek.provider.aws.endpoint) {
        if (!kms_request_add_header_field(kms->req, "Host", ctx_opts->kek.provider.aws.endpoint->host)) {
            CLIENT_ERR("error constructing KMS message: %s", kms_request_get_error(kms->req));
            _mongocrypt_status_append(status, ctx_with_status.status);
            goto done;
        }
    }

    if (!kms_request_set_region(kms->req, ctx_opts->kek.provider.aws.region)) {
        CLIENT_ERR("failed to set region: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto done;
    }

    if (!kms_request_set_access_key_id(kms->req, kc.value.aws.access_key_id)) {
        CLIENT_ERR("failed to set aws access key id: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto done;
    }
    if (!kms_request_set_secret_key(kms->req, kc.value.aws.secret_access_key)) {
        CLIENT_ERR("failed to set aws secret access key: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto done;
    }

    _mongocrypt_buffer_init(&kms->msg);
    kms->msg.data = (uint8_t *)kms_request_get_signed(kms->req);
    if (!kms->msg.data) {
        CLIENT_ERR("failed to create KMS message: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto done;
    }
    kms->msg.len = (uint32_t)strlen((char *)kms->msg.data);
    kms->msg.owned = true;

    /* construct the endpoint */
    if (ctx_opts->kek.provider.aws.endpoint) {
        kms->endpoint = bson_strdup(ctx_opts->kek.provider.aws.endpoint->host_and_port);
    } else {
        kms->endpoint = bson_strdup_printf("kms.%s.amazonaws.com", ctx_opts->kek.provider.aws.region);
    }
    _mongocrypt_apply_default_port(&kms->endpoint, DEFAULT_HTTPS_PORT);

    ret = true;
done:
    mongocrypt_status_destroy(ctx_with_status.status);
    return ret;
}

uint32_t mongocrypt_kms_ctx_bytes_needed(mongocrypt_kms_ctx_t *kms) {
    int want_bytes;

    if (!kms) {
        return 0;
    }
    /* TODO: an oddity of kms-message. After retrieving the result, it
     * resets the parser. */
    if (!mongocrypt_status_ok(kms->status) || !_mongocrypt_buffer_empty(&kms->result)) {
        return 0;
    }
    if (kms->should_retry) {
        return 0;
    }
    want_bytes = kms_response_parser_wants_bytes(kms->parser, DEFAULT_MAX_KMS_BYTE_REQUEST);
    BSON_ASSERT(want_bytes >= 0);
    return (uint32_t)want_bytes;
}

int64_t mongocrypt_kms_ctx_usleep(mongocrypt_kms_ctx_t *kms) {
    if (!kms) {
        return 0;
    }
    return kms->sleep_usec;
}

static void
_handle_non200_http_status(int http_status, const char *body, size_t body_len, mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(body);

    /* 1xx, 2xx, and 3xx HTTP status codes are not errors, but we only
     * support handling 200 response. */
    if (http_status < 400) {
        CLIENT_ERR("Unsupported HTTP code in KMS response. HTTP status=%d. "
                   "Response body=\n%s",
                   http_status,
                   body);
        return;
    }

    /* Either empty body or body containing JSON with error message. */
    if (body_len == 0) {
        CLIENT_ERR("Error in KMS response. HTTP status=%d. Empty body.", http_status);
        return;
    }

    CLIENT_ERR("Error in KMS response. HTTP status=%d. Response body=\n%s", http_status, body);
}

static int64_t backoff_time_usec(int64_t attempts) {
    static bool seeded = false;
    if (!seeded) {
        srand((uint32_t)time(NULL));
        seeded = true;
    }

    /* Exponential backoff with jitter. */
    const int64_t base = 200000;  /* 0.2 seconds */
    const int64_t max = 20000000; /* 20 seconds */
    BSON_ASSERT(attempts > 0);
    int64_t backoff = base * ((int64_t)1 << (attempts - 1));
    if (backoff > max) {
        backoff = max;
    }

    /* Full jitter: between 1 and current max */
    return (int64_t)((double)rand() / (double)RAND_MAX * (double)backoff) + 1;
}

static bool should_retry_http(int http_status, _kms_request_type_t t) {
    static const int retryable_aws[] = {408, 429, 500, 502, 503, 509};
    static const int retryable_azure[] = {408, 429, 500, 502, 503, 504};
    if (t == MONGOCRYPT_KMS_AWS_ENCRYPT || t == MONGOCRYPT_KMS_AWS_DECRYPT) {
        for (size_t i = 0; i < sizeof(retryable_aws) / sizeof(retryable_aws[0]); i++) {
            if (http_status == retryable_aws[i]) {
                return true;
            }
        }
    } else if (t == MONGOCRYPT_KMS_AZURE_WRAPKEY || t == MONGOCRYPT_KMS_AZURE_UNWRAPKEY
               || t == MONGOCRYPT_KMS_AZURE_OAUTH) {
        for (size_t i = 0; i < sizeof(retryable_azure) / sizeof(retryable_azure[0]); i++) {
            if (http_status == retryable_azure[i]) {
                return true;
            }
        }
    } else if (t == MONGOCRYPT_KMS_GCP_ENCRYPT || t == MONGOCRYPT_KMS_GCP_DECRYPT || t == MONGOCRYPT_KMS_GCP_OAUTH) {
        if (http_status == 408 || http_status == 429 || http_status / 500 == 1) {
            return true;
        }
    }
    return false;
}

static void set_retry(mongocrypt_kms_ctx_t *kms) {
    kms->should_retry = true;
    kms->attempts++;
    kms->sleep_usec = backoff_time_usec(kms->attempts);
}

/* An AWS KMS context has received full response. Parse out the result or error.
 */
static bool _ctx_done_aws(mongocrypt_kms_ctx_t *kms, const char *json_field) {
    BSON_ASSERT_PARAM(kms);
    BSON_ASSERT_PARAM(json_field);

    kms_response_t *response = NULL;
    const char *body;
    bson_t body_bson = BSON_INITIALIZER;
    bool ret;
    bson_error_t bson_error;
    bson_iter_t iter;
    uint32_t b64_strlen;
    char *b64_str;
    int http_status;
    size_t body_len;
    int result_len;
    mongocrypt_status_t *status;

    status = kms->status;
    ret = false;
    /* Parse out the {en|de}crypted result. */
    http_status = kms_response_parser_status(kms->parser);
    response = kms_response_parser_get_response(kms->parser);
    if (!response) {
        CLIENT_ERR("Failed to get response from parser: %s", kms_response_parser_error(kms->parser));
        goto fail;
    }
    body = kms_response_get_body(response, &body_len);

    if (kms->retry_enabled && should_retry_http(http_status, kms->req_type)) {
        if (kms->attempts >= kms_max_attempts) {
            // Wrap error to indicate maximum retries occurred.
            _handle_non200_http_status(http_status, body, body_len, status);
            CLIENT_ERR("KMS request failed after maximum of %d retries: %s",
                       kms_max_attempts,
                       mongocrypt_status_message(status, NULL));
            goto fail;
        } else {
            ret = true;
            set_retry(kms);
            goto fail;
        }
    }

    if (http_status != 200) {
        _handle_non200_http_status(http_status, body, body_len, status);
        goto fail;
    }

    /* If HTTP response succeeded (status 200) then body should contain JSON.
     */
    bson_destroy(&body_bson);
    if (body_len > (size_t)SSIZE_MAX) {
        CLIENT_ERR("Error parsing JSON in KMS response '%s'. "
                   "Response body exceeds maximum supported length",
                   bson_error.message);
        bson_init(&body_bson);
        goto fail;
    }
    if (!bson_init_from_json(&body_bson, body, (ssize_t)body_len, &bson_error)) {
        CLIENT_ERR("Error parsing JSON in KMS response '%s'. "
                   "HTTP status=%d. Response body=\n%s",
                   bson_error.message,
                   http_status,
                   body);
        bson_init(&body_bson);
        goto fail;
    }

    if (!bson_iter_init_find(&iter, &body_bson, json_field) || !BSON_ITER_HOLDS_UTF8(&iter)) {
        CLIENT_ERR("KMS JSON response does not include field '%s'. HTTP status=%d. "
                   "Response body=\n%s",
                   json_field,
                   http_status,
                   body);
        goto fail;
    }

    b64_str = (char *)bson_iter_utf8(&iter, &b64_strlen);
    BSON_ASSERT(b64_str);
    uint8_t *result_data = bson_malloc((size_t)b64_strlen + 1u);
    BSON_ASSERT(result_data);

    result_len = kms_message_b64_pton(b64_str, result_data, b64_strlen);
    if (result_len < 0) {
        CLIENT_ERR("Failed to base64 decode response. HTTP status=%d. Response body=\n%s", http_status, body);
        bson_free(result_data);
        goto fail;
    }
    kms->result.data = result_data;
    kms->result.len = (uint32_t)result_len;
    kms->result.owned = true;
    ret = true;
fail:
    bson_destroy(&body_bson);
    kms_response_destroy(response);
    return ret;
}

/* A Azure/GCP oauth KMS context has received full response. Parse out the
 * bearer token or error. */
static bool _ctx_done_oauth(mongocrypt_kms_ctx_t *kms) {
    BSON_ASSERT_PARAM(kms);

    kms_response_t *response = NULL;
    const char *body;
    bson_t *bson_body = NULL;
    bool ret;
    bson_error_t bson_error;
    bson_iter_t iter;
    int http_status;
    size_t body_len;
    mongocrypt_status_t *status;

    status = kms->status;
    ret = false;
    /* Parse out the oauth token result (or error). */
    http_status = kms_response_parser_status(kms->parser);
    response = kms_response_parser_get_response(kms->parser);
    if (!response) {
        CLIENT_ERR("Failed to get response from parser: %s", kms_response_parser_error(kms->parser));
        goto fail;
    }
    body = kms_response_get_body(response, &body_len);

    if (kms->retry_enabled && should_retry_http(http_status, kms->req_type)) {
        if (kms->attempts >= kms_max_attempts) {
            // Wrap error to indicate maximum retries occurred.
            _handle_non200_http_status(http_status, body, body_len, status);
            CLIENT_ERR("KMS request failed after maximum of %d retries: %s",
                       kms_max_attempts,
                       mongocrypt_status_message(status, NULL));
            goto fail;
        } else {
            ret = true;
            set_retry(kms);
            goto fail;
        }
    }

    if (body_len == 0) {
        CLIENT_ERR("Empty KMS response. HTTP status=%d", http_status);
        goto fail;
    }

    if (body_len > (size_t)SSIZE_MAX) {
        CLIENT_ERR("Error parsing JSON in KMS response '%s'. "
                   "Response body exceeds maximum supported length",
                   bson_error.message);
        goto fail;
    }
    bson_body = bson_new_from_json((const uint8_t *)body, (ssize_t)body_len, &bson_error);
    if (!bson_body) {
        CLIENT_ERR("Error parsing JSON in KMS response '%s'. "
                   "HTTP status=%d. Response body=\n%s",
                   bson_error.message,
                   http_status,
                   body);
        goto fail;
    }

    if (http_status != 200) {
        _handle_non200_http_status(http_status, body, body_len, status);
        goto fail;
    }

    if (!bson_iter_init_find(&iter, bson_body, "access_token") || !BSON_ITER_HOLDS_UTF8(&iter)) {
        CLIENT_ERR("Invalid KMS response. KMS JSON response does not include "
                   "field 'access_token'. "
                   "HTTP status=%d. Response body=\n%s",
                   http_status,
                   body);
        goto fail;
    }

    /* Store the full response, to include the expiration time. */
    _mongocrypt_buffer_steal_from_bson(&kms->result, bson_body);
    bson_body = NULL;

    ret = true;
fail:
    bson_destroy(bson_body);
    kms_response_destroy(response);
    return ret;
}

/* An Azure oauth KMS context has received full response. Parse out the bearer
 * token or error. */
static bool _ctx_done_azure_wrapkey_unwrapkey(mongocrypt_kms_ctx_t *kms) {
    BSON_ASSERT_PARAM(kms);

    kms_response_t *response = NULL;
    const char *body;
    bson_t *bson_body = NULL;
    bool ret;
    bson_error_t bson_error;
    bson_iter_t iter;
    int http_status;
    size_t body_len;
    mongocrypt_status_t *status;
    const char *b64url_data = NULL;
    uint32_t b64url_len;
    char *b64_data = NULL;
    uint32_t b64_len;
    int result_len;

    status = kms->status;
    ret = false;
    /* Parse out the oauth token result (or error). */
    http_status = kms_response_parser_status(kms->parser);
    response = kms_response_parser_get_response(kms->parser);
    if (!response) {
        CLIENT_ERR("Failed to get response from parser: %s", kms_response_parser_error(kms->parser));
        goto fail;
    }
    body = kms_response_get_body(response, &body_len);

    if (kms->retry_enabled && should_retry_http(http_status, kms->req_type)) {
        if (kms->attempts >= kms_max_attempts) {
            // Wrap error to indicate maximum retries occurred.
            _handle_non200_http_status(http_status, body, body_len, status);
            CLIENT_ERR("KMS request failed after maximum of %d retries: %s",
                       kms_max_attempts,
                       mongocrypt_status_message(status, NULL));
            goto fail;
        } else {
            ret = true;
            set_retry(kms);
            goto fail;
        }
    }

    if (body_len == 0) {
        CLIENT_ERR("Empty KMS response. HTTP status=%d", http_status);
        goto fail;
    }

    if (body_len > (size_t)SSIZE_MAX) {
        CLIENT_ERR("Error parsing JSON in KMS response '%s'. "
                   "Response body exceeds maximum supported length",
                   bson_error.message);
        goto fail;
    }
    bson_body = bson_new_from_json((const uint8_t *)body, (ssize_t)body_len, &bson_error);
    if (!bson_body) {
        CLIENT_ERR("Error parsing JSON in KMS response '%s'. "
                   "HTTP status=%d. Response body=\n%s",
                   bson_error.message,
                   http_status,
                   body);
        goto fail;
    }

    if (http_status != 200) {
        _handle_non200_http_status(http_status, body, body_len, status);
        goto fail;
    }

    if (!bson_iter_init_find(&iter, bson_body, "value") || !BSON_ITER_HOLDS_UTF8(&iter)) {
        CLIENT_ERR("KMS JSON response does not include field 'value'. HTTP status=%d. "
                   "Response body=\n%s",
                   http_status,
                   body);
        goto fail;
    }

    b64url_data = bson_iter_utf8(&iter, &b64url_len);
    BSON_ASSERT(b64url_len <= UINT32_MAX - 4u);
    /* add four for padding. */
    b64_len = b64url_len + 4;
    b64_data = bson_malloc0(b64_len);
    if (kms_message_b64url_to_b64(b64url_data, b64url_len, b64_data, b64_len) == -1) {
        CLIENT_ERR("Error converting base64url to base64");
        goto fail;
    }

    uint8_t *result_data = bson_malloc(b64_len);
    BSON_ASSERT(result_data);
    result_len = kms_message_b64_pton(b64_data, result_data, b64_len);
    if (result_len < 0) {
        CLIENT_ERR("Failed to base64 decode response. HTTP status=%d. Response body=\n%s", http_status, body);
        bson_free(result_data);
        goto fail;
    }

    kms->result.data = result_data;
    kms->result.len = (uint32_t)result_len;
    kms->result.owned = true;

    ret = true;
fail:
    bson_destroy(bson_body);
    kms_response_destroy(response);
    bson_free(b64_data);
    return ret;
}

/* A GCP KMS context has received full response. Parse out the result or error.
 */
static bool _ctx_done_gcp(mongocrypt_kms_ctx_t *kms, const char *json_field) {
    BSON_ASSERT_PARAM(kms);
    BSON_ASSERT_PARAM(json_field);

    kms_response_t *response = NULL;
    const char *body;
    bson_t body_bson = BSON_INITIALIZER;
    bool ret;
    bson_error_t bson_error;
    bson_iter_t iter;
    size_t outlen;
    char *b64_str;
    int http_status;
    size_t body_len;
    mongocrypt_status_t *status;

    status = kms->status;
    ret = false;
    /* Parse out the {en|de}crypted result. */
    http_status = kms_response_parser_status(kms->parser);
    response = kms_response_parser_get_response(kms->parser);
    if (!response) {
        CLIENT_ERR("Failed to get response from parser: %s", kms_response_parser_error(kms->parser));
        goto fail;
    }
    body = kms_response_get_body(response, &body_len);

    if (kms->retry_enabled && should_retry_http(http_status, kms->req_type)) {
        if (kms->attempts >= kms_max_attempts) {
            // Wrap error to indicate maximum retries occurred.
            _handle_non200_http_status(http_status, body, body_len, status);
            CLIENT_ERR("KMS request failed after maximum of %d retries: %s",
                       kms_max_attempts,
                       mongocrypt_status_message(status, NULL));
            goto fail;
        } else {
            ret = true;
            set_retry(kms);
            goto fail;
        }
    }

    if (http_status != 200) {
        _handle_non200_http_status(http_status, body, body_len, status);
        goto fail;
    }

    /* If HTTP response succeeded (status 200) then body should contain JSON.
     */
    bson_destroy(&body_bson);
    if (body_len > (size_t)SSIZE_MAX) {
        CLIENT_ERR("Error parsing JSON in KMS response '%s'. "
                   "Response body exceeds maximum supported length",
                   bson_error.message);
        bson_init(&body_bson);
        goto fail;
    }
    if (!bson_init_from_json(&body_bson, body, (ssize_t)body_len, &bson_error)) {
        CLIENT_ERR("Error parsing JSON in KMS response '%s'. "
                   "HTTP status=%d. Response body=\n%s",
                   bson_error.message,
                   http_status,
                   body);
        bson_init(&body_bson);
        goto fail;
    }

    if (!bson_iter_init_find(&iter, &body_bson, json_field) || !BSON_ITER_HOLDS_UTF8(&iter)) {
        CLIENT_ERR("KMS JSON response does not include field '%s'. HTTP status=%d. "
                   "Response body=\n%s",
                   json_field,
                   http_status,
                   body);
        goto fail;
    }

    b64_str = (char *)bson_iter_utf8(&iter, NULL);
    BSON_ASSERT(b64_str);
    kms->result.data = kms_message_b64_to_raw(b64_str, &outlen);
    BSON_ASSERT(outlen <= UINT32_MAX);
    kms->result.len = (uint32_t)outlen;
    kms->result.owned = true;
    ret = true;
fail:
    bson_destroy(&body_bson);
    kms_response_destroy(response);
    return ret;
}

static bool _ctx_done_kmip_register(mongocrypt_kms_ctx_t *kms_ctx) {
    BSON_ASSERT_PARAM(kms_ctx);

    kms_response_t *res = NULL;

    mongocrypt_status_t *status = kms_ctx->status;
    bool ret = false;
    char *uid;

    res = kms_response_parser_get_response(kms_ctx->parser);
    if (!res) {
        CLIENT_ERR("Error getting KMIP response: %s", kms_response_parser_error(kms_ctx->parser));
        goto done;
    }

    uid = kms_kmip_response_get_unique_identifier(res);
    if (!uid) {
        CLIENT_ERR("Error getting UniqueIdentifer from KMIP Register response: %s", kms_response_get_error(res));
        goto done;
    }

    if (!_mongocrypt_buffer_steal_from_string(&kms_ctx->result, uid)) {
        CLIENT_ERR("Error storing KMS UniqueIdentifer result");
        bson_free(uid);
        goto done;
    }
    ret = true;

done:
    kms_response_destroy(res);
    return ret;
}

static bool _ctx_done_kmip_activate(mongocrypt_kms_ctx_t *kms_ctx) {
    BSON_ASSERT_PARAM(kms_ctx);
    return _ctx_done_kmip_register(kms_ctx);
}

static bool _ctx_done_kmip_get(mongocrypt_kms_ctx_t *kms_ctx) {
    BSON_ASSERT_PARAM(kms_ctx);

    kms_response_t *res = NULL;

    mongocrypt_status_t *status = kms_ctx->status;
    bool ret = false;
    uint8_t *secretdata;
    size_t secretdata_len;

    res = kms_response_parser_get_response(kms_ctx->parser);
    if (!res) {
        CLIENT_ERR("Error getting KMIP response: %s", kms_response_parser_error(kms_ctx->parser));
        goto done;
    }

    secretdata = kms_kmip_response_get_secretdata(res, &secretdata_len);
    if (!secretdata) {
        CLIENT_ERR("Error getting SecretData from KMIP Get response: %s", kms_response_get_error(res));
        goto done;
    }

    if (!_mongocrypt_buffer_steal_from_data_and_size(&kms_ctx->result, secretdata, secretdata_len)) {
        CLIENT_ERR("Error storing KMS SecretData result");
        bson_free(secretdata);
        goto done;
    }

    ret = true;

done:
    kms_response_destroy(res);
    return ret;
}

static bool _ctx_done_kmip_create(mongocrypt_kms_ctx_t *kms_ctx) {
    BSON_ASSERT_PARAM(kms_ctx);

    kms_response_t *res = NULL;

    mongocrypt_status_t *status = kms_ctx->status;
    bool ret = false;
    char *uid;

    res = kms_response_parser_get_response(kms_ctx->parser);
    if (!res) {
        CLIENT_ERR("Error getting KMIP response: %s", kms_response_parser_error(kms_ctx->parser));
        goto done;
    }

    uid = kms_kmip_response_get_unique_identifier(res);
    if (!uid) {
        CLIENT_ERR("Error getting UniqueIdentifer from KMIP Create response: %s", kms_response_get_error(res));
        goto done;
    }

    if (!_mongocrypt_buffer_steal_from_string(&kms_ctx->result, uid)) {
        CLIENT_ERR("Error storing KMS UniqueIdentifer result");
        bson_free(uid);
        goto done;
    }
    ret = true;

done:
    kms_response_destroy(res);
    return ret;
}

static bool _ctx_done_kmip_encrypt(mongocrypt_kms_ctx_t *kms_ctx) {
    BSON_ASSERT_PARAM(kms_ctx);

    kms_response_t *res = NULL;

    mongocrypt_status_t *status = kms_ctx->status;
    bool ret = false;
    uint8_t *ciphertext;
    size_t ciphertext_len;
    uint8_t *iv;
    size_t iv_len;
    _mongocrypt_buffer_t data_buf, iv_buf;
    _mongocrypt_buffer_init(&data_buf);
    _mongocrypt_buffer_init(&iv_buf);

    res = kms_response_parser_get_response(kms_ctx->parser);
    if (!res) {
        CLIENT_ERR("Error getting KMIP response: %s", kms_response_parser_error(kms_ctx->parser));
        goto done;
    }

    ciphertext = kms_kmip_response_get_data(res, &ciphertext_len);
    if (!ciphertext) {
        CLIENT_ERR("Error getting data from KMIP Encrypt response: %s", kms_response_get_error(res));
        goto done;
    }

    iv = kms_kmip_response_get_iv(res, &iv_len);
    if (!iv) {
        CLIENT_ERR("Error getting IV from KMIP Encrypt response: %s", kms_response_get_error(res));
        bson_free(ciphertext);
        goto done;
    }

    if (iv_len != MONGOCRYPT_IV_LEN) {
        CLIENT_ERR("KMIP IV response has unexpected length: %zu", iv_len);
        bson_free(ciphertext);
        bson_free(iv);
        goto done;
    }

    if (!_mongocrypt_buffer_steal_from_data_and_size(&data_buf, ciphertext, ciphertext_len)) {
        CLIENT_ERR("Error storing KMS Encrypt result");
        bson_free(ciphertext);
        bson_free(iv);
        goto done;
    }

    if (!_mongocrypt_buffer_steal_from_data_and_size(&iv_buf, iv, iv_len)) {
        CLIENT_ERR("Error storing KMS Encrypt IV");
        bson_free(ciphertext);
        bson_free(iv);
        goto done;
    }

    const _mongocrypt_buffer_t results_buf[2] = {iv_buf, data_buf};
    if (!_mongocrypt_buffer_concat(&kms_ctx->result, results_buf, 2)) {
        CLIENT_ERR("Error concatenating IV and ciphertext");
        goto done;
    }

    ret = true;

done:
    kms_response_destroy(res);
    _mongocrypt_buffer_cleanup(&iv_buf);
    _mongocrypt_buffer_cleanup(&data_buf);
    return ret;
}

static bool _ctx_done_kmip_decrypt(mongocrypt_kms_ctx_t *kms_ctx) {
    BSON_ASSERT_PARAM(kms_ctx);

    kms_response_t *res = NULL;

    mongocrypt_status_t *status = kms_ctx->status;
    bool ret = false;
    uint8_t *ciphertext;
    size_t ciphertext_len;

    res = kms_response_parser_get_response(kms_ctx->parser);
    if (!res) {
        CLIENT_ERR("Error getting KMIP response: %s", kms_response_parser_error(kms_ctx->parser));
        goto done;
    }

    ciphertext = kms_kmip_response_get_data(res, &ciphertext_len);
    if (!ciphertext) {
        CLIENT_ERR("Error getting data from KMIP Decrypt response: %s", kms_response_get_error(res));
        goto done;
    }

    if (!_mongocrypt_buffer_steal_from_data_and_size(&kms_ctx->result, ciphertext, ciphertext_len)) {
        CLIENT_ERR("Error storing KMS Decrypt result");
        bson_free(ciphertext);
        goto done;
    }

    ret = true;

done:
    kms_response_destroy(res);
    return ret;
}

bool mongocrypt_kms_ctx_fail(mongocrypt_kms_ctx_t *kms) {
    if (!kms || !kms->retry_enabled) {
        return false;
    }

    kms->should_retry = false;
    mongocrypt_status_t *status = kms->status;

    if (!kms->retry_enabled) {
        CLIENT_ERR("KMS request failed due to network error");
        return false;
    }

    if (kms->attempts >= kms_max_attempts) {
        CLIENT_ERR("KMS request failed after %d retries due to a network error", kms_max_attempts);
        return false;
    }

    // Check if request type is retryable. Some requests are non-idempotent and cannot be safely retried.
    _kms_request_type_t retryable_types[] = {MONGOCRYPT_KMS_AZURE_OAUTH,
                                             MONGOCRYPT_KMS_GCP_OAUTH,
                                             MONGOCRYPT_KMS_AWS_ENCRYPT,
                                             MONGOCRYPT_KMS_AWS_DECRYPT,
                                             MONGOCRYPT_KMS_AZURE_WRAPKEY,
                                             MONGOCRYPT_KMS_AZURE_UNWRAPKEY,
                                             MONGOCRYPT_KMS_GCP_ENCRYPT,
                                             MONGOCRYPT_KMS_GCP_DECRYPT};
    bool is_retryable = false;
    for (size_t i = 0; i < sizeof(retryable_types) / sizeof(retryable_types[0]); i++) {
        if (retryable_types[i] == kms->req_type) {
            is_retryable = true;
            break;
        }
    }
    if (!is_retryable) {
        CLIENT_ERR("KMS request failed due to network error");
        return false;
    }

    // Mark KMS context as retryable. Return again in `mongocrypt_ctx_next_kms_ctx`.
    set_retry(kms);

    // Reset intermediate state of parser.
    if (kms->parser) {
        kms_response_parser_reset(kms->parser);
    }
    return true;
}

bool mongocrypt_kms_ctx_feed(mongocrypt_kms_ctx_t *kms, mongocrypt_binary_t *bytes) {
    if (!kms) {
        return false;
    }

    mongocrypt_status_t *status = kms->status;
    if (!mongocrypt_status_ok(status)) {
        return false;
    }

    if (!bytes) {
        CLIENT_ERR("argument 'bytes' is required");
        return false;
    }

    if (0 == bytes->len) {
        CLIENT_ERR("argument 'bytes' cannot be empty");
        return false;
    }

    if (bytes->len > mongocrypt_kms_ctx_bytes_needed(kms)) {
        CLIENT_ERR("KMS response fed too much data");
        return false;
    }

    if (kms->log && kms->log->trace_enabled) {
        _mongocrypt_log(kms->log,
                        MONGOCRYPT_LOG_LEVEL_TRACE,
                        "%s (%s=\"%.*s\")",
                        BSON_FUNC,
                        "bytes",
                        mongocrypt_binary_len(bytes),
                        mongocrypt_binary_data(bytes));
    }

    if (!kms_response_parser_feed(kms->parser, bytes->data, bytes->len)) {
        if (is_kms(kms->req_type)) {
            /* The KMIP response parser does not suport kms_response_parser_status.
             * Only report the error string. */
            CLIENT_ERR("KMS response parser error with error: '%s'", kms_response_parser_error(kms->parser));
        } else {
            CLIENT_ERR("KMS response parser error with status %d, error: '%s'",
                       kms_response_parser_status(kms->parser),
                       kms_response_parser_error(kms->parser));
        }

        return false;
    }

    if (0 == mongocrypt_kms_ctx_bytes_needed(kms)) {
        switch (kms->req_type) {
        default: CLIENT_ERR("Unknown request type"); return false;
        case MONGOCRYPT_KMS_AWS_ENCRYPT: return _ctx_done_aws(kms, "CiphertextBlob");
        case MONGOCRYPT_KMS_AWS_DECRYPT: return _ctx_done_aws(kms, "Plaintext");
        case MONGOCRYPT_KMS_AZURE_OAUTH: return _ctx_done_oauth(kms);
        case MONGOCRYPT_KMS_AZURE_WRAPKEY: return _ctx_done_azure_wrapkey_unwrapkey(kms);
        case MONGOCRYPT_KMS_AZURE_UNWRAPKEY: return _ctx_done_azure_wrapkey_unwrapkey(kms);
        case MONGOCRYPT_KMS_GCP_OAUTH: return _ctx_done_oauth(kms);
        case MONGOCRYPT_KMS_GCP_ENCRYPT: return _ctx_done_gcp(kms, "ciphertext");
        case MONGOCRYPT_KMS_GCP_DECRYPT: return _ctx_done_gcp(kms, "plaintext");
        case MONGOCRYPT_KMS_KMIP_REGISTER: return _ctx_done_kmip_register(kms);
        case MONGOCRYPT_KMS_KMIP_ACTIVATE: return _ctx_done_kmip_activate(kms);
        case MONGOCRYPT_KMS_KMIP_GET: return _ctx_done_kmip_get(kms);
        case MONGOCRYPT_KMS_KMIP_ENCRYPT: return _ctx_done_kmip_encrypt(kms);
        case MONGOCRYPT_KMS_KMIP_DECRYPT: return _ctx_done_kmip_decrypt(kms);
        case MONGOCRYPT_KMS_KMIP_CREATE: return _ctx_done_kmip_create(kms);
        }
    }
    return true;
}

bool _mongocrypt_kms_ctx_result(mongocrypt_kms_ctx_t *kms, _mongocrypt_buffer_t *out) {
    BSON_ASSERT_PARAM(kms);
    BSON_ASSERT_PARAM(out);

    mongocrypt_status_t *status = kms->status;

    /* If we have no status, we were never initialized */
    if (!status) {
        return false;
    }

    if (!mongocrypt_status_ok(status)) {
        return false;
    }

    if (mongocrypt_kms_ctx_bytes_needed(kms) > 0) {
        CLIENT_ERR("KMS response unfinished");
        return false;
    }

    _mongocrypt_buffer_init(out);
    out->data = kms->result.data;
    out->len = kms->result.len;
    return true;
}

bool mongocrypt_kms_ctx_status(mongocrypt_kms_ctx_t *kms, mongocrypt_status_t *status_out) {
    if (!kms) {
        return false;
    }

    if (!status_out) {
        mongocrypt_status_t *status = kms->status;
        CLIENT_ERR("argument 'status' is required");
        return false;
    }
    _mongocrypt_status_copy_to(kms->status, status_out);
    return mongocrypt_status_ok(status_out);
}

void _mongocrypt_kms_ctx_cleanup(mongocrypt_kms_ctx_t *kms) {
    if (!kms) {
        return;
    }
    if (kms->req) {
        kms_request_destroy(kms->req);
    }
    if (kms->parser) {
        kms_response_parser_destroy(kms->parser);
    }
    mongocrypt_status_destroy(kms->status);
    _mongocrypt_buffer_cleanup(&kms->msg);
    _mongocrypt_buffer_cleanup(&kms->result);
    bson_free(kms->endpoint);
    bson_free(kms->kmsid);
}

bool mongocrypt_kms_ctx_message(mongocrypt_kms_ctx_t *kms, mongocrypt_binary_t *msg) {
    if (!kms) {
        return false;
    }

    if (!msg) {
        mongocrypt_status_t *status = kms->status;
        CLIENT_ERR("argument 'msg' is required");
        return false;
    }
    msg->data = kms->msg.data;
    msg->len = kms->msg.len;
    return true;
}

bool mongocrypt_kms_ctx_endpoint(mongocrypt_kms_ctx_t *kms, const char **endpoint) {
    if (!kms) {
        return false;
    }
    if (!endpoint) {
        mongocrypt_status_t *status = kms->status;
        CLIENT_ERR("argument 'endpoint' is required");
        return false;
    }
    *endpoint = kms->endpoint;
    return true;
}

bool _mongocrypt_kms_ctx_init_azure_auth(mongocrypt_kms_ctx_t *kms,
                                         const mc_kms_creds_t *kc,
                                         _mongocrypt_endpoint_t *key_vault_endpoint,
                                         const char *kmsid,
                                         _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms);
    BSON_ASSERT_PARAM(kc);

    kms_request_opt_t *opt = NULL;
    mongocrypt_status_t *status;
    const _mongocrypt_endpoint_t *identity_platform_endpoint;
    char *scope = NULL;
    const char *hostname;
    char *request_string;
    bool ret = false;

    _init_common(kms, log, MONGOCRYPT_KMS_AZURE_OAUTH, kmsid);
    status = kms->status;

    BSON_ASSERT(kc->type == MONGOCRYPT_KMS_PROVIDER_AZURE);

    identity_platform_endpoint = kc->value.azure.identity_platform_endpoint;

    if (identity_platform_endpoint) {
        kms->endpoint = bson_strdup(identity_platform_endpoint->host_and_port);
        hostname = identity_platform_endpoint->host;
    } else {
        kms->endpoint = bson_strdup("login.microsoftonline.com");
        hostname = "login.microsoftonline.com";
    }
    _mongocrypt_apply_default_port(&kms->endpoint, DEFAULT_HTTPS_PORT);

    if (key_vault_endpoint) {
        /* Request a custom scope. It is URL encoded, like
         * https%3A%2F%2Fvault.azure.net%2F.default */
        scope = bson_strdup_printf("%s%s%s", "https%3A%2F%2F", key_vault_endpoint->domain, "%2F.default");
    } else {
        /* Default to commercial Azure endpoint. */
        scope = bson_strdup("https%3A%2F%2Fvault.azure.net%2F.default");
    }

    opt = kms_request_opt_new();
    BSON_ASSERT(opt);
    kms_request_opt_set_connection_close(opt, true);
    kms_request_opt_set_provider(opt, KMS_REQUEST_PROVIDER_AZURE);
    kms->req = kms_azure_request_oauth_new(hostname,
                                           scope,
                                           kc->value.azure.tenant_id,
                                           kc->value.azure.client_id,
                                           kc->value.azure.client_secret,
                                           opt);
    if (kms_request_get_error(kms->req)) {
        CLIENT_ERR("error constructing KMS message: %s", kms_request_get_error(kms->req));
        goto fail;
    }

    request_string = kms_request_to_string(kms->req);
    if (!request_string) {
        CLIENT_ERR("error getting Azure OAuth KMS message: %s", kms_request_get_error(kms->req));
        goto fail;
    }
    _mongocrypt_buffer_init(&kms->msg);
    kms->msg.data = (uint8_t *)request_string;
    kms->msg.len = (uint32_t)strlen(request_string);
    kms->msg.owned = true;

    ret = true;
fail:
    bson_free(scope);
    kms_request_opt_destroy(opt);
    return ret;
}

bool _mongocrypt_kms_ctx_init_azure_wrapkey(mongocrypt_kms_ctx_t *kms,
                                            _mongocrypt_opts_kms_providers_t *kms_providers,
                                            struct __mongocrypt_ctx_opts_t *ctx_opts,
                                            const char *access_token,
                                            _mongocrypt_buffer_t *plaintext_key_material,
                                            const char *kmsid,
                                            _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms);
    BSON_ASSERT_PARAM(ctx_opts);
    BSON_ASSERT_PARAM(plaintext_key_material);

    kms_request_opt_t *opt = NULL;
    mongocrypt_status_t *status;
    char *path_and_query = NULL;
    char *payload = NULL;
    const char *host;
    char *request_string;
    bool ret = false;

    _init_common(kms, log, MONGOCRYPT_KMS_AZURE_WRAPKEY, kmsid);
    status = kms->status;

    BSON_ASSERT(ctx_opts->kek.provider.azure.key_vault_endpoint);

    kms->endpoint = bson_strdup(ctx_opts->kek.provider.azure.key_vault_endpoint->host_and_port);
    _mongocrypt_apply_default_port(&kms->endpoint, DEFAULT_HTTPS_PORT);
    host = ctx_opts->kek.provider.azure.key_vault_endpoint->host;

    opt = kms_request_opt_new();
    BSON_ASSERT(opt);
    kms_request_opt_set_connection_close(opt, true);
    kms_request_opt_set_provider(opt, KMS_REQUEST_PROVIDER_AZURE);
    kms->req = kms_azure_request_wrapkey_new(host,
                                             access_token,
                                             ctx_opts->kek.provider.azure.key_name,
                                             ctx_opts->kek.provider.azure.key_version,
                                             plaintext_key_material->data,
                                             plaintext_key_material->len,
                                             opt);

    if (kms_request_get_error(kms->req)) {
        CLIENT_ERR("error constructing KMS wrapkey message: %s", kms_request_get_error(kms->req));
        goto fail;
    }

    request_string = kms_request_to_string(kms->req);
    if (!request_string) {
        CLIENT_ERR("error getting Azure wrapkey KMS message: %s", kms_request_get_error(kms->req));
        goto fail;
    }
    _mongocrypt_buffer_init(&kms->msg);
    kms->msg.data = (uint8_t *)request_string;
    kms->msg.len = (uint32_t)strlen(request_string);
    kms->msg.owned = true;

    ret = true;
fail:
    kms_request_opt_destroy(opt);
    bson_free(path_and_query);
    bson_free(payload);
    return ret;
}

bool _mongocrypt_kms_ctx_init_azure_unwrapkey(mongocrypt_kms_ctx_t *kms,
                                              _mongocrypt_opts_kms_providers_t *kms_providers,
                                              const char *access_token,
                                              _mongocrypt_key_doc_t *key,
                                              const char *kmsid,
                                              _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms);
    BSON_ASSERT_PARAM(key);

    kms_request_opt_t *opt = NULL;
    mongocrypt_status_t *status;
    char *path_and_query = NULL;
    char *payload = NULL;
    const char *host;
    char *request_string;
    bool ret = false;

    _init_common(kms, log, MONGOCRYPT_KMS_AZURE_UNWRAPKEY, kmsid);
    status = kms->status;

    BSON_ASSERT(key->kek.provider.azure.key_vault_endpoint);

    kms->endpoint = bson_strdup(key->kek.provider.azure.key_vault_endpoint->host_and_port);
    _mongocrypt_apply_default_port(&kms->endpoint, DEFAULT_HTTPS_PORT);
    host = key->kek.provider.azure.key_vault_endpoint->host;

    opt = kms_request_opt_new();
    BSON_ASSERT(opt);
    kms_request_opt_set_connection_close(opt, true);
    kms_request_opt_set_provider(opt, KMS_REQUEST_PROVIDER_AZURE);
    kms->req = kms_azure_request_unwrapkey_new(host,
                                               access_token,
                                               key->kek.provider.azure.key_name,
                                               key->kek.provider.azure.key_version,
                                               key->key_material.data,
                                               key->key_material.len,
                                               opt);

    if (kms_request_get_error(kms->req)) {
        CLIENT_ERR("error constructing KMS unwrapkey message: %s", kms_request_get_error(kms->req));
        goto fail;
    }

    request_string = kms_request_to_string(kms->req);
    if (!request_string) {
        CLIENT_ERR("error getting Azure unwrapkey KMS message: %s", kms_request_get_error(kms->req));
        goto fail;
    }
    _mongocrypt_buffer_init(&kms->msg);
    kms->msg.data = (uint8_t *)request_string;
    kms->msg.len = (uint32_t)strlen(request_string);
    kms->msg.owned = true;

    ret = true;
fail:
    kms_request_opt_destroy(opt);
    bson_free(path_and_query);
    bson_free(payload);
    return ret;
}

#define RSAES_PKCS1_V1_5_SIGNATURE_LEN 256

/* This is the form of the callback that KMS message calls. */
static bool _sign_rsaes_pkcs1_v1_5_trampoline(void *ctx,
                                              const char *private_key,
                                              size_t private_key_len,
                                              const char *input,
                                              size_t input_len,
                                              unsigned char *signature_out) {
    ctx_with_status_t *ctx_with_status;
    _mongocrypt_opts_t *crypt_opts;
    mongocrypt_binary_t private_key_bin;
    mongocrypt_binary_t input_bin;
    mongocrypt_binary_t output_bin;
    bool ret;

    BSON_ASSERT_PARAM(ctx);
    BSON_ASSERT_PARAM(input);
    BSON_ASSERT_PARAM(private_key);
    BSON_ASSERT_PARAM(signature_out);

    ctx_with_status = (ctx_with_status_t *)ctx;
    crypt_opts = (_mongocrypt_opts_t *)ctx_with_status->ctx;
    BSON_ASSERT(crypt_opts);
    private_key_bin.data = (uint8_t *)private_key;
    BSON_ASSERT(private_key_len <= UINT32_MAX);
    private_key_bin.len = (uint32_t)private_key_len;
    input_bin.data = (uint8_t *)input;
    BSON_ASSERT(input_len <= UINT32_MAX);
    input_bin.len = (uint32_t)input_len;
    output_bin.data = (uint8_t *)signature_out;
    output_bin.len = RSAES_PKCS1_V1_5_SIGNATURE_LEN;

    ret = crypt_opts->sign_rsaes_pkcs1_v1_5(crypt_opts->sign_ctx,
                                            &private_key_bin,
                                            &input_bin,
                                            &output_bin,
                                            ctx_with_status->status);
    return ret;
}

bool _mongocrypt_kms_ctx_init_gcp_auth(mongocrypt_kms_ctx_t *kms,
                                       _mongocrypt_opts_t *crypt_opts,
                                       const mc_kms_creds_t *kc,
                                       _mongocrypt_endpoint_t *kms_endpoint,
                                       const char *kmsid,
                                       _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms);
    BSON_ASSERT_PARAM(kc);
    BSON_ASSERT_PARAM(crypt_opts);

    kms_request_opt_t *opt = NULL;
    mongocrypt_status_t *status;
    const _mongocrypt_endpoint_t *auth_endpoint;
    char *scope = NULL;
    char *audience = NULL;
    const char *hostname;
    char *request_string;
    bool ret = false;
    ctx_with_status_t ctx_with_status;

    _init_common(kms, log, MONGOCRYPT_KMS_GCP_OAUTH, kmsid);
    status = kms->status;
    ctx_with_status.ctx = crypt_opts;
    ctx_with_status.status = mongocrypt_status_new();

    BSON_ASSERT(kc->type == MONGOCRYPT_KMS_PROVIDER_GCP);

    auth_endpoint = kc->value.gcp.endpoint;
    if (auth_endpoint) {
        kms->endpoint = bson_strdup(auth_endpoint->host_and_port);
        hostname = auth_endpoint->host;
        audience = bson_strdup_printf("https://%s/token", auth_endpoint->host);
    } else {
        kms->endpoint = bson_strdup("oauth2.googleapis.com");
        hostname = "oauth2.googleapis.com";
        audience = bson_strdup_printf("https://oauth2.googleapis.com/token");
    }
    _mongocrypt_apply_default_port(&kms->endpoint, DEFAULT_HTTPS_PORT);

    if (kms_endpoint) {
        /* Request a custom scope. */
        scope = bson_strdup_printf("https://www.%s/auth/cloudkms", kms_endpoint->domain);
    } else {
        scope = bson_strdup("https://www.googleapis.com/auth/cloudkms");
    }

    opt = kms_request_opt_new();
    BSON_ASSERT(opt);
    kms_request_opt_set_connection_close(opt, true);
    kms_request_opt_set_provider(opt, KMS_REQUEST_PROVIDER_GCP);
    if (crypt_opts->sign_rsaes_pkcs1_v1_5) {
        kms_request_opt_set_crypto_hook_sign_rsaes_pkcs1_v1_5(opt, _sign_rsaes_pkcs1_v1_5_trampoline, &ctx_with_status);
    }
    kms->req = kms_gcp_request_oauth_new(hostname,
                                         kc->value.gcp.email,
                                         audience,
                                         scope,
                                         (const char *)kc->value.gcp.private_key.data,
                                         kc->value.gcp.private_key.len,
                                         opt);
    if (kms_request_get_error(kms->req)) {
        CLIENT_ERR("error constructing KMS message: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto fail;
    }

    request_string = kms_request_to_string(kms->req);
    if (!request_string) {
        CLIENT_ERR("error getting GCP OAuth KMS message: %s", kms_request_get_error(kms->req));
        _mongocrypt_status_append(status, ctx_with_status.status);
        goto fail;
    }
    _mongocrypt_buffer_init(&kms->msg);
    kms->msg.data = (uint8_t *)request_string;
    kms->msg.len = (uint32_t)strlen(request_string);
    kms->msg.owned = true;

    ret = true;
fail:
    bson_free(scope);
    bson_free(audience);
    kms_request_opt_destroy(opt);
    mongocrypt_status_destroy(ctx_with_status.status);
    return ret;
}

bool _mongocrypt_kms_ctx_init_gcp_encrypt(mongocrypt_kms_ctx_t *kms,
                                          _mongocrypt_opts_kms_providers_t *kms_providers,
                                          struct __mongocrypt_ctx_opts_t *ctx_opts,
                                          const char *access_token,
                                          _mongocrypt_buffer_t *plaintext_key_material,
                                          const char *kmsid,
                                          _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms);
    BSON_ASSERT_PARAM(ctx_opts);
    BSON_ASSERT_PARAM(kms_providers);
    BSON_ASSERT_PARAM(access_token);
    BSON_ASSERT_PARAM(plaintext_key_material);

    kms_request_opt_t *opt = NULL;
    mongocrypt_status_t *status;
    char *path_and_query = NULL;
    char *payload = NULL;
    const char *hostname;
    char *request_string;
    bool ret = false;

    _init_common(kms, log, MONGOCRYPT_KMS_GCP_ENCRYPT, kmsid);
    status = kms->status;

    if (ctx_opts->kek.provider.gcp.endpoint) {
        kms->endpoint = bson_strdup(ctx_opts->kek.provider.gcp.endpoint->host_and_port);
        hostname = ctx_opts->kek.provider.gcp.endpoint->host;
    } else {
        kms->endpoint = bson_strdup("cloudkms.googleapis.com");
        hostname = "cloudkms.googleapis.com";
    }
    _mongocrypt_apply_default_port(&kms->endpoint, DEFAULT_HTTPS_PORT);

    opt = kms_request_opt_new();
    BSON_ASSERT(opt);
    kms_request_opt_set_connection_close(opt, true);
    kms_request_opt_set_provider(opt, KMS_REQUEST_PROVIDER_GCP);
    kms->req = kms_gcp_request_encrypt_new(hostname,
                                           access_token,
                                           ctx_opts->kek.provider.gcp.project_id,
                                           ctx_opts->kek.provider.gcp.location,
                                           ctx_opts->kek.provider.gcp.key_ring,
                                           ctx_opts->kek.provider.gcp.key_name,
                                           ctx_opts->kek.provider.gcp.key_version,
                                           plaintext_key_material->data,
                                           plaintext_key_material->len,
                                           opt);

    if (kms_request_get_error(kms->req)) {
        CLIENT_ERR("error constructing GCP KMS encrypt message: %s", kms_request_get_error(kms->req));
        goto fail;
    }

    request_string = kms_request_to_string(kms->req);
    if (!request_string) {
        CLIENT_ERR("error getting GCP KMS encrypt KMS message: %s", kms_request_get_error(kms->req));
        goto fail;
    }
    _mongocrypt_buffer_init(&kms->msg);
    kms->msg.data = (uint8_t *)request_string;
    kms->msg.len = (uint32_t)strlen(request_string);
    kms->msg.owned = true;

    ret = true;
fail:
    kms_request_opt_destroy(opt);
    bson_free(path_and_query);
    bson_free(payload);
    return ret;
}

bool _mongocrypt_kms_ctx_init_gcp_decrypt(mongocrypt_kms_ctx_t *kms,
                                          _mongocrypt_opts_kms_providers_t *kms_providers,
                                          const char *access_token,
                                          _mongocrypt_key_doc_t *key,
                                          const char *kmsid,
                                          _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms);
    BSON_ASSERT_PARAM(kms_providers);
    BSON_ASSERT_PARAM(access_token);
    BSON_ASSERT_PARAM(key);

    kms_request_opt_t *opt = NULL;
    mongocrypt_status_t *status;
    char *path_and_query = NULL;
    char *payload = NULL;
    const char *hostname;
    char *request_string;
    bool ret = false;

    _init_common(kms, log, MONGOCRYPT_KMS_GCP_DECRYPT, kmsid);
    status = kms->status;

    if (key->kek.provider.gcp.endpoint) {
        kms->endpoint = bson_strdup(key->kek.provider.gcp.endpoint->host_and_port);
        hostname = key->kek.provider.gcp.endpoint->host;
    } else {
        kms->endpoint = bson_strdup("cloudkms.googleapis.com");
        hostname = "cloudkms.googleapis.com";
    }
    _mongocrypt_apply_default_port(&kms->endpoint, DEFAULT_HTTPS_PORT);

    opt = kms_request_opt_new();
    BSON_ASSERT(opt);
    kms_request_opt_set_connection_close(opt, true);
    kms_request_opt_set_provider(opt, KMS_REQUEST_PROVIDER_GCP);
    kms->req = kms_gcp_request_decrypt_new(hostname,
                                           access_token,
                                           key->kek.provider.gcp.project_id,
                                           key->kek.provider.gcp.location,
                                           key->kek.provider.gcp.key_ring,
                                           key->kek.provider.gcp.key_name,
                                           key->key_material.data,
                                           key->key_material.len,
                                           opt);

    if (kms_request_get_error(kms->req)) {
        CLIENT_ERR("error constructing GCP KMS decrypt message: %s", kms_request_get_error(kms->req));
        goto fail;
    }

    request_string = kms_request_to_string(kms->req);
    if (!request_string) {
        CLIENT_ERR("error getting GCP KMS decrypt KMS message: %s", kms_request_get_error(kms->req));
        goto fail;
    }
    _mongocrypt_buffer_init(&kms->msg);
    kms->msg.data = (uint8_t *)request_string;
    kms->msg.len = (uint32_t)strlen(request_string);
    kms->msg.owned = true;

    ret = true;
fail:
    kms_request_opt_destroy(opt);
    bson_free(path_and_query);
    bson_free(payload);
    return ret;
}

bool _mongocrypt_kms_ctx_init_kmip_register(mongocrypt_kms_ctx_t *kms_ctx,
                                            const _mongocrypt_endpoint_t *endpoint,
                                            const uint8_t *secretdata,
                                            uint32_t secretdata_len,
                                            const char *kmsid,
                                            _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms_ctx);
    BSON_ASSERT_PARAM(endpoint);
    BSON_ASSERT_PARAM(secretdata);

    mongocrypt_status_t *status;
    bool ret = false;
    const uint8_t *reqdata;
    size_t reqlen;

    _init_common(kms_ctx, log, MONGOCRYPT_KMS_KMIP_REGISTER, kmsid);
    status = kms_ctx->status;

    kms_ctx->endpoint = bson_strdup(endpoint->host_and_port);
    _mongocrypt_apply_default_port(&kms_ctx->endpoint, DEFAULT_KMIP_PORT);
    kms_ctx->req = kms_kmip_request_register_secretdata_new(NULL /* reserved */, secretdata, secretdata_len);

    if (kms_request_get_error(kms_ctx->req)) {
        CLIENT_ERR("Error creating KMIP register request: %s", kms_request_get_error(kms_ctx->req));
        goto done;
    }

    reqdata = kms_request_to_bytes(kms_ctx->req, &reqlen);
    if (!_mongocrypt_buffer_copy_from_data_and_size(&kms_ctx->msg, reqdata, reqlen)) {
        CLIENT_ERR("Error storing KMS request payload");
        goto done;
    }

    ret = true;
done:
    return ret;
}

bool _mongocrypt_kms_ctx_init_kmip_activate(mongocrypt_kms_ctx_t *kms_ctx,
                                            const _mongocrypt_endpoint_t *endpoint,
                                            const char *unique_identifier,
                                            const char *kmsid,
                                            _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms_ctx);
    BSON_ASSERT_PARAM(endpoint);
    BSON_ASSERT_PARAM(unique_identifier);

    mongocrypt_status_t *status;
    bool ret = false;
    size_t reqlen;
    const uint8_t *reqdata;

    _init_common(kms_ctx, log, MONGOCRYPT_KMS_KMIP_ACTIVATE, kmsid);
    status = kms_ctx->status;

    kms_ctx->endpoint = bson_strdup(endpoint->host_and_port);
    _mongocrypt_apply_default_port(&kms_ctx->endpoint, DEFAULT_KMIP_PORT);
    kms_ctx->req = kms_kmip_request_activate_new(NULL /* reserved */, unique_identifier);

    if (kms_request_get_error(kms_ctx->req)) {
        CLIENT_ERR("Error creating KMIP activate request: %s", kms_request_get_error(kms_ctx->req));
        goto done;
    }

    reqdata = kms_request_to_bytes(kms_ctx->req, &reqlen);
    if (!_mongocrypt_buffer_copy_from_data_and_size(&kms_ctx->msg, reqdata, reqlen)) {
        CLIENT_ERR("Error storing KMS request payload");
        goto done;
    }

    ret = true;
done:
    return ret;
}

bool _mongocrypt_kms_ctx_init_kmip_get(mongocrypt_kms_ctx_t *kms_ctx,
                                       const _mongocrypt_endpoint_t *endpoint,
                                       const char *unique_identifier,
                                       const char *kmsid,
                                       _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms_ctx);
    BSON_ASSERT_PARAM(endpoint);
    BSON_ASSERT_PARAM(unique_identifier);

    mongocrypt_status_t *status;
    bool ret = false;
    size_t reqlen;
    const uint8_t *reqdata;

    _init_common(kms_ctx, log, MONGOCRYPT_KMS_KMIP_GET, kmsid);
    status = kms_ctx->status;

    kms_ctx->endpoint = bson_strdup(endpoint->host_and_port);
    _mongocrypt_apply_default_port(&kms_ctx->endpoint, DEFAULT_KMIP_PORT);
    kms_ctx->req = kms_kmip_request_get_new(NULL /* reserved */, unique_identifier);

    if (kms_request_get_error(kms_ctx->req)) {
        CLIENT_ERR("Error creating KMIP get request: %s", kms_request_get_error(kms_ctx->req));
        goto done;
    }

    reqdata = kms_request_to_bytes(kms_ctx->req, &reqlen);
    if (!_mongocrypt_buffer_copy_from_data_and_size(&kms_ctx->msg, reqdata, reqlen)) {
        CLIENT_ERR("Error storing KMS request payload");
        goto done;
    }

    ret = true;
done:
    return ret;
}

bool _mongocrypt_kms_ctx_init_kmip_create(mongocrypt_kms_ctx_t *kms_ctx,
                                          const _mongocrypt_endpoint_t *endpoint,
                                          const char *kmsid,
                                          _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms_ctx);
    BSON_ASSERT_PARAM(endpoint);
    bool ret = false;

    _init_common(kms_ctx, log, MONGOCRYPT_KMS_KMIP_CREATE, kmsid);
    mongocrypt_status_t *status = kms_ctx->status;
    kms_ctx->endpoint = bson_strdup(endpoint->host_and_port);
    _mongocrypt_apply_default_port(&kms_ctx->endpoint, DEFAULT_KMIP_PORT);

    kms_ctx->req = kms_kmip_request_create_new(NULL /* reserved */);

    if (kms_request_get_error(kms_ctx->req)) {
        CLIENT_ERR("Error creating KMIP create request: %s", kms_request_get_error(kms_ctx->req));
        goto done;
    }

    size_t reqlen;
    const uint8_t *reqdata = kms_request_to_bytes(kms_ctx->req, &reqlen);
    if (!_mongocrypt_buffer_copy_from_data_and_size(&kms_ctx->msg, reqdata, reqlen)) {
        CLIENT_ERR("Error storing KMS request payload");
        goto done;
    }

    ret = true;
done:
    return ret;
}

bool _mongocrypt_kms_ctx_init_kmip_encrypt(mongocrypt_kms_ctx_t *kms_ctx,
                                           const _mongocrypt_endpoint_t *endpoint,
                                           const char *unique_identifier,
                                           const char *kmsid,
                                           _mongocrypt_buffer_t *plaintext,
                                           _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms_ctx);
    BSON_ASSERT_PARAM(endpoint);
    BSON_ASSERT_PARAM(plaintext);
    bool ret = false;

    _init_common(kms_ctx, log, MONGOCRYPT_KMS_KMIP_ENCRYPT, kmsid);
    mongocrypt_status_t *status = kms_ctx->status;
    kms_ctx->endpoint = bson_strdup(endpoint->host_and_port);
    _mongocrypt_apply_default_port(&kms_ctx->endpoint, DEFAULT_KMIP_PORT);

    kms_ctx->req =
        kms_kmip_request_encrypt_new(NULL /* reserved */, unique_identifier, plaintext->data, plaintext->len);

    if (kms_request_get_error(kms_ctx->req)) {
        CLIENT_ERR("Error creating KMIP encrypt request: %s", kms_request_get_error(kms_ctx->req));
        goto done;
    }

    size_t reqlen;
    const uint8_t *reqdata = kms_request_to_bytes(kms_ctx->req, &reqlen);
    if (!_mongocrypt_buffer_copy_from_data_and_size(&kms_ctx->msg, reqdata, reqlen)) {
        CLIENT_ERR("Error storing KMS request payload");
        goto done;
    }

    ret = true;
done:
    return ret;
}

bool _mongocrypt_kms_ctx_init_kmip_decrypt(mongocrypt_kms_ctx_t *kms_ctx,
                                           const _mongocrypt_endpoint_t *endpoint,
                                           const char *kmsid,
                                           _mongocrypt_key_doc_t *key,
                                           _mongocrypt_log_t *log) {
    BSON_ASSERT_PARAM(kms_ctx);
    BSON_ASSERT_PARAM(endpoint);
    BSON_ASSERT_PARAM(key);
    bool ret = false;

    _init_common(kms_ctx, log, MONGOCRYPT_KMS_KMIP_DECRYPT, kmsid);
    mongocrypt_status_t *status = kms_ctx->status;
    kms_ctx->endpoint = bson_strdup(endpoint->host_and_port);
    _mongocrypt_apply_default_port(&kms_ctx->endpoint, DEFAULT_KMIP_PORT);

    _mongocrypt_buffer_t iv;
    if (!_mongocrypt_buffer_from_subrange(&iv, &key->key_material, 0, MONGOCRYPT_IV_LEN)) {
        CLIENT_ERR("Error getting IV from key material");
        goto done;
    }
    _mongocrypt_buffer_t ciphertext;
    if (!_mongocrypt_buffer_from_subrange(&ciphertext,
                                          &key->key_material,
                                          MONGOCRYPT_IV_LEN,
                                          key->key_material.len - MONGOCRYPT_IV_LEN)) {
        CLIENT_ERR("Error getting ciphertext from key material");
        goto done;
    }

    BSON_ASSERT(key->kek.kms_provider == MONGOCRYPT_KMS_PROVIDER_KMIP);
    BSON_ASSERT(key->kek.provider.kmip.delegated);
    kms_ctx->req = kms_kmip_request_decrypt_new(NULL /* reserved */,
                                                key->kek.provider.kmip.key_id,
                                                ciphertext.data,
                                                ciphertext.len,
                                                iv.data,
                                                iv.len);

    if (kms_request_get_error(kms_ctx->req)) {
        CLIENT_ERR("Error creating KMIP decrypt request: %s", kms_request_get_error(kms_ctx->req));
        goto done;
    }

    size_t reqlen;
    const uint8_t *reqdata = kms_request_to_bytes(kms_ctx->req, &reqlen);
    if (!_mongocrypt_buffer_copy_from_data_and_size(&kms_ctx->msg, reqdata, reqlen)) {
        CLIENT_ERR("Error storing KMS request payload");
        goto done;
    }

    ret = true;
done:
    return ret;
}

static const char *set_and_ret(const char *what, uint32_t *len) {
    BSON_ASSERT_PARAM(what);

    if (len) {
        BSON_ASSERT(size_to_uint32(strlen(what), len));
    }
    return what;
}

const char *mongocrypt_kms_ctx_get_kms_provider(mongocrypt_kms_ctx_t *kms, uint32_t *len) {
    BSON_ASSERT_PARAM(kms);
    /* len is checked in set_and_ret () before it is used */

    return set_and_ret(kms->kmsid, len);
}
