[Asterisk-code-review] res_stir_shaken: Implemented signature verification. (asterisk[16])

George Joseph asteriskteam at digium.com
Tue Oct 6 09:07:58 CDT 2020


George Joseph has submitted this change. ( https://gerrit.asterisk.org/c/asterisk/+/15028 )

Change subject: res_stir_shaken: Implemented signature verification.
......................................................................

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
---
M Makefile
A doc/UPGRADE-staging/res_stir_shaken_directory.txt
M include/asterisk/res_stir_shaken.h
M res/res_stir_shaken.c
M res/res_stir_shaken/certificate.c
A res/res_stir_shaken/curl.c
A res/res_stir_shaken/curl.h
M res/res_stir_shaken/general.c
M res/res_stir_shaken/general.h
M res/res_stir_shaken/stir_shaken.c
M res/res_stir_shaken/stir_shaken.h
11 files changed, 832 insertions(+), 18 deletions(-)

Approvals:
  Benjamin Keith Ford: Looks good to me, but someone else must approve
  George Joseph: Looks good to me, approved; Approved for Submit



diff --git a/Makefile b/Makefile
index 49861ba..fd1c1db 100644
--- a/Makefile
+++ b/Makefile
@@ -565,7 +565,7 @@
 	"$(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 0000000..160241e
--- /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 16f0139..a65a887 100644
--- a/include/asterisk/res_stir_shaken.h
+++ b/include/asterisk/res_stir_shaken.h
@@ -26,6 +26,21 @@
 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
  *
  * \retval The stir/shaken sorcery context
@@ -41,6 +56,11 @@
  * \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 3ea7ae9..3f79596 100644
--- a/res/res_stir_shaken.c
+++ b/res/res_stir_shaken.c
@@ -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;
@@ -74,6 +146,424 @@
 }
 
 /*!
+ * \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 @@
 
 	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 @@
 	/* 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 @@
 	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 @@
 	.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 812fc1e..e889a36 100644
--- a/res/res_stir_shaken/certificate.c
+++ b/res/res_stir_shaken/certificate.c
@@ -119,7 +119,7 @@
 		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 0000000..634c2bf
--- /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 <bford at sangoma.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.
+ */
+
+#include "asterisk.h"
+
+#include "asterisk/utils.h"
+#include "asterisk/logger.h"
+#include "curl.h"
+#include "general.h"
+
+#include <curl/curl.h>
+
+/* 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 0000000..d587327
--- /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 <bford at sangoma.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.
+ */
+#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 7e807bb..edf8f85 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 @@
 	);
 	/*! 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 @@
 	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 @@
 	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 0c0c5f0..357933b 100644
--- a/res/res_stir_shaken/general.h
+++ b/res/res_stir_shaken/general.h
@@ -73,6 +73,17 @@
 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
  *
  * \retval 0 on success, -1 on error
diff --git a/res/res_stir_shaken/stir_shaken.c b/res/res_stir_shaken/stir_shaken.c
index 5f5c054..10caca9 100644
--- a/res/res_stir_shaken/stir_shaken.c
+++ b/res/res_stir_shaken/stir_shaken.c
@@ -83,9 +83,9 @@
 	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 @@
 		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 933b3bb..a49050e 100644
--- a/res/res_stir_shaken/stir_shaken.h
+++ b/res/res_stir_shaken/stir_shaken.h
@@ -42,13 +42,14 @@
 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 */

-- 
To view, visit https://gerrit.asterisk.org/c/asterisk/+/15028
To unsubscribe, or for help writing mail filters, visit https://gerrit.asterisk.org/settings

Gerrit-Project: asterisk
Gerrit-Branch: 16
Gerrit-Change-Id: I3ba4c63880493bf8c7d17a9cfca1af0e934d1a1c
Gerrit-Change-Number: 15028
Gerrit-PatchSet: 2
Gerrit-Owner: Joshua Colp <jcolp at sangoma.com>
Gerrit-Reviewer: Benjamin Keith Ford <bford at digium.com>
Gerrit-Reviewer: Friendly Automation
Gerrit-Reviewer: George Joseph <gjoseph at digium.com>
Gerrit-MessageType: merged
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20201006/0b4f8a21/attachment-0001.html>


More information about the asterisk-code-review mailing list