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 <ca_path>`
Default: none
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 <crl_path>`
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 <ca_path>`
+
+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
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);
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 <certificate_file> [ <profile> ]\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");
int common_config_unload(void)
{
+ ast_cli_unregister_multiple(cli_commands, ARRAY_LEN(cli_commands));
+
profile_unload();
tn_config_unload();
as_unload();
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");
}
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;
};
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);
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);\
* at the top of the source tree.
*/
+#include <sys/stat.h>
+
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <openssl/evp.h>
#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"
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;
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);
}
}
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;
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) {
*/
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
*
* \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;
};
/*!
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
*
#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
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);
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);
<synopsis>A boolean indicating whether trusted CA certificates should be loaded from the system</synopsis>
</configOption>
<configOption name="ca_file" default="">
- <synopsis>Path to a file containing one or more CA certs</synopsis>
+ <synopsis>Path to a file containing one or more CA certs in PEM format</synopsis>
+ <description>
+ <para>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.</para>
+ <para>
+ See https://docs.asterisk.org/Deployment/STIR-SHAKEN/ for more information.
+ </para>
+ </description>
</configOption>
<configOption name="ca_path" default="">
<synopsis>Path to a directory containing one or more hashed CA certs</synopsis>
+ <description>
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='ca_file']/description/node())" />
+ <para>For this option, the individual certificates must be placed in
+ the directory specified and hashed using the <literal>openssl rehash</literal>
+ command.</para>
+ <para>
+ See https://docs.asterisk.org/Deployment/STIR-SHAKEN/ for more information.
+ </para>
+ </description>
</configOption>
<configOption name="crl_file" default="">
- <synopsis>Path to a file containing a CRL</synopsis>
+ <synopsis>Path to a file containing one or more CRLs in PEM format</synopsis>
+ <description>
+ <para>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.</para>
+ <para>
+ See https://docs.asterisk.org/Deployment/STIR-SHAKEN/ for more information.
+ </para>
+ </description>
</configOption>
<configOption name="crl_path" default="">
<synopsis>Path to a directory containing one or more hashed CRLs</synopsis>
+ <description>
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='crl_file']/description/node())" />
+ <para>For this option, the individual CRLs must be placed in
+ the directory specified and hashed using the <literal>openssl rehash</literal>
+ command.</para>
+ <para>
+ See https://docs.asterisk.org/Deployment/STIR-SHAKEN/ for more information.
+ </para>
+ </description>
+ </configOption>
+ <configOption name="untrusted_cert_file" default="">
+ <synopsis>Path to a file containing one or more untrusted cert in PEM format used to verify CRLs</synopsis>
+ <description>
+ <para>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.</para>
+ <para>
+ See https://docs.asterisk.org/Deployment/STIR-SHAKEN/ for more information.
+ </para>
+ </description>
+ </configOption>
+ <configOption name="untrusted_cert_path" default="">
+ <synopsis>Path to a directory containing one or more hashed untrusted certs used to verify CRLs</synopsis>
+ <description>
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='untrusted_cert_file']/description/node())" />
+ <para>For this option, the individual certificates must be placed in
+ the directory specified and hashed using the <literal>openssl rehash</literal>
+ command.</para>
+ <para>
+ See https://docs.asterisk.org/Deployment/STIR-SHAKEN/ for more information.
+ </para>
+ </description>
</configOption>
<configOption name="cert_cache_dir" default="">
<synopsis>Directory to cache retrieved verification certs</synopsis>
<configOption name="type">
<synopsis>Must be of type 'profile'.</synopsis>
</configOption>
- <configOption name="load_system_certs" default="">
- <synopsis>A boolean indicating whether trusted CA certificates should be loaded from the system</synopsis>
- </configOption>
- <configOption name="ca_file" default="">
- <synopsis>Path to a file containing one or more CA certs</synopsis>
- </configOption>
- <configOption name="ca_path" default="">
- <synopsis>Path to a directory containing one or more hashed CA certs</synopsis>
- </configOption>
- <configOption name="crl_file" default="">
- <synopsis>Path to a file containing a CRL</synopsis>
- </configOption>
- <configOption name="crl_path" default="">
- <synopsis>Path to a directory containing one or more hashed CRLs</synopsis>
- </configOption>
- <configOption name="cert_cache_dir" default="">
- <synopsis>Directory to cache retrieved verification certs</synopsis>
- </configOption>
- <configOption name="curl_timeout" default="2">
- <synopsis>Maximum time to wait to CURL certificates</synopsis>
- </configOption>
- <configOption name="max_iat_age" default="15">
- <synopsis>Number of seconds an iat grant may be behind current time</synopsis>
- </configOption>
- <configOption name="max_date_header_age" default="15">
- <synopsis>Number of seconds a SIP Date header may be behind current time</synopsis>
- </configOption>
- <configOption name="max_cache_entry_age" default="60">
- <synopsis>Number of seconds a cache entry may be behind current time</synopsis>
- </configOption>
- <configOption name="max_cache_size" default="1000">
- <synopsis>Maximum size to use for caching public keys</synopsis>
- </configOption>
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='load_system_certs'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='ca_file'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='ca_path'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='crl_file'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='crl_path'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='untrusted_cert_file'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='untrusted_cert_path'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='cert_cache_dir'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='curl_timeout'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='max_iat_age'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='max_date_header_age'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='max_cache_entry_age'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='max_cache_size'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='failure_action'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='use_rfc9410_responses'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='relax_x5u_port_scheme_restrictions'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='relax_x5u_path_restrictions'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='x5u_acl'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='x5u_permit'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='verification']/configOption[@name='x5u_deny'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='attestation']/configOption[@name='check_tn_cert_public_url'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='attestation']/configOption[@name='private_key_file'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='attestation']/configOption[@name='public_cert_url'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='attestation']/configOption[@name='attest_level'])" />
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='res_stir_shaken']/configFile[@name='stir_shaken.conf']/configObject[@name='attestation']/configOption[@name='send_mky'])" />
<configOption name="endpoint_behavior" default="off">
<synopsis>Actions performed when an endpoint references this profile</synopsis>
<description>
</enumlist>
</description>
</configOption>
- <configOption name="failure_action" default="continue">
- <synopsis>What do do when a verification fails</synopsis>
- <description>
- <enumlist>
- <enum name="continue">
- <para>If set to <literal>continue</literal>, continue and let
- the dialplan decide what action to take.</para>
- </enum>
- <enum name="reject_request">
- <para>If set to <literal>reject_request</literal>, reject the incoming
- request with response codes defined in RFC8224.
- </para>
- </enum>
- <enum name="return_reason">
- <para>If set to <literal>return_reason</literal>, continue to the
- dialplan but add a <literal>Reason</literal> header to the sender in
- the next provisional response.</para>
- </enum>
- </enumlist>
- </description>
- </configOption>
- <configOption name="use_rfc9410_responses" default="no">
- <synopsis>RFC9410 uses the STIR protocol on Reason headers
- instead of the SIP protocol</synopsis>
- </configOption>
- <configOption name="relax_x5u_port_scheme_restrictions" default="no">
- <synopsis>Relaxes check for "https" and port 443 or 8443
- in incoming Identity header x5u URLs.</synopsis>
- </configOption>
- <configOption name="relax_x5u_path_restrictions" default="no">
- <synopsis>Relaxes check for query parameters, user/password, etc.
- in incoming Identity header x5u URLs.</synopsis>
- </configOption>
- <configOption name="x5u_acl" default="">
- <synopsis>An existing ACL from acl.conf to use when checking
- hostnames in incoming Identity header x5u URLs.</synopsis>
- </configOption>
- <configOption name="x5u_permit" default="">
- <synopsis>An IP or subnet to permit when checking
- hostnames in incoming Identity header x5u URLs.</synopsis>
- </configOption>
- <configOption name="x5u_deny" default="">
- <synopsis>An IP or subnet to deny checking
- hostnames in incoming Identity header x5u URLs.</synopsis>
- </configOption>
- <configOption name="check_tn_cert_public_url" default="false">
- <synopsis>On load, Retrieve all TN's certificates and validate their dates</synopsis>
- </configOption>
- <configOption name="private_key_file" default="">
- <synopsis>File path to a certificate</synopsis>
- </configOption>
- <configOption name="public_cert_url" default="">
- <synopsis>URL to the public certificate</synopsis>
- <description><para>
- Must be a valid http, or https, URL.
- </para></description>
- </configOption>
- <configOption name="attest_level">
- <synopsis>Attestation level</synopsis>
- </configOption>
- <configOption name="send_mky" default="no">
- <synopsis>Send a media key (mky) grant in the attestation for DTLS calls.
- (not common)</synopsis>
- </configOption>
</configObject>
</configFile>
</configInfo>
#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
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;
}
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;
"%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,
}
}
+ 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);
}
}
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);