diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample
index ee5b5099fc..d2287cb0cf 100644
--- a/configs/samples/pjsip.conf.sample
+++ b/configs/samples/pjsip.conf.sample
@@ -333,6 +333,10 @@
;device_state_busy_at=1
;allow_subscribe=yes
;sub_min_expiry=30
+;
+; STIR/SHAKEN support.
+;
+;stir_shaken=no
;[6001]
;type=auth
@@ -856,6 +860,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 71acad23c4..957fd14df7 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 0000000000..d2fbfd0286
--- /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 = 'e658c26033ca'
+
+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 0000000000..3ad1784bdf
--- /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 2336ce1e59..dee8d2d024 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -834,6 +834,8 @@ struct ast_sip_endpoint {
unsigned int send_connected_line;
/*! 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 a115cb63ef..90438d444e 100644
--- a/res/res_pjsip.c
+++ b/res/res_pjsip.c
@@ -1158,6 +1158,14 @@
being forwarded.
+
+ Enable STIR/SHAKEN support on this endpoint
+
+ 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.
+
+
Authentication type
diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c
index 3864c7b288..a4968431c7 100644
--- a/res/res_pjsip/pjsip_configuration.c
+++ b/res/res_pjsip/pjsip_configuration.c
@@ -1967,6 +1967,7 @@ int ast_res_pjsip_initialize_configuration(void)
ast_sorcery_object_field_register(sip_sorcery, "endpoint", "accept_multiple_sdp_answers", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, media.rtp.accept_multiple_sdp_answers));
ast_sorcery_object_field_register(sip_sorcery, "endpoint", "suppress_q850_reason_headers", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, suppress_q850_reason_headers));
ast_sorcery_object_field_register(sip_sorcery, "endpoint", "ignore_183_without_sdp", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, ignore_183_without_sdp));
+ 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 7be031239c..de24b266ea 100644
--- a/res/res_pjsip_stir_shaken.c
+++ b/res/res_pjsip_stir_shaken.c
@@ -98,6 +98,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"));
@@ -134,6 +139,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);
@@ -281,6 +290,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 632fd1b38f..7a141f7c64 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;
}