*/
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
*
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
*
*/
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)
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)
{
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;
*
* Copyright (C) 2020, Sangoma Technologies Corporation
*
- * Kevin Harwell <kharwell@digium.com>
+ * Ben Ford <bford@sangoma.com>
*
* See http://www.asterisk.org for more information about
* the Asterisk project. Please do not directly contact
/*** MODULEINFO
<depend>crypto</depend>
+ <depend>pjproject</depend>
+ <depend>res_pjsip</depend>
+ <depend>res_pjsip_session</depend>
+ <depend>res_stir_shaken</depend>
<support_level>core</support_level>
***/
#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;
}
<configOption name="curl_timeout" default="2">
<synopsis>Maximum time to wait to CURL certificates</synopsis>
</configOption>
+ <configOption name="signature_timeout" default="15">
+ <synopsis>Amount of time a signature is valid for</synopsis>
+ </configOption>
</configObject>
<configObject name="store">
<synopsis>STIR/SHAKEN certificate store options</synopsis>
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
*
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;
}
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");
}
}
- 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;
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);
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;
}
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;
}
{
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);
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");
}
/* 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");
}
/* 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");
}
/* 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");
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");
--- /dev/null
+{
+ global:
+ LINKER_SYMBOL_PREFIXast_stir_*;
+ local:
+ *;
+};
#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);
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;
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;
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' "
*/
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
*