diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample
index cdb585d2a0be1866ad46deefa6019eab4d653dd1..2636f639d59e34cd054871fceb2b74e134e976c1 100644
--- a/configs/samples/pjsip.conf.sample
+++ b/configs/samples/pjsip.conf.sample
@@ -345,6 +345,10 @@
 ;device_state_busy_at=1
 ;allow_subscribe=yes
 ;sub_min_expiry=30
+;
+; STIR/SHAKEN support.
+;
+;stir_shaken=no
 
 ;[6001]
 ;type=auth
@@ -961,6 +965,20 @@
                            ; chan_sip and prevents these 183 responses from
                            ; being forwarded.
                            ; (default: no)
+;stir_shaken =
+                           ; If this is enabled, STIR/SHAKEN operations will be
+                           ; performed on this endpoint. This includes inbound
+                           ; and outbound INVITEs. On an inbound INVITE, Asterisk
+                           ; will check for an Identity header and attempt to
+                           ; verify the call. On an outbound INVITE, Asterisk will
+                           ; add an Identity header that others can use to verify
+                           ; calls from this endpoint. Additional configuration is
+                           ; done in stir_shaken.conf.
+                           ; The STIR_SHAKEN dialplan function must be used to get
+                           ; the verification results on inbound INVITEs. Nothing
+                           ; happens to the call if verification fails; it's up to
+                           ; you to determine what to do with the results.
+                           ; (default: no)
 
 ;==========================AUTH SECTION OPTIONS=========================
 ;[auth]
diff --git a/configs/samples/stir_shaken.conf.sample b/configs/samples/stir_shaken.conf.sample
index 71acad23c4d3d2c700b0579230b25728d9824660..957fd14df72f9cb7eb549a5b58f0cfd346a3c3a6 100644
--- a/configs/samples/stir_shaken.conf.sample
+++ b/configs/samples/stir_shaken.conf.sample
@@ -14,8 +14,11 @@
 ; Maximum size to use for caching public keys
 ;cache_max_size=1000
 ;
-; Maximum time to wait to CURL certificates
-;curl_timeout
+; Maximum time (in seconds) to wait to CURL certificates
+;curl_timeout=2
+;
+; Amount of time (in seconds) a signature is valid for
+;signature_timeout=15
 ;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;
@@ -48,6 +51,9 @@
 ; URL to the public key
 ;public_key_url=http://mycompany.com/alice.pub
 ;
+; The caller ID number to match on
+;caller_id_number=1234567
+;
 ; Must have an attestation of A, B, or C
 ;attestation=C
 ;
diff --git a/contrib/ast-db-manage/config/versions/61797b9fced6_add_stir_shaken.py b/contrib/ast-db-manage/config/versions/61797b9fced6_add_stir_shaken.py
new file mode 100644
index 0000000000000000000000000000000000000000..018937c7aeaf2fff7bf67a0290b2512a248780dc
--- /dev/null
+++ b/contrib/ast-db-manage/config/versions/61797b9fced6_add_stir_shaken.py
@@ -0,0 +1,31 @@
+"""add stir shaken
+
+Revision ID: 61797b9fced6
+Revises: fbb7766f17bc
+Create Date: 2020-06-29 11:52:59.946929
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '61797b9fced6'
+down_revision = 'b80485ff4dd0'
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects.postgresql import ENUM
+
+YESNO_NAME = 'yesno_values'
+YESNO_VALUES = ['yes', 'no']
+
+AST_BOOL_NAME = 'ast_bool_values'
+AST_BOOL_VALUES = [ '0', '1',
+                    'off', 'on',
+                    'false', 'true',
+                    'no', 'yes' ]
+
+def upgrade():
+    ast_bool_values = ENUM(*AST_BOOL_VALUES, name=AST_BOOL_NAME, create_type=False)
+    op.add_column('ps_endpoints', sa.Column('stir_shaken', ast_bool_values))
+
+def downgrade():
+    op.drop_column('ps_endpoints', 'stir_shaken')
diff --git a/doc/CHANGES-staging/stir_shaken.txt b/doc/CHANGES-staging/stir_shaken.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3ad1784bdf0e2c17fb13e8c38fcbf28397c8552e
--- /dev/null
+++ b/doc/CHANGES-staging/stir_shaken.txt
@@ -0,0 +1,20 @@
+Subject: STIR/SHAKEN
+
+STIR/SHAKEN support has been added to Asterisk. Configuration is done in
+stir_shaken.conf. There is a sample configuration file to help you get
+started (asterisk/configs/samples/stir_shaken.conf.sample). Once that's
+set up, you can enable STIR/SHAKEN on any endpoint by setting stir_shaken
+to yes on the endpoint configuration object. This will add an Identity
+header on outgoing INVITEs, and check for an Identity header on incoming
+INVITEs. This option has been added to Alembic as well.
+
+The information received on an incoming INVITE can be checked using the
+STIR_SHAKEN dialplan function. There are two variations:
+
+STIR_SHAKEN(count)
+STIR_SHAKEN(0, verify_result)
+
+The first variation will tell you how many STIR/SHAKEN results are on the
+channel. The second fetches information for a specific result. The first
+parameter is the index, followed by what information you want to retrieve.
+The available options are 'verify_result', 'identity', and 'attestation'.
diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h
index 0ca29ff5d81a0d4ba16386d5604d6794a8ec274d..eaa9b210bc5b12e63c52e7d3b1c88626ee381a94 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -908,6 +908,8 @@ 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 */
+	unsigned int stir_shaken;
 };
 
 /*! URI parameter for symmetric transport */
diff --git a/res/res_pjsip.c b/res/res_pjsip.c
index 847a14c886463c510da1fbeb43fc4e702f119b49..bb77e549fa856718c52a4e5426b4f536d7b69346 100644
--- a/res/res_pjsip.c
+++ b/res/res_pjsip.c
@@ -1458,6 +1458,14 @@
 						being forwarded.</para>
 					</description>
 				</configOption>
+				<configOption name="stir_shaken" default="no">
+					<synopsis>Enable STIR/SHAKEN support on this endpoint</synopsis>
+					<description><para>
+						Enable STIR/SHAKEN support on this endpoint. On incoming INVITEs,
+						the Identity header will be checked for validity. On outgoing
+						INVITEs, an Identity header will be added.</para>
+					</description>
+				</configOption>
 			</configObject>
 			<configObject name="auth">
 				<synopsis>Authentication type</synopsis>
diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c
index e3eab8ad0ed515aa9b3a6b68d367e9c3c5072218..89ae3e1a6f36890d53dccde1d547f26fe4f78dd8 100644
--- a/res/res_pjsip/pjsip_configuration.c
+++ b/res/res_pjsip/pjsip_configuration.c
@@ -2140,6 +2140,7 @@ int ast_res_pjsip_initialize_configuration(void)
 	ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "outgoing_answer_codec_prefs",
 		"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));
 
 	if (ast_sip_initialize_sorcery_transport()) {
 		ast_log(LOG_ERROR, "Failed to register SIP transport support with sorcery\n");
diff --git a/res/res_pjsip_stir_shaken.c b/res/res_pjsip_stir_shaken.c
index 3620579d84b6a650985bcbc994679871935161cd..60dfdc456d81ee37924fbf1ed88c346438a8e4d7 100644
--- a/res/res_pjsip_stir_shaken.c
+++ b/res/res_pjsip_stir_shaken.c
@@ -95,6 +95,11 @@ static int compare_timestamp(const char *json_str)
 	long int timestamp;
 	struct timeval now = ast_tvnow();
 
+#ifdef TEST_FRAMEWORK
+	ast_debug(3, "Ignoring STIR/SHAKEN timestamp\n");
+	return 0;
+#endif
+
 	json = ast_json_load_string(json_str, NULL);
 	timestamp = ast_json_integer_get(ast_json_object_get(json, "iat"));
 
@@ -131,6 +136,10 @@ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_r
 	int mismatch = 0;
 	struct ast_stir_shaken_payload *ss_payload;
 
+	if (!session->endpoint->stir_shaken) {
+		return 0;
+	}
+
 	identity_hdr_val = ast_sip_rdata_get_header_value(rdata, identity_str);
 	if (ast_strlen_zero(identity_hdr_val)) {
 		ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_NOT_PRESENT);
@@ -278,6 +287,10 @@ static void add_identity_header(const struct ast_sip_session *session, pjsip_tx_
 
 static void stir_shaken_outgoing_request(struct ast_sip_session *session, pjsip_tx_data *tdata)
 {
+	if (!session->endpoint->stir_shaken) {
+		return;
+	}
+
 	if (ast_strlen_zero(session->id.number.str) && session->id.number.valid) {
 		return;
 	}
diff --git a/res/res_stir_shaken.c b/res/res_stir_shaken.c
index 632fd1b38f13bb9746288520623cd80ff904f8f8..7a141f7c6494012fd4be1c704f89a415fb1ec65e 100644
--- a/res/res_stir_shaken.c
+++ b/res/res_stir_shaken.c
@@ -153,6 +153,11 @@ static struct ast_sorcery *stir_shaken_sorcery;
 /* The maximum length for path storage */
 #define MAX_PATH_LEN 256
 
+/* The default amount of time (in seconds) to use for certificate expiration
+ * if no cache data is available
+ */
+#define EXPIRATION_BUFFER 15
+
 struct ast_stir_shaken_payload {
 	/*! The JWT header */
 	struct ast_json *header;
@@ -381,6 +386,10 @@ static void set_public_key_expiration(const char *public_key_url, const struct c
 		}
 	}
 
+	if (ast_strlen_zero(value)) {
+		actual_expires.tv_sec += EXPIRATION_BUFFER;
+	}
+
 	snprintf(time_buf, sizeof(time_buf), "%30lu", actual_expires.tv_sec);
 
 	ast_db_put(hash, "expiration", time_buf);
@@ -1133,6 +1142,8 @@ static int stir_shaken_read(struct ast_channel *chan, const char *function,
 	struct stir_shaken_datastore *ss_datastore;
 	struct ast_datastore *datastore;
 	char *parse;
+	char *first;
+	char *second;
 	unsigned int target_index, current_index = 0;
 	AST_DECLARE_APP_ARGS(args,
 		AST_APP_ARG(first_param);
@@ -1153,17 +1164,20 @@ static int stir_shaken_read(struct ast_channel *chan, const char *function,
 
 	AST_STANDARD_APP_ARGS(args, parse);
 
-	if (ast_strlen_zero(args.first_param)) {
+	first = ast_strip(args.first_param);
+	if (ast_strlen_zero(first)) {
 		ast_log(LOG_ERROR, "An argument must be passed to %s\n", function);
 		return -1;
 	}
 
+	second = ast_strip(args.second_param);
+
 	/* Check if we are only looking for the number of STIR/SHAKEN verification results */
-	if (!strcasecmp(args.first_param, "count")) {
+	if (!strcasecmp(first, "count")) {
 
 		size_t count = 0;
 
-		if (!ast_strlen_zero(args.second_param)) {
+		if (!ast_strlen_zero(second)) {
 			ast_log(LOG_ERROR, "%s only takes 1 paramater for 'count'\n", function);
 			return -1;
 		}
@@ -1184,15 +1198,15 @@ static int stir_shaken_read(struct ast_channel *chan, const char *function,
 	/* If we aren't doing a count, then there should be two parameters. The field
 	 * we are searching for will be the second parameter. The index is the first.
 	 */
-	if (ast_strlen_zero(args.second_param)) {
+	if (ast_strlen_zero(second)) {
 		ast_log(LOG_ERROR, "Retrieving a value using %s requires two paramaters (index, value) "
-			"- only index was given (%s)\n", function, args.second_param);
+			"- only index was given (%s)\n", function, second);
 		return -1;
 	}
 
-	if (ast_str_to_uint(args.first_param, &target_index)) {
+	if (ast_str_to_uint(first, &target_index)) {
 		ast_log(LOG_ERROR, "Failed to convert index %s to integer for function %s\n",
-			args.first_param, function);
+			first, function);
 		return -1;
 	}
 
@@ -1211,19 +1225,19 @@ static int stir_shaken_read(struct ast_channel *chan, const char *function,
 	}
 	ast_channel_unlock(chan);
 	if (current_index != target_index || !datastore) {
-		ast_log(LOG_WARNING, "No STIR/SHAKEN results for index '%s'\n", args.first_param);
+		ast_log(LOG_WARNING, "No STIR/SHAKEN results for index '%s'\n", first);
 		return -1;
 	}
 	ss_datastore = datastore->data;
 
-	if (!strcasecmp(args.second_param, "identity")) {
+	if (!strcasecmp(second, "identity")) {
 		ast_copy_string(buf, ss_datastore->identity, len);
-	} else if (!strcasecmp(args.second_param, "attestation")) {
+	} else if (!strcasecmp(second, "attestation")) {
 		ast_copy_string(buf, ss_datastore->attestation, len);
-	} else if (!strcasecmp(args.second_param, "verify_result")) {
+	} else if (!strcasecmp(second, "verify_result")) {
 		ast_copy_string(buf, stir_shaken_verification_result_to_string(ss_datastore->verify_result), len);
 	} else {
-		ast_log(LOG_ERROR, "No such value '%s' for %s\n", args.second_param, function);
+		ast_log(LOG_ERROR, "No such value '%s' for %s\n", second, function);
 		return -1;
 	}