--- /dev/null
+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.
#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;
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.
*
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;
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;
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
*
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)
{
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()) {
{
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;
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 */
#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
*
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
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;
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;
}
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);
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;
}
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);
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;
}
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;
}
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;
}
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;
}