diff --git a/include/asterisk/res_stir_shaken.h b/include/asterisk/res_stir_shaken.h index 0c589a9b74b836050a3b1c0a07b341e39876f57d..16f0139ff9605ef610ebd6e6b5e0ee60a86de68e 100644 --- a/include/asterisk/res_stir_shaken.h +++ b/include/asterisk/res_stir_shaken.h @@ -21,6 +21,10 @@ #include <openssl/evp.h> #include <openssl/pem.h> +struct ast_stir_shaken_payload; + +struct ast_json; + /*! * \brief Retrieve the stir/shaken sorcery context * @@ -29,12 +33,15 @@ struct ast_sorcery *ast_stir_shaken_sorcery(void); /*! - * \brief Get the private key associated with a caller id - * - * \param caller_id_number The caller id used to look up the private key + * \brief Free a STIR/SHAKEN payload + */ +void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload); + +/*! + * \brief Sign a JSON STIR/SHAKEN payload * - * \retval The private key + * \note This function will automatically add the "attest", "iat", and "origid" fields. */ -EVP_PKEY *ast_stir_shaken_get_private_key(const char *caller_id_number); +struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json); #endif /* _RES_STIR_SHAKEN_H */ diff --git a/res/res_stir_shaken.c b/res/res_stir_shaken.c index a6656d07e91d895a5deedc4c8d31b2a07f3c5f39..cb4cc8290194410289ef50eb1dae0dcbe6da7f58 100644 --- a/res/res_stir_shaken.c +++ b/res/res_stir_shaken.c @@ -24,6 +24,8 @@ #include "asterisk/module.h" #include "asterisk/sorcery.h" +#include "asterisk/time.h" +#include "asterisk/json.h" #include "asterisk/res_stir_shaken.h" #include "res_stir_shaken/stir_shaken.h" @@ -31,16 +33,386 @@ #include "res_stir_shaken/store.h" #include "res_stir_shaken/certificate.h" +#define STIR_SHAKEN_ENCRYPTION_ALGORITHM "ES256" +#define STIR_SHAKEN_PPT "shaken" +#define STIR_SHAKEN_TYPE "passport" + static struct ast_sorcery *stir_shaken_sorcery; +struct ast_stir_shaken_payload { + /*! The JWT header */ + struct ast_json *header; + /*! The JWT payload */ + struct ast_json *payload; + /*! Signature for the payload */ + unsigned char *signature; + /*! The algorithm used */ + char *algorithm; + /*! THe URL to the public key for the certificate */ + char *public_key_url; +}; + struct ast_sorcery *ast_stir_shaken_sorcery(void) { return stir_shaken_sorcery; } -EVP_PKEY *ast_stir_shaken_get_private_key(const char *caller_id_number) +void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload) +{ + if (!payload) { + return; + } + + ast_json_unref(payload->header); + ast_json_unref(payload->payload); + ast_free(payload->algorithm); + ast_free(payload->public_key_url); + ast_free(payload->signature); + + ast_free(payload); +} + +/*! + * \brief Verifies the necessary contents are in the JSON and returns a + * ast_stir_shaken_payload with the extracted values. + * + * \param json The JSON to verify + * + * \return ast_stir_shaken_payload on success + * \return NULL on failure + */ +static struct ast_stir_shaken_payload *stir_shaken_verify_json(struct ast_json *json) +{ + struct ast_stir_shaken_payload *payload; + struct ast_json *obj; + const char *val; + + payload = ast_calloc(1, sizeof(*payload)); + if (!payload) { + ast_log(LOG_ERROR, "Failed to allocate STIR_SHAKEN payload\n"); + goto cleanup; + } + + /* Look through the header first */ + obj = ast_json_object_get(json, "header"); + if (!obj) { + ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have the required field 'header'\n"); + goto cleanup; + } + + payload->header = ast_json_deep_copy(obj); + if (!payload->header) { + ast_log(LOG_ERROR, "STIR_SHAKEN payload failed to copy 'header'\n"); + goto cleanup; + } + + /* Check the ppt value for "shaken" */ + val = ast_json_string_get(ast_json_object_get(obj, "ppt")); + if (ast_strlen_zero(val)) { + ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have the required field 'ppt'\n"); + goto cleanup; + } + if (strcmp(val, STIR_SHAKEN_PPT)) { + ast_log(LOG_ERROR, "STIR/SHAKEN JWT field 'ppt' did not have " + "required value '%s' (was '%s')\n", STIR_SHAKEN_PPT, val); + goto cleanup; + } + + /* Check the typ value for "passport" */ + val = ast_json_string_get(ast_json_object_get(obj, "typ")); + if (ast_strlen_zero(val)) { + ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have the required field 'typ'\n"); + goto cleanup; + } + if (strcmp(val, STIR_SHAKEN_TYPE)) { + ast_log(LOG_ERROR, "STIR/SHAKEN JWT field 'typ' did not have " + "required value '%s' (was '%s')\n", STIR_SHAKEN_TYPE, val); + goto cleanup; + } + + /* Check the alg value for "ES256" */ + val = ast_json_string_get(ast_json_object_get(obj, "alg")); + if (ast_strlen_zero(val)) { + ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have required field 'alg'\n"); + goto cleanup; + } + if (strcmp(val, STIR_SHAKEN_ENCRYPTION_ALGORITHM)) { + ast_log(LOG_ERROR, "STIR/SHAKEN JWT field 'alg' did not have " + "required value '%s' (was '%s')\n", STIR_SHAKEN_ENCRYPTION_ALGORITHM, val); + goto cleanup; + } + + payload->algorithm = ast_strdup(val); + if (!payload->algorithm) { + ast_log(LOG_ERROR, "STIR/SHAKEN payload failed to copy 'algorithm'\n"); + goto cleanup; + } + + /* Now let's check the payload section */ + obj = ast_json_object_get(json, "payload"); + if (!obj) { + ast_log(LOG_ERROR, "STIR/SHAKEN payload JWT did not have required field 'payload'\n"); + goto cleanup; + } + + /* Check the orig tn value for not NULL */ + val = ast_json_string_get(ast_json_object_get(ast_json_object_get(obj, "orig"), "tn")); + if (ast_strlen_zero(val)) { + ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have required field 'orig->tn'\n"); + goto cleanup; + } + + /* Payload seems sane. Copy it and return on success */ + payload->payload = ast_json_deep_copy(obj); + if (!payload->payload) { + ast_log(LOG_ERROR, "STIR/SHAKEN payload failed to copy 'payload'\n"); + goto cleanup; + } + + return payload; + +cleanup: + ast_stir_shaken_payload_free(payload); + return NULL; +} + +/*! + * \brief Signs the payload and returns the signature. + * + * \param json_str The string representation of the JSON + * \param private_key The private key used to sign the payload + * + * \retval signature on success + * \retval NULL on failure + */ +static unsigned char *stir_shaken_sign(char *json_str, EVP_PKEY *private_key) { - return stir_shaken_certificate_get_private_key(caller_id_number); + EVP_MD_CTX *mdctx = NULL; + int ret = 0; + unsigned char *encoded_signature = NULL; + unsigned char *signature = NULL; + size_t encoded_length = 0; + size_t signature_length = 0; + + mdctx = EVP_MD_CTX_create(); + if (!mdctx) { + ast_log(LOG_ERROR, "Failed to create Message Digest Context\n"); + goto cleanup; + } + + ret = EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, private_key); + if (ret != 1) { + ast_log(LOG_ERROR, "Failed to initialize Message Digest Context\n"); + goto cleanup; + } + + ret = EVP_DigestSignUpdate(mdctx, json_str, strlen(json_str)); + if (ret != 1) { + ast_log(LOG_ERROR, "Failed to update Message Digest Context\n"); + goto cleanup; + } + + ret = EVP_DigestSignFinal(mdctx, NULL, &signature_length); + if (ret != 1) { + ast_log(LOG_ERROR, "Failed initial phase of Message Digest Context signing\n"); + goto cleanup; + } + + signature = ast_calloc(1, sizeof(unsigned char) * signature_length); + if (!signature) { + ast_log(LOG_ERROR, "Failed to allocate space for signature\n"); + goto cleanup; + } + + ret = EVP_DigestSignFinal(mdctx, signature, &signature_length); + if (ret != 1) { + ast_log(LOG_ERROR, "Failed final phase of Message Digest Context signing\n"); + goto cleanup; + } + + /* There are 6 bits to 1 base64 digit, so in order to get the size of the base64 encoded + * signature, we need to multiply by the number of bits in a byte and divide by 6. Since + * there's rounding when doing base64 conversions, add 3 bytes, just in case, and account + * for padding. Add another byte for the NULL-terminator so we don't lose data. + */ + encoded_length = ((signature_length * 4 / 3 + 3) & ~3) + 1; + encoded_signature = ast_calloc(1, encoded_length); + if (!encoded_signature) { + ast_log(LOG_ERROR, "Failed to allocate space for encoded signature\n"); + goto cleanup; + } + + ast_base64encode((char *)encoded_signature, signature, signature_length, encoded_length); + +cleanup: + if (mdctx) { + EVP_MD_CTX_destroy(mdctx); + } + ast_free(signature); + + return encoded_signature; +} + +/*! + * \brief Adds the 'x5u' (public key URL) field to the JWT. + * + * \param json The JWT + * \param x5u The public key URL + * + * \retval 0 on success + * \retval -1 on failure + */ +static int stir_shaken_add_x5u(struct ast_json *json, const char *x5u) +{ + struct ast_json *value; + + value = ast_json_string_create(x5u); + if (!value) { + return -1; + } + + return ast_json_object_set(ast_json_object_get(json, "header"), "x5u", value); +} + +/*! + * \brief Adds the 'attest' field to the JWT. + * + * \param json The JWT + * \param attest The value to set attest to + * + * \retval 0 on success + * \retval -1 on failure + */ +static int stir_shaken_add_attest(struct ast_json *json, const char *attest) +{ + struct ast_json *value; + + value = ast_json_string_create(attest); + if (!value) { + return -1; + } + + return ast_json_object_set(ast_json_object_get(json, "payload"), "attest", value); +} + +/*! + * \brief Adds the 'origid' field to the JWT. + * + * \param json The JWT + * \param origid The value to set origid to + * + * \retval 0 on success + * \retval -1 on failure + */ +static int stir_shaken_add_origid(struct ast_json *json, const char *origid) +{ + struct ast_json *value; + + value = ast_json_string_create(origid); + if (!origid) { + return -1; + } + + return ast_json_object_set(ast_json_object_get(json, "payload"), "origid", value); +} + +/*! + * \brief Adds the 'iat' field to the JWT. + * + * \param json The JWT + * + * \retval 0 on success + * \retval -1 on failure + */ +static int stir_shaken_add_iat(struct ast_json *json) +{ + struct ast_json *value; + struct timeval tv; + int timestamp; + + tv = ast_tvnow(); + timestamp = tv.tv_sec + tv.tv_usec / 1000; + value = ast_json_integer_create(timestamp); + + return ast_json_object_set(ast_json_object_get(json, "payload"), "iat", value); +} + +struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json) +{ + struct ast_stir_shaken_payload *payload; + unsigned char *signature; + const char *caller_id_num; + char *json_str = NULL; + struct stir_shaken_certificate *cert = NULL; + + payload = stir_shaken_verify_json(json); + if (!payload) { + return NULL; + } + + /* From the payload section of the JSON, get the orig section, and then get + * the value of tn. This will be the caller ID number */ + caller_id_num = ast_json_string_get(ast_json_object_get(ast_json_object_get( + ast_json_object_get(json, "payload"), "orig"), "tn")); + if (!caller_id_num) { + ast_log(LOG_ERROR, "Failed to get caller ID number from JWT\n"); + goto cleanup; + } + + cert = stir_shaken_certificate_get_by_caller_id_number(caller_id_num); + if (!cert) { + ast_log(LOG_ERROR, "Failed to retrieve certificate for caller ID " + "'%s'\n", caller_id_num); + goto cleanup; + } + + if (stir_shaken_add_x5u(json, stir_shaken_certificate_get_public_key_url(cert))) { + ast_log(LOG_ERROR, "Failed to add 'x5u' (public key URL) to payload\n"); + goto cleanup; + } + + /* TODO: This is just a placeholder for adding 'attest', 'iat', and + * 'origid' to the payload. Later, additional logic will need to be + * added to determine what these values actually are, but the functions + * themselves are ready to go. + */ + if (stir_shaken_add_attest(json, "B")) { + ast_log(LOG_ERROR, "Failed to add 'attest' to payload\n"); + goto cleanup; + } + + if (stir_shaken_add_origid(json, "asterisk")) { + ast_log(LOG_ERROR, "Failed to add 'origid' to payload\n"); + goto cleanup; + } + + if (stir_shaken_add_iat(json)) { + ast_log(LOG_ERROR, "Failed to add 'iat' to payload\n"); + goto cleanup; + } + + json_str = ast_json_dump_string(json); + if (!json_str) { + ast_log(LOG_ERROR, "Failed to convert JSON to string\n"); + goto cleanup; + } + + signature = stir_shaken_sign(json_str, stir_shaken_certificate_get_private_key(cert)); + if (!signature) { + goto cleanup; + } + + payload->signature = signature; + ao2_cleanup(cert); + ast_json_free(json_str); + + return payload; + +cleanup: + ao2_cleanup(cert); + ast_stir_shaken_payload_free(payload); + ast_json_free(json_str); + return NULL; } static int reload_module(void) diff --git a/res/res_stir_shaken/certificate.c b/res/res_stir_shaken/certificate.c index 799cea13af2d8f76eac8efedd0f935e14861c0a7..812fc1e6d9a21545ce844675d85391eda697fdff 100644 --- a/res/res_stir_shaken/certificate.c +++ b/res/res_stir_shaken/certificate.c @@ -79,23 +79,34 @@ static void *stir_shaken_certificate_alloc(const char *name) return cfg; } -EVP_PKEY *stir_shaken_certificate_get_private_key(const char *caller_id_number) +struct stir_shaken_certificate *stir_shaken_certificate_get_by_caller_id_number(const char *caller_id_number) { - struct stir_shaken_certificate *cert; struct ast_variable fields = { .name = "caller_id_number", .value = caller_id_number, .next = NULL, }; - cert = ast_sorcery_retrieve_by_fields(ast_stir_shaken_sorcery(), + return ast_sorcery_retrieve_by_fields(ast_stir_shaken_sorcery(), "certificate", AST_RETRIEVE_FLAG_DEFAULT, &fields); +} + +const char *stir_shaken_certificate_get_public_key_url(struct stir_shaken_certificate *cert) +{ + if (!cert) { + return NULL; + } - if (cert) { - return cert->private_key; + return cert->public_key_url; +} + +EVP_PKEY *stir_shaken_certificate_get_private_key(struct stir_shaken_certificate *cert) +{ + if (!cert) { + return NULL; } - return NULL; + return cert->private_key; } static int stir_shaken_certificate_apply(const struct ast_sorcery *sorcery, void *obj) diff --git a/res/res_stir_shaken/certificate.h b/res/res_stir_shaken/certificate.h index 9d6ec7379c3d913d5ecfc79fe2c68a1685f28d1a..fda3bf1a019ba1658a4ec333d193f452a31c72fd 100644 --- a/res/res_stir_shaken/certificate.h +++ b/res/res_stir_shaken/certificate.h @@ -22,15 +22,29 @@ struct ast_sorcery; +struct stir_shaken_certificate; + +struct stir_shaken_certificate *stir_shaken_certificate_get_by_caller_id_number(const char *caller_id_number); + +/*! + * \brief Get the public key URL associated with a certificate + * + * \param cert The certificate to get the public key URL from + * + * \retval NULL on failure + * \retval The public key URL on success + */ +const char *stir_shaken_certificate_get_public_key_url(struct stir_shaken_certificate *cert); + /*! - * \brief Get the private key associated with a caller id + * \brief Get the private key associated with a certificate * - * \param caller_id_number The caller id used to look up the private key + * \param cert The certificate to get the private key from * * \retval NULL on failure * \retval The private key on success */ -EVP_PKEY *stir_shaken_certificate_get_private_key(const char *caller_id_number); +EVP_PKEY *stir_shaken_certificate_get_private_key(struct stir_shaken_certificate *cert); /*! * \brief Load time initialization for the stir/shaken 'certificate' configuration