From a02fc685a8a6cd09b8f27018a330eaf338029fbf Mon Sep 17 00:00:00 2001 From: George Joseph Date: Fri, 19 Jul 2024 08:46:31 -0600 Subject: [PATCH] stir_shaken: CRL fixes and a new CLI command * Fixed a bug in crypto_show_cli_store that was causing asterisk to crash if there were certificate revocation lists in the verification certificate store. We're also now prefixing certificates with "Cert:" and CRLs with "CRL:" to distinguish them in the list. * Added 'untrusted_cert_file' and 'untrusted_cert_path' options to both verification and profile objects. If you have CRLs that are signed by a different CA than the incoming X5U certificate (indirect CRL), you'll need to provide the certificate of the CRL signer here. Thse will show up as 'Untrusted" when showing the verification or profile objects. * Fixed loading of crl_path. The OpenSSL API we were using to load CRLs won't actually load them from a directory, only a file. We now scan the directory ourselves and load the files one-by-one. * Fixed the verification flags being set on the certificate store. - Removed the CRL_CHECK_ALL flag as this was causing all certificates to be checked for CRL extensions and failing to verify the cert if there was none. This basically caused all certs to fail when a CRL was provided via crl_file or crl_path. - Added the EXTENDED_CRL_SUPPORT flag as it is required to handle indirect CRLs. * Added a new CLI command... `stir_shaken verify certificate_file [ ]` which will assist troubleshooting certificate problems by allowing the user to manually verify a certificate file against either the global verification certificate store or the store for a specific profile. * Updated the XML documentation and the sample config file. Resolves: #809 --- configs/samples/stir_shaken.conf.sample | 59 +++- res/res_stir_shaken/attestation_config.c | 5 + res/res_stir_shaken/common_config.c | 149 ++++++--- res/res_stir_shaken/common_config.h | 6 + res/res_stir_shaken/crypto_utils.c | 378 ++++++++++++++++++++-- res/res_stir_shaken/crypto_utils.h | 47 ++- res/res_stir_shaken/profile_config.c | 6 +- res/res_stir_shaken/stir_shaken_doc.xml | 187 +++++------ res/res_stir_shaken/verification_config.c | 49 ++- 9 files changed, 709 insertions(+), 177 deletions(-) diff --git a/configs/samples/stir_shaken.conf.sample b/configs/samples/stir_shaken.conf.sample index c7ee89230e..5e2b3b9219 100644 --- a/configs/samples/stir_shaken.conf.sample +++ b/configs/samples/stir_shaken.conf.sample @@ -209,16 +209,22 @@ CA certififcate to you separately. Default: no -- ca_file ----------------------------------------------------------- -Path to a single file containing a CA certificate or certificate chain -to be used to validate the certificates in incoming requests. +Path to a file containing one or more CA certs in PEM format. +These certs are used to verify the chain of trust for the +certificate retrieved from the X5U Identity header parameter. This +file must have the root CA certificate, the certificate of the +issuer of the X5U certificate, and any intermediate certificates +between them. Default: none -- ca_path ----------------------------------------------------------- -Path to a directory containing one or more CA certificates to be used -to validate the certificates in incoming requests. The files in that -directory must contain only one certificate each and the directory -must be hashed using the OpenSSL 'c_rehash' utility. +Path to a directory containing one or more hashed CA certs. +See ca_file above. +For this option, each certificate must be placed in its own +PEM file in the directory specified and hashed with the +following command: +`openssl rehash ` Default: none @@ -226,21 +232,50 @@ NOTE: Both ca_file and ca_path can be specified but at least one MUST be. -- crl_file ----------------------------------------------------------- -Path to a single file containing a CA certificate revocation list -to be used to validate the certificates in incoming requests. +Path to a file containing one or more CRLs in PEM format. +If you with to check if the certificate in the X5U Identity header +parameter has been revoked, you'll need the certificate revocation +list generated by the issuer. Default: none -- crl_path ----------------------------------------------------------- -Path to a directory containing one or more CA certificate revocation -lists to be used to validate the certificates in incoming requests. -The files in that directory must contain only one certificate each and -the directory must be hashed using the OpenSSL 'c_rehash' utility. +Path to a directory containing one or more hashed CRLs. +See crl_file above. +For this option, each CRL must be placed in its own +PEM file in the directory specified and hashed with the +following command: +`openssl rehash ` Default: none NOTE: Neither crl_file nor crl_path are required. +-- untrusted_cert_file ------------------------------------------------ +Path to a file containing one or more untrusted certs in PEM format. +Unfortunately, sometimes the CRLs are signed by a different CA +than the certificate being verified. In this case, you'll need to +provide the certificate belonging to the issuer of the CRL. That +certificate is considered "untrusted" by OpenSSL and can't be placed +in the ca_file or ca_path. It has to be specified here. + +Default: none + +-- untrusted_cert_path ------------------------------------------------ +Path to a directory containing one or more hashed untrusted certs used +to verify CRLs. +See untrusted_cert_file above. +For this option, each certificates must be placed in its own +PEM file in the directory specified and hashed with the +following command: +`openssl rehash ` + +Default: none + +NOTE: Neither untrusted_cert_file nor untrusted_cert_path are required +unless you're verifying CRLs that aren't signed by the same CA as the +X5U certificate. + -- cert_cache_dir ----------------------------------------------------- Incoming Identity headers will have a URL pointing to the certificate used to sign the header. To prevent us from having to retrieve the diff --git a/res/res_stir_shaken/attestation_config.c b/res/res_stir_shaken/attestation_config.c index d7efc9e475..7a5743c9f7 100644 --- a/res/res_stir_shaken/attestation_config.c +++ b/res/res_stir_shaken/attestation_config.c @@ -245,6 +245,11 @@ static char *attestation_show(struct ast_cli_entry *e, int cmd, struct ast_cli_a return CLI_SHOWUSAGE; } + if (!as_is_config_loaded()) { + ast_log(LOG_WARNING,"Stir/Shaken attestation service disabled. Either there were errors in the 'attestation' object in stir_shaken.conf or it was missing altogether.\n"); + return CLI_FAILURE; + } + cfg = as_get_cfg(); config_object_cli_show(cfg, a, &data, 0); ao2_cleanup(cfg); diff --git a/res/res_stir_shaken/common_config.c b/res/res_stir_shaken/common_config.c index f753b41ca6..627ea81e7e 100644 --- a/res/res_stir_shaken/common_config.c +++ b/res/res_stir_shaken/common_config.c @@ -259,6 +259,112 @@ char *config_object_tab_complete_name(const char *word, struct ao2_container *co return NULL; } + +/* Remove everything except 0-9, *, and # in telephone number according to RFC 8224 + * (required by RFC 8225 as part of canonicalization) */ +char *canonicalize_tn(const char *tn, char *dest_tn) +{ + int i; + const char *s = tn; + size_t len = tn ? strlen(tn) : 0; + char *new_tn = dest_tn; + SCOPE_ENTER(3, "tn: %s\n", S_OR(tn, "(null)")); + + if (ast_strlen_zero(tn)) { + *dest_tn = '\0'; + SCOPE_EXIT_RTN_VALUE(NULL, "Empty TN\n"); + } + + if (!dest_tn) { + SCOPE_EXIT_RTN_VALUE(NULL, "No destination buffer\n"); + } + + for (i = 0; i < len; i++) { + if (isdigit(*s) || *s == '#' || *s == '*') { /* Only characters allowed */ + *new_tn++ = *s; + } + s++; + } + *new_tn = '\0'; + SCOPE_EXIT_RTN_VALUE(dest_tn, "Canonicalized '%s' -> '%s'\n", tn, dest_tn); +} + +char *canonicalize_tn_alloc(const char *tn) +{ + char *canon_tn = ast_strlen_zero(tn) ? NULL : ast_malloc(strlen(tn) + 1); + if (!canon_tn) { + return NULL; + } + return canonicalize_tn(tn, canon_tn); +} + +static char *cli_verify_cert(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + RAII_VAR(struct profile_cfg *, profile, NULL, ao2_cleanup); + RAII_VAR(struct verification_cfg *, vs_cfg, NULL, ao2_cleanup); + struct crypto_cert_store *tcs; + X509 *cert = NULL; + const char *errmsg = NULL; + + switch(cmd) { + case CLI_INIT: + e->command = "stir_shaken verify certificate_file"; + e->usage = + "Usage: stir_shaken verify certificate_file [ ]\n" + " Verify an external certificate file against the global or profile verification store\n"; + return NULL; + case CLI_GENERATE: + if (a->pos == 4) { + return config_object_tab_complete_name(a->word, profile_get_all()); + } else { + return NULL; + } + } + + if (a->argc < 4) { + return CLI_SHOWUSAGE; + } + + if (a->argc == 5) { + profile = profile_get_cfg(a->argv[4]); + if (!profile) { + ast_cli(a->fd, "Profile %s doesn't exist\n", a->argv[4]); + return CLI_SUCCESS; + } + if (!profile->vcfg_common.tcs) { + ast_cli(a->fd,"Profile %s doesn't have a certificate store\n", a->argv[4]); + return CLI_SUCCESS; + } + tcs = profile->vcfg_common.tcs; + } else { + vs_cfg = vs_get_cfg(); + if (!vs_cfg) { + ast_cli(a->fd, "No verification store found\n"); + return CLI_SUCCESS; + } + tcs = vs_cfg->vcfg_common.tcs; + } + + cert = crypto_load_cert_from_file(a->argv[3]); + if (!cert) { + ast_cli(a->fd, "Failed to load certificate from %s. See log for details\n", a->argv[3]); + return CLI_SUCCESS; + } + + if (crypto_is_cert_trusted(tcs, cert, &errmsg)) { + ast_cli(a->fd, "Certificate %s trusted\n", a->argv[3]); + } else { + ast_cli(a->fd, "Certificate %s NOT trusted: %s\n", a->argv[3], errmsg); + } + X509_free(cert); + + return CLI_SUCCESS; +} + +static struct ast_cli_entry cli_commands[] = { + AST_CLI_DEFINE(cli_verify_cert, "Verify a certificate file against the global or a profile verification store"), +}; + int common_config_reload(void) { SCOPE_ENTER(2, "Stir Shaken Reload\n"); @@ -283,6 +389,8 @@ int common_config_reload(void) int common_config_unload(void) { + ast_cli_unregister_multiple(cli_commands, ARRAY_LEN(cli_commands)); + profile_unload(); tn_config_unload(); as_unload(); @@ -348,44 +456,7 @@ int common_config_load(void) named_acl_changed_sub, ast_named_acl_change_type()); } - SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_SUCCESS, "Stir Shaken Load Done\n"); -} - + ast_cli_register_multiple(cli_commands, ARRAY_LEN(cli_commands)); -/* Remove everything except 0-9, *, and # in telephone number according to RFC 8224 - * (required by RFC 8225 as part of canonicalization) */ -char *canonicalize_tn(const char *tn, char *dest_tn) -{ - int i; - const char *s = tn; - size_t len = tn ? strlen(tn) : 0; - char *new_tn = dest_tn; - SCOPE_ENTER(3, "tn: %s\n", S_OR(tn, "(null)")); - - if (ast_strlen_zero(tn)) { - *dest_tn = '\0'; - SCOPE_EXIT_RTN_VALUE(NULL, "Empty TN\n"); - } - - if (!dest_tn) { - SCOPE_EXIT_RTN_VALUE(NULL, "No destination buffer\n"); - } - - for (i = 0; i < len; i++) { - if (isdigit(*s) || *s == '#' || *s == '*') { /* Only characters allowed */ - *new_tn++ = *s; - } - s++; - } - *new_tn = '\0'; - SCOPE_EXIT_RTN_VALUE(dest_tn, "Canonicalized '%s' -> '%s'\n", tn, dest_tn); -} - -char *canonicalize_tn_alloc(const char *tn) -{ - char *canon_tn = ast_strlen_zero(tn) ? NULL : ast_malloc(strlen(tn) + 1); - if (!canon_tn) { - return NULL; - } - return canonicalize_tn(tn, canon_tn); + SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_SUCCESS, "Stir Shaken Load Done\n"); } diff --git a/res/res_stir_shaken/common_config.h b/res/res_stir_shaken/common_config.h index b4154757c3..6a8659b8cd 100644 --- a/res/res_stir_shaken/common_config.h +++ b/res/res_stir_shaken/common_config.h @@ -334,6 +334,8 @@ struct verification_cfg_common { AST_STRING_FIELD(ca_path); AST_STRING_FIELD(crl_file); AST_STRING_FIELD(crl_path); + AST_STRING_FIELD(untrusted_cert_file); + AST_STRING_FIELD(untrusted_cert_path); AST_STRING_FIELD(cert_cache_dir); ); unsigned int curl_timeout; @@ -414,7 +416,9 @@ struct profile_cfg { }; struct profile_cfg *profile_get_cfg(const char *id); +struct ao2_container *profile_get_all(void); struct profile_cfg *eprofile_get_cfg(const char *id); +struct ao2_container *eprofile_get_all(void); int profile_load(void); int profile_reload(void); int profile_unload(void); @@ -496,6 +500,8 @@ int tn_config_unload(void); stringfield_option_register(sorcery, CONFIG_TYPE, object, ca_path, vcfg_common.ca_path, nodoc); \ stringfield_option_register(sorcery, CONFIG_TYPE, object, crl_file, vcfg_common.crl_file, nodoc); \ stringfield_option_register(sorcery, CONFIG_TYPE, object, crl_path, vcfg_common.crl_path, nodoc); \ + stringfield_option_register(sorcery, CONFIG_TYPE, object, untrusted_cert_file, vcfg_common.untrusted_cert_file, nodoc); \ + stringfield_option_register(sorcery, CONFIG_TYPE, object, untrusted_cert_path, vcfg_common.untrusted_cert_path, nodoc); \ stringfield_option_register(sorcery, CONFIG_TYPE, object, cert_cache_dir, vcfg_common.cert_cache_dir, nodoc); \ \ uint_option_register(sorcery, CONFIG_TYPE, object, curl_timeout, vcfg_common.curl_timeout, nodoc);\ diff --git a/res/res_stir_shaken/crypto_utils.c b/res/res_stir_shaken/crypto_utils.c index 7c4667fbb1..9ba26688fb 100644 --- a/res/res_stir_shaken/crypto_utils.c +++ b/res/res_stir_shaken/crypto_utils.c @@ -16,6 +16,8 @@ * at the top of the source tree. */ +#include + #include #include #include @@ -30,6 +32,8 @@ #include "crypto_utils.h" #include "asterisk.h" +#include "asterisk/cli.h" +#include "asterisk/file.h" #include "asterisk/logger.h" #include "asterisk/module.h" #include "asterisk/stringfields.h" @@ -158,6 +162,30 @@ EVP_PKEY *crypto_load_privkey_from_file(const char *filename) return key; } +X509_CRL *crypto_load_crl_from_file(const char *filename) +{ + FILE *fp; + X509_CRL *crl = NULL; + + if (ast_strlen_zero(filename)) { + ast_log(LOG_ERROR, "filename was null or empty\n"); + return NULL; + } + + fp = fopen(filename, "r"); + if (!fp) { + ast_log(LOG_ERROR, "Failed to open %s: %s\n", filename, strerror(errno)); + return NULL; + } + + crl = PEM_read_X509_CRL(fp, &crl, NULL, NULL); + fclose(fp); + if (!crl) { + crypto_log_openssl(LOG_ERROR, "Failed to create CRL from %s\n", filename); + } + return crl; +} + X509 *crypto_load_cert_from_file(const char *filename) { FILE *fp; @@ -303,12 +331,59 @@ int crypto_extract_raw_privkey(EVP_PKEY *key, unsigned char **buffer) return dump_mem_bio(bio, buffer); } +/* + * Notes on the crypto_cert_store object: + * + * We've discoverd a few issues with the X509_STORE object in OpenSSL + * that requires us to a bit more work to get the desired behavior. + * + * Basically, although X509_STORE_load_locations() and X509_STORE_load_path() + * work file for trusted certs, they refuse to load either CRLs or + * untrusted certs from directories, which is needed to support the + * crl_path and untrusted_cert_path options. So we have to brute force + * it a bit. We now use PEM_read_X509() and PEM_read_X509_CRL() to load + * the objects from files and then use X509_STORE_add_cert() and + * X509_STORE_add_crl() to add them to the store. This is a bit more + * work but it gets the job done. To load from directories, we + * simply use ast_file_read_dirs() with a callback that calls + * those functions. This also fixes an issue where certificates + * loaded using ca_path don't show up when displaying the + * verification or profile objects from the CLI. + * + * NOTE: X509_STORE_load_file() could have been used instead of + * PEM_read_X509()/PEM_read_X509_CRL() and + * X509_STORE_add_cert()/X509_STORE_add_crl() but X509_STORE_load_file() + * didn't appear in OpenSSL until version 1.1.1. :( + * + * Another issue we have is that, while X509_verify_cert() can use + * an X509_STORE of CA certificates directly, it can't use X509_STOREs + * of untrusted certs or CRLs. Instead, it needs a stack of X509 + * objects for untrusted certs and a stack of X509_CRL objects for CRLs. + * So we need to extract the untrusted certs and CRLs from their + * stores and push them onto the stacks when the configuration is + * loaded. We still use the stores as intermediaries because they + * make it easy to load the certs and CRLs from files and directories + * and they handle freeing the objects when the store is freed. + */ + static void crypto_cert_store_destructor(void *obj) { struct crypto_cert_store *store = obj; - if (store->store) { - X509_STORE_free(store->store); + if (store->certs) { + X509_STORE_free(store->certs); + } + if (store->untrusted) { + X509_STORE_free(store->untrusted); + } + if (store->untrusted_stack) { + sk_X509_free(store->untrusted_stack); + } + if (store->crls) { + X509_STORE_free(store->crls); + } + if (store->crl_stack) { + sk_X509_CRL_free(store->crl_stack); } } @@ -319,61 +394,321 @@ struct crypto_cert_store *crypto_create_cert_store(void) ast_log(LOG_ERROR, "Failed to create crypto_cert_store\n"); return NULL; } - store->store = X509_STORE_new(); - if (!store->store) { + store->certs = X509_STORE_new(); + if (!store->certs) { crypto_log_openssl(LOG_ERROR, "Failed to create X509_STORE\n"); ao2_ref(store, -1); return NULL; } + store->untrusted = X509_STORE_new(); + if (!store->untrusted) { + crypto_log_openssl(LOG_ERROR, "Failed to create untrusted X509_STORE\n"); + ao2_ref(store, -1); + return NULL; + } + store->untrusted_stack = sk_X509_new_null(); + if (!store->untrusted_stack) { + crypto_log_openssl(LOG_ERROR, "Failed to create untrusted stack\n"); + ao2_ref(store, -1); + return NULL; + } + + store->crls = X509_STORE_new(); + if (!store->crls) { + crypto_log_openssl(LOG_ERROR, "Failed to create CRL X509_STORE\n"); + ao2_ref(store, -1); + return NULL; + } + store->crl_stack = sk_X509_CRL_new_null(); + if (!store->crl_stack) { + crypto_log_openssl(LOG_ERROR, "Failed to create CRL stack\n"); + ao2_ref(store, -1); + return NULL; + } + return store; } +static int crypto_load_store_from_cert_file(X509_STORE *store, const char *file) +{ + X509 *cert; + int rc = 0; + + if (ast_strlen_zero(file)) { + ast_log(LOG_ERROR, "file was null or empty\n"); + return -1; + } + + cert = crypto_load_cert_from_file(file); + if (!cert) { + return -1; + } + rc = X509_STORE_add_cert(store, cert); + X509_free(cert); + if (!rc) { + crypto_log_openssl(LOG_ERROR, "Failed to load store from file '%s'\n", file); + return -1; + } + + return 0; +} + +static int crypto_load_store_from_crl_file(X509_STORE *store, const char *file) +{ + X509_CRL *crl; + int rc = 0; + + if (ast_strlen_zero(file)) { + ast_log(LOG_ERROR, "file was null or empty\n"); + return -1; + } + + crl = crypto_load_crl_from_file(file); + if (!crl) { + return -1; + } + rc = X509_STORE_add_crl(store, crl); + X509_CRL_free(crl); + if (!rc) { + crypto_log_openssl(LOG_ERROR, "Failed to load store from file '%s'\n", file); + return -1; + } + + return 0; +} + +struct pem_file_cb_data { + X509_STORE *store; + int is_crl; +}; + +static int pem_file_cb(const char *dir_name, const char *filename, void *obj) +{ + struct pem_file_cb_data* data = obj; + char *filename_merged = NULL; + struct stat statbuf; + int rc = 0; + + if (ast_asprintf(&filename_merged, "%s/%s", dir_name, filename) < 0) { + return -1; + } + + if (lstat(filename_merged, &statbuf)) { + printf("Error reading path stats - %s: %s\n", + filename_merged, strerror(errno)); + return -1; + } + + /* We only want the symlinks from the directory */ + if (!S_ISLNK(statbuf.st_mode)) { + return 0; + } + + if (data->is_crl) { + rc = crypto_load_store_from_crl_file(data->store, filename_merged); + } else { + rc = crypto_load_store_from_cert_file(data->store, filename_merged); + } + + return rc; +} + +static int _crypto_load_cert_store(X509_STORE *store, const char *file, const char *path) +{ + int rc = 0; + + if (!ast_strlen_zero(file)) { + rc = crypto_load_store_from_cert_file(store, file); + if (rc != 0) { + return -1; + } + } + + if (!ast_strlen_zero(path)) { + struct pem_file_cb_data data = { .store = store, .is_crl = 0 }; + if (ast_file_read_dirs(path, pem_file_cb, &data, 0)) { + return -1; + } + } + + return 0; +} + +static int _crypto_load_crl_store(X509_STORE *store, const char *file, const char *path) +{ + int rc = 0; + + if (!ast_strlen_zero(file)) { + rc = crypto_load_store_from_crl_file(store, file); + if (rc != 0) { + return -1; + } + } + + if (!ast_strlen_zero(path)) { + struct pem_file_cb_data data = { .store = store, .is_crl = 1 }; + if (ast_file_read_dirs(path, pem_file_cb, &data, 0)) { + return -1; + } + } + + return 0; +} + int crypto_load_cert_store(struct crypto_cert_store *store, const char *file, const char *path) { if (ast_strlen_zero(file) && ast_strlen_zero(path)) { - ast_log(LOG_ERROR, "Both file and path can't be NULL"); + ast_log(LOG_ERROR, "Both file and path can't be NULL\n"); + return -1; + } + + if (!store || !store->certs) { + ast_log(LOG_ERROR, "store or store->certs is NULL\n"); + return -1; + } + + return _crypto_load_cert_store(store->certs, file, path); +} + +int crypto_load_untrusted_cert_store(struct crypto_cert_store *store, const char *file, + const char *path) +{ + int rc = 0; + STACK_OF(X509_OBJECT) *objs = NULL; + int count = 0; + int i = 0; + + if (ast_strlen_zero(file) && ast_strlen_zero(path)) { + ast_log(LOG_ERROR, "Both file and path can't be NULL\n"); return -1; } - if (!store || !store->store) { - ast_log(LOG_ERROR, "store is NULL"); + if (!store || !store->untrusted || !store->untrusted_stack) { + ast_log(LOG_ERROR, "store wasn't initialized properly\n"); return -1; } + rc = _crypto_load_cert_store(store->untrusted, file, path); + if (rc != 0) { + return rc; + } + /* - * If the file or path are empty strings, we need to pass NULL - * so openssl ignores it otherwise it'll try to open a file or - * path named ''. + * We need to extract the certs from the store and push them onto the + * untrusted stack. This is because the verification context needs + * a stack of untrusted certs and not the store. + * The store holds the references to the certs so we can't + * free it. */ - if (!X509_STORE_load_locations(store->store, S_OR(file, NULL), S_OR(path, NULL))) { - crypto_log_openssl(LOG_ERROR, "Failed to load store from file '%s' or path '%s'\n", - S_OR(file, "N/A"), S_OR(path, "N/A")); + objs = X509_STORE_get0_objects(store->untrusted); + count = sk_X509_OBJECT_num(objs); + for (i = 0; i < count ; i++) { + X509_OBJECT *o = sk_X509_OBJECT_value(objs, i); + if (X509_OBJECT_get_type(o) == X509_LU_X509) { + X509 *c = X509_OBJECT_get0_X509(o); + sk_X509_push(store->untrusted_stack, c); + } + } + + return 0; +} + +int crypto_load_crl_store(struct crypto_cert_store *store, const char *file, + const char *path) +{ + int rc = 0; + STACK_OF(X509_OBJECT) *objs = NULL; + int count = 0; + int i = 0; + + if (ast_strlen_zero(file) && ast_strlen_zero(path)) { + ast_log(LOG_ERROR, "Both file and path can't be NULL\n"); + return -1; + } + + if (!store || !store->untrusted || !store->untrusted_stack) { + ast_log(LOG_ERROR, "store wasn't initialized properly\n"); return -1; } + rc = _crypto_load_crl_store(store->crls, file, path); + if (rc != 0) { + return rc; + } + + /* + * We need to extract the CRLs from the store and push them onto the + * crl stack. This is because the verification context needs + * a stack of CRLs and not the store. + * The store holds the references to the CRLs so we can't + * free it. + */ + objs = X509_STORE_get0_objects(store->crls); + count = sk_X509_OBJECT_num(objs); + for (i = 0; i < count ; i++) { + X509_OBJECT *o = sk_X509_OBJECT_value(objs, i); + if (X509_OBJECT_get_type(o) == X509_LU_CRL) { + X509_CRL *c = X509_OBJECT_get0_X509_CRL(o); + sk_X509_CRL_push(store->crl_stack, c); + } + } + return 0; } int crypto_show_cli_store(struct crypto_cert_store *store, int fd) { #if (OPENSSL_VERSION_NUMBER >= 0x10100000L) - STACK_OF(X509_OBJECT) *certs = NULL; + STACK_OF(X509_OBJECT) *objs = NULL; int count = 0; + int untrusted_count = 0; + int crl_count = 0; int i = 0; char subj[1024]; - certs = X509_STORE_get0_objects(store->store); - count = sk_X509_OBJECT_num(certs); + /* + * The CA certificates are stored in the certs store. + */ + objs = X509_STORE_get0_objects(store->certs); + count = sk_X509_OBJECT_num(objs); + for (i = 0; i < count ; i++) { - X509_OBJECT *o = sk_X509_OBJECT_value(certs, i); - X509 *c = X509_OBJECT_get0_X509(o); + X509_OBJECT *o = sk_X509_OBJECT_value(objs, i); + if (X509_OBJECT_get_type(o) == X509_LU_X509) { + X509 *c = X509_OBJECT_get0_X509(o); + X509_NAME_oneline(X509_get_subject_name(c), subj, 1024); + ast_cli(fd, "Cert: %s\n", subj); + } else { + ast_log(LOG_ERROR, "CRLs are not allowed in the CA cert store\n"); + } + } + + /* + * Although the untrusted certs are stored in the untrusted store, + * we already have the stack of certificates so we can just + * list them directly. + */ + untrusted_count = sk_X509_num(store->untrusted_stack); + for (i = 0; i < untrusted_count ; i++) { + X509 *c = sk_X509_value(store->untrusted_stack, i); X509_NAME_oneline(X509_get_subject_name(c), subj, 1024); - ast_cli(fd, "%s\n", subj); + ast_cli(fd, "Untrusted: %s\n", subj); } - return count; + + /* + * Same for the CRLs. + */ + crl_count = sk_X509_CRL_num(store->crl_stack); + for (i = 0; i < crl_count ; i++) { + X509_CRL *crl = sk_X509_CRL_value(store->crl_stack, i); + X509_NAME_oneline(X509_CRL_get_issuer(crl), subj, 1024); + ast_cli(fd, "CRL: %s\n", subj); + } + + return count + untrusted_count + crl_count; #else ast_cli(fd, "This command is not supported until OpenSSL 1.1.0\n"); return 0; @@ -409,12 +744,13 @@ int crypto_is_cert_trusted(struct crypto_cert_store *store, X509 *cert, const ch return 0; } - if (X509_STORE_CTX_init(verify_ctx, store->store, cert, NULL) != 1) { + if (X509_STORE_CTX_init(verify_ctx, store->certs, cert, store->untrusted_stack) != 1) { X509_STORE_CTX_cleanup(verify_ctx); X509_STORE_CTX_free(verify_ctx); crypto_log_openssl(LOG_ERROR, "Unable to initialize verify_ctx\n"); return 0; } + X509_STORE_CTX_set0_crls(verify_ctx, store->crl_stack); rc = X509_verify_cert(verify_ctx); if (rc != 1 && err_msg != NULL) { diff --git a/res/res_stir_shaken/crypto_utils.h b/res/res_stir_shaken/crypto_utils.h index 1f475c6521..692f25abb9 100644 --- a/res/res_stir_shaken/crypto_utils.h +++ b/res/res_stir_shaken/crypto_utils.h @@ -82,6 +82,15 @@ ASN1_OCTET_STRING *crypto_get_cert_extension_data(X509 *cert, int nid, */ X509 *crypto_load_cert_from_file(const char *filename); +/*! + * \brief Load an X509 CRL from a PEM file + * + * \param filename PEM file + * + * \returns X509_CRL* or NULL on error + */ +X509_CRL *crypto_load_crl_from_file(const char *filename); + /*! * \brief Load a private key from memory * @@ -168,7 +177,13 @@ EVP_PKEY *crypto_load_privkey_from_file(const char *filename); * \brief ao2 object wrapper for X509_STORE that provides locking and refcounting */ struct crypto_cert_store { - X509_STORE *store; + X509_STORE *certs; + X509_STORE *crls; + /*!< The verification context needs a stack of CRLs, not the store */ + STACK_OF(X509_CRL) *crl_stack; + X509_STORE *untrusted; + /*!< The verification context needs a stack of untrusted certs, not the store */ + STACK_OF(X509) *untrusted_stack; }; /*! @@ -211,6 +226,36 @@ int crypto_show_cli_store(struct crypto_cert_store *store, int fd); int crypto_load_cert_store(struct crypto_cert_store *store, const char *file, const char *path); +/*! + * \brief Load an X509 Store with certificate revocation lists + * + * \param store X509 Store to load + * \param file CRL file to load or NULL + * \param path Path to directory with hashed CRLs to load or NULL + * + * \note At least 1 file or path must be specified. + * + * \retval <= 0 failure + * \retval 0 success + */ +int crypto_load_crl_store(struct crypto_cert_store *store, const char *file, + const char *path); + +/*! + * \brief Load an X509 Store with untrusted certificates + * + * \param store X509 Store to load + * \param file Certificate file to load or NULL + * \param path Path to directory with hashed certs to load or NULL + * + * \note At least 1 file or path must be specified. + * + * \retval <= 0 failure + * \retval 0 success + */ +int crypto_load_untrusted_cert_store(struct crypto_cert_store *store, const char *file, + const char *path); + /*! * \brief Locks an X509 Store * diff --git a/res/res_stir_shaken/profile_config.c b/res/res_stir_shaken/profile_config.c index e892fb9991..6e5a78a448 100644 --- a/res/res_stir_shaken/profile_config.c +++ b/res/res_stir_shaken/profile_config.c @@ -34,6 +34,8 @@ #define DEFAULT_ca_path NULL #define DEFAULT_crl_file NULL #define DEFAULT_crl_path NULL +#define DEFAULT_untrusted_cert_file NULL +#define DEFAULT_untrusted_cert_path NULL #define DEFAULT_cert_cache_dir NULL #define DEFAULT_curl_timeout 0 @@ -100,7 +102,7 @@ static void *profile_alloc(const char *name) return profile; } -static struct ao2_container *profile_get_all(void) +struct ao2_container *profile_get_all(void) { return ast_sorcery_retrieve_by_fields(get_sorcery(), CONFIG_TYPE, AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); @@ -114,7 +116,7 @@ struct profile_cfg *profile_get_cfg(const char *id) return ast_sorcery_retrieve_by_id(get_sorcery(), CONFIG_TYPE, id); } -static struct ao2_container *eprofile_get_all(void) +struct ao2_container *eprofile_get_all(void) { return ast_sorcery_retrieve_by_fields(get_sorcery(), "eprofile", AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); diff --git a/res/res_stir_shaken/stir_shaken_doc.xml b/res/res_stir_shaken/stir_shaken_doc.xml index e14d1d2d08..6663ce9a16 100644 --- a/res/res_stir_shaken/stir_shaken_doc.xml +++ b/res/res_stir_shaken/stir_shaken_doc.xml @@ -63,16 +63,77 @@ A boolean indicating whether trusted CA certificates should be loaded from the system - Path to a file containing one or more CA certs + Path to a file containing one or more CA certs in PEM format + + These certs are used to verify the chain of trust for the + certificate retrieved from the X5U Identity header parameter. This + file must have the root CA certificate, the certificate of the + issuer of the X5U certificate, and any intermediate certificates + between them. + + See https://docs.asterisk.org/Deployment/STIR-SHAKEN/ for more information. + + Path to a directory containing one or more hashed CA certs + + + For this option, the individual certificates must be placed in + the directory specified and hashed using the openssl rehash + command. + + See https://docs.asterisk.org/Deployment/STIR-SHAKEN/ for more information. + + - Path to a file containing a CRL + Path to a file containing one or more CRLs in PEM format + + If you with to check if the certificate in the X5U Identity header + parameter has been revoked, you'll need the certificate revocation + list generated by the issuer. + + See https://docs.asterisk.org/Deployment/STIR-SHAKEN/ for more information. + + Path to a directory containing one or more hashed CRLs + + + For this option, the individual CRLs must be placed in + the directory specified and hashed using the openssl rehash + command. + + See https://docs.asterisk.org/Deployment/STIR-SHAKEN/ for more information. + + + + + Path to a file containing one or more untrusted cert in PEM format used to verify CRLs + + If you with to check if the certificate in the X5U Identity header + parameter has been revoked, you'll need the certificate revocation + list generated by the issuer. Unfortunately, sometimes the CRLs are signed by a + different CA than the certificate being verified. In this case, you + may need to provide the untrusted certificate to verify the CRL. + + See https://docs.asterisk.org/Deployment/STIR-SHAKEN/ for more information. + + + + + Path to a directory containing one or more hashed untrusted certs used to verify CRLs + + + For this option, the individual certificates must be placed in + the directory specified and hashed using the openssl rehash + command. + + See https://docs.asterisk.org/Deployment/STIR-SHAKEN/ for more information. + + Directory to cache retrieved verification certs @@ -143,39 +204,31 @@ Must be of type 'profile'. - - A boolean indicating whether trusted CA certificates should be loaded from the system - - - Path to a file containing one or more CA certs - - - Path to a directory containing one or more hashed CA certs - - - Path to a file containing a CRL - - - Path to a directory containing one or more hashed CRLs - - - Directory to cache retrieved verification certs - - - Maximum time to wait to CURL certificates - - - Number of seconds an iat grant may be behind current time - - - Number of seconds a SIP Date header may be behind current time - - - Number of seconds a cache entry may be behind current time - - - Maximum size to use for caching public keys - + + + + + + + + + + + + + + + + + + + + + + + + + Actions performed when an endpoint references this profile @@ -195,70 +248,6 @@ - - What do do when a verification fails - - - - If set to continue, continue and let - the dialplan decide what action to take. - - - If set to reject_request, reject the incoming - request with response codes defined in RFC8224. - - - - If set to return_reason, continue to the - dialplan but add a Reason header to the sender in - the next provisional response. - - - - - - RFC9410 uses the STIR protocol on Reason headers - instead of the SIP protocol - - - Relaxes check for "https" and port 443 or 8443 - in incoming Identity header x5u URLs. - - - Relaxes check for query parameters, user/password, etc. - in incoming Identity header x5u URLs. - - - An existing ACL from acl.conf to use when checking - hostnames in incoming Identity header x5u URLs. - - - An IP or subnet to permit when checking - hostnames in incoming Identity header x5u URLs. - - - An IP or subnet to deny checking - hostnames in incoming Identity header x5u URLs. - - - On load, Retrieve all TN's certificates and validate their dates - - - File path to a certificate - - - URL to the public certificate - - Must be a valid http, or https, URL. - - - - Attestation level - - - Send a media key (mky) grant in the attestation for DTLS calls. - (not common) - diff --git a/res/res_stir_shaken/verification_config.c b/res/res_stir_shaken/verification_config.c index 0cade6bd52..ef68ffc83e 100644 --- a/res/res_stir_shaken/verification_config.c +++ b/res/res_stir_shaken/verification_config.c @@ -29,6 +29,8 @@ #define DEFAULT_ca_path NULL #define DEFAULT_crl_file NULL #define DEFAULT_crl_path NULL +#define DEFAULT_untrusted_cert_file NULL +#define DEFAULT_untrusted_cert_path NULL static char DEFAULT_cert_cache_dir[PATH_MAX]; #define DEFAULT_curl_timeout 2 @@ -129,6 +131,8 @@ int vs_copy_cfg_common(const char *id, struct verification_cfg_common *cfg_dst, cfg_sf_copy_wrapper(id, cfg_dst, cfg_src, ca_path); cfg_sf_copy_wrapper(id, cfg_dst, cfg_src, crl_file); cfg_sf_copy_wrapper(id, cfg_dst, cfg_src, crl_path); + cfg_sf_copy_wrapper(id, cfg_dst, cfg_src, untrusted_cert_file); + cfg_sf_copy_wrapper(id, cfg_dst, cfg_src, untrusted_cert_path); ao2_bump(cfg_src->tcs); cfg_dst->tcs = cfg_src->tcs; } @@ -188,6 +192,20 @@ int vs_check_common_config(const char *id, id, vcfg_common->crl_path); } + if (!ast_strlen_zero(vcfg_common->untrusted_cert_file) + && !ast_file_is_readable(vcfg_common->untrusted_cert_file)) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: untrusted_cert_file '%s' not found, or is unreadable\n", + id, vcfg_common->untrusted_cert_file); + } + + if (!ast_strlen_zero(vcfg_common->untrusted_cert_path) + && !ast_file_is_readable(vcfg_common->untrusted_cert_path)) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: untrusted_cert_path '%s' not found, or is unreadable\n", + id, vcfg_common->untrusted_cert_path); + } + if (!ast_strlen_zero(vcfg_common->ca_file) || !ast_strlen_zero(vcfg_common->ca_path)) { int rc = 0; @@ -219,7 +237,7 @@ int vs_check_common_config(const char *id, "%s: Unable to create CA cert store\n", id); } } - rc = crypto_load_cert_store(vcfg_common->tcs, + rc = crypto_load_crl_store(vcfg_common->tcs, vcfg_common->crl_file, vcfg_common->crl_path); if (rc != 0) { SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, @@ -228,14 +246,34 @@ int vs_check_common_config(const char *id, } } + if (!ast_strlen_zero(vcfg_common->untrusted_cert_file) + || !ast_strlen_zero(vcfg_common->untrusted_cert_path)) { + int rc = 0; + + if (!vcfg_common->tcs) { + vcfg_common->tcs = crypto_create_cert_store(); + if (!vcfg_common->tcs) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: Unable to create CA cert store\n", id); + } + } + rc = crypto_load_untrusted_cert_store(vcfg_common->tcs, + vcfg_common->untrusted_cert_file, vcfg_common->untrusted_cert_path); + if (rc != 0) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: Unable to load CA CRL store from '%s' or '%s'\n", + id, vcfg_common->untrusted_cert_file, vcfg_common->untrusted_cert_path); + } + } + if (vcfg_common->tcs) { if (ENUM_BOOL(vcfg_common->load_system_certs, load_system_certs)) { - X509_STORE_set_default_paths(vcfg_common->tcs->store); + X509_STORE_set_default_paths(vcfg_common->tcs->certs); } if (!ast_strlen_zero(vcfg_common->crl_file) || !ast_strlen_zero(vcfg_common->crl_path)) { - X509_STORE_set_flags(vcfg_common->tcs->store, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL); + X509_STORE_set_flags(vcfg_common->tcs->certs, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_EXTENDED_CRL_SUPPORT); } } @@ -355,6 +393,11 @@ static char *cli_verification_show(struct ast_cli_entry *e, int cmd, struct ast_ return CLI_SHOWUSAGE; } + if (!vs_is_config_loaded()) { + ast_log(LOG_WARNING,"Stir/Shaken verification service disabled. Either there were errors in the 'verification' object in stir_shaken.conf or it was missing altogether.\n"); + return CLI_FAILURE; + } + cfg = vs_get_cfg(); config_object_cli_show(cfg, a, &data, 0);