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
committed by github-actions[bot]
parent 74b1cafece
commit 438649b30d
4 changed files with 280 additions and 81 deletions

View File

@@ -33,8 +33,9 @@
#include "asterisk/res_pjsip_session.h"
#include "asterisk/module.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 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;
enum ast_stir_shaken_as_response_code as_rc;
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);
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);
}
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);
if (old_identity) {
SCOPE_EXIT_RTN("Found an existing Identity header\n");

View File

@@ -33,9 +33,12 @@
#include "asterisk/module.h"
#include "asterisk/global_datastores.h"
#include "asterisk/pbx.h"
#include "asterisk/vector.h"
#include "res_stir_shaken/stir_shaken.h"
AST_VECTOR(verification_vector, struct stir_shaken_verification_ds *);
static int tn_auth_list_nid;
int get_tn_auth_nid(void)
@@ -43,22 +46,12 @@ int get_tn_auth_nid(void)
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
*
* \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) {
return;
@@ -74,84 +67,108 @@ static void stir_datastore_free(struct stir_datastore *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;
stir_datastore_free(datastore);
struct verification_vector *verifies = data;
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 stir_shaken_datastore_info = {
.type = "STIR/SHAKEN VERIFICATION",
.destroy = stir_datastore_destroy_cb,
static const struct ast_datastore_info verification_ds_info = {
.type = STIR_SHAKEN_VERIFICATION_DS,
.destroy = verification_ds_destroy_cb,
};
int ast_stir_shaken_add_result_to_channel(
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 verification_vector *verifies;
const char *chan_name;
int res = 0;
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;
}
chan_name = ast_channel_name(ctx->chan);
if (!ctx->identity_hdr) {
ast_log(LOG_ERROR, "No identity to add STIR/SHAKEN verification to channel "
"%s\n", chan_name);
ast_log(LOG_ERROR, "%s: No identity to add to datastore\n",
chan_name);
return -1;
}
if (!ctx->attestation) {
ast_log(LOG_ERROR, "Attestation cannot be NULL to add STIR/SHAKEN verification to "
"channel %s\n", chan_name);
ast_log(LOG_ERROR, "%s: Attestation cannot be NULL\n", chan_name);
return -1;
}
stir_datastore = ast_calloc(1, sizeof(*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;
}
stir_datastore->identity = ast_strdup(ctx->identity_hdr);
if (!stir_datastore->identity) {
ast_log(LOG_ERROR, "Failed to allocate space for STIR/SHAKEN datastore "
"identity for channel %s\n", chan_name);
stir_datastore_free(stir_datastore);
verification_ds_free(stir_datastore);
return -1;
}
stir_datastore->attestation = ast_strdup(ctx->attestation);
if (!stir_datastore->attestation) {
ast_log(LOG_ERROR, "Failed to allocate space for STIR/SHAKEN datastore "
"attestation for channel %s\n", chan_name);
stir_datastore_free(stir_datastore);
verification_ds_free(stir_datastore);
return -1;
}
stir_datastore->verify_result = ctx->failure_reason;
chan_datastore = ast_datastore_alloc(&stir_shaken_datastore_info, NULL);
if (!chan_datastore) {
ast_log(LOG_ERROR, "Failed to allocate space for datastore for channel "
"%s\n", chan_name);
stir_datastore_free(stir_datastore);
return -1;
ast_channel_lock(ctx->chan);
chan_datastore = ast_channel_datastore_find(ctx->chan, &verification_ds_info, NULL);
if (chan_datastore) {
verifies = chan_datastore->data;
res = AST_VECTOR_APPEND(verifies, stir_datastore);
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);
ast_channel_datastore_add(ctx->chan, chan_datastore);
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_unlock(ctx->chan);
return 0;
return res;
}
/*!
@@ -166,15 +183,18 @@ int ast_stir_shaken_add_result_to_channel(
* \retval -1 on failure
* \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)
{
struct stir_datastore *stir_datastore;
struct stir_shaken_verification_ds *stir_datastore;
struct ast_datastore *chan_datastore;
struct verification_vector *verifies;
const char *chan_name;
char *parse;
char *first;
char *second;
unsigned int target_index, current_index = 0;
unsigned int target_index = 0;
int res = 0;
AST_DECLARE_APP_ARGS(args,
AST_APP_ARG(first_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);
return -1;
}
chan_name = ast_channel_name(chan);
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);
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;
}
@@ -207,16 +229,16 @@ static int func_read(struct ast_channel *chan, const char *function,
size_t count = 0;
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;
}
ast_channel_lock(chan);
AST_LIST_TRAVERSE(ast_channel_datastores(chan), chan_datastore, entry) {
if (chan_datastore->info != &stir_shaken_datastore_info) {
continue;
}
count++;
chan_datastore = ast_channel_datastore_find(chan, &verification_ds_info, NULL);
if (chan_datastore && chan_datastore->data) {
verifies = chan_datastore->data;
count = AST_VECTOR_SIZE(verifies);
}
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.
*/
if (ast_strlen_zero(second)) {
ast_log(LOG_ERROR, "Retrieving a value using %s requires two paramaters (index, value) "
"- only index was given\n", function);
ast_log(LOG_ERROR, "%s: Retrieving a value using %s requires two paramaters (index, value) "
"- only index was given\n", chan_name, function);
return -1;
}
if (ast_str_to_uint(first, &target_index)) {
ast_log(LOG_ERROR, "Failed to convert index %s to integer for function %s\n",
first, function);
ast_log(LOG_ERROR, "%s: Failed to convert index %s to integer for function %s\n",
chan_name, first, function);
return -1;
}
/* We don't store by uid for the datastore, so just search for the specified index */
ast_channel_lock(chan);
AST_LIST_TRAVERSE(ast_channel_datastores(chan), chan_datastore, entry) {
if (chan_datastore->info != &stir_shaken_datastore_info) {
continue;
}
if (current_index == target_index) {
break;
}
current_index++;
}
ast_channel_unlock(chan);
if (current_index != target_index || !chan_datastore) {
ast_log(LOG_WARNING, "No STIR/SHAKEN results for index '%s'\n", first);
chan_datastore = ast_channel_datastore_find(chan, &verification_ds_info, NULL);
if (!chan_datastore || !chan_datastore->data) {
ast_channel_unlock(chan);
ast_log(LOG_WARNING, "%s: No STIR/SHAKEN results for index '%s'\n",
chan_name, first);
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")) {
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")) {
ast_copy_string(buf, vs_response_code_to_str(stir_datastore->verify_result), len);
} 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;
}
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;
}
static struct ast_custom_function stir_shaken_function = {
static struct ast_custom_function stir_shaken_verification = {
.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)
@@ -285,12 +415,11 @@ static int reload_module(void)
static int unload_module(void)
{
int res = 0;
common_config_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;
}
@@ -371,7 +500,13 @@ static int load_module(void)
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) {
unload_module();
return AST_MODULE_LOAD_DECLINE;

View File

@@ -29,6 +29,25 @@
#define STIR_SHAKEN_PPT "shaken"
#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
*

View File

@@ -507,4 +507,38 @@
</example>
</description>
</function>
</docs>
<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>