diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample
index 8507b4ed8caa143c40535e5b5ce342a50ef245c5..b1c1657ff7007e925ef231bedec4844cfa3c2fa7 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 c39bc9726530e0366e4f978ae72b8dd3d2c61658..677d3bb3ba987e5d9e58196f87bdeb9fec82fb2f 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 9f353936ca3d9a695fcb27040cd28840eabc1f24..209cdbfba28e7b01797a511693c51a07cc98b73f 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -878,6 +878,8 @@ struct ast_sip_endpoint {
 		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 92eb0ec70b81ca2eb3066f4080ad5f8609ac44ef..ece99b52a04ebb25596613c6342d5326d6dcaea5 100644
--- a/include/asterisk/res_stir_shaken.h
+++ b/include/asterisk/res_stir_shaken.h
@@ -38,6 +38,8 @@ enum ast_stir_shaken_verify_failure_reason {
 
 struct ast_stir_shaken_payload;
 
+struct ast_acl_list;
+
 struct ast_json;
 
 /*!
@@ -65,6 +67,38 @@ char *ast_stir_shaken_payload_get_public_cert_url(const struct ast_stir_shaken_p
  */
 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
  *
@@ -112,6 +146,26 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const
 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);
 
+/*!
+ * \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
  *
diff --git a/res/res_pjsip/pjsip_config.xml b/res/res_pjsip/pjsip_config.xml
index 3e0a6e7083a3ae383d4ca6c9e2107d3b280e0fce..ca5a266849ee626d4d564bfc693de09206492aa8 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 e32a3d20dc8ab3bb2a62ed9732d430f329b771c1..39c3daba5a80cc3bdc477a95f9e0f23b328f3c2a 100644
--- a/res/res_pjsip/pjsip_configuration.c
+++ b/res/res_pjsip/pjsip_configuration.c
@@ -2192,6 +2192,7 @@ int ast_res_pjsip_initialize_configuration(void)
 		"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 27aa4d8caf233268f6b8cc870081bb4cb0cb4a7a..19e846ff73d4bdd156fe91d7e19ba2d6d108aa8e 100644
--- a/res/res_pjsip_stir_shaken.c
+++ b/res/res_pjsip_stir_shaken.c
@@ -217,13 +217,16 @@ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_r
 	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 @@ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_r
 
 	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 add_date_header(const struct ast_sip_session *session, pjsip_tx_data
 
 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 373a1a1a926c3d66659509174eca8095a6bc9291..157500a537ca71aa3ac0293d2d2be69eb3257cf2 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 @@ unsigned int ast_stir_shaken_get_signature_timeout(void)
 	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 @@ static int stir_shaken_verify_signature(const char *msg, const char *signature,
  * \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 @@ static char *run_curl(const char *public_cert_url, const char *path)
 		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 @@ static char *run_curl(const char *public_cert_url, const char *path)
  * \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 @@ static char *curl_and_check_expiration(const char *public_cert_url, const char *
 		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 @@ static int stir_shaken_verify_check_empty_strings(const char *header, const char
  * \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 @@ static int stir_shaken_verify_setup_file_paths(const char *public_cert_url, char
 		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 @@ static int stir_shaken_verify_setup_file_paths(const char *public_cert_url, char
  * \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 @@ static int stir_shaken_verify_validate_cert(const char *public_cert_url, char **
 
 		/* 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 @@ static int stir_shaken_verify_validate_cert(const char *public_cert_url, char **
 		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;
 		}
@@ -756,6 +809,12 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const
 
 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;
@@ -764,11 +823,14 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify2(const char *header, cons
 	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 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify2(const char *header, cons
 	 * {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 @@ static int unload_module(void)
 {
 	int res = 0;
 
+	stir_shaken_profile_unload();
 	stir_shaken_certificate_unload();
 	stir_shaken_store_unload();
 	stir_shaken_general_unload();
@@ -1716,6 +1779,11 @@ static int load_module(void)
 		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 cd784614d87de083b1b28991c797535fcdfbbd3c..fb06de59ca9c19bf21e33e78f17b6dfdda7f55ca 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 @@ struct curl_cb_write_buf {
 	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 @@ void curl_cb_data_free(struct curl_cb_data *data)
 	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 @@ static size_t curl_write_cb(void *curl_data, size_t size, size_t actual_size, vo
 	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 @@ char *curl_public_key(const char *public_cert_url, const char *path, struct curl
 	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 @@ char *curl_public_key(const char *public_cert_url, const char *path, struct curl
 	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 ae8fedaa57d446b5422297062e326672f3fd2796..2dbd5d2346a1373344dabf261fa96056feee889e 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 @@ char *curl_cb_data_get_expires(const struct curl_cb_data *data);
  * \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 0000000000000000000000000000000000000000..5d4fa9b8387514553861ba061ea8ebed22bdb03e
--- /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 0000000000000000000000000000000000000000..5617e9ad1e6a5327b76f01be92927df4f004e609
--- /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 0000000000000000000000000000000000000000..536a0fe1a236c4b57fc13ddfa1944046cab2f37a
--- /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 */