18 #include <curl/curl.h>
27 #define _TRACE_PREFIX_ "v",__LINE__, ""
45 #include "stir_shaken.h"
47 #define AST_DB_FAMILY "STIR_SHAKEN"
49 static regex_t url_match_regex;
52 #define BEGIN_CERTIFICATE_STR "-----BEGIN CERTIFICATE-----"
54 static const char *vs_rc_map[] = {
55 [AST_STIR_SHAKEN_VS_SUCCESS] =
"success",
56 [AST_STIR_SHAKEN_VS_DISABLED] =
"disabled",
57 [AST_STIR_SHAKEN_VS_INVALID_ARGUMENTS] =
"invalid_arguments",
58 [AST_STIR_SHAKEN_VS_INTERNAL_ERROR] =
"internal_error",
59 [AST_STIR_SHAKEN_VS_NO_IDENTITY_HDR] =
"missing_identity_hdr",
60 [AST_STIR_SHAKEN_VS_NO_DATE_HDR] =
"missing_date_hdr",
61 [AST_STIR_SHAKEN_VS_DATE_HDR_PARSE_FAILURE] =
"date_hdr_parse_failure",
62 [AST_STIR_SHAKEN_VS_DATE_HDR_EXPIRED] =
"date_hdr_range_error",
63 [AST_STIR_SHAKEN_VS_NO_JWT_HDR] =
"missing_jwt_hdr",
64 [AST_STIR_SHAKEN_VS_CERT_CACHE_MISS] =
"cert_cache_miss",
65 [AST_STIR_SHAKEN_VS_CERT_CACHE_INVALID] =
"cert_cache_invalid",
66 [AST_STIR_SHAKEN_VS_CERT_CACHE_EXPIRED] =
"cert_cache_expired",
67 [AST_STIR_SHAKEN_VS_CERT_RETRIEVAL_FAILURE] =
"cert_retrieval_failure",
68 [AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID] =
"cert_contents_invalid",
69 [AST_STIR_SHAKEN_VS_CERT_NOT_TRUSTED] =
"cert_not_trusted",
70 [AST_STIR_SHAKEN_VS_CERT_DATE_INVALID] =
"cert_date_failure",
71 [AST_STIR_SHAKEN_VS_CERT_NO_TN_AUTH_EXT] =
"cert_no_tn_auth_ext",
72 [AST_STIR_SHAKEN_VS_CERT_NO_SPC_IN_TN_AUTH_EXT] =
"cert_no_spc_in_auth_ext",
73 [AST_STIR_SHAKEN_VS_NO_RAW_KEY] =
"no_raw_key",
74 [AST_STIR_SHAKEN_VS_SIGNATURE_VALIDATION] =
"signature_validation",
75 [AST_STIR_SHAKEN_VS_NO_IAT] =
"missing_iat",
76 [AST_STIR_SHAKEN_VS_IAT_EXPIRED] =
"iat_range_error",
77 [AST_STIR_SHAKEN_VS_INVALID_OR_NO_PPT] =
"invalid_or_no_ppt",
78 [AST_STIR_SHAKEN_VS_INVALID_OR_NO_ALG] =
"invalid_or_no_alg",
79 [AST_STIR_SHAKEN_VS_INVALID_OR_NO_TYP] =
"invalid_or_no_typ",
80 [AST_STIR_SHAKEN_VS_INVALID_OR_NO_GRANTS] =
"invalid_or_no_grants",
81 [AST_STIR_SHAKEN_VS_INVALID_OR_NO_ATTEST] =
"invalid_or_no_attest",
82 [AST_STIR_SHAKEN_VS_NO_ORIGID] =
"missing_origid",
83 [AST_STIR_SHAKEN_VS_NO_ORIG_TN] =
"missing_orig_tn",
84 [AST_STIR_SHAKEN_VS_CID_ORIG_TN_MISMATCH] =
"cid_orig_tn_mismatch",
85 [AST_STIR_SHAKEN_VS_NO_DEST_TN] =
"missing_dest_tn",
86 [AST_STIR_SHAKEN_VS_INVALID_HEADER] =
"invalid_header",
87 [AST_STIR_SHAKEN_VS_INVALID_GRANT] =
"invalid_grant",
90 const char *vs_response_code_to_str(
91 enum ast_stir_shaken_vs_response_code vs_rc)
94 vs_rc_map[vs_rc] : NULL;
97 static void cleanup_cert_from_astdb_and_fs(
109 remove(ctx->filename);
113 const char *cache_control_header,
const char *expires_header)
118 time_t current_time = time(NULL);
119 time_t max_age_hdr = 0;
120 time_t expires_hdr = 0;
121 ASN1_TIME *notAfter = NULL;
122 time_t cert_expires = 0;
123 time_t config_expires = 0;
127 config_expires = current_time + cfg->vcfg_common.max_cache_entry_age;
129 if (!ast_strlen_zero(cache_control_header)) {
132 str_max_age = strstr(cache_control_header,
"s-maxage");
134 str_max_age = strstr(cache_control_header,
"max-age");
139 char *equal = strchr(str_max_age,
'=');
141 max_age_hdr = current_time + m;
146 if (!ast_strlen_zero(expires_header)) {
147 struct ast_tm expires_time;
149 ast_strptime(expires_header,
"%a, %d %b %Y %T %z", &expires_time);
150 expires_time.tm_isdst = -1;
151 expires_hdr =
ast_mktime(&expires_time,
"GMT").tv_sec;
154 notAfter = X509_get_notAfter(cert->xcert);
155 cert_expires = crypto_asn_time_as_time_t(notAfter);
173 expires = config_expires;
175 if (max_age_hdr > expires) {
176 expires = max_age_hdr;
179 if (expires_hdr > expires) {
180 expires = expires_hdr;
187 if (cert_expires && cert_expires < expires) {
188 expires = cert_expires;
191 snprintf(time_buf,
sizeof(time_buf),
"%ld", expires);
193 rc =
ast_db_put(cert->hash_family,
"expiration", time_buf);
195 strcpy(cert->expiration, time_buf);
202 const char *cache_control_hdr,
const char *expires_hdr)
206 rc =
ast_db_put(cert->url_family, cert->public_url, cert->hash);
210 rc =
ast_db_put(cert->hash_family,
"path", cert->filename);
212 ast_db_del(cert->url_family, cert->public_url);
216 rc = add_cert_expiration_to_astdb(cert, cache_control_hdr, expires_hdr);
218 ast_db_del(cert->url_family, cert->public_url);
225 static int is_cert_cache_entry_expired(
char *expiration)
227 struct timeval current_time =
ast_tvnow();
228 struct timeval expires = { .tv_sec = 0, .tv_usec = 0 };
230 SCOPE_ENTER(3,
"Checking for cache expiration: %s\n", expiration);
232 if (ast_strlen_zero(expiration)) {
233 SCOPE_EXIT_RTN_VALUE(1,
"No expiration date provided\n");
237 SCOPE_EXIT_RTN_VALUE(1,
"Couldn't convert expiration string '%s' to ulong",
240 ast_trace(2,
"Expiration comparison: exp: %" PRIu64
" curr: %" PRIu64
" Diff: %" PRIu64
".\n",
241 expires.tv_sec, current_time.tv_sec, expires.tv_sec - current_time.tv_sec);
243 res = (
ast_tvcmp(current_time, expires) == -1 ? 0 : 1);
244 SCOPE_EXIT_RTN_VALUE(res ,
"entry was %sexpired\n", res ?
"" :
"not ");
247 #define ASN1_TAG_TNAUTH_SPC 0
248 #define ASN1_TAG_TNAUTH_TN_RANGE 1
249 #define ASN1_TAG_TNAUTH_TN 2
251 #define IS_GET_OBJ_ERR(ret) (ret & 0x80)
253 static enum ast_stir_shaken_vs_response_code
256 ASN1_OCTET_STRING *tn_exten;
257 const unsigned char* octet_str_data = NULL;
261 SCOPE_ENTER(3,
"%s: Checking TNAuthList in cert '%s'\n", ctx->tag, ctx->public_url);
263 tn_exten = crypto_get_cert_extension_data(ctx->xcert, get_tn_auth_nid(), NULL);
265 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NO_TN_AUTH_EXT,
266 LOG_ERROR,
"%s: Cert '%s' doesn't have a TNAuthList extension\n",
267 ctx->tag, ctx->public_url);
269 octet_str_data = tn_exten->data;
272 ret = ASN1_get_object(&octet_str_data, &xlen, &tag, &xclass, tn_exten->length);
273 if (IS_GET_OBJ_ERR(ret)) {
274 crypto_log_openssl(LOG_ERROR,
"%s: Cert '%s' has malformed TNAuthList extension\n",
275 ctx->tag, ctx->public_url);
276 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NO_TN_AUTH_EXT);
279 if (ret != V_ASN1_CONSTRUCTED || tag != V_ASN1_SEQUENCE) {
280 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NO_TN_AUTH_EXT,
281 LOG_ERROR,
"%s: Cert '%s' has malformed TNAuthList extension (tag %d != V_ASN1_SEQUENCE)\n",
282 ctx->tag, ctx->public_url, tag);
296 ret = ASN1_get_object(&octet_str_data, &xlen, &tag, &xclass, tn_exten->length);
297 if (IS_GET_OBJ_ERR(ret)) {
298 crypto_log_openssl(LOG_ERROR,
"%s: Cert '%s' has malformed TNAuthList extension\n",
299 ctx->tag, ctx->public_url);
300 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NO_TN_AUTH_EXT);
303 if (ret != V_ASN1_CONSTRUCTED || tag != ASN1_TAG_TNAUTH_SPC) {
304 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NO_SPC_IN_TN_AUTH_EXT,
305 LOG_ERROR,
"%s: Cert '%s' has malformed TNAuthList extension (tag %d != ASN1_TAG_TNAUTH_SPC(0))\n",
306 ctx->tag, ctx->public_url, tag);
310 ret = ASN1_get_object(&octet_str_data, &xlen, &tag, &xclass, tn_exten->length);
312 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NO_SPC_IN_TN_AUTH_EXT,
313 LOG_ERROR,
"%s: Cert '%s' has malformed TNAuthList extension (no SPC)\n",
314 ctx->tag, ctx->public_url);
318 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR);
321 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_SUCCESS,
"%s: Cert '%s' with SPC: %s CN: %s has valid TNAuthList\n",
322 ctx->tag, ctx->public_url, ctx->cert_spc, ctx->cert_cn);
324 #undef IS_GET_OBJ_ERR
326 static enum ast_stir_shaken_vs_response_code check_cert(
329 RAII_VAR(
char *, CN, NULL, ast_free);
332 SCOPE_ENTER(3,
"%s: Validating cert '%s'\n", ctx->tag, ctx->public_url);
334 CN = crypto_get_cert_subject(ctx->xcert,
"CN");
336 CN = crypto_get_cert_subject(ctx->xcert, NULL);
337 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID,
338 LOG_ERROR,
"%s: Cert '%s' has no commonName(CN) in Subject '%s'\n",
339 ctx->tag, ctx->public_url, CN);
344 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR);
347 ast_trace(3,
"%s: Checking ctx against CA ctx\n", ctx->tag);
348 res = crypto_is_cert_trusted(ctx->eprofile->vcfg_common.tcs, ctx->xcert, &err_msg);
350 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NOT_TRUSTED,
351 LOG_ERROR,
"%s: Cert '%s' not trusted: %s\n",
352 ctx->tag, ctx->public_url, err_msg);
355 ast_trace(3,
"%s: Attempting to get the raw pubkey\n", ctx->tag);
356 ctx->raw_key_len = crypto_get_raw_pubkey_from_cert(ctx->xcert,
358 if (ctx->raw_key_len <= 0) {
359 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_NO_RAW_KEY,
360 LOG_ERROR,
"%s: Unable to extract raw public key from '%s'\n",
361 ctx->tag, ctx->public_url);
364 ast_trace(3,
"%s: Checking cert '%s' validity dates\n",
365 ctx->tag, ctx->public_url);
366 if (!crypto_is_cert_time_valid(ctx->xcert, ctx->validity_check_time)) {
367 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_DATE_INVALID,
368 LOG_ERROR,
"%s: Cert '%s' dates not valid\n",
369 ctx->tag, ctx->public_url);
372 SCOPE_EXIT_RTN_VALUE(check_tn_auth_list(ctx),
373 "%s: Cert '%s' with SPC: %s CN: %s is valid\n",
374 ctx->tag, ctx->public_url, ctx->cert_spc, ctx->cert_cn);
377 static enum ast_stir_shaken_vs_response_code retrieve_cert_from_url(
383 enum ast_stir_shaken_vs_response_code vs_rc;
387 ast_calloc(1,
sizeof(*write_data)), curl_write_data_free);
389 ast_calloc(1,
sizeof(*open_socket_data)), curl_open_socket_data_free);
391 const char *cache_control;
393 SCOPE_ENTER(2,
"%s: Attempting to retrieve '%s' from net\n",
394 ctx->tag, ctx->public_url);
396 if (!
header_data || !write_data || !open_socket_data) {
397 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR,
398 LOG_ERROR,
"%s: Unable to allocate memory for curl '%s' transaction\n",
399 ctx->tag, ctx->public_url);
403 write_data->debug_info =
ast_strdup(ctx->public_url);
404 write_data->max_download_bytes = 8192;
405 write_data->stream_buffer = NULL;
406 open_socket_data->debug_info =
ast_strdup(ctx->public_url);
407 open_socket_data->acl = ctx->eprofile->vcfg_common.acl;
409 if (!
header_data->debug_info || !write_data->debug_info ||
410 !open_socket_data->debug_info) {
411 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR,
412 LOG_ERROR,
"%s: Unable to allocate memory for curl '%s' transaction\n",
413 ctx->tag, ctx->public_url);
416 http_code =
curler(ctx->public_url,
417 ctx->eprofile->vcfg_common.curl_timeout,
420 if (http_code / 100 != 2) {
421 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_RETRIEVAL_FAILURE,
422 LOG_ERROR,
"%s: Failed to retrieve cert %s: code %ld\n",
423 ctx->tag, ctx->public_url, http_code);
426 if (!
ast_begins_with(write_data->stream_buffer, BEGIN_CERTIFICATE_STR)) {
427 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID,
428 LOG_ERROR,
"%s: Cert '%s' contains invalid data\n",
429 ctx->tag, ctx->public_url);
432 ctx->xcert = crypto_load_cert_from_memory(write_data->stream_buffer,
433 write_data->stream_bytes_downloaded);
435 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID,
436 LOG_ERROR,
"%s: Cert '%s' was not parseable as an X509 certificate\n",
437 ctx->tag, ctx->public_url);
440 vs_rc = check_cert(ctx);
441 if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) {
442 X509_free(ctx->xcert);
444 SCOPE_EXIT_RTN_VALUE(vs_rc,
"%s: Cert '%s' failed validity checks\n",
445 ctx->tag, ctx->public_url);
448 cert_file = fopen(ctx->filename,
"w");
450 X509_free(ctx->xcert);
452 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR,
453 LOG_ERROR,
"%s: Failed to write cert %s: file '%s' %s (%d)\n",
454 ctx->tag, ctx->public_url, ctx->filename, strerror(errno), errno);
457 rc = fputs(write_data->stream_buffer, cert_file);
460 X509_free(ctx->xcert);
462 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR,
463 LOG_ERROR,
"%s: Failed to write cert %s: file '%s' %s (%d)\n",
464 ctx->tag, ctx->public_url, ctx->filename, strerror(errno), errno);
467 ast_trace(2,
"%s: Cert '%s' written to file '%s'\n",
468 ctx->tag, ctx->public_url, ctx->filename);
470 ast_trace(2,
"%s: Adding cert '%s' to astdb",
471 ctx->tag, ctx->public_url);
475 rc = add_cert_key_to_astdb(ctx, cache_control, expires);
477 X509_free(ctx->xcert);
479 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR,
480 LOG_ERROR,
"%s: Unable to add cert '%s' to ASTDB\n",
481 ctx->tag, ctx->public_url);
484 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_SUCCESS,
485 "%s: Cert '%s' successfully retrieved from internet and cached\n",
486 ctx->tag, ctx->public_url);
489 static enum ast_stir_shaken_vs_response_code
493 enum ast_stir_shaken_vs_response_code vs_rc;
495 SCOPE_ENTER(2,
"%s: Attempting to retrieve cert '%s' from cache\n",
496 ctx->tag, ctx->public_url);
499 cleanup_cert_from_astdb_and_fs(ctx);
500 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CACHE_MISS,
501 "%s: No cert found in astdb for '%s'\n",
502 ctx->tag, ctx->public_url);
505 rc =
ast_db_get(ctx->hash_family,
"expiration", ctx->expiration,
sizeof(ctx->expiration));
507 cleanup_cert_from_astdb_and_fs(ctx);
508 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CACHE_MISS,
509 "%s: No cert found in astdb for '%s'\n",
510 ctx->tag, ctx->public_url);
514 cleanup_cert_from_astdb_and_fs(ctx);
515 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CACHE_MISS,
516 "%s: Cert file '%s' was not found or was not readable for '%s'\n",
517 ctx->tag, ctx->filename, ctx->public_url);
520 if (is_cert_cache_entry_expired(ctx->expiration)) {
521 cleanup_cert_from_astdb_and_fs(ctx);
522 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CACHE_EXPIRED,
523 "%s: Cert file '%s' cache entry was expired for '%s'\n",
524 ctx->tag, ctx->filename, ctx->public_url);
527 ctx->xcert = crypto_load_cert_from_file(ctx->filename);
529 cleanup_cert_from_astdb_and_fs(ctx);
530 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID,
531 "%s: Cert file '%s' was not parseable as an X509 certificate for '%s'\n",
532 ctx->tag, ctx->filename, ctx->public_url);
535 vs_rc = check_cert(ctx);
536 if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) {
537 X509_free(ctx->xcert);
539 SCOPE_EXIT_RTN_VALUE(vs_rc,
"%s: Cert '%s' failed validity checks\n",
540 ctx->tag, ctx->public_url);
543 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_SUCCESS,
544 "%s: Cert '%s' successfully retrieved from cache\n",
545 ctx->tag, ctx->public_url);
548 static enum ast_stir_shaken_vs_response_code ctx_populate(
555 return AST_STIR_SHAKEN_VS_INTERNAL_ERROR;
559 ctx->eprofile->vcfg_common.cert_cache_dir, hash) != 0) {
560 return AST_STIR_SHAKEN_VS_INTERNAL_ERROR;
565 return AST_STIR_SHAKEN_VS_INTERNAL_ERROR;
569 return AST_STIR_SHAKEN_VS_INTERNAL_ERROR;
572 return AST_STIR_SHAKEN_VS_SUCCESS;
575 static enum ast_stir_shaken_vs_response_code
578 enum ast_stir_shaken_vs_response_code rc = AST_STIR_SHAKEN_VS_SUCCESS;
579 SCOPE_ENTER(3,
"%s: Retrieving cert '%s'\n", ctx->tag, ctx->public_url);
581 ast_trace(1,
"%s: Checking cache for cert '%s'\n", ctx->tag, ctx->public_url);
582 rc = retrieve_cert_from_cache(ctx);
583 if (rc == AST_STIR_SHAKEN_VS_SUCCESS) {
584 SCOPE_EXIT_RTN_VALUE(rc,
"%s: Using cert '%s' from cache\n",
585 ctx->tag, ctx->public_url);;
588 ast_trace(1,
"%s: No valid cert for '%s' available in cache\n",
589 ctx->tag, ctx->public_url);
590 ast_trace(1,
"%s: Retrieving cert directly from url '%s'\n",
591 ctx->tag, ctx->public_url);
593 rc = retrieve_cert_from_url(ctx);
594 if (rc == AST_STIR_SHAKEN_VS_SUCCESS) {
595 SCOPE_EXIT_RTN_VALUE(rc,
"%s: Using cert '%s' from internet\n",
596 ctx->tag, ctx->public_url);
599 SCOPE_EXIT_LOG_RTN_VALUE(rc, LOG_ERROR,
600 "%s: Unable to retrieve cert '%s' from cache or internet\n",
601 ctx->tag, ctx->public_url);
604 enum ast_stir_shaken_vs_response_code
605 ast_stir_shaken_vs_ctx_add_identity_hdr(
609 AST_STIR_SHAKEN_VS_SUCCESS : AST_STIR_SHAKEN_VS_INTERNAL_ERROR;
612 enum ast_stir_shaken_vs_response_code
614 const char *date_hdr)
617 AST_STIR_SHAKEN_VS_SUCCESS : AST_STIR_SHAKEN_VS_INTERNAL_ERROR;
620 enum stir_shaken_failure_action_enum
621 ast_stir_shaken_vs_get_failure_action(
624 return ctx->eprofile->vcfg_common.stir_shaken_failure_action;
627 int ast_stir_shaken_vs_get_use_rfc9410_responses(
630 return ctx->eprofile->vcfg_common.use_rfc9410_responses;
633 void ast_stir_shaken_vs_ctx_set_response_code(
635 enum ast_stir_shaken_vs_response_code vs_rc)
637 ctx->failure_reason = vs_rc;
640 static void ctx_destructor(
void *obj)
644 ao2_cleanup(ctx->eprofile);
645 ast_free(ctx->raw_key);
647 X509_free(ctx->xcert);
650 enum ast_stir_shaken_vs_response_code
651 ast_stir_shaken_vs_ctx_create(
const char *caller_id,
652 struct ast_channel *chan,
const char *profile_name,
658 RAII_VAR(
char *, canon_caller_id , canonicalize_tn_alloc(caller_id), ast_free);
660 const char *t =
S_OR(tag,
S_COR(chan, ast_channel_name(chan),
""));
661 SCOPE_ENTER(3,
"%s: Enter\n", t);
663 if (ast_strlen_zero(tag)) {
664 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_ARGUMENTS,
665 LOG_ERROR,
"%s: Must provide tag\n", t);
668 if (ast_strlen_zero(canon_caller_id)) {
669 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_ARGUMENTS,
670 LOG_ERROR,
"%s: Must provide caller_id\n", t);
673 if (ast_strlen_zero(profile_name)) {
674 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_DISABLED,
675 "%s: Disabled due to missing profile name\n", t);
679 if (vs->global_disable) {
680 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_DISABLED,
681 "%s: Globally disabled\n", t);
684 profile = eprofile_get_cfg(profile_name);
686 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_DISABLED,
687 LOG_ERROR,
"%s: No profile for profile name '%s'. Call will continue\n", tag,
691 if (!PROFILE_ALLOW_VERIFY(profile)) {
692 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_DISABLED,
693 "%s: Disabled by profile\n", t);
696 ctx = ao2_alloc_options(
sizeof(*ctx), ctx_destructor,
699 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR);
702 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR);
706 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR);
711 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR);
715 ctx->eprofile = profile;
720 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_SUCCESS,
"%s: Done\n", t);
723 static enum ast_stir_shaken_vs_response_code check_date_header(
726 struct ast_tm date_hdr_tm;
727 struct timeval date_hdr_timeval;
728 struct timeval current_timeval;
730 char timezone[80] = { 0 };
732 SCOPE_ENTER(3,
"%s: Checking date header: '%s'\n",
733 ctx->tag, ctx->date_hdr);
735 if (!(remainder =
ast_strptime(ctx->date_hdr,
"%a, %d %b %Y %T", &date_hdr_tm))) {
736 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_DATE_HDR_PARSE_FAILURE,
737 LOG_ERROR,
"%s: Failed to parse: '%s'\n",
738 ctx->tag, ctx->date_hdr);
741 sscanf(remainder,
"%79s", timezone);
743 if (ast_strlen_zero(timezone)) {
744 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_DATE_HDR_PARSE_FAILURE,
745 LOG_ERROR,
"%s: A timezone is required: '%s'\n",
746 ctx->tag, ctx->date_hdr);
749 date_hdr_timeval =
ast_mktime(&date_hdr_tm, timezone);
750 ctx->date_hdr_time = date_hdr_timeval.tv_sec;
753 time_diff =
ast_tvdiff_ms(current_timeval, date_hdr_timeval);
754 ast_trace(3,
"%zu %zu %zu %d\n", current_timeval.tv_sec,
755 date_hdr_timeval.tv_sec,
756 (current_timeval.tv_sec - date_hdr_timeval.tv_sec), (
int)time_diff);
759 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_DATE_HDR_EXPIRED,
760 LOG_ERROR,
"%s: Future date: '%s'\n",
761 ctx->tag, ctx->date_hdr);
762 }
else if (time_diff > (ctx->eprofile->vcfg_common.max_date_header_age * 1000)) {
763 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_DATE_HDR_EXPIRED,
764 LOG_ERROR,
"%s: More than %u seconds old: '%s'\n",
765 ctx->tag, ctx->eprofile->vcfg_common.max_date_header_age, ctx->date_hdr);
768 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_SUCCESS,
769 "%s: Success: '%s'\n", ctx->tag, ctx->date_hdr);
772 #define FULL_URL_REGEX "^([a-zA-Z]+)://(([^@]+@[^:]+):)?(([^:/?]+)|([0-9.]+)|([[][0-9a-fA-F:]+[]]))(:([0-9]+))?(/([^#\\?]+))?(\\?([^#]+))?(#(.*))?"
773 #define FULL_URL_REGEX_GROUPS 15
791 #define URL_MATCH_SCHEME 1
792 #define URL_MATCH_USERPASS 3
793 #define URL_MATCH_HOST 4
794 #define URL_MATCH_PORT 9
795 #define URL_MATCH_PATH 11
796 #define URL_MATCH_QUERY 13
797 #define URL_MATCH_FRAGMENT 15
799 #define get_match_string(__x5u, __pmatch, __i) \
801 char *__match = NULL; \
802 if (__pmatch[__i].rm_so >= 0) { \
803 regoff_t __len = __pmatch[__i].rm_eo - __pmatch[__i].rm_so; \
804 const char *__start = __x5u + __pmatch[__i].rm_so; \
805 __match = ast_alloca(__len + 1); \
806 ast_copy_string(__match, __start, __len + 1); \
811 #define DUMP_X5U_MATCH() \
814 if (TRACE_ATLEAST(4)) { \
815 ast_trace(-1, "%s: x5u: %s\n", ctx->tag, x5u); \
816 for (i=0;i<FULL_URL_REGEX_GROUPS;i++) { \
817 const char *m = get_match_string(x5u, pmatch, i); \
819 ast_trace(-1, "%s: %2d %s\n", ctx->tag, i, m); \
828 int max_groups = url_match_regex.re_nsub + 1;
829 regmatch_t pmatch[max_groups];
831 SCOPE_ENTER(3,
"%s: Checking x5u '%s'\n", ctx->tag, x5u);
833 rc = regexec(&url_match_regex, x5u, max_groups, pmatch, 0);
835 char regex_error[512];
836 regerror(rc, &url_match_regex, regex_error,
sizeof(regex_error));
837 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_X5U, LOG_ERROR,
838 "%s: x5u '%s' in Identity header failed basic URL validation: %s\n",
839 ctx->tag, x5u, regex_error);
842 if (ctx->eprofile->vcfg_common.relax_x5u_port_scheme_restrictions
843 != relax_x5u_port_scheme_restrictions_YES) {
844 const char *scheme = get_match_string(x5u, pmatch, URL_MATCH_SCHEME);
845 const char *port = get_match_string(x5u, pmatch, URL_MATCH_PORT);
849 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_X5U, LOG_ERROR,
850 "%s: x5u '%s': scheme '%s' not https\n",
851 ctx->tag, x5u, scheme);
853 if (!ast_strlen_zero(port)) {
857 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_X5U, LOG_ERROR,
858 "%s: x5u '%s': port '%s' not port 443 or 8443\n",
859 ctx->tag, x5u, port);
864 if (ctx->eprofile->vcfg_common.relax_x5u_path_restrictions
865 != relax_x5u_path_restrictions_YES) {
866 const char *userpass = get_match_string(x5u, pmatch, URL_MATCH_USERPASS);
867 const char *qs = get_match_string(x5u, pmatch, URL_MATCH_QUERY);
868 const char *frag = get_match_string(x5u, pmatch, URL_MATCH_FRAGMENT);
870 if (!ast_strlen_zero(userpass) || !ast_strlen_zero(qs)
871 || !ast_strlen_zero(frag)) {
873 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_X5U, LOG_ERROR,
874 "%s: x5u '%s' contains user:password, query parameters or fragment\n",
882 enum ast_stir_shaken_vs_response_code
885 RAII_VAR(
char *, jwt_encoded, NULL, ast_free);
886 RAII_VAR(jwt_t *, jwt, NULL, jwt_free);
889 char *grants_str = NULL;
891 const char *ppt_header = NULL;
892 const char *grant = NULL;
893 time_t now_s = time(NULL);
898 enum ast_stir_shaken_vs_response_code vs_rc;
899 SCOPE_ENTER(3,
"%s: Verifying\n", ctx ? ctx->tag :
"NULL");
902 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, LOG_ERROR,
903 "%s: No context object!\n",
"NULL");
906 if (ast_strlen_zero(ctx->identity_hdr)) {
907 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, LOG_ERROR,
908 "%s: No identity header in ctx\n", ctx->tag);
911 p = strchr(ctx->identity_hdr,
';');
912 len = p - ctx->identity_hdr + 1;
915 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, LOG_ERROR,
916 "%s: Failed to allocate memory for encoded jwt\n", ctx->tag);
919 memcpy(jwt_encoded, ctx->identity_hdr, len);
920 jwt_encoded[len - 1] =
'\0';
922 jwt_decode(&jwt, jwt_encoded, NULL, 0);
924 ppt_header = jwt_get_header(jwt,
"ppt");
925 if (!ppt_header || strcmp(ppt_header, STIR_SHAKEN_PPT)) {
926 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_PPT,
"%s: %s\n",
927 ctx->tag, vs_response_code_to_str(AST_STIR_SHAKEN_VS_INVALID_OR_NO_PPT));
930 vs_rc = check_date_header(ctx);
931 if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) {
932 SCOPE_EXIT_LOG_RTN_VALUE(vs_rc, LOG_ERROR,
933 "%s: Date header verification failed\n", ctx->tag);
936 x5u = jwt_get_header(jwt,
"x5u");
937 if (ast_strlen_zero(x5u)) {
938 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_X5U, LOG_ERROR,
939 "%s: No x5u in Identity header\n", ctx->tag);
942 rc = check_x5u_url(ctx, x5u);
943 if (rc != AST_STIR_SHAKEN_VS_SUCCESS) {
944 SCOPE_EXIT_RTN_VALUE(vs_rc,
945 "%s: x5u URL verification failed\n", ctx->tag);
948 ast_trace(3,
"%s: Decoded enough to get x5u: '%s'\n", ctx->tag, x5u);
950 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, LOG_ERROR,
951 "%s: Failed to set public_url '%s'\n", ctx->tag, x5u);
954 iat = jwt_get_grant_int(jwt,
"iat");
956 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_NO_IAT, LOG_ERROR,
957 "%s: No 'iat' in Identity header\n", ctx->tag);
959 ast_trace(1,
"date_hdr: %zu iat: %zu diff: %zu\n",
960 ctx->date_hdr_time, iat, ctx->date_hdr_time - iat);
961 if (iat + ctx->eprofile->vcfg_common.max_iat_age < now_s) {
962 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_IAT_EXPIRED,
963 "%s: iat %ld older than %u seconds\n", ctx->tag,
964 iat, ctx->eprofile->vcfg_common.max_iat_age);
966 ctx->validity_check_time = iat;
968 vs_rc = ctx_populate(ctx);
969 if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) {
970 SCOPE_EXIT_LOG_RTN_VALUE(vs_rc, LOG_ERROR,
971 "%s: Unable to populate ctx\n", ctx->tag);
974 vs_rc = retrieve_verification_cert(ctx);
975 if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) {
976 SCOPE_EXIT_LOG_RTN_VALUE(vs_rc, LOG_ERROR,
977 "%s: Could not get valid cert from '%s'\n", ctx->tag, ctx->public_url);
983 rc = jwt_decode(&jwt, jwt_encoded, ctx->raw_key, ctx->raw_key_len);
985 SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_SIGNATURE_VALIDATION,
986 LOG_ERROR,
"%s: Signature validation failed for '%s'\n",
987 ctx->tag, ctx->public_url);
990 ast_trace(1,
"%s: Decoding succeeded\n", ctx->tag);
992 ppt_header = jwt_get_header(jwt,
"alg");
993 if (!ppt_header || strcmp(ppt_header, STIR_SHAKEN_ENCRYPTION_ALGORITHM)) {
994 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_ALG,
995 "%s: %s\n", ctx->tag,
996 vs_response_code_to_str(AST_STIR_SHAKEN_VS_INVALID_OR_NO_ALG));
999 ppt_header = jwt_get_header(jwt,
"ppt");
1000 if (!ppt_header || strcmp(ppt_header, STIR_SHAKEN_PPT)) {
1001 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_PPT,
1002 "%s: %s\n", ctx->tag,
1003 vs_response_code_to_str(AST_STIR_SHAKEN_VS_INVALID_OR_NO_PPT));
1006 ppt_header = jwt_get_header(jwt,
"typ");
1007 if (!ppt_header || strcmp(ppt_header, STIR_SHAKEN_TYPE)) {
1008 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_TYP,
1009 "%s: %s\n", ctx->tag,
1010 vs_response_code_to_str(AST_STIR_SHAKEN_VS_INVALID_OR_NO_TYP));
1013 grants_str = jwt_get_grants_json(jwt, NULL);
1014 if (ast_strlen_zero(grants_str)) {
1015 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_GRANTS,
1016 "%s: %s\n", ctx->tag,
1017 vs_response_code_to_str(AST_STIR_SHAKEN_VS_INVALID_OR_NO_GRANTS));
1019 ast_trace(1,
"grants: %s\n", grants_str);
1021 ast_std_free(grants_str);
1023 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_GRANTS,
1024 "%s: %s\n", ctx->tag,
1025 vs_response_code_to_str(AST_STIR_SHAKEN_VS_INVALID_OR_NO_GRANTS));
1029 if (ast_strlen_zero(grant)) {
1030 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_ATTEST,
1031 "%s: No 'attest' in Identity header\n", ctx->tag);
1033 if (grant[0] <
'A' || grant[0] >
'C') {
1034 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_ATTEST,
1035 "%s: Invalid attest value '%s'\n", ctx->tag, grant);
1038 ast_trace(1,
"got attest: %s\n", grant);
1042 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_NO_DEST_TN,
1043 "%s: No 'dest' in Identity header\n", ctx->tag);
1045 if (TRACE_ATLEAST(3)) {
1047 ast_trace(1,
"got dest: %s\n", otn);
1053 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_NO_ORIG_TN,
1054 "%s: No 'orig' in Identity header\n", ctx->tag);
1056 if (TRACE_ATLEAST(3)) {
1058 ast_trace(1,
"got orig: %s\n", otn);
1063 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_NO_ORIG_TN,
1064 "%s: No 'orig.tn' in Indentity header\n", ctx->tag);
1067 if (strcmp(ctx->caller_id, ctx->orig_tn) != 0) {
1068 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CID_ORIG_TN_MISMATCH,
1069 "%s: Mismatched cid '%s' and orig_tn '%s'\n", ctx->tag,
1070 ctx->caller_id, grant);
1074 if (ast_strlen_zero(grant)) {
1075 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_NO_ORIGID,
1076 "%s: No 'origid' in Identity header\n", ctx->tag);
1079 SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_SUCCESS,
1080 "%s: verification succeeded\n", ctx->tag);
1093 if (url_match_regex.re_nsub > 0) {
1094 regfree(&url_match_regex);
1104 if (vs_config_load()) {
1108 rc = regcomp(&url_match_regex, FULL_URL_REGEX, REG_EXTENDED);
1110 char regex_error[512];
1111 regerror(rc, &url_match_regex, regex_error,
sizeof(regex_error));
1112 ast_log(LOG_ERROR,
"Verification service URL regex failed to compile: %s\n", regex_error);
1116 if (url_match_regex.re_nsub != FULL_URL_REGEX_GROUPS) {
1117 ast_log(LOG_ERROR,
"The verification service URL regex was updated without updating FULL_URL_REGEX_GROUPS\n");
#define ARRAY_IN_BOUNDS(v, a)
Checks to see if value is within the bounds of the given array.
Main Channel structure associated with a channel.
Asterisk main include file. File version handling, generic pbx functions.
Context structure passed to ast_curl_open_socket_default_cb.
Time-related functions and macros.
void ast_json_unref(struct ast_json *value)
Decrease refcount on value. If refcount reaches zero, value is freed.
int ast_file_is_readable(const char *filename)
Test that a file exists and is readable by the effective user.
Provide cryptographic signature routines.
void ast_json_free(void *p)
Asterisk's custom JSON allocator. Exposed for use by unit tests.
#define ast_json_dump_string(root)
Encode a JSON value to a compact string.
struct timeval ast_tvnow(void)
Returns current timeval. Meant to replace calls to gettimeofday().
struct ast_json * ast_json_load_string(const char *input, struct ast_json_error *error)
Parse null terminated string into a JSON object or array.
int64_t ast_tvdiff_ms(struct timeval end, struct timeval start)
Computes the difference (in milliseconds) between two struct timeval instances.
#define ast_strdup(str)
A wrapper for strdup()
Custom localtime functions for multiple timezones.
Configuration File Parser.
Context structure passed to ast_curl_write_default_cb.
General Asterisk PBX channel definitions.
Asterisk JSON abstraction layer.
Asterisk file paths, configured in asterisk.conf.
#define ast_string_field_init(x, size)
Initialize a field pool and fields.
Access Control of various sorts.
#define ao2_ref(o, delta)
Reference/unreference an object and return the old refcount.
#define ast_json_object_string_get(object, key)
Get a string field from a JSON object.
#define S_COR(a, b, c)
returns the equivalent of logic or for strings, with an additional boolean check: second one if not e...
Conversion utility functions.
const char * ast_variable_find_in_list(const struct ast_variable *list, const char *variable)
Gets the value of a variable from a variable list by name.
#define ast_malloc(len)
A wrapper for malloc()
int ast_db_exists(const char *family, const char *key)
Check if family/key exitsts.
long curler(const char *url, int request_timeout, struct curl_write_data *write_data, struct curl_header_data *header_data, struct curl_open_socket_data *open_socket_data)
Perform a curl request.
char * ast_strptime(const char *s, const char *format, struct ast_tm *tm)
Special version of strptime(3) which places the answer in the common structure ast_tm. Also, unlike strptime(3), ast_strptime() initializes its memory prior to use.
void ast_sha1_hash(char *output, const char *input)
Produces SHA1 hash based on input string.
int ast_tvcmp(struct timeval _a, struct timeval _b)
Compress two struct timeval instances returning -1, 0, 1 if the first arg is smaller, equal or greater to the second.
int ast_strings_equal(const char *str1, const char *str2)
Compare strings for equality checking for NULL.
#define ast_calloc(num, len)
A wrapper for calloc()
Support for logging to various files, console and syslog Configuration in file logger.conf.
Module has failed to load, may be in an inconsistent state.
int ast_db_get(const char *family, const char *key, char *value, int valuelen)
Get key value specified by family/key.
int ast_str_to_ulong(const char *str, unsigned long *res)
Convert the given string to an unsigned long.
int ast_str_to_uint(const char *str, unsigned int *res)
Convert the given string to an unsigned integer.
#define ast_string_field_build(x, field, fmt, args...)
Set a field to a complex (built) value.
struct timeval ast_mktime(struct ast_tm *const tmp, const char *zone)
Timezone-independent version of mktime(3).
Standard Command Line Interface.
struct ast_json * ast_json_object_get(struct ast_json *object, const char *key)
Get a field from a JSON object.
int ast_db_del(const char *family, const char *key)
Delete entry in astdb.
#define S_OR(a, b)
returns the equivalent of logic or for strings: first one if not empty, otherwise second one...
Profile configuration for stir/shaken.
static int force_inline attribute_pure ast_begins_with(const char *str, const char *prefix)
Checks whether a string begins with another.
Abstract JSON element (object, array, string, int, ...).
int ast_db_put(const char *family, const char *key, const char *value)
Store value addressed by family/key.
Asterisk module definitions.
#define RAII_VAR(vartype, varname, initval, dtor)
Declare a variable that will call a destructor function when it goes out of scope.
Persistent data storage (akin to *doze registry)
#define ast_string_field_free_memory(x)
free all memory - to be called before destroying the object
int ast_db_deltree(const char *family, const char *keytree)
Delete one or more entries in astdb.
Sorcery Data Access Layer API.
#define ast_string_field_set(x, field, data)
Set a field to a simple string value.