diff --git a/doc/UPGRADE-staging/stir_shaken_option_split.txt b/doc/UPGRADE-staging/stir_shaken_option_split.txt
new file mode 100644
index 0000000000000000000000000000000000000000..79df214a8bb393e20e40486cee4d3defff40378f
--- /dev/null
+++ b/doc/UPGRADE-staging/stir_shaken_option_split.txt
@@ -0,0 +1,7 @@
+Subject: STIR/SHAKEN
+
+The STIR/SHAKEN configuration option has been split into
+4 different choices: off, attest, verify, and on. Off and
+on behave the same way as before. Attest will only perform
+attestation on the endpoint, and verify will only perform
+verification on the endpoint.
diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h
index 7b9dcbd51d0f742af344ec5509fe726ffd19f1f5..2a20e36935c21ce657faa64939ad937e3c772cfe 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -63,6 +63,22 @@
 #define PJSIP_EXPIRES_NOT_SPECIFIED	((pj_uint32_t)-1)
 #endif
 
+/* Response codes from RFC8224 */
+#define AST_STIR_SHAKEN_RESPONSE_CODE_STALE_DATE 403
+#define AST_STIR_SHAKEN_RESPONSE_CODE_USE_IDENTITY_HEADER 428
+#define AST_STIR_SHAKEN_RESPONSE_CODE_USE_SUPPORTED_PASSPORT_FORMAT 428
+#define AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO 436
+#define AST_STIR_SHAKEN_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL 437
+#define AST_STIR_SHAKEN_RESPONSE_CODE_INVALID_IDENTITY_HEADER 438
+
+/* Response strings from RFC8224 */
+#define AST_STIR_SHAKEN_RESPONSE_STR_STALE_DATE "Stale Date"
+#define AST_STIR_SHAKEN_RESPONSE_STR_USE_IDENTITY_HEADER "Use Identity Header"
+#define AST_STIR_SHAKEN_RESPONSE_STR_USE_SUPPORTED_PASSPORT_FORMAT "Use Supported PASSporT Format"
+#define AST_STIR_SHAKEN_RESPONSE_STR_BAD_IDENTITY_INFO "Bad Identity Info"
+#define AST_STIR_SHAKEN_RESPONSE_STR_UNSUPPORTED_CREDENTIAL "Unsupported Credential"
+#define AST_STIR_SHAKEN_RESPONSE_STR_INVALID_IDENTITY_HEADER "Invalid Identity Header"
+
 /* Forward declarations of PJSIP stuff */
 struct pjsip_rx_data;
 struct pjsip_module;
@@ -527,6 +543,17 @@ enum ast_sip_session_redirect {
 	AST_SIP_REDIRECT_URI_PJSIP,
 };
 
+enum ast_sip_stir_shaken_behavior {
+	/*! Don't do any STIR/SHAKEN operations */
+	AST_SIP_STIR_SHAKEN_OFF = 0,
+	/*! Only do STIR/SHAKEN attestation */
+	AST_SIP_STIR_SHAKEN_ATTEST = 1,
+	/*! Only do STIR/SHAKEN verification */
+	AST_SIP_STIR_SHAKEN_VERIFY = 2,
+	/*! Do STIR/SHAKEN attestation and verification */
+	AST_SIP_STIR_SHAKEN_ON = 3,
+};
+
 /*!
  * \brief Incoming/Outgoing call offer/answer joint codec preference.
  *
@@ -917,7 +944,7 @@ struct ast_sip_endpoint {
 	unsigned int suppress_q850_reason_headers;
 	/*! Ignore 183 if no SDP is present */
 	unsigned int ignore_183_without_sdp;
-	/*! Enable STIR/SHAKEN support on this endpoint */
+	/*! Set which STIR/SHAKEN behaviors we want on this endpoint */
 	unsigned int stir_shaken;
 	/*! Should we authenticate OPTIONS requests per RFC 3261? */
 	unsigned int allow_unauthenticated_options;
diff --git a/include/asterisk/res_stir_shaken.h b/include/asterisk/res_stir_shaken.h
index 5175907bbdddefcdd837cb9a2d4fc4675c512fb6..92eb0ec70b81ca2eb3066f4080ad5f8609ac44ef 100644
--- a/include/asterisk/res_stir_shaken.h
+++ b/include/asterisk/res_stir_shaken.h
@@ -29,6 +29,13 @@ enum ast_stir_shaken_verification_result {
 	AST_STIR_SHAKEN_VERIFY_PASSED, /*! Signature verified and contents match signaling */
 };
 
+/*! Different from ast_stir_shaken_verification_result. Used to determine why ast_stir_shaken_verify returned NULL */
+enum ast_stir_shaken_verify_failure_reason {
+	AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC, /*! Memory allocation failure */
+	AST_STIR_SHAKEN_VERIFY_FAILED_TO_GET_CERT, /*! Failed to get the credentials to verify */
+	AST_STIR_SHAKEN_VERIFY_FAILED_SIGNATURE_VALIDATION, /*! Failed validating the signature */
+};
+
 struct ast_stir_shaken_payload;
 
 struct ast_json;
@@ -87,6 +94,24 @@ int ast_stir_shaken_add_verification(struct ast_channel *chan, const char *ident
 struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const char *payload, const char *signature,
 	const char *algorithm, const char *public_cert_url);
 
+/*!
+ * \brief Same as ast_stir_shaken_verify, but will populate a struct with additional information on failure
+ *
+ * \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
+ *
+ * \retval ast_stir_shaken_payload on success
+ * \retval NULL on failure
+ */
+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 Retrieve the stir/shaken sorcery context
  *
diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c
index c27b587076d79b3809b526331fbe77f3f3772596..49a7d452155d07791438151b6acded37f89e5d14 100644
--- a/res/res_pjsip/pjsip_configuration.c
+++ b/res/res_pjsip/pjsip_configuration.c
@@ -717,6 +717,44 @@ static int media_encryption_to_str(const void *obj, const intptr_t *args, char *
 	return 0;
 }
 
+static int stir_shaken_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
+{
+	struct ast_sip_endpoint *endpoint = obj;
+
+	if (!strcasecmp("off", var->value)) {
+		endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_OFF;
+	} else if (!strcasecmp("attest", var->value)) {
+		endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_ATTEST;
+	} else if (!strcasecmp("verify", var->value)) {
+		endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_VERIFY;
+	} else if (!strcasecmp("on", var->value)) {
+		endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_ON;
+	} else {
+		ast_log(LOG_WARNING, "'%s' is not a valid value for option "
+			"'stir_shaken' for endpoint %s\n",
+			var->value, ast_sorcery_object_get_id(endpoint));
+		return -1;
+	}
+
+	return 0;
+}
+
+static const char *stir_shaken_map[] = {
+	[AST_SIP_STIR_SHAKEN_OFF] "off",
+	[AST_SIP_STIR_SHAKEN_ATTEST] = "attest",
+	[AST_SIP_STIR_SHAKEN_VERIFY] = "verify",
+	[AST_SIP_STIR_SHAKEN_ON] = "on",
+};
+
+static int stir_shaken_to_str(const void *obj, const intptr_t *args, char **buf)
+{
+	const struct ast_sip_endpoint *endpoint = obj;
+	if (ARRAY_IN_BOUNDS(endpoint->stir_shaken, stir_shaken_map)) {
+		*buf = ast_strdup(stir_shaken_map[endpoint->stir_shaken]);
+	}
+	return 0;
+}
+
 static int group_handler(const struct aco_option *opt,
 			 struct ast_variable *var, void *obj)
 {
@@ -2153,7 +2191,7 @@ int ast_res_pjsip_initialize_configuration(void)
 	ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "codec_prefs_outgoing_answer",
 		"prefer: pending, operation: intersect, keep: all",
 		codec_prefs_handler, outgoing_answer_codec_prefs_to_str, NULL, 0, 0);
-	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "stir_shaken", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, stir_shaken));
+	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", "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_session.c b/res/res_pjsip_session.c
index b1288b59362c16155baffa8cef48e43e66580f5b..4eb855a8523c91d2290a13924c53abbd0b2c368c 100644
--- a/res/res_pjsip_session.c
+++ b/res/res_pjsip_session.c
@@ -4051,6 +4051,11 @@ static void handle_new_invite_request(pjsip_rx_data *rdata)
 {
 	RAII_VAR(struct ast_sip_endpoint *, endpoint,
 			ast_pjsip_rdata_get_endpoint(rdata), ao2_cleanup);
+	static const pj_str_t identity_str = { "Identity", 8 };
+	const pj_str_t use_identity_header_str = {
+		AST_STIR_SHAKEN_RESPONSE_STR_USE_IDENTITY_HEADER,
+		strlen(AST_STIR_SHAKEN_RESPONSE_STR_USE_IDENTITY_HEADER)
+	};
 	pjsip_inv_session *inv_session = NULL;
 	struct ast_sip_session *session;
 	struct new_invite invite;
@@ -4060,6 +4065,14 @@ static void handle_new_invite_request(pjsip_rx_data *rdata)
 
 	ast_assert(endpoint != NULL);
 
+	if ((endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_VERIFY) &&
+		!ast_sip_rdata_get_header_value(rdata, identity_str)) {
+		pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata,
+			AST_STIR_SHAKEN_RESPONSE_CODE_USE_IDENTITY_HEADER, &use_identity_header_str, NULL, NULL);
+		ast_debug(3, "No Identity header when we require one\n");
+		return;
+	}
+
 	inv_session = pre_session_setup(rdata, endpoint);
 	if (!inv_session) {
 		/* pre_session_setup() returns a response on failure */
diff --git a/res/res_pjsip_stir_shaken.c b/res/res_pjsip_stir_shaken.c
index b2b208424e3a26afb50eeea7b16fa0bf65c59c76..1bf25283949dc3bc6b8895b2f915966135b668a1 100644
--- a/res/res_pjsip_stir_shaken.c
+++ b/res/res_pjsip_stir_shaken.c
@@ -32,6 +32,9 @@
 
 #include "asterisk/res_stir_shaken.h"
 
+/*! The Date header will not be valid after this many milliseconds (60 seconds recommended) */
+#define STIR_SHAKEN_DATE_HEADER_TIMEOUT 60000
+
 /*!
  * \brief Get the attestation from the payload
  *
@@ -109,6 +112,62 @@ static int compare_timestamp(const char *json_str)
 	return 0;
 }
 
+static int check_date_header(pjsip_rx_data *rdata)
+{
+	static const pj_str_t date_hdr_str = { "Date", 4 };
+	char *date_hdr_val;
+	struct ast_tm date_hdr_tm;
+	struct timeval date_hdr_timeval;
+	struct timeval current_timeval;
+	char *remainder;
+	char timezone[80] = { 0 };
+	int64_t time_diff;
+
+	date_hdr_val = ast_sip_rdata_get_header_value(rdata, date_hdr_str);
+	if (ast_strlen_zero(date_hdr_val)) {
+		ast_log(LOG_ERROR, "Failed to get Date header from incoming INVITE for STIR/SHAKEN\n");
+		return -1;
+	}
+
+	if (!(remainder = ast_strptime(date_hdr_val, "%a, %d %b %Y %T", &date_hdr_tm))) {
+		ast_log(LOG_ERROR, "Failed to parse Date header\n");
+		return -1;
+	}
+
+	sscanf(remainder, "%79s", timezone);
+
+	if (ast_strlen_zero(timezone)) {
+		ast_log(LOG_ERROR, "A timezone is required for STIR/SHAKEN Date header, but we didn't get one\n");
+		return -1;
+	}
+
+	date_hdr_timeval = ast_mktime(&date_hdr_tm, timezone);
+	current_timeval = ast_tvnow();
+
+	time_diff = ast_tvdiff_ms(current_timeval, date_hdr_timeval);
+	if (time_diff < 0) {
+		/* An INVITE from the future! */
+		ast_log(LOG_ERROR, "STIR/SHAKEN Date header has a future date\n");
+		return -1;
+	} else if (time_diff > STIR_SHAKEN_DATE_HEADER_TIMEOUT) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN Date header was outside of the allowable range (60 seconds)\n");
+		return -1;
+	}
+
+	return 0;
+}
+
+/* Send a response back and end the session */
+static void stir_shaken_inv_end_session(struct ast_sip_session *session, pjsip_rx_data *rdata, int response_code, const pj_str_t response_str)
+{
+	pjsip_tx_data *tdata;
+
+	if (pjsip_inv_end_session(session->inv_session, response_code, &response_str, &tdata) == PJ_SUCCESS) {
+		pjsip_endpt_send_response2(ast_sip_get_pjsip_endpoint(), rdata, tdata, NULL, NULL);
+	}
+	ast_hangup(session->channel);
+}
+
 /*!
  * \internal
  * \brief Session supplement callback on an incoming INVITE request
@@ -122,6 +181,27 @@ static int compare_timestamp(const char *json_str)
 static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_rx_data *rdata)
 {
 	static const pj_str_t identity_str = { "Identity", 8 };
+	const pj_str_t bad_identity_info_str = {
+		AST_STIR_SHAKEN_RESPONSE_STR_BAD_IDENTITY_INFO,
+		strlen(AST_STIR_SHAKEN_RESPONSE_STR_BAD_IDENTITY_INFO)
+	};
+	const pj_str_t unsupported_credential_str = {
+		AST_STIR_SHAKEN_RESPONSE_STR_UNSUPPORTED_CREDENTIAL,
+		strlen(AST_STIR_SHAKEN_RESPONSE_STR_UNSUPPORTED_CREDENTIAL)
+	};
+	const pj_str_t stale_date_str = {
+		AST_STIR_SHAKEN_RESPONSE_STR_STALE_DATE,
+		strlen(AST_STIR_SHAKEN_RESPONSE_STR_STALE_DATE)
+	};
+	const pj_str_t use_supported_passport_format_str = {
+		AST_STIR_SHAKEN_RESPONSE_STR_USE_SUPPORTED_PASSPORT_FORMAT,
+		strlen(AST_STIR_SHAKEN_RESPONSE_STR_USE_SUPPORTED_PASSPORT_FORMAT)
+	};
+	const pj_str_t invalid_identity_hdr_str = {
+		AST_STIR_SHAKEN_RESPONSE_STR_INVALID_IDENTITY_HEADER,
+		strlen(AST_STIR_SHAKEN_RESPONSE_STR_INVALID_IDENTITY_HEADER)
+	};
+	const pj_str_t server_internal_error_str = { "Server Internal Error", 21 };
 	char *identity_hdr_val;
 	char *encoded_val;
 	struct ast_channel *chan = session->channel;
@@ -132,10 +212,17 @@ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_r
 	char *algorithm;
 	char *public_cert_url;
 	char *attestation;
+	char *ppt;
 	int mismatch = 0;
 	struct ast_stir_shaken_payload *ss_payload;
+	int failure_code = 0;
+
+	/* 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) {
+	if ((session->endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_VERIFY) == 0) {
 		return 0;
 	}
 
@@ -148,50 +235,100 @@ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_r
 	encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val);
 	header = ast_base64url_decode_string(encoded_val);
 	if (ast_strlen_zero(header)) {
-		ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED);
-		return 0;
+		ast_debug(3, "STIR/SHAKEN INVITE for %s is missing header\n",
+			ast_sorcery_object_get_id(session->endpoint));
+		stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str);
+		return 1;
 	}
 
 	encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val);
 	payload = ast_base64url_decode_string(encoded_val);
 	if (ast_strlen_zero(payload)) {
-		ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED);
-		return 0;
+		ast_debug(3, "STIR/SHAKEN INVITE for %s is missing payload\n",
+			ast_sorcery_object_get_id(session->endpoint));
+		stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str);
+		return 1;
 	}
 
 	/* It's fine to leave the signature encoded */
 	signature = strtok_r(identity_hdr_val, ";", &identity_hdr_val);
 	if (ast_strlen_zero(signature)) {
-		ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED);
-		return 0;
+		ast_debug(3, "STIR/SHAKEN INVITE for %s is missing signature\n",
+			ast_sorcery_object_get_id(session->endpoint));
+		stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str);
+		return 1;
 	}
 
 	/* Trim "info=<" to get public cert URL */
 	strtok_r(identity_hdr_val, "<", &identity_hdr_val);
 	public_cert_url = strtok_r(identity_hdr_val, ">", &identity_hdr_val);
-	if (ast_strlen_zero(public_cert_url)) {
-		ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED);
-		return 0;
-	}
 
 	/* Make sure the public URL is actually a URL */
-	if (!ast_begins_with(public_cert_url, "http")) {
-		ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED);
-		return 0;
+	if (ast_strlen_zero(public_cert_url) || !ast_begins_with(public_cert_url, "http")) {
+		/* RFC8224 states that if we can't acquire the credentials needed
+		 * by the verification service, we should send a 436 */
+		ast_debug(3, "STIR/SHAKEN INVITE for %s did not  have valid URL (%s)\n",
+			ast_sorcery_object_get_id(session->endpoint), public_cert_url);
+		stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str);
+		return 1;
 	}
 
 	algorithm = strtok_r(identity_hdr_val, ";", &identity_hdr_val);
 	if (ast_strlen_zero(algorithm)) {
-		ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED);
-		return 0;
+		/* RFC8224 states that if the algorithm is not specified, use ES256 */
+		algorithm = STIR_SHAKEN_ENCRYPTION_ALGORITHM;
+	} else {
+		strtok_r(algorithm, "=", &algorithm);
+		if (strcmp(algorithm, STIR_SHAKEN_ENCRYPTION_ALGORITHM)) {
+			/* RFC8224 states that if we don't support the algorithm, send a 437 */
+			ast_debug(3, "STIR/SHAKEN INVITE for %s uses an unsupported algorithm (%s)\n",
+				ast_sorcery_object_get_id(session->endpoint), algorithm);
+			stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL, unsupported_credential_str);
+			return 1;
+		}
+	}
+
+	/* The only thing left should be ppt=shaken (which could have more values later),
+	 * unless using the compact PASSport form */
+	strtok_r(identity_hdr_val, "=", &identity_hdr_val);
+	ppt = ast_strip(identity_hdr_val);
+	if (!ast_strlen_zero(ppt) && strcmp(ppt, STIR_SHAKEN_PPT)) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN INVITE for %s has unsupported ppt (%s)\n",
+			ast_sorcery_object_get_id(session->endpoint), ppt);
+		stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_USE_SUPPORTED_PASSPORT_FORMAT, use_supported_passport_format_str);
+		return 1;
+	}
+
+	if (check_date_header(rdata)) {
+		ast_debug(3, "STIR/SHAKEN INVITE for %s has old Date header\n",
+			ast_sorcery_object_get_id(session->endpoint));
+		stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_STALE_DATE, stale_date_str);
+		return 1;
 	}
 
 	attestation = get_attestation_from_payload(payload);
 
-	ss_payload = ast_stir_shaken_verify(header, payload, signature, algorithm, public_cert_url);
+	ss_payload = ast_stir_shaken_verify2(header, payload, signature, algorithm, public_cert_url, &failure_code);
 	if (!ss_payload) {
-		ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED);
-		return 0;
+
+		if (failure_code == AST_STIR_SHAKEN_VERIFY_FAILED_TO_GET_CERT) {
+			/* RFC8224 states that if we can't get the credentials we need, send a 437 */
+			ast_debug(3, "STIR/SHAKEN INVITE for %s failed to acquire cert during verification process\n",
+				ast_sorcery_object_get_id(session->endpoint));
+			stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL, unsupported_credential_str);
+		} else if (failure_code == AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC) {
+			ast_log(LOG_ERROR, "Failed to allocate memory during STIR/SHAKEN verification"
+				" for %s\n", ast_sorcery_object_get_id(session->endpoint));
+			stir_shaken_inv_end_session(session, rdata, 500, server_internal_error_str);
+		} else if (failure_code == AST_STIR_SHAKEN_VERIFY_FAILED_SIGNATURE_VALIDATION) {
+			/* RFC8224 states that if we can't validate the signature, send a 438 */
+			ast_debug(3, "STIR/SHAKEN INVITE for %s failed signature validation during verification process\n",
+				ast_sorcery_object_get_id(session->endpoint));
+			ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED);
+			stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_INVALID_IDENTITY_HEADER, invalid_identity_hdr_str);
+		}
+
+		return 1;
 	}
 	ast_stir_shaken_payload_free(ss_payload);
 
@@ -333,7 +470,7 @@ 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) {
+	if ((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 1d8c785653cb4b91743c7c26f6a5207642bc920b..373a1a1a926c3d66659509174eca8095a6bc9291 100644
--- a/res/res_stir_shaken.c
+++ b/res/res_stir_shaken.c
@@ -617,83 +617,95 @@ static char *curl_and_check_expiration(const char *public_cert_url, const char *
 	return filename;
 }
 
-struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const char *payload, const char *signature,
+/*!
+ * \brief Verifies that the string parameters are not empty for STIR/SHAKEN verification
+ *
+ * \retval 0 on success
+ * \retval 1 on failure
+ */
+static int stir_shaken_verify_check_empty_strings(const char *header, const char *payload, const char *signature,
 	const char *algorithm, const char *public_cert_url)
 {
-	struct ast_stir_shaken_payload *ret_payload;
-	EVP_PKEY *public_key;
-	int curl = 0;
-	RAII_VAR(char *, file_path, NULL, ast_free);
-	RAII_VAR(char *, dir_path, NULL, ast_free);
-	RAII_VAR(char *, combined_str, NULL, ast_free);
-	size_t combined_size;
-
 	if (ast_strlen_zero(header)) {
 		ast_log(LOG_ERROR, "'header' is required for STIR/SHAKEN verification\n");
-		return NULL;
+		return 1;
 	}
 
 	if (ast_strlen_zero(payload)) {
 		ast_log(LOG_ERROR, "'payload' is required for STIR/SHAKEN verification\n");
-		return NULL;
+		return 1;
 	}
 
 	if (ast_strlen_zero(signature)) {
 		ast_log(LOG_ERROR, "'signature' is required for STIR/SHAKEN verification\n");
-		return NULL;
+		return 1;
 	}
 
 	if (ast_strlen_zero(algorithm)) {
 		ast_log(LOG_ERROR, "'algorithm' is required for STIR/SHAKEN verification\n");
-		return NULL;
+		return 1;
 	}
 
 	if (ast_strlen_zero(public_cert_url)) {
 		ast_log(LOG_ERROR, "'public_cert_url' is required for STIR/SHAKEN verification\n");
-		return NULL;
+		return 1;
 	}
 
-	/* Check to see if we have already downloaded this public cert. 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 certs 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_cert_url);
-	if (ast_asprintf(&dir_path, "%s/keys/%s", ast_config_AST_DATA_DIR, STIR_SHAKEN_DIR_NAME) < 0) {
-		return NULL;
+	return 0;
+}
+
+/*!
+ * \brief Get or set up the file path for the certificate
+ *
+ * \note This function will allocate memory for file_path and dir_path and populate them
+ *
+ * \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)
+{
+	*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) {
+		return 1;
 	}
 
 	/* If we don't have an entry in AstDB, CURL from the provided URL */
-	if (ast_strlen_zero(file_path)) {
+	if (ast_strlen_zero(*file_path)) {
 		/* Remove this entry from the database, since we will be
 		 * downloading a new file anyways.
 		 */
 		remove_public_key_from_astdb(public_cert_url);
 
 		/* Go ahead and free file_path, in case anything was allocated above */
-		ast_free(file_path);
+		ast_free(*file_path);
 
 		/* Download to the default path */
-		file_path = run_curl(public_cert_url, dir_path);
-		if (!file_path) {
-			return NULL;
+		*file_path = run_curl(public_cert_url, *dir_path);
+		if (!(*file_path)) {
+			return 1;
 		}
 
 		/* Signal that we have already downloaded a new file, no reason to do it again */
-		curl = 1;
+		*curl = 1;
 
 		/* We should have a successful download at this point, so
 		 * add an entry to the database.
 		 */
-		add_public_key_to_astdb(public_cert_url, file_path);
+		add_public_key_to_astdb(public_cert_url, *file_path);
 	}
 
-	/* Check to see if the cert we downloaded (or already had) is expired */
+	return 0;
+}
+
+/*!
+ * \brief See if the cert is expired. If it is, remove it and try downloading again if we haven't already.
+ *
+ * \retval 0 on success
+ * \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)
+{
 	if (public_key_is_expired(public_cert_url)) {
 
 		ast_debug(3, "Public cert '%s' is expired\n", public_cert_url);
@@ -701,47 +713,95 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const
 		remove_public_key_from_astdb(public_cert_url);
 
 		/* 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);
-		if (!file_path) {
-			return NULL;
+		ast_free(*file_path);
+		*file_path = curl_and_check_expiration(public_cert_url, dir_path, curl);
+		if (!(*file_path)) {
+			return 1;
 		}
 	}
 
 	/* 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) {
+	*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);
+		ast_debug(3, "Failed first read of public key file '%s'\n", *file_path);
 
 		remove_public_key_from_astdb(public_cert_url);
 
-		ast_free(file_path);
-		file_path = curl_and_check_expiration(public_cert_url, dir_path, &curl);
-		if (!file_path) {
-			return NULL;
+		ast_free(*file_path);
+		*file_path = curl_and_check_expiration(public_cert_url, dir_path, curl);
+		if (!(*file_path)) {
+			return 1;
 		}
 
-		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);
+		*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_cert_url);
-			return NULL;
+			return 1;
 		}
 	}
 
+	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_cert_url)
+{
+	int code = 0;
+
+	return ast_stir_shaken_verify2(header, payload, signature, algorithm, public_cert_url, &code);
+}
+
+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)
+{
+	struct ast_stir_shaken_payload *ret_payload;
+	EVP_PKEY *public_key;
+	int curl = 0;
+	RAII_VAR(char *, file_path, NULL, ast_free);
+	RAII_VAR(char *, dir_path, NULL, ast_free);
+	RAII_VAR(char *, combined_str, NULL, ast_free);
+	size_t combined_size;
+
+	if (stir_shaken_verify_check_empty_strings(header, payload, signature, algorithm, public_cert_url)) {
+		return NULL;
+	}
+
+	/* Check to see if we have already downloaded this public cert. 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 certs 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.
+	 */
+	if (stir_shaken_verify_setup_file_paths(public_cert_url, &file_path, &dir_path, &curl)) {
+		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)) {
+		*failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_TO_GET_CERT;
+		return NULL;
+	}
+
 	/* Combine the header and payload to get the original signed message: header.payload */
 	combined_size = strlen(header) + strlen(payload) + 2;
 	combined_str = ast_calloc(1, combined_size);
 	if (!combined_str) {
 		ast_log(LOG_ERROR, "Failed to allocate space for message to verify\n");
 		EVP_PKEY_free(public_key);
+		*failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC;
 		return NULL;
 	}
 	snprintf(combined_str, combined_size, "%s.%s", header, payload);
 	if (stir_shaken_verify_signature(combined_str, signature, public_key)) {
 		ast_log(LOG_ERROR, "Failed to verify signature\n");
+		*failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_SIGNATURE_VALIDATION;
 		EVP_PKEY_free(public_key);
 		return NULL;
 	}
@@ -752,12 +812,14 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const
 	ret_payload = ast_calloc(1, sizeof(*ret_payload));
 	if (!ret_payload) {
 		ast_log(LOG_ERROR, "Failed to allocate STIR/SHAKEN payload\n");
+		*failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC;
 		return NULL;
 	}
 
 	ret_payload->header = ast_json_load_string(header, NULL);
 	if (!ret_payload->header) {
 		ast_log(LOG_ERROR, "Failed to create JSON from header\n");
+		*failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC;
 		ast_stir_shaken_payload_free(ret_payload);
 		return NULL;
 	}
@@ -765,6 +827,7 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const
 	ret_payload->payload = ast_json_load_string(payload, NULL);
 	if (!ret_payload->payload) {
 		ast_log(LOG_ERROR, "Failed to create JSON from payload\n");
+		*failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC;
 		ast_stir_shaken_payload_free(ret_payload);
 		return NULL;
 	}
@@ -834,15 +897,11 @@ static struct ast_stir_shaken_payload *stir_shaken_verify_json(struct ast_json *
 		goto cleanup;
 	}
 
-	/* Check the alg value for "ES256" */
+	/* Check to see if there is a value for alg */
 	val = ast_json_string_get(ast_json_object_get(obj, "alg"));
-	if (ast_strlen_zero(val)) {
-		ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have required field 'alg'\n");
-		goto cleanup;
-	}
-	if (strcmp(val, STIR_SHAKEN_ENCRYPTION_ALGORITHM)) {
-		ast_log(LOG_ERROR, "STIR/SHAKEN JWT field 'alg' did not have "
-			"required value '%s' (was '%s')\n", STIR_SHAKEN_ENCRYPTION_ALGORITHM, val);
+	if (!ast_strlen_zero(val) && strcmp(val, STIR_SHAKEN_ENCRYPTION_ALGORITHM)) {
+		/* If alg is not present that's fine; if it is and is not ES256, cleanup */
+		ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have supported type for field 'alg' (was %s)\n", val);
 		goto cleanup;
 	}