res_stir_shaken.so: Handle X5U certificate chains.

The verification process will now load a full certificate chain retrieved
via the X5U URL instead of loading only the end user cert.

* Renamed crypto_load_cert_from_file() and crypto_load_cert_from_memory()
to crypto_load_cert_chain_from_file() and crypto_load_cert_chain_from_memory()
respectively.

* The two load functions now continue to load certs from the file or memory
PEMs and store them in a separate stack of untrusted certs specific to the
current verification context.

* crypto_is_cert_trusted() now uses the stack of untrusted certs that were
extracted from the PEM in addition to any untrusted certs that were passed
in from the configuration (and any CA certs passed in from the config of
course).

Resolves: #1272

UserNote: The STIR/SHAKEN verification process will now load a full
certificate chain retrieved via the X5U URL instead of loading only
the end user cert.
This commit is contained in:
George Joseph
2025-06-18 14:38:08 -06:00
committed by github-actions[bot]
parent fe341c2b0f
commit 8b8a8c1475
6 changed files with 180 additions and 29 deletions

View File

@@ -164,8 +164,8 @@ int as_check_common_config(const char *id, struct attestation_cfg_common *acfg_c
acfg_common->public_cert_url); acfg_common->public_cert_url);
} }
public_cert = crypto_load_cert_from_memory(public_cert_data, public_cert = crypto_load_cert_chain_from_memory(public_cert_data,
public_cert_len); public_cert_len, NULL);
if (!public_cert) { if (!public_cert) {
SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, "%s: public_cert '%s' could not be parsed as a certificate\n", id, SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, "%s: public_cert '%s' could not be parsed as a certificate\n", id,
acfg_common->public_cert_url); acfg_common->public_cert_url);

View File

@@ -306,6 +306,7 @@ static char *cli_verify_cert(struct ast_cli_entry *e, int cmd, struct ast_cli_ar
RAII_VAR(struct verification_cfg *, vs_cfg, NULL, ao2_cleanup); RAII_VAR(struct verification_cfg *, vs_cfg, NULL, ao2_cleanup);
struct crypto_cert_store *tcs; struct crypto_cert_store *tcs;
X509 *cert = NULL; X509 *cert = NULL;
STACK_OF(X509) *cert_chain = NULL;
const char *errmsg = NULL; const char *errmsg = NULL;
switch(cmd) { switch(cmd) {
@@ -347,18 +348,21 @@ static char *cli_verify_cert(struct ast_cli_entry *e, int cmd, struct ast_cli_ar
tcs = vs_cfg->vcfg_common.tcs; tcs = vs_cfg->vcfg_common.tcs;
} }
cert = crypto_load_cert_from_file(a->argv[3]); cert = crypto_load_cert_chain_from_file(a->argv[3], &cert_chain);
if (!cert) { if (!cert) {
ast_cli(a->fd, "Failed to load certificate from %s. See log for details\n", a->argv[3]); ast_cli(a->fd, "Failed to load certificate from %s. See log for details\n", a->argv[3]);
return CLI_SUCCESS; return CLI_SUCCESS;
} }
if (crypto_is_cert_trusted(tcs, cert, &errmsg)) { if (crypto_is_cert_trusted(tcs, cert, cert_chain, &errmsg)) {
ast_cli(a->fd, "Certificate %s trusted\n", a->argv[3]); ast_cli(a->fd, "Certificate %s trusted\n", a->argv[3]);
} else { } else {
ast_cli(a->fd, "Certificate %s NOT trusted: %s\n", a->argv[3], errmsg); ast_cli(a->fd, "Certificate %s NOT trusted: %s\n", a->argv[3], errmsg);
} }
X509_free(cert); X509_free(cert);
if (cert_chain) {
sk_X509_pop_free(cert_chain, X509_free);
}
return CLI_SUCCESS; return CLI_SUCCESS;
} }

View File

@@ -186,10 +186,24 @@ X509_CRL *crypto_load_crl_from_file(const char *filename)
return crl; return crl;
} }
X509 *crypto_load_cert_from_file(const char *filename) #define debug_cert_chain(level, cert_chain) \
({ \
int i; \
char subj[1024]; \
if (cert_chain && sk_X509_num(cert_chain) > 0) { \
for (i = 0; i < sk_X509_num(cert_chain); i++) { \
X509 *cert = sk_X509_value(cert_chain, i); \
subj[0] = '\0'; \
X509_NAME_oneline(X509_get_subject_name(cert), subj, 1024); \
ast_debug(level, "Chain cert %d: '%s'\n", i, subj); \
} \
} \
})
X509 *crypto_load_cert_chain_from_file(const char *filename, STACK_OF(X509) **cert_chain)
{ {
FILE *fp; FILE *fp;
X509 *cert = NULL; X509 *end_cert = NULL;
if (ast_strlen_zero(filename)) { if (ast_strlen_zero(filename)) {
ast_log(LOG_ERROR, "filename was null or empty\n"); ast_log(LOG_ERROR, "filename was null or empty\n");
@@ -202,18 +216,58 @@ X509 *crypto_load_cert_from_file(const char *filename)
return NULL; return NULL;
} }
cert = PEM_read_X509(fp, &cert, NULL, NULL); end_cert = PEM_read_X509(fp, &end_cert, NULL, NULL);
fclose(fp); if (!end_cert) {
if (!cert) { crypto_log_openssl(LOG_ERROR, "Failed to create end_cert from %s\n", filename);
crypto_log_openssl(LOG_ERROR, "Failed to create cert from %s\n", filename); fclose(fp);
return NULL;
} }
return cert;
/*
* If the caller provided a stack, we will read the chain certs
* (if any) into it.
*/
if (cert_chain) {
X509 *chain_cert = NULL;
*cert_chain = sk_X509_new_null();
while ((chain_cert = PEM_read_X509(fp, &chain_cert, NULL, NULL)) != NULL) {
if (sk_X509_push(*cert_chain, chain_cert) <= 0) {
crypto_log_openssl(LOG_ERROR, "Failed to add chain cert from %s to list\n",
filename);
fclose(fp);
X509_free(end_cert);
sk_X509_pop_free(*cert_chain, X509_free);
return NULL;
}
/* chain_cert needs to be reset to NULL after every call to PEM_read_X509 */
chain_cert = NULL;
}
}
if (DEBUG_ATLEAST(4)) {
char subj[1024];
X509_NAME_oneline(X509_get_subject_name(end_cert), subj, 1024);
ast_debug(4, "Opened end cert '%s' from '%s'\n", subj, filename);
if (cert_chain && *cert_chain) {
debug_cert_chain(4, *cert_chain);
} else {
ast_debug(4, "No chain certs found in '%s'\n", filename);
}
}
fclose(fp);
return end_cert;
} }
X509 *crypto_load_cert_from_memory(const char *buffer, size_t size) X509 *crypto_load_cert_chain_from_memory(const char *buffer, size_t size,
STACK_OF(X509) **cert_chain)
{ {
RAII_VAR(BIO *, bio, NULL, BIO_free_all); RAII_VAR(BIO *, bio, NULL, BIO_free_all);
X509 *cert = NULL; X509 *end_cert = NULL;
if (ast_strlen_zero(buffer) || size <= 0) { if (ast_strlen_zero(buffer) || size <= 0) {
ast_log(LOG_ERROR, "buffer was null or empty\n"); ast_log(LOG_ERROR, "buffer was null or empty\n");
@@ -226,11 +280,46 @@ X509 *crypto_load_cert_from_memory(const char *buffer, size_t size)
return NULL; return NULL;
} }
cert = PEM_read_bio_X509(bio, NULL, NULL, NULL); end_cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
if (!cert) { if (!end_cert) {
crypto_log_openssl(LOG_ERROR, "Failed to create cert from BIO\n"); crypto_log_openssl(LOG_ERROR, "Failed to create end_cert from BIO\n");
return NULL;
} }
return cert;
/*
* If the caller provided a stack, we will read the chain certs
* (if any) into it.
*/
if (cert_chain) {
X509 *chain_cert = NULL;
*cert_chain = sk_X509_new_null();
while ((chain_cert = PEM_read_bio_X509(bio, &chain_cert, NULL, NULL)) != NULL) {
if (sk_X509_push(*cert_chain, chain_cert) <= 0) {
crypto_log_openssl(LOG_ERROR, "Failed to add chain cert from BIO to list\n");
X509_free(end_cert);
sk_X509_pop_free(*cert_chain, X509_free);
return NULL;
}
/* chain_cert needs to be reset to NULL after every call to PEM_read_X509 */
chain_cert = NULL;
}
}
if (DEBUG_ATLEAST(4)) {
char subj[1024];
X509_NAME_oneline(X509_get_subject_name(end_cert), subj, 1024);
ast_debug(4, "Opened end cert '%s' from BIO\n", subj);
if (cert_chain && *cert_chain) {
debug_cert_chain(4, *cert_chain);
} else {
ast_debug(4, "No chain certs found in BIO\n");
}
}
return end_cert;
} }
static EVP_PKEY *load_private_key_from_memory(const char *buffer, size_t size) static EVP_PKEY *load_private_key_from_memory(const char *buffer, size_t size)
@@ -441,7 +530,7 @@ static int crypto_load_store_from_cert_file(X509_STORE *store, const char *file)
return -1; return -1;
} }
cert = crypto_load_cert_from_file(file); cert = crypto_load_cert_chain_from_file(file, NULL);
if (!cert) { if (!cert) {
return -1; return -1;
} }
@@ -737,9 +826,11 @@ int crypto_is_cert_time_valid(X509*cert, time_t reftime)
X509_cmp_time(notafter, &reftime) > 0); X509_cmp_time(notafter, &reftime) > 0);
} }
int crypto_is_cert_trusted(struct crypto_cert_store *store, X509 *cert, const char **err_msg) int crypto_is_cert_trusted(struct crypto_cert_store *store, X509 *cert,
STACK_OF(X509) *cert_chain, const char **err_msg)
{ {
X509_STORE_CTX *verify_ctx = NULL; X509_STORE_CTX *verify_ctx = NULL;
RAII_VAR(STACK_OF(X509) *, untrusted_stack, NULL, sk_X509_free);
int rc = 0; int rc = 0;
if (!(verify_ctx = X509_STORE_CTX_new())) { if (!(verify_ctx = X509_STORE_CTX_new())) {
@@ -747,7 +838,45 @@ int crypto_is_cert_trusted(struct crypto_cert_store *store, X509 *cert, const ch
return 0; return 0;
} }
if (X509_STORE_CTX_init(verify_ctx, store->certs, cert, store->untrusted_stack) != 1) { if (cert_chain && sk_X509_num(cert_chain) > 0) {
int untrusted_count = store->untrusted_stack ? sk_X509_num(store->untrusted_stack) : 0;
int i = 0;
untrusted_stack = sk_X509_dup(cert_chain);
if (!untrusted_stack) {
crypto_log_openssl(LOG_ERROR, "Unable to duplicate untrusted stack\n");
X509_STORE_CTX_free(verify_ctx);
return 0;
}
/*
* If store->untrusted_stack was NULL for some reason then
* untrusted_count will be 0 so the loop will never run.
*/
for (i = 0; i < untrusted_count; i++) {
X509 *c = sk_X509_value(store->untrusted_stack, i);
if (sk_X509_push(untrusted_stack, c) <= 0) {
crypto_log_openssl(LOG_ERROR, "Unable to push untrusted cert onto stack\n");
sk_X509_free(untrusted_stack);
X509_STORE_CTX_free(verify_ctx);
return 0;
}
}
/*
* store->untrusted_stack should always be allocated even if empty
* but we'll make sure.
*/
} else if (store->untrusted_stack){
/* This is a dead simple shallow clone */
ast_debug(4, "cert_chain had no certs\n");
untrusted_stack = sk_X509_dup(store->untrusted_stack);
if (!untrusted_stack) {
crypto_log_openssl(LOG_ERROR, "Unable to duplicate untrusted stack\n");
X509_STORE_CTX_free(verify_ctx);
return 0;
}
}
if (X509_STORE_CTX_init(verify_ctx, store->certs, cert, untrusted_stack) != 1) {
X509_STORE_CTX_cleanup(verify_ctx); X509_STORE_CTX_cleanup(verify_ctx);
X509_STORE_CTX_free(verify_ctx); X509_STORE_CTX_free(verify_ctx);
crypto_log_openssl(LOG_ERROR, "Unable to initialize verify_ctx\n"); crypto_log_openssl(LOG_ERROR, "Unable to initialize verify_ctx\n");
@@ -760,6 +889,7 @@ int crypto_is_cert_trusted(struct crypto_cert_store *store, X509 *cert, const ch
int err = X509_STORE_CTX_get_error(verify_ctx); int err = X509_STORE_CTX_get_error(verify_ctx);
*err_msg = X509_verify_cert_error_string(err); *err_msg = X509_verify_cert_error_string(err);
} }
X509_STORE_CTX_cleanup(verify_ctx); X509_STORE_CTX_cleanup(verify_ctx);
X509_STORE_CTX_free(verify_ctx); X509_STORE_CTX_free(verify_ctx);

View File

@@ -74,13 +74,19 @@ ASN1_OCTET_STRING *crypto_get_cert_extension_data(X509 *cert, int nid,
const char *short_name); const char *short_name);
/*! /*!
* \brief Load an X509 Cert from a file * \brief Load an X509 Cert and any chained certs from a file
* *
* \param filename PEM file * \param filename PEM file
* \param chain_stack The address of a STACK_OF(X509) pointer to receive the
* chain of certificates if any.
*
* \note The caller is responsible for freeing the cert_chain stack and
* any certs in it.
* *
* \returns X509* or NULL on error * \returns X509* or NULL on error
*/ */
X509 *crypto_load_cert_from_file(const char *filename); X509 *crypto_load_cert_chain_from_file(const char *filename,
STACK_OF(X509) **chain_stack);
/*! /*!
* \brief Load an X509 CRL from a PEM file * \brief Load an X509 CRL from a PEM file
@@ -116,15 +122,21 @@ EVP_PKEY *crypto_load_private_key_from_memory(const char *buffer, size_t size);
int crypto_has_private_key_from_memory(const char *buffer, size_t size); int crypto_has_private_key_from_memory(const char *buffer, size_t size);
/*! /*!
* \brief Load an X509 Cert from a NULL terminated buffer * \brief Load an X509 Cert and any chained certs from a NULL terminated buffer
* *
* \param buffer containing the cert * \param buffer containing the cert
* \param size size of the buffer. * \param size size of the buffer.
* May be -1 if the buffer is NULL terminated. * May be -1 if the buffer is NULL terminated.
* \param chain_stack The address of a STACK_OF(X509) pointer to receive the
* chain of certificates if any.
*
* \note The caller is responsible for freeing the cert_chain stack and
* any certs in it.
* *
* \returns X509* or NULL on error * \returns X509* or NULL on error
*/ */
X509 *crypto_load_cert_from_memory(const char *buffer, size_t size); X509 *crypto_load_cert_chain_from_memory(const char *buffer, size_t size,
STACK_OF(X509) **cert_chain);
/*! /*!
* \brief Retrieve RAW public key from cert * \brief Retrieve RAW public key from cert
@@ -292,12 +304,14 @@ int crypto_is_cert_time_valid(X509 *cert, time_t reftime);
* *
* \param store The CA store to check against * \param store The CA store to check against
* \param cert The cert to check * \param cert The cert to check
* \param cert_chain An untrusted certificate chain that may have accompanied the cert.
* \param err_msg Optional pointer to a const char * * \param err_msg Optional pointer to a const char *
* *
* \retval 1 Cert is trusted * \retval 1 Cert is trusted
* \retval 0 Cert is not trusted * \retval 0 Cert is not trusted
*/ */
int crypto_is_cert_trusted(struct crypto_cert_store *store, X509 *cert, const char **err_msg); int crypto_is_cert_trusted(struct crypto_cert_store *store, X509 *cert,
STACK_OF(X509) *cert_chain, const char **err_msg);
/*! /*!
* \brief Return a time_t for an ASN1_TIME * \brief Return a time_t for an ASN1_TIME

View File

@@ -345,7 +345,8 @@ static enum ast_stir_shaken_vs_response_code check_cert(
} }
ast_trace(3,"%s: Checking ctx against CA ctx\n", ctx->tag); ast_trace(3,"%s: Checking ctx against CA ctx\n", ctx->tag);
res = crypto_is_cert_trusted(ctx->eprofile->vcfg_common.tcs, ctx->xcert, &err_msg); res = crypto_is_cert_trusted(ctx->eprofile->vcfg_common.tcs, ctx->xcert,
ctx->cert_chain, &err_msg);
if (!res) { if (!res) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NOT_TRUSTED, SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NOT_TRUSTED,
LOG_ERROR, "%s: Cert '%s' not trusted: %s\n", LOG_ERROR, "%s: Cert '%s' not trusted: %s\n",
@@ -429,8 +430,8 @@ static enum ast_stir_shaken_vs_response_code retrieve_cert_from_url(
ctx->tag, ctx->public_url); ctx->tag, ctx->public_url);
} }
ctx->xcert = crypto_load_cert_from_memory(write_data->stream_buffer, ctx->xcert = crypto_load_cert_chain_from_memory(write_data->stream_buffer,
write_data->stream_bytes_downloaded); write_data->stream_bytes_downloaded, &ctx->cert_chain);
if (!ctx->xcert) { if (!ctx->xcert) {
SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID, SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID,
LOG_ERROR, "%s: Cert '%s' was not parseable as an X509 certificate\n", LOG_ERROR, "%s: Cert '%s' was not parseable as an X509 certificate\n",
@@ -524,7 +525,7 @@ static enum ast_stir_shaken_vs_response_code
ctx->tag, ctx->filename, ctx->public_url); ctx->tag, ctx->filename, ctx->public_url);
} }
ctx->xcert = crypto_load_cert_from_file(ctx->filename); ctx->xcert = crypto_load_cert_chain_from_file(ctx->filename, &ctx->cert_chain);
if (!ctx->xcert) { if (!ctx->xcert) {
cleanup_cert_from_astdb_and_fs(ctx); cleanup_cert_from_astdb_and_fs(ctx);
SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID, SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID,
@@ -651,6 +652,7 @@ static void ctx_destructor(void *obj)
ast_free(ctx->raw_key); ast_free(ctx->raw_key);
ast_string_field_free_memory(ctx); ast_string_field_free_memory(ctx);
X509_free(ctx->xcert); X509_free(ctx->xcert);
sk_X509_free(ctx->cert_chain);
} }
enum ast_stir_shaken_vs_response_code enum ast_stir_shaken_vs_response_code

View File

@@ -45,6 +45,7 @@ struct ast_stir_shaken_vs_ctx {
unsigned char *raw_key; unsigned char *raw_key;
char expiration[32]; char expiration[32];
X509 *xcert; X509 *xcert;
STACK_OF(X509) *cert_chain;
enum ast_stir_shaken_vs_response_code failure_reason; enum ast_stir_shaken_vs_response_code failure_reason;
}; };