From 36dde257b21afd31113d810cf14ccb8eae6dc525 Mon Sep 17 00:00:00 2001
From: Ben Ford <bford@digium.com>
Date: Mon, 28 Feb 2022 11:19:54 -0600
Subject: [PATCH] AST-2022-002 - res_stir_shaken/curl: Add ACL checks for Identity header.

Adds a new configuration option, stir_shaken_profile, in pjsip.conf that
can be specified on a per endpoint basis. This option will reference a
stir_shaken_profile that can be configured in stir_shaken.conf. The type
of this option must be 'profile'. The stir_shaken option can be
specified on this object with the same values as before (attest, verify,
on), but it cannot be off since having the profile itself implies wanting
STIR/SHAKEN support. You can also specify an ACL from acl.conf (along
with permit and deny lines in the object itself) that will be used to
limit what interfaces Asterisk will attempt to retrieve information from
when reading the Identity header.

ASTERISK-29476

Change-Id: I87fa61f78a9ea0cd42530691a30da3c781842406
---

diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample
index 8507b4e..b1c1657 100644
--- a/configs/samples/pjsip.conf.sample
+++ b/configs/samples/pjsip.conf.sample
@@ -349,6 +349,7 @@
 ; STIR/SHAKEN support.
 ;
 ;stir_shaken=no
+;stir_shaken_profile=my_profile
 
 ;[6001]
 ;type=auth
@@ -930,6 +931,9 @@
                            ; happens to the call if verification fails; it's up to
                            ; you to determine what to do with the results.
                            ; (default: no)
+;stir_shaken_profile =
+                           ; If a profile is specified (defined in stir_shaken.conf),
+                           ; this endpoint will follow the rules defined there.
 ;allow_unauthenticated_options =
                            ; By default, chan_pjsip will challenge an incoming
                            ; OPTIONS request for authentication credentials just
diff --git a/configs/samples/stir_shaken.conf.sample b/configs/samples/stir_shaken.conf.sample
index c39bc97..677d3bb 100644
--- a/configs/samples/stir_shaken.conf.sample
+++ b/configs/samples/stir_shaken.conf.sample
@@ -83,3 +83,21 @@
 ;
 ; Must have an attestation of A, B, or C
 ;attestation=C
+;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;
+; Profiles can be defined here which can be referenced by channel drivers.
+;[my_profile]
+;
+; type must be "profile"
+;type=profile
+;
+; Set stir_shaken to 'attest', 'verify', or 'on', which is the default
+;stir_shaken=on
+;
+; You can specify an ACL that will be used strictly for the Identity header when downloading public certificates
+;acllist=myacllist
+;
+; You can also do permit / deny lines if you want (also supports IPv6)
+;permit=0.0.0.0/0.0.0.0
+;deny=127.0.0.1
diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h
index 9f35393..209cdbf 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -878,6 +878,8 @@
 		AST_STRING_FIELD(accountcode);
 		/*! If set, we'll push incoming MWI NOTIFYs to stasis using this mailbox */
 		AST_STRING_FIELD(incoming_mwi_mailbox);
+		/*! STIR/SHAKEN profile to use */
+		AST_STRING_FIELD(stir_shaken_profile);
 	);
 	/*! Configuration for extensions */
 	struct ast_sip_endpoint_extensions extensions;
diff --git a/include/asterisk/res_stir_shaken.h b/include/asterisk/res_stir_shaken.h
index 92eb0ec..ece99b5 100644
--- a/include/asterisk/res_stir_shaken.h
+++ b/include/asterisk/res_stir_shaken.h
@@ -38,6 +38,8 @@
 
 struct ast_stir_shaken_payload;
 
+struct ast_acl_list;
+
 struct ast_json;
 
 /*!
@@ -66,6 +68,38 @@
 unsigned int ast_stir_shaken_get_signature_timeout(void);
 
 /*!
+ * \brief Retrieve a stir_shaken_profile by id
+ *
+ * \note The profile will need to be unref'd when not needed anymore
+ *
+ * \param id The id of the stir_shaken_profile to get
+ *
+ * \retval stir_shaken_profile on success
+ * \retval NULL on failure
+ */
+struct stir_shaken_profile *ast_stir_shaken_get_profile(const char *id);
+
+/*!
+ * \brief Check if a stir_shaken_profile supports attestation
+ *
+ * \param profile The stir_shaken_profile to test
+ *
+ * \retval 0 if not supported
+ * \retval 1 if supported
+ */
+unsigned int ast_stir_shaken_profile_supports_attestation(const struct stir_shaken_profile *profile);
+
+/*!
+ * \brief Check if a stir_shaken_profile supports verification
+ *
+ * \param profile The stir_shaken_profile to test
+ *
+ * \retval 0 if not supported
+ * \retval 1 if supported
+ */
+unsigned int ast_stir_shaken_profile_supports_verification(const struct stir_shaken_profile *profile);
+
+/*!
  * \brief Add a STIR/SHAKEN verification result to a channel
  *
  * \param chan The channel
@@ -113,6 +147,26 @@
 	const char *algorithm, const char *public_cert_url, int *failure_code);
 
 /*!
+ * \brief Same as ast_stir_shaken_verify2, but passes in a stir_shaken_profile with additional configuration
+ *
+ * \note failure_code will be written to in this function
+ *
+ * \param header The payload header
+ * \param payload The payload section
+ * \param signature The payload signature
+ * \param algorithm The signature algorithm
+ * \param public_cert_url The public key URL
+ * \param failure_code Additional failure information
+ * \param profile The stir_shaken_profile
+ *
+ * \retval ast_stir_shaken_payload on success
+ * \retval NULL on failure
+ */
+struct ast_stir_shaken_payload *ast_stir_shaken_verify_with_profile(const char *header, const char *payload,
+	const char *signature, const char *algorithm, const char *public_cert_url, int *failure,
+	const struct stir_shaken_profile *profile);
+
+/*!
  * \brief Retrieve the stir/shaken sorcery context
  *
  * \retval The stir/shaken sorcery context
diff --git a/res/res_pjsip/pjsip_config.xml b/res/res_pjsip/pjsip_config.xml
index 3e0a6e7..ca5a266 100644
--- a/res/res_pjsip/pjsip_config.xml
+++ b/res/res_pjsip/pjsip_config.xml
@@ -1424,6 +1424,13 @@
 						INVITEs, an Identity header will be added.</para>
 					</description>
 				</configOption>
+				<configOption name="stir_shaken_profile" default="">
+					<synopsis>STIR/SHAKEN profile containing additional configuration options</synopsis>
+					<description><para>
+						A STIR/SHAKEN profile that is defined in stir_shaken.conf. Contains
+						several options and rules used for STIR/SHAKEN.</para>
+					</description>
+				</configOption>
 				<configOption name="allow_unauthenticated_options" default="no">
 					<synopsis>Skip authentication when receiving OPTIONS requests</synopsis>
 					<description><para>
diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c
index e32a3d2..39c3dab 100644
--- a/res/res_pjsip/pjsip_configuration.c
+++ b/res/res_pjsip/pjsip_configuration.c
@@ -2192,6 +2192,7 @@
 		"prefer: pending, operation: intersect, keep: all",
 		codec_prefs_handler, outgoing_answer_codec_prefs_to_str, NULL, 0, 0);
 	ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "stir_shaken", "off", stir_shaken_handler, stir_shaken_to_str, NULL, 0, 0);
+	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "stir_shaken_profile", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, stir_shaken_profile));
 	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "allow_unauthenticated_options", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, allow_unauthenticated_options));
 
 	if (ast_sip_initialize_sorcery_transport()) {
diff --git a/res/res_pjsip_stir_shaken.c b/res/res_pjsip_stir_shaken.c
index 27aa4d8..19e846f 100644
--- a/res/res_pjsip_stir_shaken.c
+++ b/res/res_pjsip_stir_shaken.c
@@ -217,13 +217,16 @@
 	int mismatch = 0;
 	struct ast_stir_shaken_payload *ss_payload;
 	int failure_code = 0;
+	RAII_VAR(struct stir_shaken_profile *, profile, NULL, ao2_cleanup);
 
 	/* Check if this is a reinvite. If it is, we don't need to do anything */
 	if (rdata->msg_info.to->tag.slen) {
 		return 0;
 	}
 
-	if ((session->endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_VERIFY) == 0) {
+	profile = ast_stir_shaken_get_profile(session->endpoint->stir_shaken_profile);
+	if ((profile && !ast_stir_shaken_profile_supports_verification(profile))
+		&& ((session->endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_VERIFY) == 0)) {
 		return 0;
 	}
 
@@ -309,7 +312,8 @@
 
 	attestation = get_attestation_from_payload(payload);
 
-	ss_payload = ast_stir_shaken_verify2(header, payload, signature, algorithm, public_cert_url, &failure_code);
+	ss_payload = ast_stir_shaken_verify_with_profile(header, payload, signature, algorithm, public_cert_url, &failure_code, profile);
+
 	if (!ss_payload) {
 
 		if (failure_code == AST_STIR_SHAKEN_VERIFY_FAILED_TO_GET_CERT) {
@@ -471,7 +475,11 @@
 
 static void stir_shaken_outgoing_request(struct ast_sip_session *session, pjsip_tx_data *tdata)
 {
-	if ((session->endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_ATTEST) == 0) {
+	RAII_VAR(struct stir_shaken_profile *, profile, NULL, ao2_cleanup);
+
+	profile = ast_stir_shaken_get_profile(session->endpoint->stir_shaken_profile);
+	if ((profile && !ast_stir_shaken_profile_supports_attestation(profile))
+		&& ((session->endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_ATTEST) == 0)) {
 		return;
 	}
 
diff --git a/res/res_stir_shaken.c b/res/res_stir_shaken.c
index 373a1a1..157500a 100644
--- a/res/res_stir_shaken.c
+++ b/res/res_stir_shaken.c
@@ -38,6 +38,7 @@
 #include "asterisk/global_datastores.h"
 #include "asterisk/app.h"
 #include "asterisk/test.h"
+#include "asterisk/acl.h"
 
 #include "asterisk/res_stir_shaken.h"
 #include "res_stir_shaken/stir_shaken.h"
@@ -45,6 +46,7 @@
 #include "res_stir_shaken/store.h"
 #include "res_stir_shaken/certificate.h"
 #include "res_stir_shaken/curl.h"
+#include "res_stir_shaken/profile.h"
 
 /*** DOCUMENTATION
 	<configInfo name="res_stir_shaken" language="en_US">
@@ -108,6 +110,29 @@
 					<synopsis>The caller ID number to match on.</synopsis>
 				</configOption>
 			</configObject>
+			<configObject name="profile">
+				<synopsis>STIR/SHAKEN profile configuration options</synopsis>
+				<configOption name="type">
+					<synopsis>Must be of type 'profile'.</synopsis>
+				</configOption>
+				<configOption name="stir_shaken" default="on">
+					<synopsis>STIR/SHAKEN configuration settings</synopsis>
+					<description><para>
+					        Attest, verify, or do both STIR/SHAKEN operations. On incoming
+						INVITEs, the Identity header will be checked for validity. On
+						outgoing INVITEs, an Identity header will be added.</para>
+					</description>
+				</configOption>
+				<configOption name="acllist" default="">
+					<synopsis>An existing ACL from acl.conf to use</synopsis>
+				</configOption>
+				<configOption name="permit" default="">
+					<synopsis>An IP or subnet to permit</synopsis>
+				</configOption>
+				<configOption name="deny" default="">
+					<synopsis>An IP or subnet to deny</synopsis>
+				</configOption>
+			</configObject>
 		</configFile>
 	</configInfo>
 	<function name="STIR_SHAKEN" language="en_US">
@@ -205,6 +230,33 @@
 	return ast_stir_shaken_signature_timeout(stir_shaken_general_get());
 }
 
+struct stir_shaken_profile *ast_stir_shaken_get_profile(const char *id)
+{
+	if (ast_strlen_zero(id)) {
+		return NULL;
+	}
+
+	return ast_stir_shaken_get_profile_by_name(id);
+}
+
+unsigned int ast_stir_shaken_profile_supports_attestation(const struct stir_shaken_profile *profile)
+{
+	if (!profile) {
+		return 0;
+	}
+
+	return (profile->stir_shaken & STIR_SHAKEN_ATTEST);
+}
+
+unsigned int ast_stir_shaken_profile_supports_verification(const struct stir_shaken_profile *profile)
+{
+	if (!profile) {
+		return 0;
+	}
+
+	return (profile->stir_shaken & STIR_SHAKEN_VERIFY);
+}
+
 /*!
  * \brief Convert an ast_stir_shaken_verification_result to string representation
  *
@@ -554,7 +606,7 @@
  * \retval NULL on failure
  * \retval full path filename on success
  */
-static char *run_curl(const char *public_cert_url, const char *path)
+static char *run_curl(const char *public_cert_url, const char *path, const struct ast_acl_list *acl)
 {
 	struct curl_cb_data *data;
 	char *filename;
@@ -565,7 +617,7 @@
 		return NULL;
 	}
 
-	filename = curl_public_key(public_cert_url, path, data);
+	filename = curl_public_key(public_cert_url, path, data, acl);
 	if (!filename) {
 		ast_log(LOG_ERROR, "Could not retrieve public key for '%s'\n", public_cert_url);
 		curl_cb_data_free(data);
@@ -591,7 +643,7 @@
  * \retval NULL on failure
  * \retval full path filename on success
  */
-static char *curl_and_check_expiration(const char *public_cert_url, const char *path, int *curl)
+static char *curl_and_check_expiration(const char *public_cert_url, const char *path, int *curl, const struct ast_acl_list *acl)
 {
 	char *filename;
 
@@ -600,7 +652,7 @@
 		return NULL;
 	}
 
-	filename = run_curl(public_cert_url, path);
+	filename = run_curl(public_cert_url, path, acl);
 	if (!filename) {
 		return NULL;
 	}
@@ -662,7 +714,8 @@
  * \retval 0 on success
  * \retval 1 on failure
  */
-static int stir_shaken_verify_setup_file_paths(const char *public_cert_url, char **file_path, char **dir_path, int *curl)
+static int stir_shaken_verify_setup_file_paths(const char *public_cert_url, char **file_path, char **dir_path, int *curl,
+	const struct ast_acl_list *acl)
 {
 	*file_path = get_path_to_public_key(public_cert_url);
 	if (ast_asprintf(dir_path, "%s/keys/%s", ast_config_AST_DATA_DIR, STIR_SHAKEN_DIR_NAME) < 0) {
@@ -680,7 +733,7 @@
 		ast_free(*file_path);
 
 		/* Download to the default path */
-		*file_path = run_curl(public_cert_url, *dir_path);
+		*file_path = run_curl(public_cert_url, *dir_path, acl);
 		if (!(*file_path)) {
 			return 1;
 		}
@@ -704,7 +757,7 @@
  * \retval 1 on failure
  */
 static int stir_shaken_verify_validate_cert(const char *public_cert_url, char **file_path, char *dir_path, int *curl,
-	EVP_PKEY **public_key)
+	EVP_PKEY **public_key, const struct ast_acl_list *acl)
 {
 	if (public_key_is_expired(public_cert_url)) {
 
@@ -714,7 +767,7 @@
 
 		/* If this fails, then there's nothing we can do */
 		ast_free(*file_path);
-		*file_path = curl_and_check_expiration(public_cert_url, dir_path, curl);
+		*file_path = curl_and_check_expiration(public_cert_url, dir_path, curl, acl);
 		if (!(*file_path)) {
 			return 1;
 		}
@@ -730,7 +783,7 @@
 		remove_public_key_from_astdb(public_cert_url);
 
 		ast_free(*file_path);
-		*file_path = curl_and_check_expiration(public_cert_url, dir_path, curl);
+		*file_path = curl_and_check_expiration(public_cert_url, dir_path, curl, acl);
 		if (!(*file_path)) {
 			return 1;
 		}
@@ -757,6 +810,12 @@
 struct ast_stir_shaken_payload *ast_stir_shaken_verify2(const char *header, const char *payload, const char *signature,
 	const char *algorithm, const char *public_cert_url, int *failure_code)
 {
+	return ast_stir_shaken_verify_with_profile(header, payload, signature, algorithm, public_cert_url, failure_code, NULL);
+}
+
+struct ast_stir_shaken_payload *ast_stir_shaken_verify_with_profile(const char *header, const char *payload, const char *signature,
+	const char *algorithm, const char *public_cert_url, int *failure_code, const struct stir_shaken_profile *profile)
+{
 	struct ast_stir_shaken_payload *ret_payload;
 	EVP_PKEY *public_key;
 	int curl = 0;
@@ -764,11 +823,14 @@
 	RAII_VAR(char *, dir_path, NULL, ast_free);
 	RAII_VAR(char *, combined_str, NULL, ast_free);
 	size_t combined_size;
+	const struct ast_acl_list *acl;
 
 	if (stir_shaken_verify_check_empty_strings(header, payload, signature, algorithm, public_cert_url)) {
 		return NULL;
 	}
 
+	acl = profile ? (const struct ast_acl_list *)profile->acl : NULL;
+
 	/* Check to see if we have already downloaded this public cert. The reason we
 	 * store the file path is because:
 	 *
@@ -779,12 +841,12 @@
 	 * {configurable) directories, we already have the storage mechanism in place.
 	 * The only thing that would be left to do is pull from the configuration.
 	 */
-	if (stir_shaken_verify_setup_file_paths(public_cert_url, &file_path, &dir_path, &curl)) {
+	if (stir_shaken_verify_setup_file_paths(public_cert_url, &file_path, &dir_path, &curl, acl)) {
 		return NULL;
 	}
 
 	/* Check to see if the cert we downloaded (or already had) is expired */
-	if (stir_shaken_verify_validate_cert(public_cert_url, &file_path, dir_path, &curl, &public_key)) {
+	if (stir_shaken_verify_validate_cert(public_cert_url, &file_path, dir_path, &curl, &public_key, acl)) {
 		*failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_TO_GET_CERT;
 		return NULL;
 	}
@@ -1677,6 +1739,7 @@
 {
 	int res = 0;
 
+	stir_shaken_profile_unload();
 	stir_shaken_certificate_unload();
 	stir_shaken_store_unload();
 	stir_shaken_general_unload();
@@ -1716,6 +1779,11 @@
 		return AST_MODULE_LOAD_DECLINE;
 	}
 
+	if (stir_shaken_profile_load()) {
+		unload_module();
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
 	ast_sorcery_load(ast_stir_shaken_sorcery());
 
 	res |= ast_custom_function_register(&stir_shaken_function);
diff --git a/res/res_stir_shaken/curl.c b/res/res_stir_shaken/curl.c
index cd78461..fb06de5 100644
--- a/res/res_stir_shaken/curl.c
+++ b/res/res_stir_shaken/curl.c
@@ -21,9 +21,12 @@
 #include "asterisk/utils.h"
 #include "asterisk/logger.h"
 #include "asterisk/file.h"
+#include "asterisk/acl.h"
+
 #include "curl.h"
 #include "general.h"
 #include "stir_shaken.h"
+#include "profile.h"
 
 #include <curl/curl.h>
 #include <sys/stat.h>
@@ -52,6 +55,11 @@
 	const char *url;
 };
 
+struct curl_cb_open_socket {
+	const struct ast_acl_list *acl;
+	curl_socket_t *sockfd;
+};
+
 struct curl_cb_data *curl_cb_data_create(void)
 {
 	struct curl_cb_data *data;
@@ -73,6 +81,18 @@
 	ast_free(data);
 }
 
+static void curl_cb_open_socket_free(struct curl_cb_open_socket *data)
+{
+	if (!data) {
+		return;
+	}
+
+	close(*data->sockfd);
+
+	/* We don't need to free the ACL since we just use a reference */
+	ast_free(data);
+}
+
 char *curl_cb_data_get_cache_control(const struct curl_cb_data *data)
 {
 	if (!data) {
@@ -200,7 +220,26 @@
 	return real_size;
 }
 
-char *curl_public_key(const char *public_cert_url, const char *path, struct curl_cb_data *data)
+static curl_socket_t stir_shaken_curl_open_socket_callback(void *our_data, curlsocktype purpose, struct curl_sockaddr *address)
+{
+	struct curl_cb_open_socket *data = our_data;
+
+	if (!ast_acl_list_is_empty((struct ast_acl_list *)data->acl)) {
+		struct ast_sockaddr ast_address = { {0,} };
+
+		ast_sockaddr_copy_sockaddr(&ast_address, &address->addr, address->addrlen);
+
+		if (ast_apply_acl((struct ast_acl_list *)data->acl, &ast_address, NULL) != AST_SENSE_ALLOW) {
+			return CURLE_COULDNT_CONNECT;
+		}
+	}
+
+	*data->sockfd = socket(address->family, address->socktype, address->protocol);
+
+	return *data->sockfd;
+}
+
+char *curl_public_key(const char *public_cert_url, const char *path, struct curl_cb_data *data, const struct ast_acl_list *acl)
 {
 	FILE *public_key_file;
 	char *filename;
@@ -209,13 +248,25 @@
 	CURL *curl;
 	char curl_errbuf[CURL_ERROR_SIZE + 1];
 	struct curl_cb_write_buf *buf;
+	struct curl_cb_open_socket *open_socket_data;
+	curl_socket_t sockfd;
 
-	buf = ast_calloc(1, sizeof(*buf));
+	curl_errbuf[CURL_ERROR_SIZE] = '\0';
+
+ 	buf = ast_calloc(1, sizeof(*buf));
 	if (!buf) {
 		ast_log(LOG_ERROR, "Failed to allocate memory for CURL write buffer for %s\n", public_cert_url);
 		return NULL;
 	}
 
+	open_socket_data = ast_calloc(1, sizeof(*open_socket_data));
+	if (!open_socket_data) {
+		ast_log(LOG_ERROR, "Failed to allocate memory for open socket callback\n");
+		return NULL;
+	}
+	open_socket_data->acl = acl;
+	open_socket_data->sockfd = &sockfd;
+
 	buf->url = public_cert_url;
 	curl_errbuf[CURL_ERROR_SIZE] = '\0';
 
@@ -231,14 +282,19 @@
 	curl_easy_setopt(curl, CURLOPT_WRITEDATA, buf);
 	curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
 	curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, MAX_BUF_SIZE_PER_WRITE);
+	curl_easy_setopt(curl, CURLOPT_OPENSOCKETFUNCTION, stir_shaken_curl_open_socket_callback);
+	curl_easy_setopt(curl, CURLOPT_OPENSOCKETDATA, open_socket_data);
 
 	if (curl_easy_perform(curl)) {
 		ast_log(LOG_ERROR, "%s\n", curl_errbuf);
 		curl_easy_cleanup(curl);
 		ast_free(buf);
+		curl_cb_open_socket_free(open_socket_data);
 		return NULL;
 	}
 
+	curl_cb_open_socket_free(open_socket_data);
+
 	curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
 
 	curl_easy_cleanup(curl);
diff --git a/res/res_stir_shaken/curl.h b/res/res_stir_shaken/curl.h
index ae8feda..2dbd5d2 100644
--- a/res/res_stir_shaken/curl.h
+++ b/res/res_stir_shaken/curl.h
@@ -18,6 +18,8 @@
 #ifndef _STIR_SHAKEN_CURL_H
 #define _STIR_SHAKEN_CURL_H
 
+struct ast_acl_list;
+
 /* Forward declaration for CURL callback data */
 struct curl_cb_data;
 
@@ -66,10 +68,11 @@
  * \param public_cert_url The public cert URL
  * \param path The path to download the file to
  * \param data The curl_cb_data
+ * \param acl The ACL to use for cURL (if not NULL)
  *
  * \retval NULL on failure
  * \retval full path filename on success
  */
-char *curl_public_key(const char *public_cert_url, const char *path, struct curl_cb_data *data);
+char *curl_public_key(const char *public_cert_url, const char *path, struct curl_cb_data *data, const struct ast_acl_list *acl);
 
 #endif /* _STIR_SHAKEN_CURL_H */
diff --git a/res/res_stir_shaken/profile.c b/res/res_stir_shaken/profile.c
new file mode 100644
index 0000000..5d4fa9b
--- /dev/null
+++ b/res/res_stir_shaken/profile.c
@@ -0,0 +1,241 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * Ben Ford <bford@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/cli.h"
+#include "asterisk/sorcery.h"
+
+#include "stir_shaken.h"
+#include "profile.h"
+#include "asterisk/res_stir_shaken.h"
+
+#define CONFIG_TYPE "profile"
+
+static void stir_shaken_profile_destructor(void *obj)
+{
+	struct stir_shaken_profile *cfg = obj;
+
+	ast_free_acl_list(cfg->acl);
+
+	return;
+}
+
+static void *stir_shaken_profile_alloc(const char *name)
+{
+	struct stir_shaken_profile *cfg;
+
+	cfg = ast_sorcery_generic_alloc(sizeof(*cfg), stir_shaken_profile_destructor);
+	if (!cfg) {
+		return NULL;
+	}
+
+	return cfg;
+}
+
+static struct stir_shaken_profile *stir_shaken_profile_get(const char *id)
+{
+	return ast_sorcery_retrieve_by_id(ast_stir_shaken_sorcery(), CONFIG_TYPE, id);
+}
+
+static struct ao2_container *stir_shaken_profile_get_all(void)
+{
+	return ast_sorcery_retrieve_by_fields(ast_stir_shaken_sorcery(), CONFIG_TYPE,
+		AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL);
+}
+
+struct stir_shaken_profile *ast_stir_shaken_get_profile_by_name(const char *name)
+{
+	return ast_sorcery_retrieve_by_id(ast_stir_shaken_sorcery(), CONFIG_TYPE, name);
+}
+
+static int stir_shaken_profile_apply(const struct ast_sorcery *sorcery, void *obj)
+{
+	return 0;
+}
+
+static int stir_shaken_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
+{
+	struct stir_shaken_profile *cfg = obj;
+
+	if (!strcasecmp("attest", var->value)) {
+		cfg->stir_shaken = STIR_SHAKEN_ATTEST;
+	} else if (!strcasecmp("verify", var->value)) {
+		cfg->stir_shaken = STIR_SHAKEN_VERIFY;
+	} else if (!strcasecmp("on", var->value)) {
+		cfg->stir_shaken = STIR_SHAKEN_ON;
+	} else {
+		ast_log(LOG_WARNING, "'%s' is not a valid value for option "
+			"'stir_shaken' for %s %s\n",
+		var->value, CONFIG_TYPE, ast_sorcery_object_get_id(cfg));
+		return -1;
+	}
+
+	return 0;
+}
+
+static const char *stir_shaken_map[] = {
+	[STIR_SHAKEN_ATTEST] = "attest",
+	[STIR_SHAKEN_VERIFY] = "verify",
+	[STIR_SHAKEN_ON] = "on",
+};
+
+static int stir_shaken_to_str(const void *obj, const intptr_t *args, char **buf)
+{
+	const struct stir_shaken_profile *cfg = obj;
+	if (ARRAY_IN_BOUNDS(cfg->stir_shaken, stir_shaken_map)) {
+		*buf = ast_strdup(stir_shaken_map[cfg->stir_shaken]);
+	}
+	return 0;
+}
+
+static int stir_shaken_acl_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
+{
+	struct stir_shaken_profile *cfg = obj;
+	int error = 0;
+	int ignore;
+
+	if (ast_strlen_zero(var->value)) {
+		return 0;
+	}
+
+	ast_append_acl(var->name, var->value, &cfg->acl, &error, &ignore);
+
+	return error;
+}
+
+static int acl_to_str(const void *obj, const intptr_t *args, char **buf)
+{
+	const struct stir_shaken_profile *cfg = obj;
+	struct ast_acl_list *acl_list;
+	struct ast_acl *first_acl;
+
+	if (cfg && !ast_acl_list_is_empty(acl_list=cfg->acl)) {
+		AST_LIST_LOCK(acl_list);
+		first_acl = AST_LIST_FIRST(acl_list);
+		if (ast_strlen_zero(first_acl->name)) {
+			*buf = "deny/permit";
+		} else {
+			*buf = first_acl->name;
+		}
+		AST_LIST_UNLOCK(acl_list);
+	}
+
+	*buf = ast_strdup(*buf);
+	return 0;
+}
+
+static char *stir_shaken_profile_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	struct stir_shaken_profile *cfg;
+
+	switch(cmd) {
+	case CLI_INIT:
+		e->command = "stir_shaken show profile";
+		e->usage =
+			"Usage: stir_shaken show profile <id>\n"
+			"       Show the stir/shaken profile settings for a given id\n";
+		return NULL;
+	case CLI_GENERATE:
+		if (a->pos == 3) {
+			return stir_shaken_tab_complete_name(a->word, stir_shaken_profile_get_all());
+		} else {
+			return NULL;
+		}
+	}
+
+	if (a->argc != 4) {
+		return CLI_SHOWUSAGE;
+	}
+
+	cfg = stir_shaken_profile_get(a->argv[3]);
+	stir_shaken_cli_show(cfg, a, 0);
+	ast_acl_output(a->fd, cfg->acl, NULL);
+	ao2_cleanup(cfg);
+
+	return CLI_SUCCESS;
+}
+
+static char *stir_shaken_profile_show_all(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	struct ao2_container *container;
+
+	switch(cmd) {
+	case CLI_INIT:
+		e->command = "stir_shaken show profiles";
+		e->usage =
+			"Usage: stir_shaken show profiles\n"
+			"       Show all profiles for stir/shaken\n";
+		return NULL;
+	case CLI_GENERATE:
+		return NULL;
+	}
+
+	if (a->argc != 3) {
+		return CLI_SHOWUSAGE;
+	}
+
+	container = stir_shaken_profile_get_all();
+	if (!container || ao2_container_count(container) == 0) {
+		ast_cli(a->fd, "No stir/shaken ACLs found\n");
+		ao2_cleanup(container);
+		return CLI_SUCCESS;
+	}
+
+	ao2_callback(container, OBJ_NODATA, stir_shaken_cli_show, a);
+	ao2_ref(container, -1);
+
+	return CLI_SUCCESS;
+}
+
+static struct ast_cli_entry stir_shaken_profile_cli[] = {
+	AST_CLI_DEFINE(stir_shaken_profile_show, "Show stir/shaken profile by id"),
+	AST_CLI_DEFINE(stir_shaken_profile_show_all, "Show all stir/shaken profiles"),
+};
+
+int stir_shaken_profile_unload(void)
+{
+	ast_cli_unregister_multiple(stir_shaken_profile_cli,
+		ARRAY_LEN(stir_shaken_profile_cli));
+
+	return 0;
+}
+
+int stir_shaken_profile_load(void)
+{
+	struct ast_sorcery *sorcery = ast_stir_shaken_sorcery();
+
+	ast_sorcery_apply_default(sorcery, CONFIG_TYPE, "config", "stir_shaken.conf,criteria=type=profile");
+
+	if (ast_sorcery_object_register(sorcery, CONFIG_TYPE, stir_shaken_profile_alloc,
+		NULL, stir_shaken_profile_apply)) {
+		ast_log(LOG_ERROR, "stir/shaken - failed to register '%s' sorcery object\n", CONFIG_TYPE);
+		return -1;
+	}
+
+	ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "type", "", OPT_NOOP_T, 0, 0);
+	ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "stir_shaken", "on", stir_shaken_handler, stir_shaken_to_str, NULL, 0, 0);
+	ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "deny", "", stir_shaken_acl_handler, NULL, NULL, 0, 0);
+	ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "permit", "", stir_shaken_acl_handler, NULL, NULL, 0, 0);
+	ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "acllist", "", stir_shaken_acl_handler, acl_to_str, NULL, 0, 0);
+
+	ast_cli_register_multiple(stir_shaken_profile_cli,
+		ARRAY_LEN(stir_shaken_profile_cli));
+
+	return 0;
+}
diff --git a/res/res_stir_shaken/profile.h b/res/res_stir_shaken/profile.h
new file mode 100644
index 0000000..5617e9a
--- /dev/null
+++ b/res/res_stir_shaken/profile.h
@@ -0,0 +1,39 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * Ben Ford <bford@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_PROFILE_H
+#define _STIR_SHAKEN_PROFILE_H
+
+#include "profile_private.h"
+
+struct stir_shaken_profile *ast_stir_shaken_get_profile_by_name(const char *name);
+
+/*!
+ * \brief Load time initialization for the stir/shaken 'profile' object
+ *
+ * \retval 0 on success, -1 on error
+ */
+int stir_shaken_profile_load(void);
+
+/*!
+ * \brief Unload time cleanup for the stir/shaken 'profile'
+ *
+ * \retval 0 on success, -1 on error
+ */
+int stir_shaken_profile_unload(void);
+
+#endif /* _STIR_SHAKEN_PROFILE_H */
diff --git a/res/res_stir_shaken/profile_private.h b/res/res_stir_shaken/profile_private.h
new file mode 100644
index 0000000..536a0fe
--- /dev/null
+++ b/res/res_stir_shaken/profile_private.h
@@ -0,0 +1,40 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * Ben Ford <bford@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_PROFILE_PRIVATE_H
+#define _STIR_SHAKEN_PROFILE_PRIVATE_H
+
+#include "asterisk/sorcery.h"
+
+#include "asterisk/acl.h"
+
+enum stir_shaken_profile_behavior {
+	/*! Only do STIR/SHAKEN attestation */
+	STIR_SHAKEN_ATTEST = 1,
+	/*! Only do STIR/SHAKEN verification */
+	STIR_SHAKEN_VERIFY = 2,
+	/*! Do STIR/SHAKEN attestation and verification */
+	STIR_SHAKEN_ON = 3,
+};
+
+struct stir_shaken_profile {
+	SORCERY_OBJECT(details);
+	unsigned int stir_shaken;
+	struct ast_acl_list *acl;
+};
+
+#endif /* _STIR_SHAKEN_PROFILE_PRIVATE_H */
