mirror of
https://github.com/asterisk/asterisk.git
synced 2025-09-03 11:25:35 +00:00
478 lines
12 KiB
C
478 lines
12 KiB
C
/*
|
|
* Asterisk -- An open source telephony toolkit.
|
|
*
|
|
* Copyright (C) 2020, Sangoma Technologies Corporation
|
|
*
|
|
* Kevin Harwell <kharwell@digium.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.
|
|
*/
|
|
|
|
/*** MODULEINFO
|
|
<depend>crypto</depend>
|
|
<support_level>core</support_level>
|
|
***/
|
|
|
|
#include "asterisk.h"
|
|
|
|
#include "asterisk/module.h"
|
|
#include "asterisk/sorcery.h"
|
|
#include "asterisk/time.h"
|
|
#include "asterisk/json.h"
|
|
|
|
#include "asterisk/res_stir_shaken.h"
|
|
#include "res_stir_shaken/stir_shaken.h"
|
|
#include "res_stir_shaken/general.h"
|
|
#include "res_stir_shaken/store.h"
|
|
#include "res_stir_shaken/certificate.h"
|
|
|
|
#define STIR_SHAKEN_ENCRYPTION_ALGORITHM "ES256"
|
|
#define STIR_SHAKEN_PPT "shaken"
|
|
#define STIR_SHAKEN_TYPE "passport"
|
|
|
|
static struct ast_sorcery *stir_shaken_sorcery;
|
|
|
|
struct ast_stir_shaken_payload {
|
|
/*! The JWT header */
|
|
struct ast_json *header;
|
|
/*! The JWT payload */
|
|
struct ast_json *payload;
|
|
/*! Signature for the payload */
|
|
unsigned char *signature;
|
|
/*! The algorithm used */
|
|
char *algorithm;
|
|
/*! THe URL to the public key for the certificate */
|
|
char *public_key_url;
|
|
};
|
|
|
|
struct ast_sorcery *ast_stir_shaken_sorcery(void)
|
|
{
|
|
return stir_shaken_sorcery;
|
|
}
|
|
|
|
void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload)
|
|
{
|
|
if (!payload) {
|
|
return;
|
|
}
|
|
|
|
ast_json_unref(payload->header);
|
|
ast_json_unref(payload->payload);
|
|
ast_free(payload->algorithm);
|
|
ast_free(payload->public_key_url);
|
|
ast_free(payload->signature);
|
|
|
|
ast_free(payload);
|
|
}
|
|
|
|
/*!
|
|
* \brief Verifies the necessary contents are in the JSON and returns a
|
|
* ast_stir_shaken_payload with the extracted values.
|
|
*
|
|
* \param json The JSON to verify
|
|
*
|
|
* \return ast_stir_shaken_payload on success
|
|
* \return NULL on failure
|
|
*/
|
|
static struct ast_stir_shaken_payload *stir_shaken_verify_json(struct ast_json *json)
|
|
{
|
|
struct ast_stir_shaken_payload *payload;
|
|
struct ast_json *obj;
|
|
const char *val;
|
|
|
|
payload = ast_calloc(1, sizeof(*payload));
|
|
if (!payload) {
|
|
ast_log(LOG_ERROR, "Failed to allocate STIR_SHAKEN payload\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Look through the header first */
|
|
obj = ast_json_object_get(json, "header");
|
|
if (!obj) {
|
|
ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have the required field 'header'\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
payload->header = ast_json_deep_copy(obj);
|
|
if (!payload->header) {
|
|
ast_log(LOG_ERROR, "STIR_SHAKEN payload failed to copy 'header'\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Check the ppt value for "shaken" */
|
|
val = ast_json_string_get(ast_json_object_get(obj, "ppt"));
|
|
if (ast_strlen_zero(val)) {
|
|
ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have the required field 'ppt'\n");
|
|
goto cleanup;
|
|
}
|
|
if (strcmp(val, STIR_SHAKEN_PPT)) {
|
|
ast_log(LOG_ERROR, "STIR/SHAKEN JWT field 'ppt' did not have "
|
|
"required value '%s' (was '%s')\n", STIR_SHAKEN_PPT, val);
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Check the typ value for "passport" */
|
|
val = ast_json_string_get(ast_json_object_get(obj, "typ"));
|
|
if (ast_strlen_zero(val)) {
|
|
ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have the required field 'typ'\n");
|
|
goto cleanup;
|
|
}
|
|
if (strcmp(val, STIR_SHAKEN_TYPE)) {
|
|
ast_log(LOG_ERROR, "STIR/SHAKEN JWT field 'typ' did not have "
|
|
"required value '%s' (was '%s')\n", STIR_SHAKEN_TYPE, val);
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Check the alg value for "ES256" */
|
|
val = ast_json_string_get(ast_json_object_get(obj, "alg"));
|
|
if (ast_strlen_zero(val)) {
|
|
ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have required field 'alg'\n");
|
|
goto cleanup;
|
|
}
|
|
if (strcmp(val, STIR_SHAKEN_ENCRYPTION_ALGORITHM)) {
|
|
ast_log(LOG_ERROR, "STIR/SHAKEN JWT field 'alg' did not have "
|
|
"required value '%s' (was '%s')\n", STIR_SHAKEN_ENCRYPTION_ALGORITHM, val);
|
|
goto cleanup;
|
|
}
|
|
|
|
payload->algorithm = ast_strdup(val);
|
|
if (!payload->algorithm) {
|
|
ast_log(LOG_ERROR, "STIR/SHAKEN payload failed to copy 'algorithm'\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Now let's check the payload section */
|
|
obj = ast_json_object_get(json, "payload");
|
|
if (!obj) {
|
|
ast_log(LOG_ERROR, "STIR/SHAKEN payload JWT did not have required field 'payload'\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Check the orig tn value for not NULL */
|
|
val = ast_json_string_get(ast_json_object_get(ast_json_object_get(obj, "orig"), "tn"));
|
|
if (ast_strlen_zero(val)) {
|
|
ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have required field 'orig->tn'\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Payload seems sane. Copy it and return on success */
|
|
payload->payload = ast_json_deep_copy(obj);
|
|
if (!payload->payload) {
|
|
ast_log(LOG_ERROR, "STIR/SHAKEN payload failed to copy 'payload'\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
return payload;
|
|
|
|
cleanup:
|
|
ast_stir_shaken_payload_free(payload);
|
|
return NULL;
|
|
}
|
|
|
|
/*!
|
|
* \brief Signs the payload and returns the signature.
|
|
*
|
|
* \param json_str The string representation of the JSON
|
|
* \param private_key The private key used to sign the payload
|
|
*
|
|
* \retval signature on success
|
|
* \retval NULL on failure
|
|
*/
|
|
static unsigned char *stir_shaken_sign(char *json_str, EVP_PKEY *private_key)
|
|
{
|
|
EVP_MD_CTX *mdctx = NULL;
|
|
int ret = 0;
|
|
unsigned char *encoded_signature = NULL;
|
|
unsigned char *signature = NULL;
|
|
size_t encoded_length = 0;
|
|
size_t signature_length = 0;
|
|
|
|
mdctx = EVP_MD_CTX_create();
|
|
if (!mdctx) {
|
|
ast_log(LOG_ERROR, "Failed to create Message Digest Context\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
ret = EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, private_key);
|
|
if (ret != 1) {
|
|
ast_log(LOG_ERROR, "Failed to initialize Message Digest Context\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
ret = EVP_DigestSignUpdate(mdctx, json_str, strlen(json_str));
|
|
if (ret != 1) {
|
|
ast_log(LOG_ERROR, "Failed to update Message Digest Context\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
ret = EVP_DigestSignFinal(mdctx, NULL, &signature_length);
|
|
if (ret != 1) {
|
|
ast_log(LOG_ERROR, "Failed initial phase of Message Digest Context signing\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
signature = ast_calloc(1, sizeof(unsigned char) * signature_length);
|
|
if (!signature) {
|
|
ast_log(LOG_ERROR, "Failed to allocate space for signature\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
ret = EVP_DigestSignFinal(mdctx, signature, &signature_length);
|
|
if (ret != 1) {
|
|
ast_log(LOG_ERROR, "Failed final phase of Message Digest Context signing\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
/* There are 6 bits to 1 base64 digit, so in order to get the size of the base64 encoded
|
|
* signature, we need to multiply by the number of bits in a byte and divide by 6. Since
|
|
* there's rounding when doing base64 conversions, add 3 bytes, just in case, and account
|
|
* for padding. Add another byte for the NULL-terminator so we don't lose data.
|
|
*/
|
|
encoded_length = ((signature_length * 4 / 3 + 3) & ~3) + 1;
|
|
encoded_signature = ast_calloc(1, encoded_length);
|
|
if (!encoded_signature) {
|
|
ast_log(LOG_ERROR, "Failed to allocate space for encoded signature\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
ast_base64encode((char *)encoded_signature, signature, signature_length, encoded_length);
|
|
|
|
cleanup:
|
|
if (mdctx) {
|
|
EVP_MD_CTX_destroy(mdctx);
|
|
}
|
|
ast_free(signature);
|
|
|
|
return encoded_signature;
|
|
}
|
|
|
|
/*!
|
|
* \brief Adds the 'x5u' (public key URL) field to the JWT.
|
|
*
|
|
* \param json The JWT
|
|
* \param x5u The public key URL
|
|
*
|
|
* \retval 0 on success
|
|
* \retval -1 on failure
|
|
*/
|
|
static int stir_shaken_add_x5u(struct ast_json *json, const char *x5u)
|
|
{
|
|
struct ast_json *value;
|
|
|
|
value = ast_json_string_create(x5u);
|
|
if (!value) {
|
|
return -1;
|
|
}
|
|
|
|
return ast_json_object_set(ast_json_object_get(json, "header"), "x5u", value);
|
|
}
|
|
|
|
/*!
|
|
* \brief Adds the 'attest' field to the JWT.
|
|
*
|
|
* \param json The JWT
|
|
* \param attest The value to set attest to
|
|
*
|
|
* \retval 0 on success
|
|
* \retval -1 on failure
|
|
*/
|
|
static int stir_shaken_add_attest(struct ast_json *json, const char *attest)
|
|
{
|
|
struct ast_json *value;
|
|
|
|
value = ast_json_string_create(attest);
|
|
if (!value) {
|
|
return -1;
|
|
}
|
|
|
|
return ast_json_object_set(ast_json_object_get(json, "payload"), "attest", value);
|
|
}
|
|
|
|
/*!
|
|
* \brief Adds the 'origid' field to the JWT.
|
|
*
|
|
* \param json The JWT
|
|
* \param origid The value to set origid to
|
|
*
|
|
* \retval 0 on success
|
|
* \retval -1 on failure
|
|
*/
|
|
static int stir_shaken_add_origid(struct ast_json *json, const char *origid)
|
|
{
|
|
struct ast_json *value;
|
|
|
|
value = ast_json_string_create(origid);
|
|
if (!origid) {
|
|
return -1;
|
|
}
|
|
|
|
return ast_json_object_set(ast_json_object_get(json, "payload"), "origid", value);
|
|
}
|
|
|
|
/*!
|
|
* \brief Adds the 'iat' field to the JWT.
|
|
*
|
|
* \param json The JWT
|
|
*
|
|
* \retval 0 on success
|
|
* \retval -1 on failure
|
|
*/
|
|
static int stir_shaken_add_iat(struct ast_json *json)
|
|
{
|
|
struct ast_json *value;
|
|
struct timeval tv;
|
|
int timestamp;
|
|
|
|
tv = ast_tvnow();
|
|
timestamp = tv.tv_sec + tv.tv_usec / 1000;
|
|
value = ast_json_integer_create(timestamp);
|
|
|
|
return ast_json_object_set(ast_json_object_get(json, "payload"), "iat", value);
|
|
}
|
|
|
|
struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json)
|
|
{
|
|
struct ast_stir_shaken_payload *payload;
|
|
unsigned char *signature;
|
|
const char *caller_id_num;
|
|
char *json_str = NULL;
|
|
struct stir_shaken_certificate *cert = NULL;
|
|
|
|
payload = stir_shaken_verify_json(json);
|
|
if (!payload) {
|
|
return NULL;
|
|
}
|
|
|
|
/* From the payload section of the JSON, get the orig section, and then get
|
|
* the value of tn. This will be the caller ID number */
|
|
caller_id_num = ast_json_string_get(ast_json_object_get(ast_json_object_get(
|
|
ast_json_object_get(json, "payload"), "orig"), "tn"));
|
|
if (!caller_id_num) {
|
|
ast_log(LOG_ERROR, "Failed to get caller ID number from JWT\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
cert = stir_shaken_certificate_get_by_caller_id_number(caller_id_num);
|
|
if (!cert) {
|
|
ast_log(LOG_ERROR, "Failed to retrieve certificate for caller ID "
|
|
"'%s'\n", caller_id_num);
|
|
goto cleanup;
|
|
}
|
|
|
|
if (stir_shaken_add_x5u(json, stir_shaken_certificate_get_public_key_url(cert))) {
|
|
ast_log(LOG_ERROR, "Failed to add 'x5u' (public key URL) to payload\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
/* TODO: This is just a placeholder for adding 'attest', 'iat', and
|
|
* 'origid' to the payload. Later, additional logic will need to be
|
|
* added to determine what these values actually are, but the functions
|
|
* themselves are ready to go.
|
|
*/
|
|
if (stir_shaken_add_attest(json, "B")) {
|
|
ast_log(LOG_ERROR, "Failed to add 'attest' to payload\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
if (stir_shaken_add_origid(json, "asterisk")) {
|
|
ast_log(LOG_ERROR, "Failed to add 'origid' to payload\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
if (stir_shaken_add_iat(json)) {
|
|
ast_log(LOG_ERROR, "Failed to add 'iat' to payload\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
json_str = ast_json_dump_string(json);
|
|
if (!json_str) {
|
|
ast_log(LOG_ERROR, "Failed to convert JSON to string\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
signature = stir_shaken_sign(json_str, stir_shaken_certificate_get_private_key(cert));
|
|
if (!signature) {
|
|
goto cleanup;
|
|
}
|
|
|
|
payload->signature = signature;
|
|
ao2_cleanup(cert);
|
|
ast_json_free(json_str);
|
|
|
|
return payload;
|
|
|
|
cleanup:
|
|
ao2_cleanup(cert);
|
|
ast_stir_shaken_payload_free(payload);
|
|
ast_json_free(json_str);
|
|
return NULL;
|
|
}
|
|
|
|
static int reload_module(void)
|
|
{
|
|
if (stir_shaken_sorcery) {
|
|
ast_sorcery_reload(stir_shaken_sorcery);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int unload_module(void)
|
|
{
|
|
stir_shaken_certificate_unload();
|
|
stir_shaken_store_unload();
|
|
stir_shaken_general_unload();
|
|
|
|
ast_sorcery_unref(stir_shaken_sorcery);
|
|
stir_shaken_sorcery = NULL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int load_module(void)
|
|
{
|
|
if (!(stir_shaken_sorcery = ast_sorcery_open())) {
|
|
ast_log(LOG_ERROR, "stir/shaken - failed to open sorcery\n");
|
|
return AST_MODULE_LOAD_DECLINE;
|
|
}
|
|
|
|
if (stir_shaken_general_load()) {
|
|
unload_module();
|
|
return AST_MODULE_LOAD_DECLINE;
|
|
}
|
|
|
|
if (stir_shaken_store_load()) {
|
|
unload_module();
|
|
return AST_MODULE_LOAD_DECLINE;
|
|
}
|
|
|
|
if (stir_shaken_certificate_load()) {
|
|
unload_module();
|
|
return AST_MODULE_LOAD_DECLINE;
|
|
}
|
|
|
|
ast_sorcery_load(ast_stir_shaken_sorcery());
|
|
|
|
return AST_MODULE_LOAD_SUCCESS;
|
|
}
|
|
|
|
#undef AST_BUILDOPT_SUM
|
|
#define AST_BUILDOPT_SUM ""
|
|
|
|
AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER,
|
|
"STIR/SHAKEN Module for Asterisk",
|
|
.support_level = AST_MODULE_SUPPORT_CORE,
|
|
.load = load_module,
|
|
.unload = unload_module,
|
|
.reload = reload_module,
|
|
.load_pri = AST_MODPRI_CHANNEL_DEPEND - 1,
|
|
);
|