asterisk/res/res_stir_shaken.c

1650 lines
48 KiB
C
Raw Normal View History

/*
* 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>
<depend>curl</depend>
<depend>res_curl</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/astdb.h"
#include "asterisk/paths.h"
#include "asterisk/conversions.h"
#include "asterisk/pbx.h"
#include "asterisk/global_datastores.h"
#include "asterisk/app.h"
#include "asterisk/test.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"
#include "res_stir_shaken/curl.h"
/*** DOCUMENTATION
<configInfo name="res_stir_shaken" language="en_US">
<synopsis>STIR/SHAKEN module for Asterisk</synopsis>
<configFile name="stir_shaken.conf">
<configObject name="general">
<synopsis>STIR/SHAKEN general options</synopsis>
<configOption name="type">
<synopsis>Must be of type 'general'.</synopsis>
</configOption>
<configOption name="ca_file" default="">
<synopsis>File path to the certificate authority certificate</synopsis>
</configOption>
<configOption name="ca_path" default="">
<synopsis>File path to a chain of trust</synopsis>
</configOption>
<configOption name="cache_max_size" default="1000">
<synopsis>Maximum size to use for caching public keys</synopsis>
</configOption>
<configOption name="curl_timeout" default="2">
<synopsis>Maximum time to wait to CURL certificates</synopsis>
</configOption>
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
<configOption name="signature_timeout" default="15">
<synopsis>Amount of time a signature is valid for</synopsis>
</configOption>
</configObject>
<configObject name="store">
<synopsis>STIR/SHAKEN certificate store options</synopsis>
<configOption name="type">
<synopsis>Must be of type 'store'.</synopsis>
</configOption>
<configOption name="path" default="">
<synopsis>Path to a directory containing certificates</synopsis>
</configOption>
<configOption name="public_key_url" default="">
<synopsis>URL to the public key(s)</synopsis>
<description><para>
Must be a valid http, or https, URL. The URL must also contain the ${CERTIFICATE} variable, which is used for public key name substitution.
For example: http://mycompany.com/${CERTIFICATE}.pub
</para></description>
</configOption>
</configObject>
<configObject name="certificate">
<synopsis>STIR/SHAKEN certificate options</synopsis>
<configOption name="type">
<synopsis>Must be of type 'certificate'.</synopsis>
</configOption>
<configOption name="path" default="">
<synopsis>File path to a certificate</synopsis>
</configOption>
<configOption name="public_key_url" default="">
<synopsis>URL to the public key</synopsis>
<description><para>
Must be a valid http, or https, URL.
</para></description>
</configOption>
<configOption name="attestation">
<synopsis>Attestation level</synopsis>
</configOption>
<configOption name="origid" default="">
<synopsis>The origination ID</synopsis>
</configOption>
<configOption name="caller_id_number" default="">
<synopsis>The caller ID number to match on.</synopsis>
</configOption>
</configObject>
</configFile>
</configInfo>
<function name="STIR_SHAKEN" language="en_US">
<synopsis>
Gets the number of STIR/SHAKEN results or a specific STIR/SHAKEN value from a result on the channel.
</synopsis>
<syntax>
<parameter name="index" required="true">
<para>The index of the STIR/SHAKEN result to get. If only 'count' is passed in, gets the number of STIR/SHAKEN results instead.</para>
</parameter>
<parameter name="value" required="false">
<para>The value to get from the STIR/SHAKEN result. Only used when an index is passed in (instead of 'count'). Allowable values:</para>
<enumlist>
<enum name = "identity" />
<enum name = "attestation" />
<enum name = "verify_result" />
</enumlist>
</parameter>
</syntax>
<description>
<para>This function will either return the number of STIR/SHAKEN identities, or return information on the specified identity.
To get the number of identities, just pass 'count' as the only parameter to the function. If you want to get information on a
specific STIR/SHAKEN identity, you can get the number of identities and then pass an index as the first parameter and one of
the values you would like to retrieve as the second parameter.
</para>
<example title="Get count and retrieve value">
same => n,NoOp(Number of STIR/SHAKEN identities: ${STIR_SHAKEN(count)})
same => n,NoOp(Identity ${STIR_SHAKEN(0, identity)} has attestation level ${STIR_SHAKEN(0, attestation)})
</example>
</description>
</function>
***/
static struct ast_sorcery *stir_shaken_sorcery;
/* Used for AstDB entries */
#define AST_DB_FAMILY "STIR_SHAKEN"
/* The directory name to store keys in. Appended to ast_config_DATA_DIR */
#define STIR_SHAKEN_DIR_NAME "stir_shaken"
/* The maximum length for path storage */
#define MAX_PATH_LEN 256
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);
}
unsigned char *ast_stir_shaken_payload_get_signature(const struct ast_stir_shaken_payload *payload)
{
return payload ? payload->signature : NULL;
}
char *ast_stir_shaken_payload_get_public_key_url(const struct ast_stir_shaken_payload *payload)
{
return payload ? payload->public_key_url : NULL;
}
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
unsigned int ast_stir_shaken_get_signature_timeout(void)
{
return ast_stir_shaken_signature_timeout(stir_shaken_general_get());
}
/*!
* \brief Convert an ast_stir_shaken_verification_result to string representation
*
* \param result The result to convert
*
* \retval empty string if not a valid enum value
* \retval string representation of result otherwise
*/
static const char *stir_shaken_verification_result_to_string(enum ast_stir_shaken_verification_result result)
{
switch (result) {
case AST_STIR_SHAKEN_VERIFY_NOT_PRESENT:
return "Verification not present";
case AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED:
return "Signature failed";
case AST_STIR_SHAKEN_VERIFY_MISMATCH:
return "Verification mismatch";
case AST_STIR_SHAKEN_VERIFY_PASSED:
return "Verification passed";
default:
break;
}
return "";
}
/* The datastore struct holding verification information for the channel */
struct stir_shaken_datastore {
/* The identitifier for the STIR/SHAKEN verification */
char *identity;
/* The attestation value */
char *attestation;
/* The actual verification result */
enum ast_stir_shaken_verification_result verify_result;
};
/*!
* \brief Frees a stir_shaken_datastore structure
*
* \param datastore The datastore to free
*/
static void stir_shaken_datastore_free(struct stir_shaken_datastore *datastore)
{
if (!datastore) {
return;
}
ast_free(datastore->identity);
ast_free(datastore->attestation);
ast_free(datastore);
}
/*!
* \brief The callback to destroy a stir_shaken_datastore
*
* \param data The stir_shaken_datastore
*/
static void stir_shaken_datastore_destroy_cb(void *data)
{
struct stir_shaken_datastore *datastore = data;
stir_shaken_datastore_free(datastore);
}
/* 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_shaken_datastore_destroy_cb,
};
int ast_stir_shaken_add_verification(struct ast_channel *chan, const char *identity, const char *attestation,
enum ast_stir_shaken_verification_result result)
{
struct stir_shaken_datastore *ss_datastore;
struct ast_datastore *datastore;
const char *chan_name;
if (!chan) {
ast_log(LOG_ERROR, "Channel is required to add STIR/SHAKEN verification\n");
return -1;
}
chan_name = ast_channel_name(chan);
if (!identity) {
ast_log(LOG_ERROR, "No identity to add STIR/SHAKEN verification to channel "
"%s\n", chan_name);
return -1;
}
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
if (!attestation) {
ast_log(LOG_ERROR, "Attestation cannot be NULL to add STIR/SHAKEN verification to "
"channel %s\n", chan_name);
return -1;
}
ss_datastore = ast_calloc(1, sizeof(*ss_datastore));
if (!ss_datastore) {
ast_log(LOG_ERROR, "Failed to allocate space for STIR/SHAKEN datastore for "
"channel %s\n", chan_name);
return -1;
}
ss_datastore->identity = ast_strdup(identity);
if (!ss_datastore->identity) {
ast_log(LOG_ERROR, "Failed to allocate space for STIR/SHAKEN datastore "
"identity for channel %s\n", chan_name);
stir_shaken_datastore_free(ss_datastore);
return -1;
}
ss_datastore->attestation = ast_strdup(attestation);
if (!ss_datastore->attestation) {
ast_log(LOG_ERROR, "Failed to allocate space for STIR/SHAKEN datastore "
"attestation for channel %s\n", chan_name);
stir_shaken_datastore_free(ss_datastore);
return -1;
}
ss_datastore->verify_result = result;
datastore = ast_datastore_alloc(&stir_shaken_datastore_info, NULL);
if (!datastore) {
ast_log(LOG_ERROR, "Failed to allocate space for datastore for channel "
"%s\n", chan_name);
stir_shaken_datastore_free(ss_datastore);
return -1;
}
datastore->data = ss_datastore;
ast_channel_lock(chan);
ast_channel_datastore_add(chan, datastore);
ast_channel_unlock(chan);
return 0;
}
/*!
* \brief Sets the expiration for the public key based on the provided fields.
* If Cache-Control is present, use it. Otherwise, use Expires.
*
* \param hash The hash for the public key URL
* \param data The CURL callback data containing expiration data
*/
static void set_public_key_expiration(const char *public_key_url, const struct curl_cb_data *data)
{
char time_buf[32];
char *value;
struct timeval actual_expires = ast_tvnow();
char hash[41];
ast_sha1_hash(hash, public_key_url);
value = curl_cb_data_get_cache_control(data);
if (!ast_strlen_zero(value)) {
char *str_max_age;
str_max_age = strstr(value, "s-maxage");
if (!str_max_age) {
str_max_age = strstr(value, "max-age");
}
if (str_max_age) {
unsigned int max_age;
char *equal = strchr(str_max_age, '=');
if (equal && !ast_str_to_uint(equal + 1, &max_age)) {
actual_expires.tv_sec += max_age;
}
}
} else {
value = curl_cb_data_get_expires(data);
if (!ast_strlen_zero(value)) {
struct tm expires_time;
strptime(value, "%a, %d %b %Y %T %z", &expires_time);
expires_time.tm_isdst = -1;
actual_expires.tv_sec = mktime(&expires_time);
}
}
snprintf(time_buf, sizeof(time_buf), "%30lu", actual_expires.tv_sec);
ast_db_put(hash, "expiration", time_buf);
}
/*!
* \brief Check to see if the public key is expired
*
* \param public_key_url The public key URL
*
* \retval 1 if expired
* \retval 0 if not expired
*/
static int public_key_is_expired(const char *public_key_url)
{
struct timeval current_time = ast_tvnow();
struct timeval expires = { .tv_sec = 0, .tv_usec = 0 };
char expiration[32];
char hash[41];
ast_sha1_hash(hash, public_key_url);
ast_db_get(hash, "expiration", expiration, sizeof(expiration));
if (ast_strlen_zero(expiration)) {
return 1;
}
if (ast_str_to_ulong(expiration, (unsigned long *)&expires.tv_sec)) {
return 1;
}
return ast_tvcmp(current_time, expires) == -1 ? 0 : 1;
}
/*!
* \brief Returns the path to the downloaded file for the provided URL
*
* \param public_key_url The public key URL
*
* \retval Empty string if not present in AstDB
* \retval The file path if present in AstDB
*/
static char *get_path_to_public_key(const char *public_key_url)
{
char hash[41];
char file_path[MAX_PATH_LEN];
ast_sha1_hash(hash, public_key_url);
ast_db_get(hash, "path", file_path, sizeof(file_path));
if (ast_strlen_zero(file_path)) {
file_path[0] = '\0';
}
return ast_strdup(file_path);
}
/*!
* \brief Add the public key details and file path to AstDB
*
* \param public_key_url The public key URL
* \param filepath The path to the file
*/
static void add_public_key_to_astdb(const char *public_key_url, const char *filepath)
{
char hash[41];
ast_sha1_hash(hash, public_key_url);
ast_db_put(AST_DB_FAMILY, public_key_url, hash);
ast_db_put(hash, "path", filepath);
}
/*!
* \brief Remove the public key details and associated information from AstDB
*
* \param public_key_url The public key URL
*/
static void remove_public_key_from_astdb(const char *public_key_url)
{
char hash[41];
char filepath[MAX_PATH_LEN];
ast_sha1_hash(hash, public_key_url);
/* Remove this public key from storage */
ast_db_get(hash, "path", filepath, sizeof(filepath));
/* Remove the actual file from the system */
remove(filepath);
ast_db_del(AST_DB_FAMILY, public_key_url);
ast_db_deltree(hash, NULL);
}
/*!
* \brief Verifies the signature using a public key
*
* \param msg The payload
* \param signature The signature to verify
* \param public_key The public key used for verification
*
* \retval -1 on failure
* \retval 0 on success
*/
static int stir_shaken_verify_signature(const char *msg, const char *signature, EVP_PKEY *public_key)
{
EVP_MD_CTX *mdctx = NULL;
int ret = 0;
unsigned char *decoded_signature;
size_t signature_length, decoded_signature_length, padding = 0;
mdctx = EVP_MD_CTX_create();
if (!mdctx) {
ast_log(LOG_ERROR, "Failed to create Message Digest Context\n");
return -1;
}
ret = EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, public_key);
if (ret != 1) {
ast_log(LOG_ERROR, "Failed to initialize Message Digest Context\n");
EVP_MD_CTX_destroy(mdctx);
return -1;
}
ret = EVP_DigestVerifyUpdate(mdctx, (unsigned char *)msg, strlen(msg));
if (ret != 1) {
ast_log(LOG_ERROR, "Failed to update Message Digest Context\n");
EVP_MD_CTX_destroy(mdctx);
return -1;
}
/* We need to decode the signature from base64 to bytes. Make sure we have
* at least enough characters for this check */
signature_length = strlen(signature);
if (signature_length > 2 && signature[signature_length - 1] == '=') {
padding++;
if (signature[signature_length - 2] == '=') {
padding++;
}
}
decoded_signature_length = (signature_length / 4 * 3) - padding;
decoded_signature = ast_calloc(1, decoded_signature_length);
ast_base64decode(decoded_signature, signature, decoded_signature_length);
ret = EVP_DigestVerifyFinal(mdctx, decoded_signature, decoded_signature_length);
if (ret != 1) {
ast_log(LOG_ERROR, "Failed final phase of signature verification\n");
EVP_MD_CTX_destroy(mdctx);
ast_free(decoded_signature);
return -1;
}
EVP_MD_CTX_destroy(mdctx);
ast_free(decoded_signature);
return 0;
}
/*!
* \brief CURL the file located at public_key_url to the specified path
*
* \param public_key_url The public key URL
* \param path The path to download the file to
*
* \retval -1 on failure
* \retval 0 on success
*/
static int run_curl(const char *public_key_url, const char *path)
{
struct curl_cb_data *data;
data = curl_cb_data_create();
if (!data) {
ast_log(LOG_ERROR, "Failed to create CURL callback data\n");
return -1;
}
if (curl_public_key(public_key_url, path, data)) {
ast_log(LOG_ERROR, "Could not retrieve public key for '%s'\n", public_key_url);
curl_cb_data_free(data);
return -1;
}
set_public_key_expiration(public_key_url, data);
curl_cb_data_free(data);
return 0;
}
/*!
* \brief Downloads the public key from public_key_url. If curl is non-zero, that signals
* CURL has already been run, and we should bail here. The entry is added to AstDB as well.
*
* \param public_key_url The public key URL
* \param path The path to download the file to
* \param curl Flag signaling if we have run CURL or not
*
* \retval -1 on failure
* \retval 0 on success
*/
static int curl_and_check_expiration(const char *public_key_url, const char *path, int *curl)
{
if (curl) {
ast_log(LOG_ERROR, "Already downloaded public key '%s'\n", path);
return -1;
}
if (run_curl(public_key_url, path)) {
return -1;
}
if (public_key_is_expired(public_key_url)) {
ast_log(LOG_ERROR, "Newly downloaded public key '%s' is expired\n", path);
return -1;
}
*curl = 1;
add_public_key_to_astdb(public_key_url, path);
return 0;
}
struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const char *payload, const char *signature,
const char *algorithm, const char *public_key_url)
{
struct ast_stir_shaken_payload *ret_payload;
EVP_PKEY *public_key;
char *filename;
int curl = 0;
RAII_VAR(char *, file_path, NULL, ast_free);
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
RAII_VAR(char *, combined_str, NULL, ast_free);
size_t combined_size;
if (ast_strlen_zero(header)) {
ast_log(LOG_ERROR, "'header' is required for STIR/SHAKEN verification\n");
return NULL;
}
if (ast_strlen_zero(payload)) {
ast_log(LOG_ERROR, "'payload' is required for STIR/SHAKEN verification\n");
return NULL;
}
if (ast_strlen_zero(signature)) {
ast_log(LOG_ERROR, "'signature' is required for STIR/SHAKEN verification\n");
return NULL;
}
if (ast_strlen_zero(algorithm)) {
ast_log(LOG_ERROR, "'algorithm' is required for STIR/SHAKEN verification\n");
return NULL;
}
if (ast_strlen_zero(public_key_url)) {
ast_log(LOG_ERROR, "'public_key_url' is required for STIR/SHAKEN verification\n");
return NULL;
}
/* Check to see if we have already downloaded this public key. The reason we
* store the file path is because:
*
* 1. If, for some reason, the default directory changes, we still know where
* to look for the files we already have.
*
* 2. In the future, if we want to add a way to store the keys in multiple
* {configurable) directories, we already have the storage mechanism in place.
* The only thing that would be left to do is pull from the configuration.
*/
file_path = get_path_to_public_key(public_key_url);
/* If we don't have an entry in AstDB, CURL from the provided URL */
if (ast_strlen_zero(file_path)) {
/* Remove this entry from the database, since we will be
* downloading a new file anyways.
*/
remove_public_key_from_astdb(public_key_url);
/* Go ahead and free file_path, in case anything was allocated above */
ast_free(file_path);
/* Set up the default path */
filename = basename(public_key_url);
if (ast_asprintf(&file_path, "%s/keys/%s/%s", ast_config_AST_DATA_DIR, STIR_SHAKEN_DIR_NAME, filename) < 0) {
return NULL;
}
/* Download to the default path */
if (run_curl(public_key_url, file_path)) {
return NULL;
}
/* Signal that we have already downloaded a new file, no reason to do it again */
curl = 1;
/* We should have a successful download at this point, so
* add an entry to the database.
*/
add_public_key_to_astdb(public_key_url, file_path);
}
/* Check to see if the key we downloaded (or already had) is expired */
if (public_key_is_expired(public_key_url)) {
ast_debug(3, "Public key '%s' is expired\n", public_key_url);
remove_public_key_from_astdb(public_key_url);
/* If this fails, then there's nothing we can do */
if (curl_and_check_expiration(public_key_url, file_path, &curl)) {
return NULL;
}
}
/* First attempt to read the key. If it fails, try downloading the file,
* unless we already did. Check for expiration again */
public_key = stir_shaken_read_key(file_path, 0);
if (!public_key) {
ast_debug(3, "Failed first read of public key file '%s'\n", file_path);
remove_public_key_from_astdb(public_key_url);
if (curl_and_check_expiration(public_key_url, file_path, &curl)) {
return NULL;
}
public_key = stir_shaken_read_key(file_path, 0);
if (!public_key) {
ast_log(LOG_ERROR, "Failed to read public key from '%s'\n", file_path);
remove_public_key_from_astdb(public_key_url);
return NULL;
}
}
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
/* Combine the header and payload to get the original signed message: header.payload */
combined_size = strlen(header) + strlen(payload) + 2;
combined_str = ast_calloc(1, combined_size);
if (!combined_str) {
ast_log(LOG_ERROR, "Failed to allocate space for message to verify\n");
EVP_PKEY_free(public_key);
return NULL;
}
snprintf(combined_str, combined_size, "%s.%s", header, payload);
if (stir_shaken_verify_signature(combined_str, signature, public_key)) {
ast_log(LOG_ERROR, "Failed to verify signature\n");
EVP_PKEY_free(public_key);
return NULL;
}
/* We don't need the public key anymore */
EVP_PKEY_free(public_key);
ret_payload = ast_calloc(1, sizeof(*ret_payload));
if (!ret_payload) {
ast_log(LOG_ERROR, "Failed to allocate STIR/SHAKEN payload\n");
return NULL;
}
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
ret_payload->header = ast_json_load_string(header, NULL);
if (!ret_payload->header) {
ast_log(LOG_ERROR, "Failed to create JSON from header\n");
ast_stir_shaken_payload_free(ret_payload);
return NULL;
}
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
ret_payload->payload = ast_json_load_string(payload, NULL);
if (!ret_payload->payload) {
ast_log(LOG_ERROR, "Failed to create JSON from payload\n");
ast_stir_shaken_payload_free(ret_payload);
return NULL;
}
ret_payload->signature = (unsigned char *)ast_strdup(signature);
ret_payload->algorithm = ast_strdup(algorithm);
ret_payload->public_key_url = ast_strdup(public_key_url);
return ret_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.
*/
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)
{
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
struct ast_stir_shaken_payload *ss_payload;
unsigned char *signature;
const char *public_key_url;
const char *caller_id_num;
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
const char *header;
const char *payload;
struct ast_json *tmp_json;
char *msg = NULL;
size_t msg_len;
struct stir_shaken_certificate *cert = NULL;
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
ss_payload = stir_shaken_verify_json(json);
if (!ss_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;
}
public_key_url = stir_shaken_certificate_get_public_key_url(cert);
if (stir_shaken_add_x5u(json, public_key_url)) {
ast_log(LOG_ERROR, "Failed to add 'x5u' (public key URL) to payload\n");
goto cleanup;
}
ss_payload->public_key_url = ast_strdup(public_key_url);
if (stir_shaken_add_attest(json, stir_shaken_certificate_get_attestation(cert))) {
ast_log(LOG_ERROR, "Failed to add 'attest' to payload\n");
goto cleanup;
}
if (stir_shaken_add_origid(json, stir_shaken_certificate_get_origid(cert))) {
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;
}
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
/* Get the header and the payload. Combine them to get the message to sign */
tmp_json = ast_json_object_get(json, "header");
header = ast_json_dump_string(tmp_json);
tmp_json = ast_json_object_get(json, "payload");
payload = ast_json_dump_string(tmp_json);
msg_len = strlen(header) + strlen(payload) + 2;
msg = ast_calloc(1, msg_len);
if (!msg) {
ast_log(LOG_ERROR, "Failed to allocate space for message to sign\n");
goto cleanup;
}
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
snprintf(msg, msg_len, "%s.%s", header, payload);
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
signature = stir_shaken_sign(msg, stir_shaken_certificate_get_private_key(cert));
if (!signature) {
goto cleanup;
}
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
ss_payload->signature = signature;
ao2_cleanup(cert);
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
ast_free(msg);
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
return ss_payload;
cleanup:
ao2_cleanup(cert);
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
ast_stir_shaken_payload_free(ss_payload);
ast_free(msg);
return NULL;
}
/*!
* \brief Retrieves STIR/SHAKEN verification information for the channel via dialplan.
* Examples:
*
* STIR_SHAKEN(count)
* STIR_SHAKEN(0, identity)
* STIR_SHAKEN(1, attestation)
* STIR_SHAKEN(27, verify_result)
*
* \retval -1 on failure
* \retval 0 on success
*/
static int stir_shaken_read(struct ast_channel *chan, const char *function,
char *data, char *buf, size_t len)
{
struct stir_shaken_datastore *ss_datastore;
struct ast_datastore *datastore;
char *parse;
unsigned int target_index, current_index = 0;
AST_DECLARE_APP_ARGS(args,
AST_APP_ARG(first_param);
AST_APP_ARG(second_param);
);
if (ast_strlen_zero(data)) {
ast_log(LOG_WARNING, "%s requires at least one argument\n", function);
return -1;
}
if (!chan) {
ast_log(LOG_ERROR, "No channel for %s function\n", function);
return -1;
}
parse = ast_strdupa(data);
AST_STANDARD_APP_ARGS(args, parse);
if (ast_strlen_zero(args.first_param)) {
ast_log(LOG_ERROR, "An argument must be passed to %s\n", function);
return -1;
}
/* Check if we are only looking for the number of STIR/SHAKEN verification results */
if (!strcasecmp(args.first_param, "count")) {
size_t count = 0;
if (!ast_strlen_zero(args.second_param)) {
ast_log(LOG_ERROR, "%s only takes 1 paramater for 'count'\n", function);
return -1;
}
ast_channel_lock(chan);
AST_LIST_TRAVERSE(ast_channel_datastores(chan), datastore, entry) {
if (datastore->info != &stir_shaken_datastore_info) {
continue;
}
count++;
}
ast_channel_unlock(chan);
snprintf(buf, len, "%zu", count);
return 0;
}
/* If we aren't doing a count, then there should be two parameters. The field
* we are searching for will be the second parameter. The index is the first.
*/
if (ast_strlen_zero(args.second_param)) {
ast_log(LOG_ERROR, "Retrieving a value using %s requires two paramaters (index, value) "
"- only index was given (%s)\n", function, args.second_param);
return -1;
}
if (ast_str_to_uint(args.first_param, &target_index)) {
ast_log(LOG_ERROR, "Failed to convert index %s to integer for function %s\n",
args.first_param, 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), datastore, entry) {
if (datastore->info != &stir_shaken_datastore_info) {
continue;
}
if (current_index == target_index) {
break;
}
current_index++;
}
ast_channel_unlock(chan);
if (current_index != target_index || !datastore) {
ast_log(LOG_WARNING, "No STIR/SHAKEN results for index '%s'\n", args.first_param);
return -1;
}
ss_datastore = datastore->data;
if (!strcasecmp(args.second_param, "identity")) {
ast_copy_string(buf, ss_datastore->identity, len);
} else if (!strcasecmp(args.second_param, "attestation")) {
ast_copy_string(buf, ss_datastore->attestation, len);
} else if (!strcasecmp(args.second_param, "verify_result")) {
ast_copy_string(buf, stir_shaken_verification_result_to_string(ss_datastore->verify_result), len);
} else {
ast_log(LOG_ERROR, "No such value '%s' for %s\n", args.second_param, function);
return -1;
}
return 0;
}
static struct ast_custom_function stir_shaken_function = {
.name = "STIR_SHAKEN",
.read = stir_shaken_read,
};
#ifdef TEST_FRAMEWORK
static void test_stir_shaken_add_fake_astdb_entry(const char *public_key_url, const char *file_path)
{
struct timeval expires = ast_tvnow();
char time_buf[32];
char hash[41];
ast_sha1_hash(hash, public_key_url);
add_public_key_to_astdb(public_key_url, file_path);
snprintf(time_buf, sizeof(time_buf), "%30lu", expires.tv_sec + 300);
ast_db_put(hash, "expiration", time_buf);
}
/*!
* \brief Create a private or public key certificate
*
* \param file_path The path of the file to create
* \param private Set to 0 if public, 1 if private
*
* \retval -1 on failure
* \retval 0 on success
*/
static int test_stir_shaken_write_temp_key(char *file_path, int private)
{
FILE *file;
int fd;
char *data;
char *type = private ? "private" : "public";
char *private_data =
"-----BEGIN EC PRIVATE KEY-----\n"
"MHcCAQEEIFkNGlrmRky2j7wmjGBGoPFBsyEQELmEYN02BiiG508noAoGCCqGSM49\n"
"AwEHoUQDQgAECwCaeAYwVG/FAnEnkwaucz6o047iSWq3cJBBUc0n2ZlUDr5VywAz\n"
"MZ86EthIqF3CGZjhLHn0xRITXYwfqTtWBw==\n"
"-----END EC PRIVATE KEY-----";
char *public_data =
"-----BEGIN PUBLIC KEY-----\n"
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECwCaeAYwVG/FAnEnkwaucz6o047i\n"
"SWq3cJBBUc0n2ZlUDr5VywAzMZ86EthIqF3CGZjhLHn0xRITXYwfqTtWBw==\n"
"-----END PUBLIC KEY-----";
fd = mkstemp(file_path);
if (fd < 0) {
ast_log(LOG_ERROR, "Failed to create temp %s file: %s\n", type, strerror(errno));
return -1;
}
file = fdopen(fd, "w");
if (!file) {
ast_log(LOG_ERROR, "Failed to create temp %s key file: %s\n", type, strerror(errno));
return -1;
}
data = private ? private_data : public_data;
if (fputs(data, file) == EOF) {
ast_log(LOG_ERROR, "Failed to write temp %s key file\n", type);
fclose(file);
return -1;
}
fclose(file);
return 0;
}
AST_TEST_DEFINE(test_stir_shaken_sign)
{
char *caller_id_number = "1234567";
char file_path[] = "/tmp/stir_shaken_private.XXXXXX";
RAII_VAR(char *, rm_on_exit, file_path, unlink);
RAII_VAR(struct ast_json *, json, NULL, ast_json_free);
RAII_VAR(struct ast_stir_shaken_payload *, payload, NULL, ast_stir_shaken_payload_free);
switch (cmd) {
case TEST_INIT:
info->name = "stir_shaken_sign";
info->category = "/res/res_stir_shaken/";
info->summary = "STIR/SHAKEN sign unit test";
info->description =
"Tests signing a JWT with a private key.";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
/* We only need a private key to sign */
test_stir_shaken_write_temp_key(file_path, 1);
test_stir_shaken_create_cert(caller_id_number, file_path);
/* Test missing header section */
json = ast_json_pack("{s: {s: {s: s}}}", "payload", "orig", "tn", caller_id_number);
payload = ast_stir_shaken_sign(json);
if (payload) {
ast_test_status_update(test, "Signed an invalid JWT (missing 'header')\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test missing payload section */
ast_json_free(json);
json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}}", "header", "alg",
STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE,
"x5u", "http://testing123");
payload = ast_stir_shaken_sign(json);
if (payload) {
ast_test_status_update(test, "Signed an invalid JWT (missing 'payload')\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test missing alg section */
ast_json_free(json);
json = ast_json_pack("{s: {s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "ppt",
STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE, "x5u", "http://testing123", "payload",
"orig", "tn", caller_id_number);
payload = ast_stir_shaken_sign(json);
if (payload) {
ast_test_status_update(test, "Signed an invalid JWT (missing 'alg')\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test invalid alg value */
ast_json_free(json);
json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg",
"invalid algorithm", "ppt", STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE,
"x5u", "http://testing123", "payload", "orig", "tn", caller_id_number);
payload = ast_stir_shaken_sign(json);
if (payload) {
ast_test_status_update(test, "Signed an invalid JWT (wrong 'alg')\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test missing ppt section */
ast_json_free(json);
json = ast_json_pack("{s: {s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg",
STIR_SHAKEN_ENCRYPTION_ALGORITHM, "typ", STIR_SHAKEN_TYPE, "x5u", "http://testing123",
"payload", "orig", "tn", caller_id_number);
payload = ast_stir_shaken_sign(json);
if (payload) {
ast_test_status_update(test, "Signed an invalid JWT (missing 'ppt')\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test invalid ppt value */
ast_json_free(json);
json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg",
STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", "invalid ppt", "typ", STIR_SHAKEN_TYPE,
"x5u", "http://testing123", "payload", "orig", "tn", caller_id_number);
payload = ast_stir_shaken_sign(json);
if (payload) {
ast_test_status_update(test, "Signed an invalid JWT (wrong 'ppt')\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test missing typ section */
ast_json_free(json);
json = ast_json_pack("{s: {s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg",
STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "x5u", "http://testing123",
"payload", "orig", "tn", caller_id_number);
payload = ast_stir_shaken_sign(json);
if (payload) {
ast_test_status_update(test, "Signed an invalid JWT (missing 'typ')\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test invalid typ value */
ast_json_free(json);
json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg",
STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "typ", "invalid typ",
"x5u", "http://testing123", "payload", "orig", "tn", caller_id_number);
payload = ast_stir_shaken_sign(json);
if (payload) {
ast_test_status_update(test, "Signed an invalid JWT (wrong 'typ')\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test missing orig section */
ast_json_free(json);
json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: s}}", "header", "alg",
STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE,
"x5u", "http://testing123", "payload", "filler", "filler");
payload = ast_stir_shaken_sign(json);
if (payload) {
ast_test_status_update(test, "Signed an invalid JWT (missing 'orig')\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test missing tn section */
ast_json_free(json);
json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: s}}", "header", "alg",
STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE,
"x5u", "http://testing123", "payload", "orig", "filler");
payload = ast_stir_shaken_sign(json);
if (payload) {
ast_test_status_update(test, "Signed an invalid JWT (missing 'tn')\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test valid JWT */
ast_json_free(json);
json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg",
STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE,
"x5u", "http://testing123", "payload", "orig", "tn", caller_id_number);
payload = ast_stir_shaken_sign(json);
if (!payload) {
ast_test_status_update(test, "Failed to sign a valid JWT\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_PASS;
}
AST_TEST_DEFINE(test_stir_shaken_verify)
{
char *caller_id_number = "1234567";
char *public_key_url = "http://testing123";
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
char *header;
char *payload;
struct ast_json *tmp_json;
char public_path[] = "/tmp/stir_shaken_public.XXXXXX";
char private_path[] = "/tmp/stir_shaken_public.XXXXXX";
RAII_VAR(char *, rm_on_exit_public, public_path, unlink);
RAII_VAR(char *, rm_on_exit_private, private_path, unlink);
RAII_VAR(struct ast_json *, json, NULL, ast_json_free);
RAII_VAR(struct ast_stir_shaken_payload *, signed_payload, NULL, ast_stir_shaken_payload_free);
RAII_VAR(struct ast_stir_shaken_payload *, returned_payload, NULL, ast_stir_shaken_payload_free);
switch (cmd) {
case TEST_INIT:
info->name = "stir_shaken_verify";
info->category = "/res/res_stir_shaken/";
info->summary = "STIR/SHAKEN verify unit test";
info->description =
"Tests verifying a signature with a public key";
return AST_TEST_NOT_RUN;
case TEST_EXECUTE:
break;
}
/* We need the private key to sign, but we also need the corresponding
* public key to verify */
test_stir_shaken_write_temp_key(public_path, 0);
test_stir_shaken_write_temp_key(private_path, 1);
test_stir_shaken_create_cert(caller_id_number, private_path);
/* Get the signature */
json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg",
STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE,
"x5u", public_key_url, "payload", "orig", "tn", caller_id_number);
signed_payload = ast_stir_shaken_sign(json);
if (!signed_payload) {
ast_test_status_update(test, "Failed to sign a valid JWT\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
/* Get the header and payload for ast_stir_shaken_verify */
tmp_json = ast_json_object_get(json, "header");
header = ast_json_dump_string(tmp_json);
tmp_json = ast_json_object_get(json, "payload");
payload = ast_json_dump_string(tmp_json);
/* Test empty header parameter */
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
returned_payload = ast_stir_shaken_verify("", payload, (const char *)signed_payload->signature,
STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url);
if (returned_payload) {
ast_test_status_update(test, "Verified a signature with missing 'header'\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test empty payload parameter */
returned_payload = ast_stir_shaken_verify(header, "", (const char *)signed_payload->signature,
STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url);
if (returned_payload) {
ast_test_status_update(test, "Verified a signature with missing 'payload'\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test empty signature parameter */
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
returned_payload = ast_stir_shaken_verify(header, payload, "",
STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url);
if (returned_payload) {
ast_test_status_update(test, "Verified a signature with missing 'signature'\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test empty algorithm parameter */
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
returned_payload = ast_stir_shaken_verify(header, payload, (const char *)signed_payload->signature,
"", public_key_url);
if (returned_payload) {
ast_test_status_update(test, "Verified a signature with missing 'algorithm'\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Test empty public key URL */
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
returned_payload = ast_stir_shaken_verify(header, payload, (const char *)signed_payload->signature,
STIR_SHAKEN_ENCRYPTION_ALGORITHM, "");
if (returned_payload) {
ast_test_status_update(test, "Verified a signature with missing 'public key URL'\n");
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
/* Trick the function into thinking we've already downloaded the key */
test_stir_shaken_add_fake_astdb_entry(public_key_url, public_path);
/* Verify a valid signature */
res_stir_shaken: Add inbound INVITE support. Integrated STIR/SHAKEN support with incoming INVITES. Upon receiving an INVITE, the Identity header is retrieved, parsing the message to verify the signature. If any of the parsing fails, AST_STIR_SHAKEN_VERIFY_NOT_PRESENT will be added to the channel for this caller ID. If verification itself fails, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED will be added. If anything in the payload does not line up with the SIP signaling, AST_STIR_SHAKEN_VERIFY_MISMATCH will be added. If all of the above steps pass, then AST_STIR_SHAKEN_VERIFY_PASSED will be added, completing the verification process. A new config option has been added to the general section for stir_shaken.conf. "signature_timeout" is the amount of time a signature will be considered valid. If an INVITE is received and the amount of time between when it was received and when it was signed is greater than signature_timeout, verification will fail. Some changes were also made to signing and verification. There was an error where the whole JSON string was being signed rather than the header combined with the payload. This has been changed to sign the correct thing. Verification has been changed to do this as well, and the unit tests have been updated to reflect these changes. A couple of utility functions have also been added. One decodes a BASE64 string and returns the decoded string, doing all the length calculations for you. The other retrieves a string value from a header in a rdata object. Change-Id: I855f857be3d1c63b64812ac35d9ce0534085b913
2020-05-19 19:46:45 +00:00
returned_payload = ast_stir_shaken_verify(header, payload, (const char *)signed_payload->signature,
STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url);
if (!returned_payload) {
ast_test_status_update(test, "Failed to verify a valid signature\n");
remove_public_key_from_astdb(public_key_url);
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
remove_public_key_from_astdb(public_key_url);
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_PASS;
}
#endif /* TEST_FRAMEWORK */
static int reload_module(void)
{
if (stir_shaken_sorcery) {
ast_sorcery_reload(stir_shaken_sorcery);
}
return 0;
}
static int unload_module(void)
{
int res = 0;
stir_shaken_certificate_unload();
stir_shaken_store_unload();
stir_shaken_general_unload();
ast_sorcery_unref(stir_shaken_sorcery);
stir_shaken_sorcery = NULL;
res |= ast_custom_function_unregister(&stir_shaken_function);
AST_TEST_UNREGISTER(test_stir_shaken_sign);
AST_TEST_UNREGISTER(test_stir_shaken_verify);
return res;
}
static int load_module(void)
{
int res = 0;
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());
res |= ast_custom_function_register(&stir_shaken_function);
AST_TEST_REGISTER(test_stir_shaken_sign);
AST_TEST_REGISTER(test_stir_shaken_verify);
return res;
}
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,
.requires = "res_curl",
);