diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h index fd80581003..363c9a424f 100644 --- a/include/asterisk/res_pjsip.h +++ b/include/asterisk/res_pjsip.h @@ -2218,6 +2218,19 @@ int ast_sip_create_request_with_auth(const struct ast_sip_auth_vector *auths, pj */ struct ast_sip_endpoint *ast_sip_identify_endpoint(pjsip_rx_data *rdata); +/*! + * \brief Get a specific header value from rdata + * + * \note The returned value does not need to be freed since it's from the rdata pool + * + * \param rdata The rdata + * \param str The header to find + * + * \retval NULL on failure + * \retval The header value on success + */ +char *ast_sip_rdata_get_header_value(pjsip_rx_data *rdata, const pj_str_t str); + /*! * \brief Set the outbound proxy for an outbound SIP message * diff --git a/include/asterisk/res_stir_shaken.h b/include/asterisk/res_stir_shaken.h index 48bfa00085..997054d4d7 100644 --- a/include/asterisk/res_stir_shaken.h +++ b/include/asterisk/res_stir_shaken.h @@ -32,6 +32,13 @@ struct ast_stir_shaken_payload; struct ast_json; +/*! + * \brief Retrieve the value for 'signature_timeout' from 'general' config object + * + * \retval The signature timeout + */ +unsigned int ast_stir_shaken_get_signature_timeout(void); + /*! * \brief Add a STIR/SHAKEN verification result to a channel * diff --git a/include/asterisk/utils.h b/include/asterisk/utils.h index 10dfe83c95..da14eb6e70 100644 --- a/include/asterisk/utils.h +++ b/include/asterisk/utils.h @@ -250,6 +250,19 @@ int ast_base64encode(char *dst, const unsigned char *src, int srclen, int max); */ int ast_base64decode(unsigned char *dst, const char *src, int max); +/*! + * \brief Same as ast_base64decode, but does the math for you and returns + * a decoded string + * + * \note The returned string will need to be freed later + * + * \param src The source buffer + * + * \retval NULL on failure + * \retval Decoded string on success + */ +char *ast_base64decode_string(const char *src); + #define AST_URI_ALPHANUM (1 << 0) #define AST_URI_MARK (1 << 1) #define AST_URI_UNRESERVED (AST_URI_ALPHANUM | AST_URI_MARK) diff --git a/main/utils.c b/main/utils.c index b45e7b5179..59880fde62 100644 --- a/main/utils.c +++ b/main/utils.c @@ -310,6 +310,37 @@ int ast_base64decode(unsigned char *dst, const char *src, int max) return cnt; } +/*! \brief Decode BASE64 encoded text and return the string */ +char *ast_base64decode_string(const char *src) +{ + size_t encoded_len; + size_t decoded_len; + int padding = 0; + unsigned char *decoded_string; + + if (ast_strlen_zero(src)) { + return NULL; + } + + encoded_len = strlen(src); + if (encoded_len > 2 && src[encoded_len - 1] == '=') { + padding++; + if (src[encoded_len - 2] == '=') { + padding++; + } + } + + decoded_len = (encoded_len / 4 * 3) - padding; + decoded_string = ast_calloc(1, decoded_len); + if (!decoded_string) { + return NULL; + } + + ast_base64decode(decoded_string, src, decoded_len); + + return (char *)decoded_string; +} + /*! \brief encode text to BASE64 coding */ int ast_base64encode_full(char *dst, const unsigned char *src, int srclen, int max, int linebreaks) { diff --git a/res/res_pjsip.c b/res/res_pjsip.c index 659c631f69..3820243c08 100644 --- a/res/res_pjsip.c +++ b/res/res_pjsip.c @@ -3183,6 +3183,21 @@ struct ast_sip_endpoint *ast_sip_identify_endpoint(pjsip_rx_data *rdata) return endpoint; } +char *ast_sip_rdata_get_header_value(pjsip_rx_data *rdata, const pj_str_t str) +{ + pjsip_generic_string_hdr *hdr; + pj_str_t hdr_val; + + hdr = pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, &str, NULL); + if (!hdr) { + return NULL; + } + + pj_strdup_with_null(rdata->tp_info.pool, &hdr_val, &hdr->hvalue); + + return hdr_val.ptr; +} + static int do_cli_dump_endpt(void *v_a) { struct ast_cli_args *a = v_a; diff --git a/res/res_pjsip_stir_shaken.c b/res/res_pjsip_stir_shaken.c index 702383c5a6..68665988db 100644 --- a/res/res_pjsip_stir_shaken.c +++ b/res/res_pjsip_stir_shaken.c @@ -3,7 +3,7 @@ * * Copyright (C) 2020, Sangoma Technologies Corporation * - * Kevin Harwell + * Ben Ford * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact @@ -18,22 +18,197 @@ /*** MODULEINFO crypto + pjproject + res_pjsip + res_pjsip_session + res_stir_shaken core ***/ #include "asterisk.h" +#include "asterisk/res_pjsip.h" +#include "asterisk/res_pjsip_session.h" #include "asterisk/module.h" #include "asterisk/res_stir_shaken.h" +/*! + * \brief Get the attestation from the payload + * + * \param json_str The JSON string representation of the payload + * + * \retval Empty string on failure + * \retval The attestation on success + */ +static char *get_attestation_from_payload(const char *json_str) +{ + RAII_VAR(struct ast_json *, json, NULL, ast_json_free); + char *attestation; + + json = ast_json_load_string(json_str, NULL); + attestation = (char *)ast_json_string_get(ast_json_object_get(json, "attest")); + + if (!ast_strlen_zero(attestation)) { + return attestation; + } + + return ""; +} + +/*! + * \brief Compare the caller ID from the INVITE with the one in the payload + * + * \param json_str The JSON string represntation of the payload + * + * \retval -1 on failure + * \retval 0 on success + */ +static int compare_caller_id(char *caller_id, const char *json_str) +{ + RAII_VAR(struct ast_json *, json, NULL, ast_json_free); + char *caller_id_other; + + json = ast_json_load_string(json_str, NULL); + caller_id_other = (char *)ast_json_string_get(ast_json_object_get( + ast_json_object_get(json, "orig"), "tn")); + + if (strcmp(caller_id, caller_id_other)) { + return -1; + } + + return 0; +} + +/*! + * \brief Compare the current timestamp with the one in the payload. If the difference + * is greater than the signature timeout, it's not valid anymore + * + * \param json_str The JSON string representation of the payload + * + * \retval -1 on failure + * \retval 0 on success + */ +static int compare_timestamp(const char *json_str) +{ + RAII_VAR(struct ast_json *, json, NULL, ast_json_free); + long int timestamp; + struct timeval now = ast_tvnow(); + + json = ast_json_load_string(json_str, NULL); + timestamp = ast_json_integer_get(ast_json_object_get(json, "iat")); + + if (now.tv_sec - timestamp > ast_stir_shaken_get_signature_timeout()) { + return -1; + } + + return 0; +} + +/*! + * \internal + * \brief Session supplement callback on an incoming INVITE request + * + * When we receive an INVITE, check it for STIR/SHAKEN information and + * decide what to do from there + * + * \param session The session that has received an INVITE + * \param rdata The incoming INVITE + */ +static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_rx_data *rdata) +{ + static const pj_str_t identity_str = { "Identity", 8 }; + char *identity_hdr_val; + char *encoded_val; + struct ast_channel *chan = session->channel; + char *caller_id = session->id.number.str; + RAII_VAR(char *, header, NULL, ast_free); + RAII_VAR(char *, payload, NULL, ast_free); + char *signature; + char *algorithm; + char *public_key_url; + char *attestation; + int mismatch = 0; + struct ast_stir_shaken_payload *ss_payload; + + identity_hdr_val = ast_sip_rdata_get_header_value(rdata, identity_str); + if (ast_strlen_zero(identity_hdr_val)) { + ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_NOT_PRESENT); + return 0; + } + + encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val); + header = ast_base64decode_string(encoded_val); + if (ast_strlen_zero(header)) { + ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED); + return 0; + } + + encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val); + payload = ast_base64decode_string(encoded_val); + if (ast_strlen_zero(payload)) { + ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED); + return 0; + } + + /* It's fine to leave the signature encoded */ + signature = strtok_r(identity_hdr_val, ";", &identity_hdr_val); + if (ast_strlen_zero(signature)) { + ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED); + return 0; + } + + /* Trim "info=<" to get public key URL */ + strtok_r(identity_hdr_val, "<", &identity_hdr_val); + public_key_url = strtok_r(identity_hdr_val, ">", &identity_hdr_val); + if (ast_strlen_zero(public_key_url)) { + ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED); + return 0; + } + + algorithm = strtok_r(identity_hdr_val, ";", &identity_hdr_val); + if (ast_strlen_zero(algorithm)) { + ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED); + return 0; + } + + attestation = get_attestation_from_payload(payload); + + ss_payload = ast_stir_shaken_verify(header, payload, signature, algorithm, public_key_url); + if (!ss_payload) { + ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED); + return 0; + } + ast_stir_shaken_payload_free(ss_payload); + + mismatch |= compare_caller_id(caller_id, payload); + mismatch |= compare_timestamp(payload); + + if (mismatch) { + ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_MISMATCH); + return 0; + } + + ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_PASSED); + + return 0; +} + +static struct ast_sip_session_supplement stir_shaken_supplement = { + .method = "INVITE", + .priority = AST_SIP_SUPPLEMENT_PRIORITY_CHANNEL + 1, /* Run AFTER channel creation */ + .incoming_request = stir_shaken_incoming_request, +}; + static int unload_module(void) { + ast_sip_session_unregister_supplement(&stir_shaken_supplement); return 0; } static int load_module(void) { + ast_sip_session_register_supplement(&stir_shaken_supplement); return AST_MODULE_LOAD_SUCCESS; } diff --git a/res/res_stir_shaken.c b/res/res_stir_shaken.c index 86117cdb65..5183c7e957 100644 --- a/res/res_stir_shaken.c +++ b/res/res_stir_shaken.c @@ -65,6 +65,9 @@ Maximum time to wait to CURL certificates + + Amount of time a signature is valid for + STIR/SHAKEN certificate store options @@ -181,6 +184,11 @@ void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload) ast_free(payload); } +unsigned int ast_stir_shaken_get_signature_timeout(void) +{ + return ast_stir_shaken_signature_timeout(stir_shaken_general_get()); +} + /*! * \brief Convert an ast_stir_shaken_verification_result to string representation * @@ -270,8 +278,8 @@ int ast_stir_shaken_add_verification(struct ast_channel *chan, const char *ident return -1; } - if (ast_strlen_zero(attestation)) { - ast_log(LOG_ERROR, "No attestation to add STIR/SHAKEN verification to " + if (!attestation) { + ast_log(LOG_ERROR, "Attestation cannot be NULL to add STIR/SHAKEN verification to " "channel %s\n", chan_name); return -1; } @@ -593,8 +601,9 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const EVP_PKEY *public_key; char *filename; int curl = 0; - struct ast_json_error err; RAII_VAR(char *, file_path, NULL, ast_free); + RAII_VAR(char *, combined_str, NULL, ast_free); + size_t combined_size; if (ast_strlen_zero(header)) { ast_log(LOG_ERROR, "'header' is required for STIR/SHAKEN verification\n"); @@ -697,7 +706,16 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const } } - if (stir_shaken_verify_signature(payload, signature, public_key)) { + /* Combine the header and payload to get the original signed message: header.payload */ + combined_size = strlen(header) + strlen(payload) + 2; + combined_str = ast_calloc(1, combined_size); + if (!combined_str) { + ast_log(LOG_ERROR, "Failed to allocate space for message to verify\n"); + EVP_PKEY_free(public_key); + return NULL; + } + snprintf(combined_str, combined_size, "%s.%s", header, payload); + if (stir_shaken_verify_signature(combined_str, signature, public_key)) { ast_log(LOG_ERROR, "Failed to verify signature\n"); EVP_PKEY_free(public_key); return NULL; @@ -712,14 +730,14 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const return NULL; } - ret_payload->header = ast_json_load_string(header, &err); + ret_payload->header = ast_json_load_string(header, NULL); if (!ret_payload->header) { ast_log(LOG_ERROR, "Failed to create JSON from header\n"); ast_stir_shaken_payload_free(ret_payload); return NULL; } - ret_payload->payload = ast_json_load_string(payload, &err); + ret_payload->payload = ast_json_load_string(payload, NULL); if (!ret_payload->payload) { ast_log(LOG_ERROR, "Failed to create JSON from payload\n"); ast_stir_shaken_payload_free(ret_payload); @@ -1000,14 +1018,18 @@ static int stir_shaken_add_iat(struct ast_json *json) struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json) { - struct ast_stir_shaken_payload *payload; + struct ast_stir_shaken_payload *ss_payload; unsigned char *signature; const char *caller_id_num; - char *json_str = NULL; + const char *header; + const char *payload; + struct ast_json *tmp_json; + char *msg = NULL; + size_t msg_len; struct stir_shaken_certificate *cert = NULL; - payload = stir_shaken_verify_json(json); - if (!payload) { + ss_payload = stir_shaken_verify_json(json); + if (!ss_payload) { return NULL; } @@ -1052,27 +1074,34 @@ struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json) goto cleanup; } - json_str = ast_json_dump_string(json); - if (!json_str) { - ast_log(LOG_ERROR, "Failed to convert JSON to string\n"); + /* Get the header and the payload. Combine them to get the message to sign */ + tmp_json = ast_json_object_get(json, "header"); + header = ast_json_dump_string(tmp_json); + tmp_json = ast_json_object_get(json, "payload"); + payload = ast_json_dump_string(tmp_json); + msg_len = strlen(header) + strlen(payload) + 2; + msg = ast_calloc(1, msg_len); + if (!msg) { + ast_log(LOG_ERROR, "Failed to allocate space for message to sign\n"); goto cleanup; } + snprintf(msg, msg_len, "%s.%s", header, payload); - signature = stir_shaken_sign(json_str, stir_shaken_certificate_get_private_key(cert)); + signature = stir_shaken_sign(msg, stir_shaken_certificate_get_private_key(cert)); if (!signature) { goto cleanup; } - payload->signature = signature; + ss_payload->signature = signature; ao2_cleanup(cert); - ast_json_free(json_str); + ast_free(msg); - return payload; + return ss_payload; cleanup: ao2_cleanup(cert); - ast_stir_shaken_payload_free(payload); - ast_json_free(json_str); + ast_stir_shaken_payload_free(ss_payload); + ast_free(msg); return NULL; } @@ -1424,12 +1453,13 @@ AST_TEST_DEFINE(test_stir_shaken_verify) { char *caller_id_number = "1234567"; char *public_key_url = "http://testing123"; - char *header = "{\"header\": \"placeholder\"}"; + char *header; + char *payload; + struct ast_json *tmp_json; char public_path[] = "/tmp/stir_shaken_public.XXXXXX"; char private_path[] = "/tmp/stir_shaken_public.XXXXXX"; RAII_VAR(char *, rm_on_exit_public, public_path, unlink); RAII_VAR(char *, rm_on_exit_private, private_path, unlink); - RAII_VAR(char *, json_str, NULL, ast_json_free); RAII_VAR(struct ast_json *, json, NULL, ast_json_free); RAII_VAR(struct ast_stir_shaken_payload *, signed_payload, NULL, ast_stir_shaken_payload_free); RAII_VAR(struct ast_stir_shaken_payload *, returned_payload, NULL, ast_stir_shaken_payload_free); @@ -1463,16 +1493,14 @@ AST_TEST_DEFINE(test_stir_shaken_verify) return AST_TEST_FAIL; } - /* Get the message to use for verification */ - json_str = ast_json_dump_string(json); - if (!json_str) { - ast_test_status_update(test, "Failed to create string from JSON\n"); - test_stir_shaken_cleanup_cert(caller_id_number); - return AST_TEST_FAIL; - } + /* Get the header and payload for ast_stir_shaken_verify */ + tmp_json = ast_json_object_get(json, "header"); + header = ast_json_dump_string(tmp_json); + tmp_json = ast_json_object_get(json, "payload"); + payload = ast_json_dump_string(tmp_json); /* Test empty header parameter */ - returned_payload = ast_stir_shaken_verify("", json_str, (const char *)signed_payload->signature, + returned_payload = ast_stir_shaken_verify("", payload, (const char *)signed_payload->signature, STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url); if (returned_payload) { ast_test_status_update(test, "Verified a signature with missing 'header'\n"); @@ -1490,7 +1518,7 @@ AST_TEST_DEFINE(test_stir_shaken_verify) } /* Test empty signature parameter */ - returned_payload = ast_stir_shaken_verify(header, json_str, "", + returned_payload = ast_stir_shaken_verify(header, payload, "", STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url); if (returned_payload) { ast_test_status_update(test, "Verified a signature with missing 'signature'\n"); @@ -1499,7 +1527,7 @@ AST_TEST_DEFINE(test_stir_shaken_verify) } /* Test empty algorithm parameter */ - returned_payload = ast_stir_shaken_verify(header, json_str, (const char *)signed_payload->signature, + returned_payload = ast_stir_shaken_verify(header, payload, (const char *)signed_payload->signature, "", public_key_url); if (returned_payload) { ast_test_status_update(test, "Verified a signature with missing 'algorithm'\n"); @@ -1508,7 +1536,7 @@ AST_TEST_DEFINE(test_stir_shaken_verify) } /* Test empty public key URL */ - returned_payload = ast_stir_shaken_verify(header, json_str, (const char *)signed_payload->signature, + returned_payload = ast_stir_shaken_verify(header, payload, (const char *)signed_payload->signature, STIR_SHAKEN_ENCRYPTION_ALGORITHM, ""); if (returned_payload) { ast_test_status_update(test, "Verified a signature with missing 'public key URL'\n"); @@ -1520,7 +1548,7 @@ AST_TEST_DEFINE(test_stir_shaken_verify) test_stir_shaken_add_fake_astdb_entry(public_key_url, public_path); /* Verify a valid signature */ - returned_payload = ast_stir_shaken_verify(header, json_str, (const char *)signed_payload->signature, + returned_payload = ast_stir_shaken_verify(header, payload, (const char *)signed_payload->signature, STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url); if (!returned_payload) { ast_test_status_update(test, "Failed to verify a valid signature\n"); diff --git a/res/res_stir_shaken.exports.in b/res/res_stir_shaken.exports.in new file mode 100644 index 0000000000..10d214f914 --- /dev/null +++ b/res/res_stir_shaken.exports.in @@ -0,0 +1,6 @@ +{ + global: + LINKER_SYMBOL_PREFIXast_stir_*; + local: + *; +}; diff --git a/res/res_stir_shaken/general.c b/res/res_stir_shaken/general.c index edf8f85dd0..d241082411 100644 --- a/res/res_stir_shaken/general.c +++ b/res/res_stir_shaken/general.c @@ -31,6 +31,7 @@ #define DEFAULT_CA_PATH "" #define DEFAULT_CACHE_MAX_SIZE 1000 #define DEFAULT_CURL_TIMEOUT 2 +#define DEFAULT_SIGNATURE_TIMEOUT 15 struct stir_shaken_general { SORCERY_OBJECT(details); @@ -44,6 +45,8 @@ struct stir_shaken_general { unsigned int cache_max_size; /*! Maximum time to wait to CURL certificates */ unsigned int curl_timeout; + /*! Amount of time a signature is valid for */ + unsigned int signature_timeout; }; static struct stir_shaken_general *default_config = NULL; @@ -86,6 +89,11 @@ unsigned int ast_stir_shaken_curl_timeout(const struct stir_shaken_general *cfg) return cfg ? cfg->curl_timeout : DEFAULT_CURL_TIMEOUT; } +unsigned int ast_stir_shaken_signature_timeout(const struct stir_shaken_general *cfg) +{ + return cfg ? cfg->signature_timeout : DEFAULT_SIGNATURE_TIMEOUT; +} + static void stir_shaken_general_destructor(void *obj) { struct stir_shaken_general *cfg = obj; @@ -261,6 +269,9 @@ int stir_shaken_general_load(void) ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "curl_timeout", __stringify(DEFAULT_CURL_TIMEOUT), OPT_UINT_T, 0, FLDSET(struct stir_shaken_general, curl_timeout)); + ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "signature_timeout", + __stringify(DEFAULT_SIGNATURE_TIMEOUT), OPT_UINT_T, 0, + FLDSET(struct stir_shaken_general, signature_timeout)); if (ast_sorcery_instance_observer_add(sorcery, &stir_shaken_general_observer)) { ast_log(LOG_ERROR, "stir/shaken - failed to register loaded observer for '%s' " diff --git a/res/res_stir_shaken/general.h b/res/res_stir_shaken/general.h index 357933b82a..3ea1d693f4 100644 --- a/res/res_stir_shaken/general.h +++ b/res/res_stir_shaken/general.h @@ -83,6 +83,17 @@ unsigned int ast_stir_shaken_cache_max_size(const struct stir_shaken_general *cf */ unsigned int ast_stir_shaken_curl_timeout(const struct stir_shaken_general *cfg); +/*! + * \brief Retrieve the 'signature_timeout' general configuration option value + * + * \note if a NULL configuration is given, then the default value is returned + * + * \param cfg A 'general' configuration object + * + * \retval The 'signature_timeout' value + */ +unsigned int ast_stir_shaken_signature_timeout(const struct stir_shaken_general *cfg); + /*! * \brief Load time initialization for the stir/shaken 'general' configuration *