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 <certificate_file> [ <profile> ]`
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
pull/742/head
George Joseph 9 months ago
parent 5c437c5724
commit bf830d6293

@ -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 <ca_path>`
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 <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

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

@ -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 <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");
@ -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");
}

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

@ -16,6 +16,8 @@
* at the top of the source tree.
*/
#include <sys/stat.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <openssl/evp.h>
@ -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) {

@ -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
*

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

@ -63,16 +63,77 @@
<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>
@ -143,39 +204,31 @@
<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>
@ -195,70 +248,6 @@
</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>

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

Loading…
Cancel
Save