asterisk/res/res_stir_shaken/attestation.c

444 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 <jwt.h>
#define _TRACE_PREFIX_ "a",__LINE__, ""
#include "asterisk.h"
#include "asterisk/module.h"
#include "asterisk/uuid.h"
#include "asterisk/json.h"
#include "asterisk/channel.h"
#include "stir_shaken.h"
static const char *as_rc_map[] = {
[AST_STIR_SHAKEN_AS_SUCCESS] = "success",
[AST_STIR_SHAKEN_AS_DISABLED] = "disabled",
[AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS] = "invalid_arguments",
[AST_STIR_SHAKEN_AS_MISSING_PARAMETERS] = "missing_parameters",
[AST_STIR_SHAKEN_AS_INTERNAL_ERROR] = "internal_error",
[AST_STIR_SHAKEN_AS_NO_TN_FOR_CALLERID] = "no_tn_for_callerid",
[AST_STIR_SHAKEN_AS_NO_PRIVATE_KEY_AVAIL] = "no_private_key_avail",
[AST_STIR_SHAKEN_AS_NO_PUBLIC_CERT_URL_AVAIL] = "no_public_cert_url_avail",
[AST_STIR_SHAKEN_AS_NO_ATTEST_LEVEL] = "no_attest_level",
[AST_STIR_SHAKEN_AS_IDENTITY_HDR_EXISTS] = "identity_header_exists",
[AST_STIR_SHAKEN_AS_NO_TO_HDR] = "no_to_hdr",
[AST_STIR_SHAKEN_AS_TO_HDR_BAD_URI] = "to_hdr_bad_uri",
[AST_STIR_SHAKEN_AS_SIGN_ENCODE_FAILURE] "sign_encode_failure",
};
const char *as_response_code_to_str(
enum ast_stir_shaken_as_response_code as_rc)
{
return ARRAY_IN_BOUNDS(as_rc, as_rc_map) ?
as_rc_map[as_rc] : NULL;
}
static void ctx_destructor(void *obj)
{
struct ast_stir_shaken_as_ctx *ctx = obj;
ao2_cleanup(ctx->etn);
ast_channel_cleanup(ctx->chan);
ast_string_field_free_memory(ctx);
AST_VECTOR_RESET(&ctx->fingerprints, ast_free);
AST_VECTOR_FREE(&ctx->fingerprints);
}
enum ast_stir_shaken_as_response_code
ast_stir_shaken_as_ctx_create(const char *orig_tn,
const char *dest_tn, struct ast_channel *chan,
const char *profile_name,
const char *tag, struct ast_stir_shaken_as_ctx **ctxout)
{
RAII_VAR(struct ast_stir_shaken_as_ctx *, ctx, NULL, ao2_cleanup);
RAII_VAR(struct profile_cfg *, eprofile, NULL, ao2_cleanup);
RAII_VAR(struct attestation_cfg *, as_cfg, NULL, ao2_cleanup);
RAII_VAR(struct tn_cfg *, etn, NULL, ao2_cleanup);
SCOPE_ENTER(3, "%s: Enter\n", tag);
if (ast_strlen_zero(orig_tn)) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS,
LOG_ERROR, "%s: Must provide caller_id/orig_tn\n", tag);
}
if (ast_strlen_zero(dest_tn)) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS,
LOG_ERROR, "%s: Must provide dest_tn\n", tag);
}
if (ast_strlen_zero(tag)) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS,
LOG_ERROR, "%s: Must provide tag\n", tag);
}
if (!ctxout) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS,
LOG_ERROR, "%s: Must provide ctxout\n", tag);
}
if (ast_strlen_zero(profile_name)) {
SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_DISABLED,
"%s: Disabled due to missing profile name\n", tag);
}
as_cfg = as_get_cfg();
if (as_cfg->global_disable) {
SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_DISABLED,
"%s: Globally disabled\n", tag);
}
eprofile = eprofile_get_cfg(profile_name);
if (!eprofile) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_DISABLED,
LOG_ERROR, "%s: No profile for profile name '%s'. Call will continue\n", tag,
profile_name);
}
if (!PROFILE_ALLOW_ATTEST(eprofile)) {
SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_DISABLED,
"%s: Disabled by profile\n", tag);
}
etn = tn_get_etn(orig_tn, eprofile);
if (!etn) {
SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_DISABLED,
"%s: No tn for orig_tn '%s'\n", tag, orig_tn);
}
/* We don't need eprofile or as_cfg anymore so let's clean em up */
ao2_cleanup(as_cfg);
as_cfg = NULL;
ao2_cleanup(eprofile);
eprofile = NULL;
if (etn->acfg_common.attest_level == attest_level_NOT_SET) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_MISSING_PARAMETERS,
LOG_ERROR,
"'%s': No attest_level specified in tn, profile or attestation objects\n",
tag);
}
if (ast_strlen_zero(etn->acfg_common.public_cert_url)) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_NO_PUBLIC_CERT_URL_AVAIL,
LOG_ERROR, "%s: No public cert url in tn %s, profile or attestation objects\n",
tag, orig_tn);
}
if (etn->acfg_common.raw_key_length == 0) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_NO_PRIVATE_KEY_AVAIL,
LOG_ERROR, "%s: No private key in tn %s, profile or attestation objects\n",
orig_tn, tag);
}
ctx = ao2_alloc_options(sizeof(*ctx), ctx_destructor,
AO2_ALLOC_OPT_LOCK_NOLOCK);
if (!ctx) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR,
LOG_ERROR, "%s: Unable to allocate memory for ctx\n", tag);
}
if (ast_string_field_init(ctx, 1024) != 0) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR,
LOG_ERROR, "%s: Unable to allocate memory for ctx\n", tag);
}
if (ast_string_field_set(ctx, tag, tag) != 0) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR,
LOG_ERROR, "%s: Unable to allocate memory for ctx\n", tag);
}
if (ast_string_field_set(ctx, orig_tn, orig_tn) != 0) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR,
LOG_ERROR, "%s: Unable to allocate memory for ctx\n", tag);
}
if (ast_string_field_set(ctx, dest_tn, dest_tn)) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR,
LOG_ERROR, "%s: Unable to allocate memory for ctx\n", tag);
}
ctx->chan = chan;
ast_channel_ref(ctx->chan);
if (AST_VECTOR_INIT(&ctx->fingerprints, 1) != 0) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR,
LOG_ERROR, "%s: Unable to allocate memory for ctx\n", tag);
}
/* Transfer the references */
ctx->etn = etn;
etn = NULL;
*ctxout = ctx;
ctx = NULL;
SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_SUCCESS, "%s: Done\n", tag);
}
int ast_stir_shaken_as_ctx_wants_fingerprints(struct ast_stir_shaken_as_ctx *ctx)
{
return ENUM_BOOL(ctx->etn->acfg_common.send_mky, send_mky);
}
enum ast_stir_shaken_as_response_code
ast_stir_shaken_as_ctx_add_fingerprint(
struct ast_stir_shaken_as_ctx *ctx, const char *alg, const char *fingerprint)
{
char *compacted_fp = ast_alloca(strlen(fingerprint) + 1);
const char *f = fingerprint;
char *fp = compacted_fp;
char *combined;
int rc;
SCOPE_ENTER(4, "%s: Add fingerprint %s:%s\n", ctx ? ctx->tag : "",
alg, fingerprint);
if (!ctx || ast_strlen_zero(alg) || ast_strlen_zero(fingerprint)) {
SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS,
"%s: Missing arguments\n", ctx->tag);
}
if (!ENUM_BOOL(ctx->etn->acfg_common.send_mky, send_mky)) {
SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_DISABLED,
"%s: Not needed\n", ctx->tag);
}
/* De-colonize */
while (*f != '\0') {
if (*f != ':') {
*fp++ = *f;
}
f++;
}
*fp = '\0';
rc = ast_asprintf(&combined, "%s:%s", alg, compacted_fp);
if (rc < 0) {
SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR,
"%s: Can't allocate memory for comobined string\n", ctx->tag);
}
rc = AST_VECTOR_ADD_SORTED(&ctx->fingerprints, combined, strcasecmp);
if (rc < 0) {
SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR,
"%s: Can't add entry to vector\n", ctx->tag);
}
SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_SUCCESS,
"%s: Done\n", ctx->tag);
}
/*
* We have to construct the PASSporT payload manually instead of
* using ast_json_pack. These macros help make sure nothing
* leaks if there are errors creating the individual objects.
*/
#define CREATE_JSON_SET_OBJ(__val, __obj, __name) \
({ \
struct ast_json *__var; \
if (!(__var = __val)) {\
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, \
LOG_ERROR, "%s: Cannot allocate one of the JSON objects\n", \
ctx->tag); \
} else { \
if (ast_json_object_set(__obj, __name, __var)) { \
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, \
LOG_ERROR, "%s: Cannot set one of the JSON objects\n", \
ctx->tag); \
} \
} \
(__var); \
})
#define CREATE_JSON_APPEND_ARRAY(__val, __obj) \
({ \
struct ast_json *__var; \
if (!(__var = __val)) {\
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, \
LOG_ERROR, "%s: Cannot allocate one of the JSON objects\n", \
ctx->tag); \
} else { \
if (ast_json_array_append(__obj, __var)) { \
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, \
LOG_ERROR, "%s: Cannot set one of the JSON objects\n", \
ctx->tag); \
} \
} \
(__var); \
})
static enum ast_stir_shaken_as_response_code pack_payload(
struct ast_stir_shaken_as_ctx *ctx, jwt_t *jwt)
{
RAII_VAR(struct ast_json *, payload, ast_json_object_create(), ast_json_unref);
/*
* These don't need RAII because once they're added to payload,
* they'll get destroyed when payload gets unreffed.
*/
struct ast_json *dest;
struct ast_json *tns;
struct ast_json *orig;
char origid[AST_UUID_STR_LEN];
char *payload_str = NULL;
SCOPE_ENTER(3, "%s: Enter\n", ctx->tag);
/*
* All fields added need to be in alphabetical order
* and there must be no whitespace in the result.
*
* We can't use ast_json_pack here because the entries
* need to be kept in order and the "mky" array may
* not be present.
*/
/*
* The order of the calls matters. We want to add an object
* to its parent as soon as it's created, then add things
* to it. This way if something later fails, the whole thing
* will get destroyed when its parent gets destroyed.
*/
CREATE_JSON_SET_OBJ(ast_json_string_create(
attest_level_to_str(ctx->etn->acfg_common.attest_level)),
payload, "attest");
dest = CREATE_JSON_SET_OBJ(ast_json_object_create(), payload, "dest");
tns = CREATE_JSON_SET_OBJ(ast_json_array_create(), dest, "tn");
CREATE_JSON_APPEND_ARRAY(ast_json_string_create(ctx->dest_tn), tns);
CREATE_JSON_SET_OBJ(ast_json_integer_create(time(NULL)), payload, "iat");
if (AST_VECTOR_SIZE(&ctx->fingerprints)
&& ENUM_BOOL(ctx->etn->acfg_common.send_mky, send_mky)) {
struct ast_json *mky;
int i;
mky = CREATE_JSON_SET_OBJ(ast_json_array_create(), payload, "mky");
for (i = 0; i < AST_VECTOR_SIZE(&ctx->fingerprints); i++) {
struct ast_json *mk;
char *afp = AST_VECTOR_GET(&ctx->fingerprints, i);
char *fp = strchr(afp, ':');
*fp++ = '\0';
mk = CREATE_JSON_APPEND_ARRAY(ast_json_object_create(), mky);
CREATE_JSON_SET_OBJ(ast_json_string_create(afp), mk, "alg");
CREATE_JSON_SET_OBJ(ast_json_string_create(fp), mk, "dig");
}
}
orig = CREATE_JSON_SET_OBJ(ast_json_object_create(), payload, "orig");
CREATE_JSON_SET_OBJ(ast_json_string_create(ctx->orig_tn), orig, "tn");
ast_uuid_generate_str(origid, sizeof(origid));
CREATE_JSON_SET_OBJ(ast_json_string_create(origid), payload, "origid");
payload_str = ast_json_dump_string_format(payload, AST_JSON_COMPACT);
ast_trace(2, "Payload: %s\n", payload_str);
jwt_add_grants_json(jwt, payload_str);
ast_json_free(payload_str);
SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_SUCCESS, "Done\n");
}
enum ast_stir_shaken_as_response_code ast_stir_shaken_attest(
struct ast_stir_shaken_as_ctx *ctx, char **header)
{
RAII_VAR(jwt_t *, jwt, NULL, jwt_free);
jwt_alg_t alg;
char *encoded = NULL;
enum ast_stir_shaken_as_response_code as_rc;
int rc = 0;
SCOPE_ENTER(3, "%s: Attestation: orig: %s dest: %s\n",
ctx ? ctx->tag : "NULL", ctx ? ctx->orig_tn : "NULL",
ctx ? ctx->dest_tn : "NULL");
if (!ctx) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, LOG_ERROR,
"%s: No context object!\n", "NULL");
}
if (header == NULL) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS,
LOG_ERROR, "%s: Header buffer was NULL\n", ctx->tag);
}
rc = jwt_new(&jwt);
if (rc != 0) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR,
LOG_ERROR, "%s: Cannot create JWT\n", ctx->tag);
}
/*
* All headers added need to be in alphabetical order!
*/
alg = jwt_str_alg(STIR_SHAKEN_ENCRYPTION_ALGORITHM);
jwt_set_alg(jwt, alg, (const unsigned char *)ctx->etn->acfg_common.raw_key,
ctx->etn->acfg_common.raw_key_length);
jwt_add_header(jwt, "ppt", STIR_SHAKEN_PPT);
jwt_add_header(jwt, "typ", STIR_SHAKEN_TYPE);
jwt_add_header(jwt, "x5u", ctx->etn->acfg_common.public_cert_url);
as_rc = pack_payload(ctx, jwt);
if (as_rc != AST_STIR_SHAKEN_AS_SUCCESS) {
SCOPE_EXIT_LOG_RTN_VALUE(as_rc,
LOG_ERROR, "%s: Cannot pack payload\n", ctx->tag);
}
encoded = jwt_encode_str(jwt);
if (!encoded) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_SIGN_ENCODE_FAILURE,
LOG_ERROR, "%s: Unable to sign/encode JWT\n", ctx->tag);
}
rc = ast_asprintf(header, "%s;info=<%s>;alg=%s;ppt=%s",
encoded, ctx->etn->acfg_common.public_cert_url, jwt_alg_str(alg),
STIR_SHAKEN_PPT);
ast_std_free(encoded);
if (rc < 0) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR,
LOG_ERROR, "%s: Unable to allocate memory for identity header\n",
ctx->tag);
}
SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_SUCCESS, "%s: Done\n", ctx->tag);
}
int as_reload()
{
as_config_reload();
return 0;
}
int as_unload()
{
as_config_unload();
return 0;
}
int as_load()
{
if (as_config_load()) {
return AST_MODULE_LOAD_DECLINE;
}
return AST_MODULE_LOAD_SUCCESS;
}