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