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; }