res_stir_shaken: Add STIR_SHAKEN_ATTESTATION dialplan function.

Also...

* Refactored the verification datastore process so instead of having
a separate channel datastore for each verification result, there's only
one channel datastore with a vector of results.

* Refactored some log messages to include channel name and removed
some that would be redundant if a memory allocation failed.

Resolves: #781

UserNote: The STIR_SHAKEN_ATTESTATION dialplan function has been added
which will allow suppressing attestation on a call-by-call basis
regardless of the profile attached to the outgoing endpoint.
This commit is contained in:
George Joseph
2025-10-24 09:34:47 -06:00
parent 323f779a58
commit cbfbc4d0e8
4 changed files with 280 additions and 81 deletions

View File

@@ -33,8 +33,9 @@
#include "asterisk/res_pjsip_session.h" #include "asterisk/res_pjsip_session.h"
#include "asterisk/module.h" #include "asterisk/module.h"
#include "asterisk/rtp_engine.h" #include "asterisk/rtp_engine.h"
#include "asterisk/datastore.h"
#include "asterisk/res_stir_shaken.h" #include "res_stir_shaken/stir_shaken.h"
static const pj_str_t identity_hdr_str = { "Identity", 8 }; static const pj_str_t identity_hdr_str = { "Identity", 8 };
static const pj_str_t date_hdr_str = { "Date", 4 }; static const pj_str_t date_hdr_str = { "Date", 4 };
@@ -395,6 +396,8 @@ static void stir_shaken_outgoing_request(struct ast_sip_session *session,
struct ast_stir_shaken_as_ctx *ctx = NULL; struct ast_stir_shaken_as_ctx *ctx = NULL;
enum ast_stir_shaken_as_response_code as_rc; enum ast_stir_shaken_as_response_code as_rc;
const char *session_name = ast_sip_session_get_name(session); const char *session_name = ast_sip_session_get_name(session);
struct stir_shaken_attestation_ds *attestation_ds;
SCOPE_ENTER(1, "%s: Enter\n", session_name); SCOPE_ENTER(1, "%s: Enter\n", session_name);
if (!session) { if (!session) {
@@ -407,6 +410,14 @@ static void stir_shaken_outgoing_request(struct ast_sip_session *session,
SCOPE_EXIT_LOG_RTN(LOG_ERROR, "%s: No tdata\n", session_name); SCOPE_EXIT_LOG_RTN(LOG_ERROR, "%s: No tdata\n", session_name);
} }
ast_channel_lock(session->channel);
attestation_ds = ast_stir_shaken_get_attestation_datastore(session->channel);
if (attestation_ds && attestation_ds->suppress) {
ast_channel_unlock(session->channel);
SCOPE_EXIT_RTN("Attestation suppressed by dialplan\n");
}
ast_channel_unlock(session->channel);
old_identity = pjsip_msg_find_hdr_by_name(tdata->msg, &identity_hdr_str, NULL); old_identity = pjsip_msg_find_hdr_by_name(tdata->msg, &identity_hdr_str, NULL);
if (old_identity) { if (old_identity) {
SCOPE_EXIT_RTN("Found an existing Identity header\n"); SCOPE_EXIT_RTN("Found an existing Identity header\n");

View File

@@ -33,9 +33,12 @@
#include "asterisk/module.h" #include "asterisk/module.h"
#include "asterisk/global_datastores.h" #include "asterisk/global_datastores.h"
#include "asterisk/pbx.h" #include "asterisk/pbx.h"
#include "asterisk/vector.h"
#include "res_stir_shaken/stir_shaken.h" #include "res_stir_shaken/stir_shaken.h"
AST_VECTOR(verification_vector, struct stir_shaken_verification_ds *);
static int tn_auth_list_nid; static int tn_auth_list_nid;
int get_tn_auth_nid(void) int get_tn_auth_nid(void)
@@ -43,22 +46,12 @@ int get_tn_auth_nid(void)
return tn_auth_list_nid; return tn_auth_list_nid;
} }
/* The datastore struct holding verification information for the channel */
struct stir_datastore {
/* The identitifier for the STIR/SHAKEN verification */
char *identity;
/* The attestation value */
char *attestation;
/* The actual verification result */
enum ast_stir_shaken_vs_response_code verify_result;
};
/*! /*!
* \brief Frees a stir_shaken_datastore structure * \brief Frees a stir_shaken_datastore structure
* *
* \param datastore The datastore to free * \param datastore The datastore to free
*/ */
static void stir_datastore_free(struct stir_datastore *datastore) static void verification_ds_free(struct stir_shaken_verification_ds *datastore)
{ {
if (!datastore) { if (!datastore) {
return; return;
@@ -74,84 +67,108 @@ static void stir_datastore_free(struct stir_datastore *datastore)
* *
* \param data The stir_shaken_datastore * \param data The stir_shaken_datastore
*/ */
static void stir_datastore_destroy_cb(void *data) static void verification_ds_destroy_cb(void *data)
{ {
struct stir_datastore *datastore = data; struct verification_vector *verifies = data;
stir_datastore_free(datastore);
AST_VECTOR_RESET(verifies, verification_ds_free);
ast_free(verifies);
} }
/* The stir_shaken_datastore info used to add and compare stir_shaken_datastores on the channel */ static const struct ast_datastore_info verification_ds_info = {
static const struct ast_datastore_info stir_shaken_datastore_info = { .type = STIR_SHAKEN_VERIFICATION_DS,
.type = "STIR/SHAKEN VERIFICATION", .destroy = verification_ds_destroy_cb,
.destroy = stir_datastore_destroy_cb,
}; };
int ast_stir_shaken_add_result_to_channel( int ast_stir_shaken_add_result_to_channel(
struct ast_stir_shaken_vs_ctx *ctx) struct ast_stir_shaken_vs_ctx *ctx)
{ {
struct stir_datastore *stir_datastore; struct stir_shaken_verification_ds *stir_datastore;
struct ast_datastore *chan_datastore; struct ast_datastore *chan_datastore;
struct verification_vector *verifies;
const char *chan_name; const char *chan_name;
int res = 0;
if (!ctx->chan) { if (!ctx->chan) {
ast_log(LOG_ERROR, "Channel is required to add STIR/SHAKEN verification\n"); ast_log(LOG_ERROR, "Channel is required to add verification\n");
return -1; return -1;
} }
chan_name = ast_channel_name(ctx->chan); chan_name = ast_channel_name(ctx->chan);
if (!ctx->identity_hdr) { if (!ctx->identity_hdr) {
ast_log(LOG_ERROR, "No identity to add STIR/SHAKEN verification to channel " ast_log(LOG_ERROR, "%s: No identity to add to datastore\n",
"%s\n", chan_name); chan_name);
return -1; return -1;
} }
if (!ctx->attestation) { if (!ctx->attestation) {
ast_log(LOG_ERROR, "Attestation cannot be NULL to add STIR/SHAKEN verification to " ast_log(LOG_ERROR, "%s: Attestation cannot be NULL\n", chan_name);
"channel %s\n", chan_name);
return -1; return -1;
} }
stir_datastore = ast_calloc(1, sizeof(*stir_datastore)); stir_datastore = ast_calloc(1, sizeof(*stir_datastore));
if (!stir_datastore) { if (!stir_datastore) {
ast_log(LOG_ERROR, "Failed to allocate space for STIR/SHAKEN datastore for "
"channel %s\n", chan_name);
return -1; return -1;
} }
stir_datastore->identity = ast_strdup(ctx->identity_hdr); stir_datastore->identity = ast_strdup(ctx->identity_hdr);
if (!stir_datastore->identity) { if (!stir_datastore->identity) {
ast_log(LOG_ERROR, "Failed to allocate space for STIR/SHAKEN datastore " verification_ds_free(stir_datastore);
"identity for channel %s\n", chan_name);
stir_datastore_free(stir_datastore);
return -1; return -1;
} }
stir_datastore->attestation = ast_strdup(ctx->attestation); stir_datastore->attestation = ast_strdup(ctx->attestation);
if (!stir_datastore->attestation) { if (!stir_datastore->attestation) {
ast_log(LOG_ERROR, "Failed to allocate space for STIR/SHAKEN datastore " verification_ds_free(stir_datastore);
"attestation for channel %s\n", chan_name);
stir_datastore_free(stir_datastore);
return -1; return -1;
} }
stir_datastore->verify_result = ctx->failure_reason; stir_datastore->verify_result = ctx->failure_reason;
chan_datastore = ast_datastore_alloc(&stir_shaken_datastore_info, NULL); ast_channel_lock(ctx->chan);
if (!chan_datastore) { chan_datastore = ast_channel_datastore_find(ctx->chan, &verification_ds_info, NULL);
ast_log(LOG_ERROR, "Failed to allocate space for datastore for channel " if (chan_datastore) {
"%s\n", chan_name); verifies = chan_datastore->data;
stir_datastore_free(stir_datastore); res = AST_VECTOR_APPEND(verifies, stir_datastore);
return -1; if (res != 0) {
verification_ds_free(stir_datastore);
}
ast_channel_unlock(ctx->chan);
return res;
} }
chan_datastore->data = stir_datastore; chan_datastore = ast_datastore_alloc(&verification_ds_info, NULL);
if (!chan_datastore) {
verification_ds_free(stir_datastore);
return -1;
}
/*
* We don't pass this datastore to other channels at the current time.
* Inheritance is disabled by default but it's called out here for clarity.
*/
chan_datastore->inheritance = 0;
ast_channel_lock(ctx->chan); verifies = ast_calloc(sizeof(*verifies), 1);
if (!verifies) {
ast_channel_unlock(ctx->chan);
verification_ds_free(stir_datastore);
ast_datastore_free(chan_datastore);
return -1;
}
AST_VECTOR_INIT(verifies, 0);
res = AST_VECTOR_APPEND(verifies, stir_datastore);
if (res != 0) {
ast_free(verifies);
verification_ds_free(stir_datastore);
} else {
chan_datastore->data = verifies;
ast_channel_datastore_add(ctx->chan, chan_datastore); ast_channel_datastore_add(ctx->chan, chan_datastore);
}
ast_channel_unlock(ctx->chan); ast_channel_unlock(ctx->chan);
return 0; return res;
} }
/*! /*!
@@ -166,15 +183,18 @@ int ast_stir_shaken_add_result_to_channel(
* \retval -1 on failure * \retval -1 on failure
* \retval 0 on success * \retval 0 on success
*/ */
static int func_read(struct ast_channel *chan, const char *function, static int func_read_verification(struct ast_channel *chan, const char *function,
char *data, char *buf, size_t len) char *data, char *buf, size_t len)
{ {
struct stir_datastore *stir_datastore; struct stir_shaken_verification_ds *stir_datastore;
struct ast_datastore *chan_datastore; struct ast_datastore *chan_datastore;
struct verification_vector *verifies;
const char *chan_name;
char *parse; char *parse;
char *first; char *first;
char *second; char *second;
unsigned int target_index, current_index = 0; unsigned int target_index = 0;
int res = 0;
AST_DECLARE_APP_ARGS(args, AST_DECLARE_APP_ARGS(args,
AST_APP_ARG(first_param); AST_APP_ARG(first_param);
AST_APP_ARG(second_param); AST_APP_ARG(second_param);
@@ -189,6 +209,7 @@ static int func_read(struct ast_channel *chan, const char *function,
ast_log(LOG_ERROR, "No channel for %s function\n", function); ast_log(LOG_ERROR, "No channel for %s function\n", function);
return -1; return -1;
} }
chan_name = ast_channel_name(chan);
parse = ast_strdupa(data); parse = ast_strdupa(data);
@@ -196,7 +217,8 @@ static int func_read(struct ast_channel *chan, const char *function,
first = ast_strip(args.first_param); first = ast_strip(args.first_param);
if (ast_strlen_zero(first)) { if (ast_strlen_zero(first)) {
ast_log(LOG_ERROR, "An argument must be passed to %s\n", function); ast_log(LOG_ERROR, "%s: An argument must be passed to %s\n",
chan_name, function);
return -1; return -1;
} }
@@ -207,16 +229,16 @@ static int func_read(struct ast_channel *chan, const char *function,
size_t count = 0; size_t count = 0;
if (!ast_strlen_zero(second)) { if (!ast_strlen_zero(second)) {
ast_log(LOG_ERROR, "%s only takes 1 paramater for 'count'\n", function); ast_log(LOG_ERROR, "%s: %s only takes 1 paramater for 'count'\n",
chan_name, function);
return -1; return -1;
} }
ast_channel_lock(chan); ast_channel_lock(chan);
AST_LIST_TRAVERSE(ast_channel_datastores(chan), chan_datastore, entry) { chan_datastore = ast_channel_datastore_find(chan, &verification_ds_info, NULL);
if (chan_datastore->info != &stir_shaken_datastore_info) { if (chan_datastore && chan_datastore->data) {
continue; verifies = chan_datastore->data;
} count = AST_VECTOR_SIZE(verifies);
count++;
} }
ast_channel_unlock(chan); ast_channel_unlock(chan);
@@ -228,36 +250,33 @@ static int func_read(struct ast_channel *chan, const char *function,
* we are searching for will be the second parameter. The index is the first. * we are searching for will be the second parameter. The index is the first.
*/ */
if (ast_strlen_zero(second)) { if (ast_strlen_zero(second)) {
ast_log(LOG_ERROR, "Retrieving a value using %s requires two paramaters (index, value) " ast_log(LOG_ERROR, "%s: Retrieving a value using %s requires two paramaters (index, value) "
"- only index was given\n", function); "- only index was given\n", chan_name, function);
return -1; return -1;
} }
if (ast_str_to_uint(first, &target_index)) { if (ast_str_to_uint(first, &target_index)) {
ast_log(LOG_ERROR, "Failed to convert index %s to integer for function %s\n", ast_log(LOG_ERROR, "%s: Failed to convert index %s to integer for function %s\n",
first, function); chan_name, first, function);
return -1; return -1;
} }
/* We don't store by uid for the datastore, so just search for the specified index */
ast_channel_lock(chan); ast_channel_lock(chan);
AST_LIST_TRAVERSE(ast_channel_datastores(chan), chan_datastore, entry) { chan_datastore = ast_channel_datastore_find(chan, &verification_ds_info, NULL);
if (chan_datastore->info != &stir_shaken_datastore_info) { if (!chan_datastore || !chan_datastore->data) {
continue;
}
if (current_index == target_index) {
break;
}
current_index++;
}
ast_channel_unlock(chan); ast_channel_unlock(chan);
if (current_index != target_index || !chan_datastore) { ast_log(LOG_WARNING, "%s: No STIR/SHAKEN results for index '%s'\n",
ast_log(LOG_WARNING, "No STIR/SHAKEN results for index '%s'\n", first); chan_name, first);
return -1; return -1;
} }
stir_datastore = chan_datastore->data; verifies = chan_datastore->data;
if (target_index >= AST_VECTOR_SIZE(verifies)) {
ast_channel_unlock(chan);
ast_log(LOG_WARNING, "%s: No STIR/SHAKEN results for index '%s'\n",
chan_name, first);
return -1;
}
stir_datastore = AST_VECTOR_GET(verifies, target_index);
if (!strcasecmp(second, "identity")) { if (!strcasecmp(second, "identity")) {
ast_copy_string(buf, stir_datastore->identity, len); ast_copy_string(buf, stir_datastore->identity, len);
@@ -266,16 +285,127 @@ static int func_read(struct ast_channel *chan, const char *function,
} else if (!strcasecmp(second, "verify_result")) { } else if (!strcasecmp(second, "verify_result")) {
ast_copy_string(buf, vs_response_code_to_str(stir_datastore->verify_result), len); ast_copy_string(buf, vs_response_code_to_str(stir_datastore->verify_result), len);
} else { } else {
ast_log(LOG_ERROR, "No such value '%s' for %s\n", second, function); ast_log(LOG_ERROR, "%s: No such value '%s' for %s\n",
chan_name, second, function);
res = -1;
}
ast_channel_unlock(chan);
return res;
}
/*
* Unlike verification, on an outgoing channel there can be at most
* one attestation datastore so there's no need for a vector. One
* can be added at a later date if this changes.
*/
static void attestation_ds_destroy(void *data)
{
ast_free(data);
}
static const struct ast_datastore_info attestation_ds_info = {
.type = STIR_SHAKEN_ATTESTATION_DS,
.destroy = attestation_ds_destroy,
};
struct stir_shaken_attestation_ds *ast_stir_shaken_get_attestation_datastore(
struct ast_channel *chan)
{
struct stir_shaken_attestation_ds *attestation_ds;
struct ast_datastore *chan_datastore;
chan_datastore = ast_channel_datastore_find(chan, &attestation_ds_info, NULL);
if (!chan_datastore) {
return NULL;
}
attestation_ds = chan_datastore->data;
return attestation_ds;
}
static int func_write_attestation(struct ast_channel *chan, const char *function, char *data,
const char *value)
{
struct stir_shaken_attestation_ds *attestation_ds;
struct ast_datastore *chan_datastore;
char *parse;
char *field;
char *stripped_value;
const char *channel_name = chan ? ast_channel_name(chan) : "unknown_channel";
AST_DECLARE_APP_ARGS(args,
AST_APP_ARG(field);
);
if (!chan) {
ast_log(LOG_ERROR, "No channel for %s function\n", function);
return -1; return -1;
} }
if (ast_strlen_zero(data)) {
ast_log(LOG_WARNING, "%s: %s requires a field to set\n", channel_name, function);
return -1;
}
parse = ast_strdupa(data);
AST_STANDARD_APP_ARGS(args, parse);
field = ast_strip(args.field);
if (ast_strlen_zero(field)) {
ast_log(LOG_WARNING, "%s: %s requires a field to set\n", channel_name, function);
return -1;
}
if (!ast_strings_equal(field, "suppress")) {
ast_log(LOG_ERROR, "%s: %s was passed invalid field '%s'\n",
channel_name, function, field);
return -1;
}
stripped_value = ast_strip(ast_strdupa(value));
if (ast_strlen_zero(stripped_value)) {
ast_log(LOG_ERROR, "%s: %s requires a boolean value\n", channel_name, function);
return -1;
}
ast_channel_lock(chan);
chan_datastore = ast_channel_datastore_find(chan, &attestation_ds_info, NULL);
if (chan_datastore) {
attestation_ds = chan_datastore->data;
} else {
attestation_ds = ast_calloc(1, sizeof(*attestation_ds));
chan_datastore = ast_datastore_alloc(&attestation_ds_info, NULL);
if (!attestation_ds || !chan_datastore) {
ast_channel_unlock(chan);
attestation_ds_destroy(attestation_ds);
ast_datastore_free(chan_datastore);
return -1;
}
chan_datastore->data = attestation_ds;
chan_datastore->inheritance = 0;
ast_channel_datastore_add(chan, chan_datastore);
}
attestation_ds->suppress = ast_true(stripped_value);
ast_channel_unlock(chan);
return 0; return 0;
} }
static struct ast_custom_function stir_shaken_function = { static struct ast_custom_function stir_shaken_verification = {
.name = "STIR_SHAKEN", .name = "STIR_SHAKEN",
.read = func_read, .read = func_read_verification,
};
static struct ast_custom_function stir_shaken_attestation = {
.name = "STIR_SHAKEN_ATTESTATION",
.write = func_write_attestation,
}; };
static int reload_module(void) static int reload_module(void)
@@ -285,12 +415,11 @@ static int reload_module(void)
static int unload_module(void) static int unload_module(void)
{ {
int res = 0;
common_config_unload(); common_config_unload();
crypto_unload(); crypto_unload();
res |= ast_custom_function_unregister(&stir_shaken_function); ast_custom_function_unregister(&stir_shaken_verification);
ast_custom_function_unregister(&stir_shaken_attestation);
return 0; return 0;
} }
@@ -371,7 +500,13 @@ static int load_module(void)
return res; return res;
} }
res = ast_custom_function_register(&stir_shaken_function); res = ast_custom_function_register(&stir_shaken_verification);
if (res != 0) {
unload_module();
return AST_MODULE_LOAD_DECLINE;
}
res = ast_custom_function_register(&stir_shaken_attestation);
if (res != 0) { if (res != 0) {
unload_module(); unload_module();
return AST_MODULE_LOAD_DECLINE; return AST_MODULE_LOAD_DECLINE;

View File

@@ -29,6 +29,25 @@
#define STIR_SHAKEN_PPT "shaken" #define STIR_SHAKEN_PPT "shaken"
#define STIR_SHAKEN_TYPE "passport" #define STIR_SHAKEN_TYPE "passport"
#define STIR_SHAKEN_VERIFICATION_DS "STIR/SHAKEN/VERIFICATION"
struct stir_shaken_verification_ds {
/*! The identitifier for the STIR/SHAKEN verification */
char *identity;
/*! The attestation value */
char *attestation;
/*! The actual verification result */
enum ast_stir_shaken_vs_response_code verify_result;
};
#define STIR_SHAKEN_ATTESTATION_DS "STIR/SHAKEN/ATTESTATION"
struct stir_shaken_attestation_ds {
/*! Whether to suppress attestation on outgoing call */
int suppress;
};
struct stir_shaken_attestation_ds *ast_stir_shaken_get_attestation_datastore(
struct ast_channel *chan);
/*! /*!
* \brief Retrieve the stir/shaken sorcery context * \brief Retrieve the stir/shaken sorcery context
* *

View File

@@ -507,4 +507,38 @@
</example> </example>
</description> </description>
</function> </function>
<function name="STIR_SHAKEN_ATTESTATION" language="en_US">
<since>
<version>20.17.0</version>
<version>22.7.0</version>
<version>23.1.0</version>
</since>
<synopsis>
Sets STIR/SHAKEN Attestation parameters on an outgoing channel.
</synopsis>
<syntax>
<parameter name="field" required="true">
<para>The attestation field to set. Allowable values:</para>
<enumlist>
<enum name = "suppress">
<para>
When set to <literal>true</literal>, attestation is suppressed
on the channel regardless of the profile on the endpoint.
</para>
</enum>
</enumlist>
</parameter>
</syntax>
<description>
<para>When used on an outgoing channel, this function can override the
profile on an endpoint. Currently, only the ability to suppress
attestation is supported.
</para>
<para>
</para>
<example title="Suppress attestation on an outgoing channel">
same => n,Set(STIR_SHAKEN_ATTESTATION(suppress)=yes)
</example>
</description>
</function>
</docs> </docs>