From a7f22299616d1cb2fa34d58d41ea35c865e7b9e6 Mon Sep 17 00:00:00 2001 From: Piotr Gregor Date: Fri, 5 Feb 2021 18:23:11 +0000 Subject: [PATCH] modules: Add stirshaken module This module uses libstirshaken to implement STIR-Shaken authentication and verification functions (STI-AS/VS). It allows for easy verification of a SIP call containing PASSporT wrapped into SIP Identity Header using a specific certificate, or a specific key, or by performing complete check on PASSporT including downloading certificate referenced in it's x5u header, and optionally checking it against trusted root certificates (X509 cert path check). Certificates may be cached and loaded from disk. Example usage: loadmodule "stirshaken" modparam("stirshaken", "as_default_key", "/path/to/key") modparam("stirshaken", "vs_verify_x509_cert_path", 1) modparam("stirshaken", "vs_ca_dir", "/path/to/ca") modparam("stirshaken", "vs_cache_certificates", 1) modparam("stirshaken", "vs_cache_dir", "/tmp/cert_cache") modparam("stirshaken", "vs_cache_expire_s", 90) request_route { (...) stirshaken_add_identity("https://sp.com/sp.pem", "B", "+44100", "+44200", "ref"); (...) request_route { (...) stirshaken_check_identity(); (...) --- src/modules/stirshaken/Makefile | 28 + src/modules/stirshaken/stirshaken_mod.c | 972 ++++++++++++++++++++++++ 2 files changed, 1000 insertions(+) create mode 100644 src/modules/stirshaken/Makefile create mode 100644 src/modules/stirshaken/stirshaken_mod.c diff --git a/src/modules/stirshaken/Makefile b/src/modules/stirshaken/Makefile new file mode 100644 index 00000000000..7383665fcdc --- /dev/null +++ b/src/modules/stirshaken/Makefile @@ -0,0 +1,28 @@ +# +# stirshaken module makefile +# +# WARNING: do not run this directly, it should be run by the main Makefile + +include ../../Makefile.defs +auto_gen= +NAME=stirshaken.so + +ifeq ($(CROSS_COMPILE),) + BUILDER = $(shell which pkg-config) +ifneq ($(BUILDER),) + PKGLIBSECSIPID = $(shell $(BUILDER) --exists stirshaken > /dev/null 2>&1 ; echo $$? ) +ifneq ($(PKGLIBSECSIPID),0) + BUILDER = +endif +endif +endif + +ifeq ($(BUILDER),) + DEFS+= -I. + LIBS = -L. -lstirshaken +else + DEFS+= $(shell pkg-config --cflags stirshaken) + LIBS = $(shell pkg-config --libs stirshaken) +endif + +include ../../Makefile.modules diff --git a/src/modules/stirshaken/stirshaken_mod.c b/src/modules/stirshaken/stirshaken_mod.c new file mode 100644 index 00000000000..42d305996b3 --- /dev/null +++ b/src/modules/stirshaken/stirshaken_mod.c @@ -0,0 +1,972 @@ +/** + * Copyright (C) 2021 kamailio.org + * + * This file is part of Kamailio, a free SIP server. + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version + * + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include +#include +#include + +#include + +#include "../../core/sr_module.h" +#include "../../core/dprint.h" +#include "../../core/mod_fix.h" +#include "../../core/data_lump.h" +#include "../../core/lvalue.h" +#include "../../core/kemi.h" + +MODULE_VERSION + +// Authentication service +static str stirshaken_as_default_key = str_init(""); + +// Verification service +static int stirshaken_vs_verify_x509_cert_path = 0; +static str stirshaken_vs_ca_dir = str_init(""); +static str stirshaken_vs_crl_dir = str_init(""); +static int stirshaken_vs_identity_expire_s = 300; +static int stirshaken_vs_connect_timeout_s = 5; + +static int stirshaken_vs_cache_certificates = 0; +static size_t stirshaken_vs_cache_expire_s = 3600; +static str stirshaken_vs_cache_dir = str_init(""); + +static int mod_init(void); +static int child_init(int); +static void mod_destroy(void); + +static int w_stirshaken_check_identity(sip_msg_t *msg, char *str1, char *str2); +static int w_stirshaken_check_identity_with_cert(sip_msg_t *msg, char *cert_path, char *str2); +static int w_stirshaken_check_identity_with_key(sip_msg_t *msg, char *pkey_path, char *str2); +static int w_stirshaken_add_identity(sip_msg_t *msg, str *px5u, str *pattest, str *porigtn_val, str *pdesttn_val, str *porigid); +static int w_stirshaken_add_identity_with_key(sip_msg_t *msg, str *px5u, str *pattest, str *porigtn_val, str *pdesttn_val, str *porigid, str *pkeypath); + + +/* clang-format off */ +static cmd_export_t cmds[]={ + {"stirshaken_check_identity", (cmd_function)w_stirshaken_check_identity, 0, + 0, 0, ANY_ROUTE}, + {"stirshaken_check_identity_with_cert", (cmd_function)w_stirshaken_check_identity_with_cert, 1, + fixup_spve_null, fixup_free_spve_null, ANY_ROUTE}, + {"stirshaken_check_identity_with_key", (cmd_function)w_stirshaken_check_identity_with_key, 1, + fixup_spve_null, fixup_free_spve_null, ANY_ROUTE}, + {"stirshaken_add_identity", (cmd_function)w_stirshaken_add_identity, 5, + fixup_spve_all, fixup_free_spve_all, ANY_ROUTE}, + {"stirshaken_add_identity_with_key", (cmd_function)w_stirshaken_add_identity_with_key, 6, + fixup_spve_all, fixup_free_spve_all, ANY_ROUTE}, + {0, 0, 0, 0, 0, 0} +}; + +static param_export_t params[] = { + + // Authentication service + {"as_default_key", PARAM_STR, &stirshaken_as_default_key}, + + // Verification service + {"vs_verify_x509_cert_path",PARAM_INT, &stirshaken_vs_verify_x509_cert_path}, + {"vs_ca_dir", PARAM_STR, &stirshaken_vs_ca_dir}, + {"vs_crl_dir", PARAM_STR, &stirshaken_vs_crl_dir}, + {"vs_identity_expire_s", PARAM_INT, &stirshaken_vs_identity_expire_s}, + {"vs_connect_timeout_s", PARAM_INT, &stirshaken_vs_connect_timeout_s}, + {"vs_cache_certificates", PARAM_INT, &stirshaken_vs_cache_certificates}, + {"vs_cache_expire_s", PARAM_INT, &stirshaken_vs_cache_expire_s}, + {"vs_cache_dir", PARAM_STR, &stirshaken_vs_cache_dir}, + {0, 0, 0} +}; + +struct module_exports exports = { + "stirshaken", + DEFAULT_DLFLAGS, /* dlopen flags */ + cmds, + params, + 0, /* exported RPC methods */ + 0, /* exported pseudo-variables */ + 0, /* response function */ + mod_init, /* module initialization function */ + child_init, /* per child init function */ + mod_destroy /* destroy function */ +}; +/* clang-format on */ + +static void stirshaken_print_error_details(void *context) +{ + const char *error_description = NULL; + stir_shaken_error_t error_code = STIR_SHAKEN_ERROR_GENERAL; + + if (stir_shaken_is_error_set(context)) { + error_description = stir_shaken_get_error(context, &error_code); + LM_DBG("failure details:\n"); + LM_DBG("failure reason is: %s\n", error_description); + LM_DBG("failure error code is: %d\n", error_code); + } +} + +static unsigned long hash_to_long(const char *str) +{ + unsigned long hash = 5381; + int c; + + while ((c = *str++)) + hash = ((hash << 5) + hash) + c; + + return hash; +} + +static void hash_to_string(const char *url, char *buf, int buf_len) +{ + unsigned long hash_long = hash_to_long(url); + snprintf(buf, buf_len, "%lu.pem", hash_long); +} + +static int get_cert_name_hashed(const char *name, char *buf, int buf_len) +{ + char cert_hash[64] = { 0 }; + + hash_to_string(name, cert_hash, sizeof(cert_hash)); + + if (!stir_shaken_make_complete_path(buf, buf_len, stirshaken_vs_cache_dir.s, cert_hash, "/")) { + LM_ERR("Cannot create cert name hashed\n"); + return -1; + } + + return 0; +} + +static stir_shaken_status_t shaken_callback(stir_shaken_callback_arg_t *arg) +{ + stir_shaken_context_t ss = { 0 }; + stir_shaken_cert_t cache_copy = { 0 }; + char cert_full_path[STIR_SHAKEN_BUFLEN] = { 0 }; + + switch (arg->action) { + + case STIR_SHAKEN_CALLBACK_ACTION_CERT_FETCH_ENQUIRY: + + // Default behaviour for certificate fetch enquiry is to request downloading, but in some cases it would be useful to avoid that and use pre-cached certificate. + // Here, we supply libstirshaken with certificate we cached earlier, avoiding HTTP(S) download. + // We must return STIR_SHAKEN_STATUS_HANDLED to signal this to the library, otherwise it would execute HTTP(S) download + + if (!stirshaken_vs_cache_certificates) { + LM_DBG("Certificate caching is turned off - requesting certificate %s to be downloaded...\n", arg->cert.public_url); + return STIR_SHAKEN_STATUS_NOT_HANDLED; + } + + if (-1 == get_cert_name_hashed(arg->cert.public_url, cert_full_path, STIR_SHAKEN_BUFLEN)) { + LM_ERR("Cannot get cert name hashed\n"); + goto exit; + } + + LM_DBG("Checking for certificate %s in cache (looking for name: %s)\n", arg->cert.public_url, cert_full_path); + + if (STIR_SHAKEN_STATUS_OK == stir_shaken_file_exists(cert_full_path)) { + + LM_DBG("Certificate %s found in cache\n", arg->cert.public_url); + + if (stirshaken_vs_cache_expire_s) { + + struct stat attr = { 0 }; + time_t now_s = time(NULL), diff = 0; + + LM_DBG("Checking cached certificate against expiration setting of %zus\n", stirshaken_vs_cache_expire_s); + + if (-1 == stat(cert_full_path, &attr)) { + LM_ERR("Cannot get modification timestamp on certificate %s. Error code is %d (%s)\n", cert_full_path, errno, strerror(errno)); + goto exit; + } + + if (now_s < attr.st_mtime) { + LM_ERR("Modification timestamp on certificate %s is invalid\n", cert_full_path); + goto exit; + } + + diff = now_s - attr.st_mtime; + + LM_DBG("Checking cached certificate against expiration setting of %zus (now is: %zu, file modification timestamp is: %zu, difference is: %zu)\n", + stirshaken_vs_cache_expire_s, now_s, attr.st_mtime, diff); + + if (diff > stirshaken_vs_cache_expire_s) { + LM_WARN("Cached certificate %s is behind expiration threshold (%zu > %zu). Need to download new certificate...\n", cert_full_path, diff, stirshaken_vs_cache_expire_s); + goto exit; + } else { + LM_WARN("Cached certificate %s is valid for next %zus\n", cert_full_path, stirshaken_vs_cache_expire_s - diff); + } + } + + if (!(cache_copy.x = stir_shaken_load_x509_from_file(&ss, cert_full_path))) { + LM_ERR("Cannot load X509 from file %s\n", cert_full_path); + goto exit; + } + + if (STIR_SHAKEN_STATUS_OK != stir_shaken_cert_copy(&ss, &arg->cert, &cache_copy)) { + LM_ERR("Cannot copy certificate %s\n", cert_full_path); + stir_shaken_cert_deinit(&cache_copy); + goto exit; + } + + stir_shaken_cert_deinit(&cache_copy); + + return STIR_SHAKEN_STATUS_HANDLED; + } + + default: + LM_DBG("Certificate %s not found in cache\n", arg->cert.public_url); + return STIR_SHAKEN_STATUS_NOT_HANDLED; + } + +exit: + + return STIR_SHAKEN_STATUS_NOT_HANDLED; +} + +stir_shaken_as_t *as = NULL; +stir_shaken_vs_t *vs = NULL; + +static int start_as(stir_shaken_context_t *ss) +{ + as = stir_shaken_as_create(ss); + if (!as) { + LM_ERR("Cannot create Authentication Service\n"); + stirshaken_print_error_details(ss); + return -1; + } + + if (stirshaken_as_default_key.len > 0) { + + if (STIR_SHAKEN_STATUS_OK != stir_shaken_as_load_private_key(ss, as, stirshaken_as_default_key.s)) { + LM_ERR("Failed to load private key (%s). Please check @as_default_key param\n", stirshaken_as_default_key.s); + stirshaken_print_error_details(ss); + return -1; + } + } + + return 0; +} + +static int start_vs(stir_shaken_context_t *ss) +{ + vs = stir_shaken_vs_create(ss); + if (!vs) { + LM_ERR("Cannot create Verification Service\n"); + stirshaken_print_error_details(ss); + return -1; + } + + // Handle settings + + stir_shaken_vs_set_connect_timeout(ss, vs, stirshaken_vs_connect_timeout_s); + stir_shaken_vs_set_callback(ss, vs, shaken_callback); + + if (stirshaken_vs_cache_certificates) { + + if (!stirshaken_vs_cache_dir.len) { + LM_ERR("Certificate caching is turned on but cache dir is not set. Please set cache dir\n"); + return -1; + } + + if (STIR_SHAKEN_STATUS_OK != stir_shaken_dir_exists(stirshaken_vs_cache_dir.s)) { + LM_ERR("Certificate caching is turned on but cache dir %s does not exist. Please check cache dir name\n", stirshaken_vs_cache_dir.s); + return -1; + } + } + + if (stirshaken_vs_verify_x509_cert_path) { + + stir_shaken_vs_set_x509_cert_path_check(ss, vs, 1); + + if (stirshaken_vs_ca_dir.len > 0) { + if (STIR_SHAKEN_STATUS_OK != stir_shaken_vs_load_ca_dir(ss, vs, stirshaken_vs_ca_dir.s)) { + LM_ERR("Failed to init X509 cert store with CA dir\n"); + stirshaken_print_error_details(ss); + return -1; + } + } else { + LM_WARN("Cert path check is turned on, but CA dir is not set. No end entity certificate will ever pass this check. Did you forget to set CA dir?\n"); + } + + if (stirshaken_vs_crl_dir.len > 0) { + if (STIR_SHAKEN_STATUS_OK != stir_shaken_vs_load_crl_dir(ss, vs, stirshaken_vs_crl_dir.s)) { + LM_ERR("Failed to init X509 cert store with CRL dir\n"); + stirshaken_print_error_details(ss); + return -1; + } + } else { + LM_WARN("Cert path check is turned on, but CRL dir is not set (it's not mandatory). Did you forget to set CRL dir?\n"); + } + } + + return 0; +} + +static int mod_init(void) +{ + stir_shaken_context_t ss = { 0 }; + + LM_INFO("Initialising STIR-Shaken\n"); + + if (STIR_SHAKEN_STATUS_OK != stir_shaken_init(&ss, STIR_SHAKEN_LOGLEVEL_NOTHING)) { + LM_ERR("Cannot init libstirshaken\n"); + stirshaken_print_error_details(&ss); + return -1; + } + + if (0 == start_as(&ss)) { + if (stirshaken_as_default_key.len > 0) { + LM_INFO("Authentication Service ready (with default key %s)\n", stirshaken_as_default_key.s); + } else { + LM_INFO("Authentication Service ready (without default key)\n"); + } + } else { + LM_WARN("Cannot start Authentication Service (%s)\n", stirshaken_as_default_key.len > 0 ? "with default key" : "without default key"); + } + + if (stirshaken_as_default_key.len == 0) { + LM_WARN("Authentication Service using default key will not be available, because 'as_default_key' is not set (only authentication through w_stirshaken_add_identity_with_key is possible using a specific key)\n"); + } + + if (0 == start_vs(&ss)) { + LM_INFO("Verification Service ready (%s)\n", stirshaken_vs_verify_x509_cert_path ? "with X509 cert path check" : "without X509 cert path check"); + } else { + LM_WARN("Cannot start Verification Service (%s)\n", stirshaken_vs_verify_x509_cert_path ? "with X509 cert path check" : "without X509 cert path check"); + stir_shaken_vs_destroy(&vs); + } + + if ((vs && !stirshaken_vs_verify_x509_cert_path) || !vs) { + LM_WARN("A complete Shaken check with X509 certificate path verification will not be available (stirshaken_check_identity). " + "Only checks of PASSporT decoding will be performed via stirshaken_check_identity_with_key() " + "and stirshaken_check_identity_with_cert(). If you want complete check with downloaded certificate and/or X509 cert path verification " + "then please set @vs_verify_x509_cert_path param to 1 and configure @vs_ca_dir (and optionally @vs_crl_dir)\n"); + } + + return 0; +} + +/** + * @brief Initialize async module children + */ +static int child_init(int rank) +{ + LM_INFO("mod stirshaken child init\n"); + return 0; +} + +static void mod_destroy(void) +{ + LM_INFO("mod stirshaken destroy\n"); + stir_shaken_as_destroy(&as); + stir_shaken_vs_destroy(&vs); + stir_shaken_deinit(); + return; +} + +static int stirshaken_handle_cache(stir_shaken_context_t *ss, stir_shaken_passport_t *passport, stir_shaken_cert_t *cert) +{ + if (!passport || !cert) + return -1; + + LM_DBG("Handling certificate cache...\n"); + + if (!stirshaken_vs_cache_dir.len) { + LM_ERR("Cache dir not set\n"); + return -1; + } + + if (!ss->cert_fetched_from_cache) { + + // save certificate to cache with url as a key + char cert_full_path[STIR_SHAKEN_BUFLEN] = { 0 }; + const char *x5u = stir_shaken_passport_get_header(ss, passport, "x5u"); + + if (stir_shaken_zstr(x5u)) { + + // This should never happen as stir_shaken_sih_verify returns error in such case + + LM_ERR("PASSporT has no x5u\n"); + return -1; + } + + if (-1 == get_cert_name_hashed(x5u, cert_full_path, STIR_SHAKEN_BUFLEN)) { + LM_ERR("Cannot get cert name hashed\n"); + return -1; + } + + LM_DBG("Checking for presence of expired version of freshly downloaded certificate %s in cache (looking for name: %s)\n", x5u, cert_full_path); + + if (STIR_SHAKEN_STATUS_OK == stir_shaken_file_exists(cert_full_path)) { + + LM_DBG("Expired version of certificate %s found in cache (with name: %s). Removing it...\n", x5u, cert_full_path); + + if (STIR_SHAKEN_STATUS_OK != stir_shaken_file_remove(cert_full_path)) { + LM_ERR("Couldn't remove certificate %s from cache\n", cert_full_path); + return -1; + } + } + + LM_DBG("Saving fresh certificate %s in cache (with name: %s)...\n", x5u, cert_full_path); + + if (STIR_SHAKEN_STATUS_OK != stir_shaken_x509_to_disk(ss, cert->x, cert_full_path)) { + LM_ERR("Failed to write cert %s to disk (as: %s)", x5u, cert_full_path); + } + + } else { + LM_DBG("Certificate was fetched from cache, so skipping saving it\n"); + } + + return 0; +} + +#define STIRSHAKEN_HDR_IDENTITY "Identity" +#define STIRSHAKEN_HDR_IDENTITY_LEN (sizeof(STIRSHAKEN_HDR_IDENTITY) - 1) + +static int ki_stirshaken_check_identity(sip_msg_t *msg) +{ + str ibody = STR_NULL; + hdr_field_t *hf = NULL; + + stir_shaken_context_t ss = { 0 }; + stir_shaken_passport_t *passport_out = NULL; + stir_shaken_cert_t *cert_out = NULL; + + for (hf = msg->headers; hf; hf = hf->next) { + if (hf->name.len == STIRSHAKEN_HDR_IDENTITY_LEN + && strncasecmp(hf->name.s, STIRSHAKEN_HDR_IDENTITY, + STIRSHAKEN_HDR_IDENTITY_LEN) == 0) + break; + } + + if (hf == NULL) { + LM_DBG("no identity header\n"); + goto fail; + } + + ibody = hf->body; + + if (STIR_SHAKEN_STATUS_OK != stir_shaken_vs_sih_verify(&ss, vs, ibody.s, &cert_out, &passport_out)) { + LM_ERR("SIP Identity Header did not pass verification\n"); + stirshaken_print_error_details(&ss); + goto fail; + } + + if (stirshaken_vs_cache_certificates) { + stirshaken_handle_cache(&ss, passport_out, cert_out); + } + + // Check that PASSporT applies to the current moment in time + if (STIR_SHAKEN_STATUS_OK != stir_shaken_passport_validate_iat_against_freshness(&ss, passport_out, stirshaken_vs_identity_expire_s)) { + + stir_shaken_error_t error_code = 0; + stir_shaken_get_error(&ss, &error_code); + + if (error_code == STIR_SHAKEN_ERROR_PASSPORT_INVALID_IAT_VALUE_FUTURE) { + LM_ERR("PASSporT not valid yet\n"); + } else if (error_code == STIR_SHAKEN_ERROR_PASSPORT_INVALID_IAT_VALUE_EXPIRED) { + LM_ERR("PASSporT expired\n"); + } else if (error_code == STIR_SHAKEN_ERROR_PASSPORT_INVALID_IAT) { + LM_ERR("PASSporT is missing @iat grant\n"); + } + + LM_ERR("PASSporT doesn't apply to the current moment in time\n"); + goto fail; + } + + if (stirshaken_vs_verify_x509_cert_path) { + + LM_DBG("Running X509 certificate path verification\n"); + + if (!vs) { + LM_ERR("Verification Service not started\n"); + goto fail; + } + + if (STIR_SHAKEN_STATUS_OK != stir_shaken_verify_cert_path(&ss, cert_out, vs->store)) { + LM_ERR("Cert did not pass X509 path validation\n"); + stirshaken_print_error_details(&ss); + goto fail; + } + } + + LM_DBG("identity check: ok (%s)\n", ss.x509_cert_path_checked ? "with X509 cert path check" : "without X509 cert path check"); + stir_shaken_passport_destroy(&passport_out); + stir_shaken_cert_destroy(&cert_out); + return 1; + +fail: + stir_shaken_passport_destroy(&passport_out); + stir_shaken_cert_destroy(&cert_out); + LM_ERR("identity check: fail\n"); + return -1; +} + +/** + * Verify SIP Identity Header (involves call from libstirshaken to cache_callback, + * wich will supply requested certificate from cache [if configured to do so] + * or will let libstirshaken to perform HTTP(s) GET request to download certificate). + * Verify a call with STIR-Shaken. + * + * This method checks if SIP Identity Header covers semantically valid PASSporT. + * This method consults cache_callback in an attempt to obtain certificate that is referenced in PASSporT's x5u header. + * This method checks if PASSporT verifies successfully with a public key retrieved from obtained certificate. + * Optionally (if Verification Service is configured to do so with stirshaken_vs_verify_x509_cert_path param set to 1) + * this method checks if certificate is trusted, by execution of X509 certificate path check. + * + * Optionally: + * - retrieve PASSporT from SIP Identity Header + * - retrieve certificate referenced in PASSporT's x5u header + * - cache certificate + * + * Kamailio config usage example: + * + * stirshaken_check_identity(); + */ +static int w_stirshaken_check_identity(sip_msg_t *msg, char *str1, char *str2) +{ + LM_INFO("identity check\n"); + + if (!vs) { + LM_ERR("Cannot perform identity check involving certificate downloading, caching, or X509 cert pach checking, " + "because Verification Service is not running. Please turn on and configure Verification Service, " + "otherwise only stirshaken_check_identity_with_cert() and stirshaken_check_identity_with_key() " + "methods will be available. Check log file for module's initialisation errors\n"); + return -1; + } + + return ki_stirshaken_check_identity(msg); +} + +static int ki_stirshaken_check_identity_with_cert(sip_msg_t *msg, char *cert_path) +{ + str ibody = STR_NULL; + hdr_field_t *hf = NULL; + + stir_shaken_context_t ss = { 0 }; + stir_shaken_passport_t *passport_out = NULL; + stir_shaken_cert_t cert = { 0 }; + + for (hf = msg->headers; hf; hf = hf->next) { + if (hf->name.len == STIRSHAKEN_HDR_IDENTITY_LEN + && strncasecmp(hf->name.s, STIRSHAKEN_HDR_IDENTITY, + STIRSHAKEN_HDR_IDENTITY_LEN) == 0) + break; + } + + if (hf == NULL) { + LM_DBG("no identity header\n"); + goto fail; + } + + ibody = hf->body; + + if (!(cert.x = stir_shaken_load_x509_from_file(&ss, cert_path))) { + LM_DBG("Cannot load X509 from file\n"); + stirshaken_print_error_details(&ss); + goto fail; + } + + if (STIR_SHAKEN_STATUS_OK != stir_shaken_sih_verify_with_cert(&ss, ibody.s, &cert, &passport_out)) { + LM_ERR("SIP Identity Header did not pass verification against certificate\n"); + stirshaken_print_error_details(&ss); + goto fail; + } + + // Check that PASSporT applies to the current moment in time + if (STIR_SHAKEN_STATUS_OK != stir_shaken_passport_validate_iat_against_freshness(&ss, passport_out, stirshaken_vs_identity_expire_s)) { + + stir_shaken_error_t error_code = 0; + stir_shaken_get_error(&ss, &error_code); + + if (error_code == STIR_SHAKEN_ERROR_PASSPORT_INVALID_IAT_VALUE_FUTURE) { + LM_ERR("PASSporT not valid yet\n"); + } else if (error_code == STIR_SHAKEN_ERROR_PASSPORT_INVALID_IAT_VALUE_EXPIRED) { + LM_ERR("PASSporT expired\n"); + } else if (error_code == STIR_SHAKEN_ERROR_PASSPORT_INVALID_IAT) { + LM_ERR("PASSporT is missing @iat grant\n"); + } + + LM_ERR("PASSporT doesn't apply to the current moment in time\n"); + goto fail; + } + + LM_DBG("identity check: ok (%s)\n", ss.x509_cert_path_checked ? "with X509 cert path check" : "without X509 cert path check"); + + stir_shaken_passport_destroy(&passport_out); + stir_shaken_cert_deinit(&cert); + return 1; + +fail: + stir_shaken_passport_destroy(&passport_out); + stir_shaken_cert_deinit(&cert); + LM_ERR("identity check: fail\n"); + return -1; +} + +/** + * Verify SIP Identity Header against specified certificate (does not involve HTTP(s) GET request). + * WARNING: + * This method only checks if SIP Identity Header was signed by a key from certificate given as argument. + * This method doesn't attempt to obtain certificate referenced in PASSporT (but PASSporT should be checked with key corresponding to that certificate). + * Therefore it is possible that this check will be successful, while PASSporT is not valid (could be signed with key that doesn't match certificate referenced in x5u header). + * If you want a complete Shaken check or if you are not sure what you're doing, then you should execute w_stirshaken_check_identity() instead + * (and configure Verification Service to perform X509 certificate path verification with stirshaken_vs_verify_x509_cert_path param set to 1). + * + * Kamailio config usage example: + * + * stirshaken_check_identity_with_cert("/path/to/cert"); + */ +static int w_stirshaken_check_identity_with_cert(sip_msg_t *msg, char *cert_path, char *str2) +{ + str keyval = STR_NULL; + + LM_INFO("identity check using certificate\n"); + + if(fixup_get_svalue(msg, (gparam_t*)cert_path, &keyval)<0) { + LM_ERR("failed to get certificate path parameter\n"); + return -1; + } + + return ki_stirshaken_check_identity_with_cert(msg, cert_path); +} + +static int ki_stirshaken_check_identity_with_key(sip_msg_t *msg, str *keypath) +{ + str ibody = STR_NULL; + hdr_field_t *hf = NULL; + + stir_shaken_context_t ss = { 0 }; + stir_shaken_passport_t *passport_out = NULL; + unsigned char key[STIR_SHAKEN_PUB_KEY_RAW_BUF_LEN] = { 0 }; + uint32_t key_len = STIR_SHAKEN_PUB_KEY_RAW_BUF_LEN; + + for (hf = msg->headers; hf; hf = hf->next) { + if (hf->name.len == STIRSHAKEN_HDR_IDENTITY_LEN + && strncasecmp(hf->name.s, STIRSHAKEN_HDR_IDENTITY, + STIRSHAKEN_HDR_IDENTITY_LEN) == 0) + break; + } + + if (hf == NULL) { + LM_DBG("no identity header\n"); + goto fail; + } + + ibody = hf->body; + + if (keypath && keypath->s) { + + if (STIR_SHAKEN_STATUS_OK != stir_shaken_load_key_raw(&ss, keypath->s, key, &key_len)) { + LM_ERR("Failed to load private key\n"); + stirshaken_print_error_details(&ss); + goto fail; + } + } + + if (STIR_SHAKEN_STATUS_OK != stir_shaken_sih_verify_with_key(&ss, ibody.s, key, key_len, &passport_out)) { + LM_ERR("SIP Identity Header did not pass verification against key\n"); + stirshaken_print_error_details(&ss); + goto fail; + } + + // We can do something with PASSporT here or just realease it + stir_shaken_passport_destroy(&passport_out); + + LM_DBG("identity check: ok (%s)\n", ss.x509_cert_path_checked ? "with X509 cert path check" : "without X509 cert path check"); + return 1; + +fail: + stir_shaken_passport_destroy(&passport_out); + LM_ERR("identity check: fail\n"); + return -1; +} + +/** + * Verify SIP Identity Header against specified public key (does not involve HTTP(s) GET request). + * WARNING: + * This method only checks if SIP Identity Header was signed by a key corresponding to specified public key. + * This method doesn't attempt to obtain certificate referenced in PASSporT (but PASSporT should be checked with key corresponding to that certificate). + * Therefore it is possible that this check will be successful, while PASSporT is not valid (could be signed with key that doesn't match certificate referenced in x5u header). + * If you want a complete Shaken check or if you are not sure what you're doing, then you should execute w_stirshaken_check_identity() instead + * (and configure Verification Service to perform X509 certificate path verification with stirshaken_vs_verify_x509_cert_path param set to 1). + * + * Kamailio config usage example: + * + * stirshaken_check_identity_with_key("/path/to/key"); + */ +static int w_stirshaken_check_identity_with_key(sip_msg_t *msg, char *pkey_path, char *str2) +{ + str keypath = STR_NULL; + + LM_INFO("identity check using public key\n"); + + if(fixup_get_svalue(msg, (gparam_t*)pkey_path, &keypath)<0) { + LM_ERR("failed to get key path parameter\n"); + return -1; + } + + return ki_stirshaken_check_identity_with_key(msg, &keypath); +} + +static int ki_stirshaken_add_identity_with_key(sip_msg_t *msg, str *x5u, str *attest, + str *origtn_val, str *desttn_val, str *origid, str *keypath) +{ + stir_shaken_context_t ss = { 0 }; + char *sih = NULL; + stir_shaken_passport_t *passport = NULL; + str ibody = STR_NULL; + str hdr = STR_NULL; + sr_lump_t *anchor = NULL; + stir_shaken_passport_params_t params = { + .x5u = x5u ? x5u->s : NULL, + .attest = attest ? attest->s : NULL, + .desttn_key = "tn", + .desttn_val = desttn_val ? desttn_val->s : NULL, + .iat = time(NULL), + .origtn_key = "tn", + .origtn_val = origtn_val ? origtn_val->s : NULL, + .origid = origid ? origid->s : NULL + }; + + + if (keypath && keypath->s) { + + unsigned char key[STIR_SHAKEN_PRIV_KEY_RAW_BUF_LEN]; + uint32_t key_len = STIR_SHAKEN_PRIV_KEY_RAW_BUF_LEN; + + if (STIR_SHAKEN_STATUS_OK != stir_shaken_load_key_raw(&ss, keypath->s, key, &key_len)) { + LM_ERR("Failed to load private key\n"); + stirshaken_print_error_details(&ss); + goto error; + } + + sih = stir_shaken_authenticate_to_sih_with_key(&ss, ¶ms, &passport, key, key_len); + if (!sih) { + LM_ERR("Failed to create SIP Identity Header with key %s\n", keypath->s); + stirshaken_print_error_details(&ss); + goto error; + } + + } else { + + if (!as) { + LM_ERR("Authentication Service not started. Is default key set for Authentication Service? Please check @as_default_key param\n"); + goto error; + } + + sih = stir_shaken_as_authenticate_to_sih(&ss, as, ¶ms, &passport); + if (!sih) { + LM_ERR("Failed to create SIP Identity Header with default key. Is a default key set for Authentication Service? Please check @as_default_key param\n"); + stirshaken_print_error_details(&ss); + goto error; + } + } + + // We can do something with PASSporT here or just realease it + stir_shaken_passport_destroy(&passport); + + ibody.s = sih; + ibody.len = strlen(sih); + + if (ibody.len <= 0) { + LM_DBG("Got SIP Identity Header with 0 length\n"); + goto error; + } + + LM_DBG("appending identity: %.*s\n", ibody.len, ibody.s); + if (parse_headers(msg, HDR_EOH_F, 0) == -1) { + LM_ERR("error while parsing message\n"); + goto error; + } + + hdr.len = STIRSHAKEN_HDR_IDENTITY_LEN + 1 + 1 + ibody.len + 2; + hdr.s = (char*)pkg_malloc(hdr.len + 1); + if (hdr.s == NULL) { + PKG_MEM_ERROR; + goto error; + } + memcpy(hdr.s, STIRSHAKEN_HDR_IDENTITY, STIRSHAKEN_HDR_IDENTITY_LEN); + *(hdr.s + STIRSHAKEN_HDR_IDENTITY_LEN) = ':'; + *(hdr.s + STIRSHAKEN_HDR_IDENTITY_LEN + 1) = ' '; + + memcpy(hdr.s + STIRSHAKEN_HDR_IDENTITY_LEN + 2, ibody.s, ibody.len); + *(hdr.s + STIRSHAKEN_HDR_IDENTITY_LEN + ibody.len + 2) = '\r'; + *(hdr.s + STIRSHAKEN_HDR_IDENTITY_LEN + ibody.len + 3) = '\n'; + + /* anchor after last header */ + anchor = anchor_lump(msg, msg->unparsed - msg->buf, 0, 0); + if((anchor == NULL) + || (insert_new_lump_before(anchor, hdr.s, hdr.len, 0) == 0)) { + LM_ERR("cannot insert identity header\n"); + pkg_free(hdr.s); + goto error; + } + + if(sih) { + free(sih); + } + return 1; + +error: + stir_shaken_passport_destroy(&passport); + if(sih) { + free(sih); + } + return -1; +} + +/** + * Add SIP Identity Header to the call using specific key. Authenticate call with STIR-Shaken. + * + * Kamailio config usage example: + * + * stirshaken_add_identity_with_key("https://sp.com/sp.pem", "B", "+44100", "+44200", "ref", "/path/to/key"); + */ +static int w_stirshaken_add_identity_with_key(sip_msg_t *msg, str *px5u, str *pattest, + str *porigtn_val, str *pdesttn_val, str *porigid, str *pkeypath) +{ + str x5u = STR_NULL; + str attest = STR_NULL; + str origtn_val = STR_NULL; + str desttn_val = STR_NULL; + str origid = STR_NULL; + str keypath = STR_NULL; + + if (fixup_get_svalue(msg, (gparam_t*)px5u, &x5u)<0) { + LM_ERR("failed to get x5u parameter\n"); + return -1; + } + + if (fixup_get_svalue(msg, (gparam_t*)pattest, &attest)<0) { + LM_ERR("failed to get attest parameter\n"); + return -1; + } + + if (fixup_get_svalue(msg, (gparam_t*)porigtn_val, &origtn_val)<0) { + LM_ERR("failed to get origtn_val parameter\n"); + return -1; + } + + if (fixup_get_svalue(msg, (gparam_t*)pdesttn_val, &desttn_val)<0) { + LM_ERR("failed to get desttn_val parameter\n"); + return -1; + } + + if (fixup_get_svalue(msg, (gparam_t*)porigid, &origid)<0) { + LM_ERR("failed to get origid parameter\n"); + return -1; + } + + if (fixup_get_svalue(msg, (gparam_t*)pkeypath, &keypath)<0) { + LM_ERR("failed to get keypath parameter\n"); + return -1; + } + + return ki_stirshaken_add_identity_with_key(msg, &x5u, &attest, &origtn_val, &desttn_val, &origid, &keypath); +} + +static int ki_stirshaken_add_identity(sip_msg_t *msg, str *x5u, str *attest, str *origtn_val, str *desttn_val, str *origid) +{ + return ki_stirshaken_add_identity_with_key(msg, x5u, attest, origtn_val, desttn_val, origid, NULL); +} + +/** + * Add SIP Identity Header to the call using default private key. Authenticate call with STIR-Shaken. + * + * Kamailio config usage example: + * + * stirshaken_add_identity("https://sp.com/sp.pem", "B", "+44100", "+44200", "ref"); + */ +static int w_stirshaken_add_identity(sip_msg_t *msg, str *px5u, str *pattest, str *porigtn_val, str *pdesttn_val, str *porigid) +{ + str x5u = STR_NULL; + str attest = STR_NULL; + str origtn_val = STR_NULL; + str desttn_val = STR_NULL; + str origid = STR_NULL; + + if (fixup_get_svalue(msg, (gparam_t*)px5u, &x5u)<0) { + LM_ERR("failed to get x5u parameter\n"); + return -1; + } + + if (fixup_get_svalue(msg, (gparam_t*)pattest, &attest)<0) { + LM_ERR("failed to get attest parameter\n"); + return -1; + } + + if (fixup_get_svalue(msg, (gparam_t*)porigtn_val, &origtn_val)<0) { + LM_ERR("failed to get origtn_val parameter\n"); + return -1; + } + + if (fixup_get_svalue(msg, (gparam_t*)pdesttn_val, &desttn_val)<0) { + LM_ERR("failed to get desttn_val parameter\n"); + return -1; + } + + if (fixup_get_svalue(msg, (gparam_t*)porigid, &origid)<0) { + LM_ERR("failed to get origid parameter\n"); + return -1; + } + + return ki_stirshaken_add_identity(msg, &x5u, &attest, &origtn_val, &desttn_val, &origid); +} + + +/** + * + */ +/* clang-format off */ +static sr_kemi_t sr_kemi_stirshaken_exports[] = { + { str_init("stirshaken"), str_init("stirshaken_check_identity"), + SR_KEMIP_INT, ki_stirshaken_check_identity, + { SR_KEMIP_NONE, SR_KEMIP_NONE, SR_KEMIP_NONE, + SR_KEMIP_NONE, SR_KEMIP_NONE, SR_KEMIP_NONE } + }, + { str_init("stirshaken"), str_init("stirshaken_check_identity_with_cert"), + SR_KEMIP_INT, ki_stirshaken_check_identity_with_cert, + { SR_KEMIP_STR, SR_KEMIP_NONE, SR_KEMIP_NONE, + SR_KEMIP_NONE, SR_KEMIP_NONE, SR_KEMIP_NONE } + }, + { str_init("stirshaken"), str_init("stirshaken_check_identity_with_key"), + SR_KEMIP_INT, ki_stirshaken_check_identity_with_key, + { SR_KEMIP_STR, SR_KEMIP_STR, SR_KEMIP_STR, + SR_KEMIP_STR, SR_KEMIP_STR, SR_KEMIP_STR } + }, + { str_init("stirshaken"), str_init("stirshaken_add_identity"), + SR_KEMIP_INT, ki_stirshaken_add_identity, + { SR_KEMIP_STR, SR_KEMIP_STR, SR_KEMIP_STR, + SR_KEMIP_STR, SR_KEMIP_STR, SR_KEMIP_NONE } + }, + { str_init("stirshaken"), str_init("stirshaken_add_identity_with_key"), + SR_KEMIP_INT, ki_stirshaken_add_identity_with_key, + { SR_KEMIP_STR, SR_KEMIP_STR, SR_KEMIP_STR, + SR_KEMIP_STR, SR_KEMIP_STR, SR_KEMIP_STR } + }, + { {0, 0}, {0, 0}, 0, NULL, { 0, 0, 0, 0, 0, 0 } } +}; +/* clang-format on */ + +/** + * + */ +int mod_register(char *path, int *dlflags, void *p1, void *p2) +{ + sr_kemi_modules_add(sr_kemi_stirshaken_exports); + return 0; +}