diff --git a/Makefile b/Makefile index d2b17fff48..3d11ccd472 100644 --- a/Makefile +++ b/Makefile @@ -565,7 +565,7 @@ INSTALLDIRS="$(ASTLIBDIR)" "$(ASTMODDIR)" "$(ASTSBINDIR)" "$(ASTETCDIR)" "$(ASTV "$(ASTDATADIR)/firmware/iax" "$(ASTDATADIR)/images" "$(ASTDATADIR)/keys" \ "$(ASTDATADIR)/phoneprov" "$(ASTDATADIR)/rest-api" "$(ASTDATADIR)/static-http" \ "$(ASTDATADIR)/sounds" "$(ASTDATADIR)/moh" "$(ASTMANDIR)/man8" "$(AGI_DIR)" "$(ASTDBDIR)" \ - "$(ASTDATADIR)/third-party" + "$(ASTDATADIR)/third-party" "${ASTDATADIR}/keys/stir_shaken" installdirs: @for i in $(INSTALLDIRS); do \ diff --git a/doc/UPGRADE-staging/res_stir_shaken_directory.txt b/doc/UPGRADE-staging/res_stir_shaken_directory.txt new file mode 100644 index 0000000000..160241e742 --- /dev/null +++ b/doc/UPGRADE-staging/res_stir_shaken_directory.txt @@ -0,0 +1,5 @@ +Subject: res_stir_shaken + +A new directory has been added under the default (e.g., /var/lib/asterisk) - +inside the 'keys' directory - named 'stir_shaken'. This directory will +hold public keys that have been downloaded for STIR/SHAKEN verification. diff --git a/include/asterisk/res_stir_shaken.h b/include/asterisk/res_stir_shaken.h index 16f0139ff9..a65a887cff 100644 --- a/include/asterisk/res_stir_shaken.h +++ b/include/asterisk/res_stir_shaken.h @@ -25,6 +25,21 @@ struct ast_stir_shaken_payload; struct ast_json; +/*! + * \brief Verify a JSON STIR/SHAKEN payload + * + * \param header The payload header + * \param payload The payload section + * \param signature The payload signature + * \param algorithm The signature algorithm + * \param public_key_url The public key URL + * + * \retval ast_stir_shaken_payload on success + * \retval NULL on failure + */ +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); + /*! * \brief Retrieve the stir/shaken sorcery context * @@ -41,6 +56,11 @@ void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload); * \brief Sign a JSON STIR/SHAKEN payload * * \note This function will automatically add the "attest", "iat", and "origid" fields. + * + * \param json The JWT to sign + * + * \retval ast_stir_shaken_payload on success + * \retval NULL on failure */ struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json); diff --git a/res/res_stir_shaken.c b/res/res_stir_shaken.c index 3ea7ae9702..3f79596201 100644 --- a/res/res_stir_shaken.c +++ b/res/res_stir_shaken.c @@ -18,6 +18,8 @@ /*** MODULEINFO crypto + curl + res_curl core ***/ @@ -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 + + STIR/SHAKEN module for Asterisk + + + STIR/SHAKEN general options + + Must be of type 'general'. + + + File path to the certificate authority certificate + + + File path to a chain of trust + + + Maximum size to use for caching public keys + + + Maximum time to wait to CURL certificates + + + + STIR/SHAKEN certificate store options + + Must be of type 'store'. + + + Path to a directory containing certificates + + + URL to the public key(s) + + 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 + + + + + STIR/SHAKEN certificate options + + Must be of type 'certificate'. + + + File path to a certificate + + + URL to the public key + + Must be a valid http, or https, URL. + + + + + + ***/ #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", ); diff --git a/res/res_stir_shaken/certificate.c b/res/res_stir_shaken/certificate.c index 812fc1e6d9..e889a36281 100644 --- a/res/res_stir_shaken/certificate.c +++ b/res/res_stir_shaken/certificate.c @@ -119,7 +119,7 @@ static int stir_shaken_certificate_apply(const struct ast_sorcery *sorcery, void return -1; } - private_key = read_private_key(cert->path); + private_key = stir_shaken_read_key(cert->path, 1); if (!private_key) { return -1; } diff --git a/res/res_stir_shaken/curl.c b/res/res_stir_shaken/curl.c new file mode 100644 index 0000000000..634c2bf68a --- /dev/null +++ b/res/res_stir_shaken/curl.c @@ -0,0 +1,199 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2020, Sangoma Technologies Corporation + * + * Ben Ford + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#include "asterisk.h" + +#include "asterisk/utils.h" +#include "asterisk/logger.h" +#include "curl.h" +#include "general.h" + +#include + +/* Used to check CURL headers */ +#define MAX_HEADER_LENGTH 1023 + +/* Used for CURL requests */ +#define GLOBAL_USERAGENT "asterisk-libcurl-agent/1.0" + +/* CURL callback data to avoid storing useless info in AstDB */ +struct curl_cb_data { + char *cache_control; + char *expires; +}; + +struct curl_cb_data *curl_cb_data_create(void) +{ + struct curl_cb_data *data; + + data = ast_calloc(1, sizeof(data)); + + return data; +} + +void curl_cb_data_free(struct curl_cb_data *data) +{ + if (!data) { + return; + } + + ast_free(data->cache_control); + ast_free(data->expires); + + ast_free(data); +} + +char *curl_cb_data_get_cache_control(const struct curl_cb_data *data) +{ + if (!data) { + return NULL; + } + + return data->cache_control; +} + +char *curl_cb_data_get_expires(const struct curl_cb_data *data) +{ + if (!data) { + return NULL; + } + + return data->expires; +} + +/*! + * \brief Called when a CURL request completes + * + * \param data The curl_cb_data structure to store expiration info + */ +static size_t curl_header_callback(char *buffer, size_t size, size_t nitems, void *data) +{ + struct curl_cb_data *cb_data = data; + size_t realsize; + char *header; + char *value; + + realsize = size * nitems; + + if (realsize > MAX_HEADER_LENGTH) { + ast_log(LOG_WARNING, "CURL header length is too large (size: '%zu' | max: '%d')\n", + realsize, MAX_HEADER_LENGTH); + return 0; + } + + header = ast_alloca(realsize + 1); + memcpy(header, buffer, realsize); + header[realsize] = '\0'; + value = strchr(header, ':'); + if (!value) { + return realsize; + } + *value++ = '\0'; + value = ast_trim_blanks(ast_skip_blanks(value)); + + if (!strcasecmp(header, "Cache-Control")) { + cb_data->cache_control = ast_strdup(value); + } else if (!strcasecmp(header, "Expires")) { + cb_data->expires = ast_strdup(value); + } + + return realsize; +} + +/*! + * \brief Prepare a CURL instance to use + * + * \param data The CURL callback data + * + * \retval NULL on failure + * \retval CURL instance on success + */ +static CURL *get_curl_instance(struct curl_cb_data *data) +{ + CURL *curl; + struct stir_shaken_general *cfg; + unsigned int curl_timeout; + + cfg = stir_shaken_general_get(); + curl_timeout = ast_stir_shaken_curl_timeout(cfg); + ao2_cleanup(cfg); + + curl = curl_easy_init(); + if (!curl) { + return NULL; + } + + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, curl_timeout); + curl_easy_setopt(curl, CURLOPT_USERAGENT, GLOBAL_USERAGENT); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_callback); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, data); + + return curl; +} + +int curl_public_key(const char *public_key_url, const char *path, struct curl_cb_data *data) +{ + FILE *public_key_file; + long http_code; + CURL *curl; + char curl_errbuf[CURL_ERROR_SIZE + 1]; + char hash[41]; + + ast_sha1_hash(hash, public_key_url); + + curl_errbuf[CURL_ERROR_SIZE] = '\0'; + + public_key_file = fopen(path, "wb"); + if (!public_key_file) { + ast_log(LOG_ERROR, "Failed to open file '%s' to write public key from '%s': %s (%d)\n", + path, public_key_url, strerror(errno), errno); + return -1; + } + + curl = get_curl_instance(data); + if (!curl) { + ast_log(LOG_ERROR, "Failed to set up CURL isntance for '%s'\n", public_key_url); + fclose(public_key_file); + return -1; + } + + curl_easy_setopt(curl, CURLOPT_URL, public_key_url); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, public_key_file); + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf); + + if (curl_easy_perform(curl)) { + ast_log(LOG_ERROR, "%s\n", curl_errbuf); + curl_easy_cleanup(curl); + fclose(public_key_file); + return -1; + } + + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_easy_cleanup(curl); + fclose(public_key_file); + + if (http_code / 100 != 2) { + ast_log(LOG_ERROR, "Failed to retrieve URL '%s': code %ld\n", public_key_url, http_code); + return -1; + } + + return 0; +} diff --git a/res/res_stir_shaken/curl.h b/res/res_stir_shaken/curl.h new file mode 100644 index 0000000000..d587327ddf --- /dev/null +++ b/res/res_stir_shaken/curl.h @@ -0,0 +1,73 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2020, Sangoma Technologies Corporation + * + * Ben Ford + * + * 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. + */ +#ifndef _STIR_SHAKEN_CURL_H +#define _STIR_SHAKEN_CURL_H + +/* Forward declarion for CURL callback data */ +struct curl_cb_data; + +/*! + * \brief Allocate memory for a curl_cb_data struct + * + * \note This will need to be freed by the consumer using curl_cb_data_free + * + * \retval NULL on failure + * \retval curl_cb_struct on success + */ +struct curl_cb_data *curl_cb_data_create(void); + +/*! + * \brief Free a curl_cb_data struct + * + * \param data The curl_cb_data struct to free + */ +void curl_cb_data_free(struct curl_cb_data *data); + +/*! + * \brief Get the cache_control field from a curl_cb_data struct + * + * \param data The curl_cb_data + * + * \retval cache_control on success + * \retval NULL otherwise + */ +char *curl_cb_data_get_cache_control(const struct curl_cb_data *data); + +/*! + * \brief Get the expires field from a curl_cb_data struct + * + * \param data The curl_cb_data + * + * \retval expires on success + * \retval NULL otherwise + */ +char *curl_cb_data_get_expires(const struct curl_cb_data *data); + +/*! + * \brief CURL the public key from the provided URL to the specified path + * + * \param public_key_url The public key URL + * \param path The path to download the file to + * \param data The curl_cb_data + * + * \retval 1 on failure + * \retval 0 on success + */ +int curl_public_key(const char *public_key_url, const char *path, struct curl_cb_data *data); + +#endif /* _STIR_SHAKEN_CURL_H */ diff --git a/res/res_stir_shaken/general.c b/res/res_stir_shaken/general.c index 7e807bb61c..edf8f85dd0 100644 --- a/res/res_stir_shaken/general.c +++ b/res/res_stir_shaken/general.c @@ -30,6 +30,7 @@ #define DEFAULT_CA_FILE "" #define DEFAULT_CA_PATH "" #define DEFAULT_CACHE_MAX_SIZE 1000 +#define DEFAULT_CURL_TIMEOUT 2 struct stir_shaken_general { SORCERY_OBJECT(details); @@ -41,6 +42,8 @@ struct stir_shaken_general { ); /*! Maximum size of public keys cache */ unsigned int cache_max_size; + /*! Maximum time to wait to CURL certificates */ + unsigned int curl_timeout; }; static struct stir_shaken_general *default_config = NULL; @@ -78,6 +81,11 @@ unsigned int ast_stir_shaken_cache_max_size(const struct stir_shaken_general *cf return cfg ? cfg->cache_max_size : DEFAULT_CACHE_MAX_SIZE; } +unsigned int ast_stir_shaken_curl_timeout(const struct stir_shaken_general *cfg) +{ + return cfg ? cfg->curl_timeout : DEFAULT_CURL_TIMEOUT; +} + static void stir_shaken_general_destructor(void *obj) { struct stir_shaken_general *cfg = obj; @@ -250,6 +258,9 @@ int stir_shaken_general_load(void) ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "cache_max_size", __stringify(DEFAULT_CACHE_MAX_SIZE), OPT_UINT_T, 0, FLDSET(struct stir_shaken_general, cache_max_size)); + ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "curl_timeout", + __stringify(DEFAULT_CURL_TIMEOUT), OPT_UINT_T, 0, + FLDSET(struct stir_shaken_general, curl_timeout)); if (ast_sorcery_instance_observer_add(sorcery, &stir_shaken_general_observer)) { ast_log(LOG_ERROR, "stir/shaken - failed to register loaded observer for '%s' " diff --git a/res/res_stir_shaken/general.h b/res/res_stir_shaken/general.h index 0c0c5f09ac..357933b82a 100644 --- a/res/res_stir_shaken/general.h +++ b/res/res_stir_shaken/general.h @@ -72,6 +72,17 @@ const char *ast_stir_shaken_ca_path(const struct stir_shaken_general *cfg); */ unsigned int ast_stir_shaken_cache_max_size(const struct stir_shaken_general *cfg); +/*! + * \brief Retrieve the 'curl_timeout' general configuration option value + * + * \note If a NULL configuration is given, then the default value is returned + * + * \param cfg A 'general' configuration object + * + * \retval The 'curl_timeout' value + */ +unsigned int ast_stir_shaken_curl_timeout(const struct stir_shaken_general *cfg); + /*! * \brief Load time initialization for the stir/shaken 'general' configuration * diff --git a/res/res_stir_shaken/stir_shaken.c b/res/res_stir_shaken/stir_shaken.c index 5f5c05412d..10caca9851 100644 --- a/res/res_stir_shaken/stir_shaken.c +++ b/res/res_stir_shaken/stir_shaken.c @@ -83,9 +83,9 @@ char *stir_shaken_tab_complete_name(const char *word, struct ao2_container *cont return NULL; } -EVP_PKEY *read_private_key(const char *path) +EVP_PKEY *stir_shaken_read_key(const char *path, int priv) { - EVP_PKEY *private_key = NULL; + EVP_PKEY *key = NULL; FILE *fp; fp = fopen(path, "r"); @@ -94,20 +94,26 @@ EVP_PKEY *read_private_key(const char *path) return NULL; } - if (!PEM_read_PrivateKey(fp, &private_key, NULL, NULL)) { - ast_log(LOG_ERROR, "Failed to read private key from file '%s'\n", path); + if (priv) { + key = PEM_read_PrivateKey(fp, NULL, NULL, NULL); + } else { + key = PEM_read_PUBKEY(fp, NULL, NULL, NULL); + } + + if (!key) { + ast_log(LOG_ERROR, "Failed to read %s key from file '%s'\n", priv ? "private" : "public", path); fclose(fp); return NULL; } - if (EVP_PKEY_id(private_key) != EVP_PKEY_EC) { - ast_log(LOG_ERROR, "Private key from '%s' must be of type EVP_PKEY_EC\n", path); + if (EVP_PKEY_id(key) != EVP_PKEY_EC) { + ast_log(LOG_ERROR, "%s key from '%s' must be of type EVP_PKEY_EC\n", priv ? "private" : "public", path); fclose(fp); - EVP_PKEY_free(private_key); + EVP_PKEY_free(key); return NULL; } fclose(fp); - return private_key; + return key; } diff --git a/res/res_stir_shaken/stir_shaken.h b/res/res_stir_shaken/stir_shaken.h index 933b3bb834..a49050e539 100644 --- a/res/res_stir_shaken/stir_shaken.h +++ b/res/res_stir_shaken/stir_shaken.h @@ -42,13 +42,14 @@ int stir_shaken_cli_show(void *obj, void *arg, int flags); char *stir_shaken_tab_complete_name(const char *word, struct ao2_container *container); /*! - * \brief Reads the private key from the specified path + * \brief Reads the public (or private) key from the specified path * * \param path The path to the file containing the private key + * \param priv Specify 0 for public, 1 for private * * \retval NULL on failure - * \retval The private key on success + * \retval The public/private key on success */ -EVP_PKEY *read_private_key(const char *path); +EVP_PKEY *stir_shaken_read_key(const char *path, int priv); #endif /* _STIR_SHAKEN_H */