Files
asterisk/res/res_stir_shaken/common_config.c
George Joseph 6e9c33caad res_stir_shaken: Add "ignore_sip_date_header" config option.
UserNote: A new STIR/SHAKEN verification option "ignore_sip_date_header" has
been added that when set to true, will cause the verification process to
not consider a missing or invalid SIP "Date" header to be a failure.  This
will make the IAT the sole "truth" for Date in the verification process.
The option can be set in the "verification" and "profile" sections of
stir_shaken.conf.

Also fixed a bug in the port match logic.

Resolves: #1251
Resolves: #1271
2025-06-18 15:26:42 +00:00

465 lines
13 KiB
C

/*
* Asterisk -- An open source telephony toolkit.
*
* Copyright (C) 2023, Sangoma Technologies Corporation
*
* George Joseph <gjoseph@sangoma.com>
*
* See http://www.asterisk.org for more information about
* the Asterisk project. Please do not directly contact
* any of the maintainers of this project for assistance;
* the project provides a web site, mailing lists and IRC
* channels for your use.
*
* This program is free software, distributed under the terms of
* the GNU General Public License Version 2. See the LICENSE file
* at the top of the source tree.
*/
#include "asterisk.h"
#include "asterisk/cli.h"
#include "asterisk/cli.h"
#include "asterisk/logger.h"
#include "asterisk/module.h"
#include "asterisk/utils.h"
#include "asterisk/stasis.h"
#include "asterisk/security_events.h"
#define AST_API_MODULE
#include "stir_shaken.h"
static struct ast_sorcery *sorcery;
struct stasis_subscription *named_acl_changed_sub = NULL;
struct ast_sorcery *get_sorcery(void)
{
return sorcery;
}
#define generate_bool_handler_functions(param_name) \
static const char *param_name ## _map[] = { \
[ param_name ## _NOT_SET ] = "not_set", \
[ param_name ## _YES ] = "yes", \
[ param_name ## _NO ] = "no", \
}; \
enum param_name ## _enum \
param_name ## _from_str(const char *value) \
{ \
if (!strcasecmp(value, param_name ## _map[param_name ## _NOT_SET])) { \
return param_name ## _NOT_SET; \
} else if (ast_true(value)) { \
return param_name ## _YES; \
} else if (ast_false(value)) { \
return param_name ## _NO; \
} \
ast_log(LOG_WARNING, "Unknown " #param_name " response value '%s'\n", value); \
return param_name ## _UNKNOWN; \
}\
const char *param_name ## _to_str(enum param_name ## _enum value) \
{ \
return ARRAY_IN_BOUNDS(value, param_name ## _map) ? \
param_name ## _map[value] : NULL; \
}
generate_bool_handler_functions(use_rfc9410_responses);
generate_bool_handler_functions(ignore_sip_date_header);
generate_bool_handler_functions(send_mky);
generate_bool_handler_functions(check_tn_cert_public_url);
generate_bool_handler_functions(relax_x5u_port_scheme_restrictions);
generate_bool_handler_functions(relax_x5u_path_restrictions);
generate_bool_handler_functions(load_system_certs);
struct enum_name_xref_entry {
int value;
const char *name;
};
#define generate_enum_string_functions(param_name, default_value, ...)\
static struct enum_name_xref_entry param_name ## _map[] = { \
__VA_ARGS__ \
} ; \
enum param_name ## _enum param_name ## _from_str( \
const char *value) \
{ \
int i; \
for (i = 0; i < ARRAY_LEN(param_name ## _map); i++) { \
if (strcasecmp(value, param_name ##_map[i].name) == 0) { \
return param_name ##_map[i].value; \
} \
} \
return param_name ## _ ## default_value; \
} \
const char *param_name ## _to_str( \
enum param_name ## _enum value) \
{ \
int i; \
for (i = 0; i < ARRAY_LEN(param_name ## _map); i++) { \
if (value == param_name ## _map[i].value) return param_name ## _map[i].name; \
} \
return NULL; \
}
generate_enum_string_functions(attest_level, UNKNOWN,
{attest_level_NOT_SET, "not_set"},
{attest_level_A, "A"},
{attest_level_B, "B"},
{attest_level_C, "C"},
);
generate_enum_string_functions(endpoint_behavior, OFF,
{endpoint_behavior_OFF, "off"},
{endpoint_behavior_OFF, "none"},
{endpoint_behavior_ATTEST, "attest"},
{endpoint_behavior_VERIFY, "verify"},
{endpoint_behavior_ON, "on"},
{endpoint_behavior_ON, "both"}
);
generate_enum_string_functions(stir_shaken_failure_action, CONTINUE,
{stir_shaken_failure_action_CONTINUE, "continue"},
{stir_shaken_failure_action_REJECT_REQUEST, "reject_request"},
{stir_shaken_failure_action_CONTINUE_RETURN_REASON, "continue_return_reason"},
);
static const char *translate_value(const char *val)
{
if (val[0] == '0'
|| val[0] == '\0'
|| strcmp(val, "not_set") == 0) {
return "";
}
return val;
}
static void print_acl(int fd, struct ast_acl_list *acl_list, const char *prefix)
{
struct ast_acl *acl;
AST_LIST_LOCK(acl_list);
AST_LIST_TRAVERSE(acl_list, acl, list) {
if (ast_strlen_zero(acl->name)) {
ast_cli(fd, "%s(permit/deny)\n", prefix);
} else {
ast_cli(fd, "%s%s\n", prefix, acl->name);
}
ast_ha_output(fd, acl->acl, prefix);
}
AST_LIST_UNLOCK(acl_list);
}
#define print_acl_cert_store(cfg, a, max_name_len) \
({ \
if (cfg->vcfg_common.acl) { \
ast_cli(a->fd, "x5u_acl:\n"); \
print_acl(a->fd, cfg->vcfg_common.acl, " "); \
} else { \
ast_cli(a->fd, "%-*s: (none)\n", max_name_len, "x5u_acl"); \
}\
if (cfg->vcfg_common.tcs) { \
int count = 0; \
ast_cli(a->fd, "%-*s:\n", max_name_len, "Verification CA certificate store"); \
count = crypto_show_cli_store(cfg->vcfg_common.tcs, a->fd); \
if (count == 0 && (!ast_strlen_zero(cfg->vcfg_common.ca_path) \
|| !ast_strlen_zero(cfg->vcfg_common.crl_path))) { \
ast_cli(a->fd, " Note: Certs in ca_path or crl_path won't show until used.\n"); \
} \
} else { \
ast_cli(a->fd, "%-*s: (none)\n", max_name_len, "Verification CA certificate store"); \
} \
})
int config_object_cli_show(void *obj, void *arg, void *data, int flags)
{
struct ast_cli_args *a = arg;
struct config_object_cli_data *cli_data = data;
struct ast_variable *options;
struct ast_variable *i;
const char *title = NULL;
const char *cfg_name = NULL;
int max_name_len = 0;
if (!obj) {
ast_cli(a->fd, "No stir/shaken configuration found\n");
return 0;
}
if (!ast_strlen_zero(cli_data->title)) {
title = cli_data->title;
} else {
title = ast_sorcery_object_get_type(obj);
}
max_name_len = strlen(title);
if (cli_data->object_type == config_object_type_profile
|| cli_data->object_type == config_object_type_tn) {
cfg_name = ast_sorcery_object_get_id(obj);
max_name_len += strlen(cfg_name) + 2 /* ": " */;
}
options = ast_variable_list_sort(ast_sorcery_objectset_create2(
get_sorcery(), obj, AST_HANDLER_ONLY_STRING));
if (!options) {
return 0;
}
for (i = options; i; i = i->next) {
int nlen = strlen(i->name);
max_name_len = (nlen > max_name_len) ? nlen : max_name_len;
}
ast_cli(a->fd, "\n==============================================================================\n");
if (ast_strlen_zero(cfg_name)) {
ast_cli(a->fd, "%s\n", title);
} else {
ast_cli(a->fd, "%s: %s\n", title, cfg_name);
}
ast_cli(a->fd, "------------------------------------------------------------------------------\n");
for (i = options; i; i = i->next) {
if (!ast_strings_equal(i->name, "x5u_acl")) {
ast_cli(a->fd, "%-*s: %s\n", max_name_len, i->name,
translate_value(i->value));
}
}
ast_variables_destroy(options);
if (cli_data->object_type == config_object_type_profile) {
struct profile_cfg *cfg = obj;
print_acl_cert_store(cfg, a, max_name_len);
} else if (cli_data->object_type == config_object_type_verification) {
struct verification_cfg *cfg = obj;
print_acl_cert_store(cfg, a, max_name_len);
}
ast_cli(a->fd, "---------------------------------------------\n\n"); \
return 0;
}
char *config_object_tab_complete_name(const char *word, struct ao2_container *container)
{
void *obj;
struct ao2_iterator it;
int wordlen = strlen(word);
int ret;
it = ao2_iterator_init(container, 0);
while ((obj = ao2_iterator_next(&it))) {
if (!strncasecmp(word, ast_sorcery_object_get_id(obj), wordlen)) {
ret = ast_cli_completion_add(ast_strdup(ast_sorcery_object_get_id(obj)));
if (ret) {
ao2_ref(obj, -1);
break;
}
}
ao2_ref(obj, -1);
}
ao2_iterator_destroy(&it);
return NULL;
}
/* Remove everything except 0-9, *, and # in telephone number according to RFC 8224
* (required by RFC 8225 as part of canonicalization) */
char *canonicalize_tn(const char *tn, char *dest_tn)
{
int i;
const char *s = tn;
size_t len = tn ? strlen(tn) : 0;
char *new_tn = dest_tn;
SCOPE_ENTER(3, "tn: %s\n", S_OR(tn, "(null)"));
if (ast_strlen_zero(tn)) {
*dest_tn = '\0';
SCOPE_EXIT_RTN_VALUE(NULL, "Empty TN\n");
}
if (!dest_tn) {
SCOPE_EXIT_RTN_VALUE(NULL, "No destination buffer\n");
}
for (i = 0; i < len; i++) {
if (isdigit(*s) || *s == '#' || *s == '*') { /* Only characters allowed */
*new_tn++ = *s;
}
s++;
}
*new_tn = '\0';
SCOPE_EXIT_RTN_VALUE(dest_tn, "Canonicalized '%s' -> '%s'\n", tn, dest_tn);
}
char *canonicalize_tn_alloc(const char *tn)
{
char *canon_tn = ast_strlen_zero(tn) ? NULL : ast_malloc(strlen(tn) + 1);
if (!canon_tn) {
return NULL;
}
return canonicalize_tn(tn, canon_tn);
}
static char *cli_verify_cert(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
{
RAII_VAR(struct profile_cfg *, profile, NULL, ao2_cleanup);
RAII_VAR(struct verification_cfg *, vs_cfg, NULL, ao2_cleanup);
struct crypto_cert_store *tcs;
X509 *cert = NULL;
const char *errmsg = NULL;
switch(cmd) {
case CLI_INIT:
e->command = "stir_shaken verify certificate_file";
e->usage =
"Usage: stir_shaken verify certificate_file <certificate_file> [ <profile> ]\n"
" Verify an external certificate file against the global or profile verification store\n";
return NULL;
case CLI_GENERATE:
if (a->pos == 4) {
return config_object_tab_complete_name(a->word, profile_get_all());
} else {
return NULL;
}
}
if (a->argc < 4) {
return CLI_SHOWUSAGE;
}
if (a->argc == 5) {
profile = profile_get_cfg(a->argv[4]);
if (!profile) {
ast_cli(a->fd, "Profile %s doesn't exist\n", a->argv[4]);
return CLI_SUCCESS;
}
if (!profile->vcfg_common.tcs) {
ast_cli(a->fd,"Profile %s doesn't have a certificate store\n", a->argv[4]);
return CLI_SUCCESS;
}
tcs = profile->vcfg_common.tcs;
} else {
vs_cfg = vs_get_cfg();
if (!vs_cfg) {
ast_cli(a->fd, "No verification store found\n");
return CLI_SUCCESS;
}
tcs = vs_cfg->vcfg_common.tcs;
}
cert = crypto_load_cert_from_file(a->argv[3]);
if (!cert) {
ast_cli(a->fd, "Failed to load certificate from %s. See log for details\n", a->argv[3]);
return CLI_SUCCESS;
}
if (crypto_is_cert_trusted(tcs, cert, &errmsg)) {
ast_cli(a->fd, "Certificate %s trusted\n", a->argv[3]);
} else {
ast_cli(a->fd, "Certificate %s NOT trusted: %s\n", a->argv[3], errmsg);
}
X509_free(cert);
return CLI_SUCCESS;
}
static struct ast_cli_entry cli_commands[] = {
AST_CLI_DEFINE(cli_verify_cert, "Verify a certificate file against the global or a profile verification store"),
};
int common_config_reload(void)
{
SCOPE_ENTER(2, "Stir Shaken Reload\n");
if (vs_reload()) {
SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken VS Reload failed\n");
}
if (as_reload()) {
SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken AS Reload failed\n");
}
if (tn_config_reload()) {
SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken TN Reload failed\n");
}
if (profile_reload()) {
SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken Profile Reload failed\n");
}
SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_SUCCESS, "Stir Shaken Reload Done\n");
}
int common_config_unload(void)
{
ast_cli_unregister_multiple(cli_commands, ARRAY_LEN(cli_commands));
profile_unload();
tn_config_unload();
as_unload();
vs_unload();
if (named_acl_changed_sub) {
stasis_unsubscribe(named_acl_changed_sub);
named_acl_changed_sub = NULL;
}
ast_sorcery_unref(sorcery);
sorcery = NULL;
return 0;
}
static void named_acl_changed_cb(void *data,
struct stasis_subscription *sub, struct stasis_message *message)
{
if (stasis_message_type(message) != ast_named_acl_change_type()) {
return;
}
ast_log(LOG_NOTICE, "Named acl changed. Reloading verification and profile\n");
common_config_reload();
}
int common_config_load(void)
{
SCOPE_ENTER(2, "Stir Shaken Load\n");
if (!(sorcery = ast_sorcery_open())) {
common_config_unload();
SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken sorcery load failed\n");
}
if (vs_load()) {
common_config_unload();
SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken VS load failed\n");
}
if (as_load()) {
common_config_unload();
SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken AS load failed\n");
}
if (tn_config_load()) {
common_config_unload();
SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken TN load failed\n");
}
if (profile_load()) {
common_config_unload();
SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken profile load failed\n");
}
if (!named_acl_changed_sub) {
named_acl_changed_sub = stasis_subscribe(ast_security_topic(),
named_acl_changed_cb, NULL);
if (!named_acl_changed_sub) {
common_config_unload();
SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken acl change subscribe failed\n");
}
stasis_subscription_accept_message_type(
named_acl_changed_sub, ast_named_acl_change_type());
}
ast_cli_register_multiple(cli_commands, ARRAY_LEN(cli_commands));
SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_SUCCESS, "Stir Shaken Load Done\n");
}