diff --git a/doc/UPGRADE-staging/stir_shaken_option_split.txt b/doc/UPGRADE-staging/stir_shaken_option_split.txt new file mode 100644 index 0000000000000000000000000000000000000000..79df214a8bb393e20e40486cee4d3defff40378f --- /dev/null +++ b/doc/UPGRADE-staging/stir_shaken_option_split.txt @@ -0,0 +1,7 @@ +Subject: STIR/SHAKEN + +The STIR/SHAKEN configuration option has been split into +4 different choices: off, attest, verify, and on. Off and +on behave the same way as before. Attest will only perform +attestation on the endpoint, and verify will only perform +verification on the endpoint. diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h index 7b9dcbd51d0f742af344ec5509fe726ffd19f1f5..2a20e36935c21ce657faa64939ad937e3c772cfe 100644 --- a/include/asterisk/res_pjsip.h +++ b/include/asterisk/res_pjsip.h @@ -63,6 +63,22 @@ #define PJSIP_EXPIRES_NOT_SPECIFIED ((pj_uint32_t)-1) #endif +/* Response codes from RFC8224 */ +#define AST_STIR_SHAKEN_RESPONSE_CODE_STALE_DATE 403 +#define AST_STIR_SHAKEN_RESPONSE_CODE_USE_IDENTITY_HEADER 428 +#define AST_STIR_SHAKEN_RESPONSE_CODE_USE_SUPPORTED_PASSPORT_FORMAT 428 +#define AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO 436 +#define AST_STIR_SHAKEN_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL 437 +#define AST_STIR_SHAKEN_RESPONSE_CODE_INVALID_IDENTITY_HEADER 438 + +/* Response strings from RFC8224 */ +#define AST_STIR_SHAKEN_RESPONSE_STR_STALE_DATE "Stale Date" +#define AST_STIR_SHAKEN_RESPONSE_STR_USE_IDENTITY_HEADER "Use Identity Header" +#define AST_STIR_SHAKEN_RESPONSE_STR_USE_SUPPORTED_PASSPORT_FORMAT "Use Supported PASSporT Format" +#define AST_STIR_SHAKEN_RESPONSE_STR_BAD_IDENTITY_INFO "Bad Identity Info" +#define AST_STIR_SHAKEN_RESPONSE_STR_UNSUPPORTED_CREDENTIAL "Unsupported Credential" +#define AST_STIR_SHAKEN_RESPONSE_STR_INVALID_IDENTITY_HEADER "Invalid Identity Header" + /* Forward declarations of PJSIP stuff */ struct pjsip_rx_data; struct pjsip_module; @@ -527,6 +543,17 @@ enum ast_sip_session_redirect { AST_SIP_REDIRECT_URI_PJSIP, }; +enum ast_sip_stir_shaken_behavior { + /*! Don't do any STIR/SHAKEN operations */ + AST_SIP_STIR_SHAKEN_OFF = 0, + /*! Only do STIR/SHAKEN attestation */ + AST_SIP_STIR_SHAKEN_ATTEST = 1, + /*! Only do STIR/SHAKEN verification */ + AST_SIP_STIR_SHAKEN_VERIFY = 2, + /*! Do STIR/SHAKEN attestation and verification */ + AST_SIP_STIR_SHAKEN_ON = 3, +}; + /*! * \brief Incoming/Outgoing call offer/answer joint codec preference. * @@ -917,7 +944,7 @@ struct ast_sip_endpoint { unsigned int suppress_q850_reason_headers; /*! Ignore 183 if no SDP is present */ unsigned int ignore_183_without_sdp; - /*! Enable STIR/SHAKEN support on this endpoint */ + /*! Set which STIR/SHAKEN behaviors we want on this endpoint */ unsigned int stir_shaken; /*! Should we authenticate OPTIONS requests per RFC 3261? */ unsigned int allow_unauthenticated_options; diff --git a/include/asterisk/res_stir_shaken.h b/include/asterisk/res_stir_shaken.h index 5175907bbdddefcdd837cb9a2d4fc4675c512fb6..92eb0ec70b81ca2eb3066f4080ad5f8609ac44ef 100644 --- a/include/asterisk/res_stir_shaken.h +++ b/include/asterisk/res_stir_shaken.h @@ -29,6 +29,13 @@ enum ast_stir_shaken_verification_result { AST_STIR_SHAKEN_VERIFY_PASSED, /*! Signature verified and contents match signaling */ }; +/*! Different from ast_stir_shaken_verification_result. Used to determine why ast_stir_shaken_verify returned NULL */ +enum ast_stir_shaken_verify_failure_reason { + AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC, /*! Memory allocation failure */ + AST_STIR_SHAKEN_VERIFY_FAILED_TO_GET_CERT, /*! Failed to get the credentials to verify */ + AST_STIR_SHAKEN_VERIFY_FAILED_SIGNATURE_VALIDATION, /*! Failed validating the signature */ +}; + struct ast_stir_shaken_payload; struct ast_json; @@ -87,6 +94,24 @@ int ast_stir_shaken_add_verification(struct ast_channel *chan, const char *ident struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const char *payload, const char *signature, const char *algorithm, const char *public_cert_url); +/*! + * \brief Same as ast_stir_shaken_verify, but will populate a struct with additional information on failure + * + * \note failure_code will be written to in this function + * + * \param header The payload header + * \param payload The payload section + * \param signature The payload signature + * \param algorithm The signature algorithm + * \param public_cert_url The public key URL + * \param failure_code Additional failure information + * + * \retval ast_stir_shaken_payload on success + * \retval NULL on failure + */ +struct ast_stir_shaken_payload *ast_stir_shaken_verify2(const char *header, const char *payload, const char *signature, + const char *algorithm, const char *public_cert_url, int *failure_code); + /*! * \brief Retrieve the stir/shaken sorcery context * diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c index c27b587076d79b3809b526331fbe77f3f3772596..49a7d452155d07791438151b6acded37f89e5d14 100644 --- a/res/res_pjsip/pjsip_configuration.c +++ b/res/res_pjsip/pjsip_configuration.c @@ -717,6 +717,44 @@ static int media_encryption_to_str(const void *obj, const intptr_t *args, char * return 0; } +static int stir_shaken_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_endpoint *endpoint = obj; + + if (!strcasecmp("off", var->value)) { + endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_OFF; + } else if (!strcasecmp("attest", var->value)) { + endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_ATTEST; + } else if (!strcasecmp("verify", var->value)) { + endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_VERIFY; + } else if (!strcasecmp("on", var->value)) { + endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_ON; + } else { + ast_log(LOG_WARNING, "'%s' is not a valid value for option " + "'stir_shaken' for endpoint %s\n", + var->value, ast_sorcery_object_get_id(endpoint)); + return -1; + } + + return 0; +} + +static const char *stir_shaken_map[] = { + [AST_SIP_STIR_SHAKEN_OFF] "off", + [AST_SIP_STIR_SHAKEN_ATTEST] = "attest", + [AST_SIP_STIR_SHAKEN_VERIFY] = "verify", + [AST_SIP_STIR_SHAKEN_ON] = "on", +}; + +static int stir_shaken_to_str(const void *obj, const intptr_t *args, char **buf) +{ + const struct ast_sip_endpoint *endpoint = obj; + if (ARRAY_IN_BOUNDS(endpoint->stir_shaken, stir_shaken_map)) { + *buf = ast_strdup(stir_shaken_map[endpoint->stir_shaken]); + } + return 0; +} + static int group_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) { @@ -2153,7 +2191,7 @@ int ast_res_pjsip_initialize_configuration(void) ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "codec_prefs_outgoing_answer", "prefer: pending, operation: intersect, keep: all", codec_prefs_handler, outgoing_answer_codec_prefs_to_str, NULL, 0, 0); - ast_sorcery_object_field_register(sip_sorcery, "endpoint", "stir_shaken", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, stir_shaken)); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "stir_shaken", "off", stir_shaken_handler, stir_shaken_to_str, NULL, 0, 0); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "allow_unauthenticated_options", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, allow_unauthenticated_options)); if (ast_sip_initialize_sorcery_transport()) { diff --git a/res/res_pjsip_session.c b/res/res_pjsip_session.c index b1288b59362c16155baffa8cef48e43e66580f5b..4eb855a8523c91d2290a13924c53abbd0b2c368c 100644 --- a/res/res_pjsip_session.c +++ b/res/res_pjsip_session.c @@ -4051,6 +4051,11 @@ static void handle_new_invite_request(pjsip_rx_data *rdata) { RAII_VAR(struct ast_sip_endpoint *, endpoint, ast_pjsip_rdata_get_endpoint(rdata), ao2_cleanup); + static const pj_str_t identity_str = { "Identity", 8 }; + const pj_str_t use_identity_header_str = { + AST_STIR_SHAKEN_RESPONSE_STR_USE_IDENTITY_HEADER, + strlen(AST_STIR_SHAKEN_RESPONSE_STR_USE_IDENTITY_HEADER) + }; pjsip_inv_session *inv_session = NULL; struct ast_sip_session *session; struct new_invite invite; @@ -4060,6 +4065,14 @@ static void handle_new_invite_request(pjsip_rx_data *rdata) ast_assert(endpoint != NULL); + if ((endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_VERIFY) && + !ast_sip_rdata_get_header_value(rdata, identity_str)) { + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, + AST_STIR_SHAKEN_RESPONSE_CODE_USE_IDENTITY_HEADER, &use_identity_header_str, NULL, NULL); + ast_debug(3, "No Identity header when we require one\n"); + return; + } + inv_session = pre_session_setup(rdata, endpoint); if (!inv_session) { /* pre_session_setup() returns a response on failure */ diff --git a/res/res_pjsip_stir_shaken.c b/res/res_pjsip_stir_shaken.c index b2b208424e3a26afb50eeea7b16fa0bf65c59c76..1bf25283949dc3bc6b8895b2f915966135b668a1 100644 --- a/res/res_pjsip_stir_shaken.c +++ b/res/res_pjsip_stir_shaken.c @@ -32,6 +32,9 @@ #include "asterisk/res_stir_shaken.h" +/*! The Date header will not be valid after this many milliseconds (60 seconds recommended) */ +#define STIR_SHAKEN_DATE_HEADER_TIMEOUT 60000 + /*! * \brief Get the attestation from the payload * @@ -109,6 +112,62 @@ static int compare_timestamp(const char *json_str) return 0; } +static int check_date_header(pjsip_rx_data *rdata) +{ + static const pj_str_t date_hdr_str = { "Date", 4 }; + char *date_hdr_val; + struct ast_tm date_hdr_tm; + struct timeval date_hdr_timeval; + struct timeval current_timeval; + char *remainder; + char timezone[80] = { 0 }; + int64_t time_diff; + + date_hdr_val = ast_sip_rdata_get_header_value(rdata, date_hdr_str); + if (ast_strlen_zero(date_hdr_val)) { + ast_log(LOG_ERROR, "Failed to get Date header from incoming INVITE for STIR/SHAKEN\n"); + return -1; + } + + if (!(remainder = ast_strptime(date_hdr_val, "%a, %d %b %Y %T", &date_hdr_tm))) { + ast_log(LOG_ERROR, "Failed to parse Date header\n"); + return -1; + } + + sscanf(remainder, "%79s", timezone); + + if (ast_strlen_zero(timezone)) { + ast_log(LOG_ERROR, "A timezone is required for STIR/SHAKEN Date header, but we didn't get one\n"); + return -1; + } + + date_hdr_timeval = ast_mktime(&date_hdr_tm, timezone); + current_timeval = ast_tvnow(); + + time_diff = ast_tvdiff_ms(current_timeval, date_hdr_timeval); + if (time_diff < 0) { + /* An INVITE from the future! */ + ast_log(LOG_ERROR, "STIR/SHAKEN Date header has a future date\n"); + return -1; + } else if (time_diff > STIR_SHAKEN_DATE_HEADER_TIMEOUT) { + ast_log(LOG_ERROR, "STIR/SHAKEN Date header was outside of the allowable range (60 seconds)\n"); + return -1; + } + + return 0; +} + +/* Send a response back and end the session */ +static void stir_shaken_inv_end_session(struct ast_sip_session *session, pjsip_rx_data *rdata, int response_code, const pj_str_t response_str) +{ + pjsip_tx_data *tdata; + + if (pjsip_inv_end_session(session->inv_session, response_code, &response_str, &tdata) == PJ_SUCCESS) { + pjsip_endpt_send_response2(ast_sip_get_pjsip_endpoint(), rdata, tdata, NULL, NULL); + } + ast_hangup(session->channel); +} + /*! * \internal * \brief Session supplement callback on an incoming INVITE request @@ -122,6 +181,27 @@ static int compare_timestamp(const char *json_str) static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_rx_data *rdata) { static const pj_str_t identity_str = { "Identity", 8 }; + const pj_str_t bad_identity_info_str = { + AST_STIR_SHAKEN_RESPONSE_STR_BAD_IDENTITY_INFO, + strlen(AST_STIR_SHAKEN_RESPONSE_STR_BAD_IDENTITY_INFO) + }; + const pj_str_t unsupported_credential_str = { + AST_STIR_SHAKEN_RESPONSE_STR_UNSUPPORTED_CREDENTIAL, + strlen(AST_STIR_SHAKEN_RESPONSE_STR_UNSUPPORTED_CREDENTIAL) + }; + const pj_str_t stale_date_str = { + AST_STIR_SHAKEN_RESPONSE_STR_STALE_DATE, + strlen(AST_STIR_SHAKEN_RESPONSE_STR_STALE_DATE) + }; + const pj_str_t use_supported_passport_format_str = { + AST_STIR_SHAKEN_RESPONSE_STR_USE_SUPPORTED_PASSPORT_FORMAT, + strlen(AST_STIR_SHAKEN_RESPONSE_STR_USE_SUPPORTED_PASSPORT_FORMAT) + }; + const pj_str_t invalid_identity_hdr_str = { + AST_STIR_SHAKEN_RESPONSE_STR_INVALID_IDENTITY_HEADER, + strlen(AST_STIR_SHAKEN_RESPONSE_STR_INVALID_IDENTITY_HEADER) + }; + const pj_str_t server_internal_error_str = { "Server Internal Error", 21 }; char *identity_hdr_val; char *encoded_val; struct ast_channel *chan = session->channel; @@ -132,10 +212,17 @@ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_r char *algorithm; char *public_cert_url; char *attestation; + char *ppt; int mismatch = 0; struct ast_stir_shaken_payload *ss_payload; + int failure_code = 0; + + /* Check if this is a reinvite. If it is, we don't need to do anything */ + if (rdata->msg_info.to->tag.slen) { + return 0; + } - if (!session->endpoint->stir_shaken) { + if ((session->endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_VERIFY) == 0) { return 0; } @@ -148,50 +235,100 @@ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_r encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val); header = ast_base64url_decode_string(encoded_val); if (ast_strlen_zero(header)) { - ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED); - return 0; + ast_debug(3, "STIR/SHAKEN INVITE for %s is missing header\n", + ast_sorcery_object_get_id(session->endpoint)); + stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str); + return 1; } encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val); payload = ast_base64url_decode_string(encoded_val); if (ast_strlen_zero(payload)) { - ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED); - return 0; + ast_debug(3, "STIR/SHAKEN INVITE for %s is missing payload\n", + ast_sorcery_object_get_id(session->endpoint)); + stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str); + return 1; } /* 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; + ast_debug(3, "STIR/SHAKEN INVITE for %s is missing signature\n", + ast_sorcery_object_get_id(session->endpoint)); + stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str); + return 1; } /* Trim "info=<" to get public cert URL */ strtok_r(identity_hdr_val, "<", &identity_hdr_val); public_cert_url = strtok_r(identity_hdr_val, ">", &identity_hdr_val); - if (ast_strlen_zero(public_cert_url)) { - ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED); - return 0; - } /* Make sure the public URL is actually a URL */ - if (!ast_begins_with(public_cert_url, "http")) { - ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED); - return 0; + if (ast_strlen_zero(public_cert_url) || !ast_begins_with(public_cert_url, "http")) { + /* RFC8224 states that if we can't acquire the credentials needed + * by the verification service, we should send a 436 */ + ast_debug(3, "STIR/SHAKEN INVITE for %s did not have valid URL (%s)\n", + ast_sorcery_object_get_id(session->endpoint), public_cert_url); + stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str); + return 1; } 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; + /* RFC8224 states that if the algorithm is not specified, use ES256 */ + algorithm = STIR_SHAKEN_ENCRYPTION_ALGORITHM; + } else { + strtok_r(algorithm, "=", &algorithm); + if (strcmp(algorithm, STIR_SHAKEN_ENCRYPTION_ALGORITHM)) { + /* RFC8224 states that if we don't support the algorithm, send a 437 */ + ast_debug(3, "STIR/SHAKEN INVITE for %s uses an unsupported algorithm (%s)\n", + ast_sorcery_object_get_id(session->endpoint), algorithm); + stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL, unsupported_credential_str); + return 1; + } + } + + /* The only thing left should be ppt=shaken (which could have more values later), + * unless using the compact PASSport form */ + strtok_r(identity_hdr_val, "=", &identity_hdr_val); + ppt = ast_strip(identity_hdr_val); + if (!ast_strlen_zero(ppt) && strcmp(ppt, STIR_SHAKEN_PPT)) { + ast_log(LOG_ERROR, "STIR/SHAKEN INVITE for %s has unsupported ppt (%s)\n", + ast_sorcery_object_get_id(session->endpoint), ppt); + stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_USE_SUPPORTED_PASSPORT_FORMAT, use_supported_passport_format_str); + return 1; + } + + if (check_date_header(rdata)) { + ast_debug(3, "STIR/SHAKEN INVITE for %s has old Date header\n", + ast_sorcery_object_get_id(session->endpoint)); + stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_STALE_DATE, stale_date_str); + return 1; } attestation = get_attestation_from_payload(payload); - ss_payload = ast_stir_shaken_verify(header, payload, signature, algorithm, public_cert_url); + ss_payload = ast_stir_shaken_verify2(header, payload, signature, algorithm, public_cert_url, &failure_code); if (!ss_payload) { - ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED); - return 0; + + if (failure_code == AST_STIR_SHAKEN_VERIFY_FAILED_TO_GET_CERT) { + /* RFC8224 states that if we can't get the credentials we need, send a 437 */ + ast_debug(3, "STIR/SHAKEN INVITE for %s failed to acquire cert during verification process\n", + ast_sorcery_object_get_id(session->endpoint)); + stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL, unsupported_credential_str); + } else if (failure_code == AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC) { + ast_log(LOG_ERROR, "Failed to allocate memory during STIR/SHAKEN verification" + " for %s\n", ast_sorcery_object_get_id(session->endpoint)); + stir_shaken_inv_end_session(session, rdata, 500, server_internal_error_str); + } else if (failure_code == AST_STIR_SHAKEN_VERIFY_FAILED_SIGNATURE_VALIDATION) { + /* RFC8224 states that if we can't validate the signature, send a 438 */ + ast_debug(3, "STIR/SHAKEN INVITE for %s failed signature validation during verification process\n", + ast_sorcery_object_get_id(session->endpoint)); + ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED); + stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_INVALID_IDENTITY_HEADER, invalid_identity_hdr_str); + } + + return 1; } ast_stir_shaken_payload_free(ss_payload); @@ -333,7 +470,7 @@ static void add_date_header(const struct ast_sip_session *session, pjsip_tx_data static void stir_shaken_outgoing_request(struct ast_sip_session *session, pjsip_tx_data *tdata) { - if (!session->endpoint->stir_shaken) { + if ((session->endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_ATTEST) == 0) { return; } diff --git a/res/res_stir_shaken.c b/res/res_stir_shaken.c index 1d8c785653cb4b91743c7c26f6a5207642bc920b..373a1a1a926c3d66659509174eca8095a6bc9291 100644 --- a/res/res_stir_shaken.c +++ b/res/res_stir_shaken.c @@ -617,83 +617,95 @@ static char *curl_and_check_expiration(const char *public_cert_url, const char * return filename; } -struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const char *payload, const char *signature, +/*! + * \brief Verifies that the string parameters are not empty for STIR/SHAKEN verification + * + * \retval 0 on success + * \retval 1 on failure + */ +static int stir_shaken_verify_check_empty_strings(const char *header, const char *payload, const char *signature, const char *algorithm, const char *public_cert_url) { - struct ast_stir_shaken_payload *ret_payload; - EVP_PKEY *public_key; - int curl = 0; - RAII_VAR(char *, file_path, NULL, ast_free); - RAII_VAR(char *, dir_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"); - return NULL; + return 1; } if (ast_strlen_zero(payload)) { ast_log(LOG_ERROR, "'payload' is required for STIR/SHAKEN verification\n"); - return NULL; + return 1; } if (ast_strlen_zero(signature)) { ast_log(LOG_ERROR, "'signature' is required for STIR/SHAKEN verification\n"); - return NULL; + return 1; } if (ast_strlen_zero(algorithm)) { ast_log(LOG_ERROR, "'algorithm' is required for STIR/SHAKEN verification\n"); - return NULL; + return 1; } if (ast_strlen_zero(public_cert_url)) { ast_log(LOG_ERROR, "'public_cert_url' is required for STIR/SHAKEN verification\n"); - return NULL; + return 1; } - /* Check to see if we have already downloaded this public cert. The reason we - * store the file path is because: - * - * 1. If, for some reason, the default directory changes, we still know where - * to look for the files we already have. - * - * 2. In the future, if we want to add a way to store the certs in multiple - * {configurable) directories, we already have the storage mechanism in place. - * The only thing that would be left to do is pull from the configuration. - */ - file_path = get_path_to_public_key(public_cert_url); - if (ast_asprintf(&dir_path, "%s/keys/%s", ast_config_AST_DATA_DIR, STIR_SHAKEN_DIR_NAME) < 0) { - return NULL; + return 0; +} + +/*! + * \brief Get or set up the file path for the certificate + * + * \note This function will allocate memory for file_path and dir_path and populate them + * + * \retval 0 on success + * \retval 1 on failure + */ +static int stir_shaken_verify_setup_file_paths(const char *public_cert_url, char **file_path, char **dir_path, int *curl) +{ + *file_path = get_path_to_public_key(public_cert_url); + if (ast_asprintf(dir_path, "%s/keys/%s", ast_config_AST_DATA_DIR, STIR_SHAKEN_DIR_NAME) < 0) { + return 1; } /* If we don't have an entry in AstDB, CURL from the provided URL */ - if (ast_strlen_zero(file_path)) { + if (ast_strlen_zero(*file_path)) { /* Remove this entry from the database, since we will be * downloading a new file anyways. */ remove_public_key_from_astdb(public_cert_url); /* Go ahead and free file_path, in case anything was allocated above */ - ast_free(file_path); + ast_free(*file_path); /* Download to the default path */ - file_path = run_curl(public_cert_url, dir_path); - if (!file_path) { - return NULL; + *file_path = run_curl(public_cert_url, *dir_path); + if (!(*file_path)) { + return 1; } /* Signal that we have already downloaded a new file, no reason to do it again */ - curl = 1; + *curl = 1; /* We should have a successful download at this point, so * add an entry to the database. */ - add_public_key_to_astdb(public_cert_url, file_path); + add_public_key_to_astdb(public_cert_url, *file_path); } - /* Check to see if the cert we downloaded (or already had) is expired */ + return 0; +} + +/*! + * \brief See if the cert is expired. If it is, remove it and try downloading again if we haven't already. + * + * \retval 0 on success + * \retval 1 on failure + */ +static int stir_shaken_verify_validate_cert(const char *public_cert_url, char **file_path, char *dir_path, int *curl, + EVP_PKEY **public_key) +{ if (public_key_is_expired(public_cert_url)) { ast_debug(3, "Public cert '%s' is expired\n", public_cert_url); @@ -701,47 +713,95 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const remove_public_key_from_astdb(public_cert_url); /* If this fails, then there's nothing we can do */ - ast_free(file_path); - file_path = curl_and_check_expiration(public_cert_url, dir_path, &curl); - if (!file_path) { - return NULL; + ast_free(*file_path); + *file_path = curl_and_check_expiration(public_cert_url, dir_path, curl); + if (!(*file_path)) { + return 1; } } /* First attempt to read the key. If it fails, try downloading the file, * unless we already did. Check for expiration again */ - public_key = stir_shaken_read_key(file_path, 0); - if (!public_key) { + *public_key = stir_shaken_read_key(*file_path, 0); + if (!(*public_key)) { - ast_debug(3, "Failed first read of public key file '%s'\n", file_path); + ast_debug(3, "Failed first read of public key file '%s'\n", *file_path); remove_public_key_from_astdb(public_cert_url); - ast_free(file_path); - file_path = curl_and_check_expiration(public_cert_url, dir_path, &curl); - if (!file_path) { - return NULL; + ast_free(*file_path); + *file_path = curl_and_check_expiration(public_cert_url, dir_path, curl); + if (!(*file_path)) { + return 1; } - public_key = stir_shaken_read_key(file_path, 0); - if (!public_key) { - ast_log(LOG_ERROR, "Failed to read public key from '%s'\n", file_path); + *public_key = stir_shaken_read_key(*file_path, 0); + if (!(*public_key)) { + ast_log(LOG_ERROR, "Failed to read public key from '%s'\n", *file_path); remove_public_key_from_astdb(public_cert_url); - return NULL; + return 1; } } + return 0; +} + +struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const char *payload, const char *signature, + const char *algorithm, const char *public_cert_url) +{ + int code = 0; + + return ast_stir_shaken_verify2(header, payload, signature, algorithm, public_cert_url, &code); +} + +struct ast_stir_shaken_payload *ast_stir_shaken_verify2(const char *header, const char *payload, const char *signature, + const char *algorithm, const char *public_cert_url, int *failure_code) +{ + struct ast_stir_shaken_payload *ret_payload; + EVP_PKEY *public_key; + int curl = 0; + RAII_VAR(char *, file_path, NULL, ast_free); + RAII_VAR(char *, dir_path, NULL, ast_free); + RAII_VAR(char *, combined_str, NULL, ast_free); + size_t combined_size; + + if (stir_shaken_verify_check_empty_strings(header, payload, signature, algorithm, public_cert_url)) { + return NULL; + } + + /* Check to see if we have already downloaded this public cert. The reason we + * store the file path is because: + * + * 1. If, for some reason, the default directory changes, we still know where + * to look for the files we already have. + * + * 2. In the future, if we want to add a way to store the certs in multiple + * {configurable) directories, we already have the storage mechanism in place. + * The only thing that would be left to do is pull from the configuration. + */ + if (stir_shaken_verify_setup_file_paths(public_cert_url, &file_path, &dir_path, &curl)) { + return NULL; + } + + /* Check to see if the cert we downloaded (or already had) is expired */ + if (stir_shaken_verify_validate_cert(public_cert_url, &file_path, dir_path, &curl, &public_key)) { + *failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_TO_GET_CERT; + return NULL; + } + /* 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); + *failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC; 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"); + *failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_SIGNATURE_VALIDATION; EVP_PKEY_free(public_key); return NULL; } @@ -752,12 +812,14 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const ret_payload = ast_calloc(1, sizeof(*ret_payload)); if (!ret_payload) { ast_log(LOG_ERROR, "Failed to allocate STIR/SHAKEN payload\n"); + *failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC; return NULL; } ret_payload->header = ast_json_load_string(header, NULL); if (!ret_payload->header) { ast_log(LOG_ERROR, "Failed to create JSON from header\n"); + *failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC; ast_stir_shaken_payload_free(ret_payload); return NULL; } @@ -765,6 +827,7 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const ret_payload->payload = ast_json_load_string(payload, NULL); if (!ret_payload->payload) { ast_log(LOG_ERROR, "Failed to create JSON from payload\n"); + *failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC; ast_stir_shaken_payload_free(ret_payload); return NULL; } @@ -834,15 +897,11 @@ static struct ast_stir_shaken_payload *stir_shaken_verify_json(struct ast_json * goto cleanup; } - /* Check the alg value for "ES256" */ + /* Check to see if there is a value for alg */ 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); + if (!ast_strlen_zero(val) && strcmp(val, STIR_SHAKEN_ENCRYPTION_ALGORITHM)) { + /* If alg is not present that's fine; if it is and is not ES256, cleanup */ + ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have supported type for field 'alg' (was %s)\n", val); goto cleanup; }