res_stir_shaken: Implemented signature verification.

There are a lot of moving parts in this patch, but the focus of it is on
the verification of the signature using a public key located at the
public key URL provided in the JSON payload. First, we check the
database to see if we have already downloaded the key. If so, check to
see if it has expired. If it has, redownload from the URL. If we don't
have an entry in the database, just go ahead and download the public
key. The expiration is tested each time we download the file. After
that, read the public key from the file and use it to verify the
signature. All sanity checking is done when the payload is first
received, so the verification is complete once this point is reached.

The XML has also been added since a new config option was added to
general (curl_timeout). The maximum amount of time to wait for a
download can be configured through this option, with a low value by
default.

Change-Id: I3ba4c63880493bf8c7d17a9cfca1af0e934d1a1c
This commit is contained in:
Ben Ford
2020-04-15 13:15:21 -05:00
committed by Joshua Colp
parent 7baf2c4bf1
commit 9acf840f7c
11 changed files with 832 additions and 18 deletions

View File

@@ -18,6 +18,8 @@
/*** MODULEINFO
<depend>crypto</depend>
<depend>curl</depend>
<depend>res_curl</depend>
<support_level>core</support_level>
***/
@@ -27,12 +29,73 @@
#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/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>
</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>
</configObject>
</configFile>
</configInfo>
***/
#define STIR_SHAKEN_ENCRYPTION_ALGORITHM "ES256"
#define STIR_SHAKEN_PPT "shaken"
@@ -40,6 +103,15 @@
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;
@@ -73,6 +145,424 @@ void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload)
ast_free(payload);
}
/*!
* \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;
struct ast_json_error err;
RAII_VAR(char *, file_path, NULL, ast_free);
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)) {
size_t file_path_size;
/* 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);
file_path_size = strlen(ast_config_AST_DATA_DIR) + 3 + strlen(STIR_SHAKEN_DIR_NAME) + strlen(filename) + 1;
file_path = ast_calloc(1, file_path_size);
snprintf(file_path, sizeof(*file_path), "%s/keys/%s/%s", ast_config_AST_DATA_DIR, STIR_SHAKEN_DIR_NAME, filename);
/* 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;
}
}
if (stir_shaken_verify_signature(payload, 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, &err);
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, &err);
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.
@@ -90,7 +580,7 @@ static struct ast_stir_shaken_payload *stir_shaken_verify_json(struct ast_json *
payload = ast_calloc(1, sizeof(*payload));
if (!payload) {
ast_log(LOG_ERROR, "Failed to allocate STIR_SHAKEN payload\n");
ast_log(LOG_ERROR, "Failed to allocate STIR/SHAKEN payload\n");
goto cleanup;
}
@@ -234,7 +724,7 @@ static unsigned char *stir_shaken_sign(char *json_str, EVP_PKEY *private_key)
/* There are 6 bits to 1 base64 digit, so in order to get the size of the base64 encoded
* signature, we need to multiply by the number of bits in a byte and divide by 6. Since
* there's rounding when doing base64 conversions, add 3 bytes, just in case, and account
* for padding. Add another byte for the NULL-terminator so we don't lose data.
* 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);
@@ -464,9 +954,6 @@ static int load_module(void)
return AST_MODULE_LOAD_SUCCESS;
}
#undef AST_BUILDOPT_SUM
#define AST_BUILDOPT_SUM ""
AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER,
"STIR/SHAKEN Module for Asterisk",
.support_level = AST_MODULE_SUPPORT_CORE,
@@ -474,4 +961,5 @@ AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_
.unload = unload_module,
.reload = reload_module,
.load_pri = AST_MODPRI_CHANNEL_DEPEND - 1,
.requires = "res_curl",
);