mirror of
				https://github.com/asterisk/asterisk.git
				synced 2025-10-31 02:37:10 +00:00 
			
		
		
		
	This avoids the inclusion of the OpenSSL headers in the public header, which avoids one external library dependency in res_pjsip_stir_shaken. Change-Id: I6a07e2d81d2b5442e24e99b8cc733a99f881dcf4
		
			
				
	
	
		
			1666 lines
		
	
	
		
			48 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			1666 lines
		
	
	
		
			48 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>
 | |
| 	<depend>curl</depend>
 | |
| 	<depend>res_curl</depend>
 | |
| 	<support_level>core</support_level>
 | |
|  ***/
 | |
| 
 | |
| #include "asterisk.h"
 | |
| 
 | |
| #include <openssl/evp.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>
 | |
| 				<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
 | |
| 
 | |
| /* The default amount of time (in seconds) to use for certificate expiration
 | |
|  * if no cache data is available
 | |
|  */
 | |
| #define EXPIRATION_BUFFER 15
 | |
| 
 | |
| 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;
 | |
| }
 | |
| 
 | |
| 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;
 | |
| 	}
 | |
| 
 | |
| 	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);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (ast_strlen_zero(value)) {
 | |
| 		actual_expires.tv_sec += EXPIRATION_BUFFER;
 | |
| 	}
 | |
| 
 | |
| 	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);
 | |
| 	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;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/* 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;
 | |
| 	}
 | |
| 
 | |
| 	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;
 | |
| 	}
 | |
| 
 | |
| 	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)
 | |
| {
 | |
| 	struct ast_stir_shaken_payload *ss_payload;
 | |
| 	unsigned char *signature;
 | |
| 	const char *public_key_url;
 | |
| 	const char *caller_id_num;
 | |
| 	const char *header;
 | |
| 	const char *payload;
 | |
| 	struct ast_json *tmp_json;
 | |
| 	char *msg = NULL;
 | |
| 	size_t msg_len;
 | |
| 	struct stir_shaken_certificate *cert = NULL;
 | |
| 
 | |
| 	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;
 | |
| 	}
 | |
| 
 | |
| 	/* 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;
 | |
| 	}
 | |
| 	snprintf(msg, msg_len, "%s.%s", header, payload);
 | |
| 
 | |
| 	signature = stir_shaken_sign(msg, stir_shaken_certificate_get_private_key(cert));
 | |
| 	if (!signature) {
 | |
| 		goto cleanup;
 | |
| 	}
 | |
| 
 | |
| 	ss_payload->signature = signature;
 | |
| 	ao2_cleanup(cert);
 | |
| 	ast_free(msg);
 | |
| 
 | |
| 	return ss_payload;
 | |
| 
 | |
| cleanup:
 | |
| 	ao2_cleanup(cert);
 | |
| 	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;
 | |
| 	char *first;
 | |
| 	char *second;
 | |
| 	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);
 | |
| 
 | |
| 	first = ast_strip(args.first_param);
 | |
| 	if (ast_strlen_zero(first)) {
 | |
| 		ast_log(LOG_ERROR, "An argument must be passed to %s\n", function);
 | |
| 		return -1;
 | |
| 	}
 | |
| 
 | |
| 	second = ast_strip(args.second_param);
 | |
| 
 | |
| 	/* Check if we are only looking for the number of STIR/SHAKEN verification results */
 | |
| 	if (!strcasecmp(first, "count")) {
 | |
| 
 | |
| 		size_t count = 0;
 | |
| 
 | |
| 		if (!ast_strlen_zero(second)) {
 | |
| 			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(second)) {
 | |
| 		ast_log(LOG_ERROR, "Retrieving a value using %s requires two paramaters (index, value) "
 | |
| 			"- only index was given\n", function);
 | |
| 		return -1;
 | |
| 	}
 | |
| 
 | |
| 	if (ast_str_to_uint(first, &target_index)) {
 | |
| 		ast_log(LOG_ERROR, "Failed to convert index %s to integer for function %s\n",
 | |
| 			first, function);
 | |
| 		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", first);
 | |
| 		return -1;
 | |
| 	}
 | |
| 	ss_datastore = datastore->data;
 | |
| 
 | |
| 	if (!strcasecmp(second, "identity")) {
 | |
| 		ast_copy_string(buf, ss_datastore->identity, len);
 | |
| 	} else if (!strcasecmp(second, "attestation")) {
 | |
| 		ast_copy_string(buf, ss_datastore->attestation, len);
 | |
| 	} else if (!strcasecmp(second, "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", second, 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";
 | |
| 	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;
 | |
| 	}
 | |
| 
 | |
| 	/* 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 */
 | |
| 	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 */
 | |
| 	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 */
 | |
| 	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 */
 | |
| 	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 */
 | |
| 	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",
 | |
| );
 |