mirror of
https://github.com/asterisk/asterisk.git
synced 2025-09-04 20:04:50 +00:00
Why do we need a refactor? The original stir/shaken implementation was started over 3 years ago when little was understood about practical implementation. The result was an implementation that wouldn't actually interoperate with any other stir-shaken implementations. There were also a number of stir-shaken features and RFC requirements that were never implemented such as TNAuthList certificate validation, sending Reason headers in SIP responses when verification failed but we wished to continue the call, and the ability to send Media Key(mky) grants in the Identity header when the call involved DTLS. Finally, there were some performance concerns around outgoing calls and selection of the correct certificate and private key. The configuration was keyed by an arbitrary name which meant that for every outgoing call, we had to scan the entire list of configured TNs to find the correct cert to use. With only a few TNs configured, this wasn't an issue but if you have a thousand, it could be. What's changed? * Configuration objects have been refactored to be clearer about their uses and to fix issues. * The "general" object was renamed to "verification" since it contains parameters specific to the incoming verification process. It also never handled ca_path and crl_path correctly. * A new "attestation" object was added that controls the outgoing attestation process. It sets default certificates, keys, etc. * The "certificate" object was renamed to "tn" and had it's key change to telephone number since outgoing call attestation needs to look up certificates by telephone number. * The "profile" object had more parameters added to it that can override default parameters specified in the "attestation" and "verification" objects. * The "store" object was removed altogther as it was never implemented. * We now use libjwt to create outgoing Identity headers and to parse and validate signatures on incoming Identiy headers. Our previous custom implementation was much of the source of the interoperability issues. * General code cleanup and refactor. * Moved things to better places. * Separated some of the complex functions to smaller ones. * Using context objects rather than passing tons of parameters in function calls. * Removed some complexity and unneeded encapsuation from the config objects. Resolves: #351 Resolves: #46 UserNote: Asterisk's stir-shaken feature has been refactored to correct interoperability, RFC compliance, and performance issues. See https://docs.asterisk.org/Deployment/STIR-SHAKEN for more information. UpgradeNote: The stir-shaken refactor is a breaking change but since it's not working now we don't think it matters. The stir_shaken.conf file has changed significantly which means that existing ones WILL need to be changed. The stir_shaken.conf.sample file in configs/samples/ has quite a bit more information. This is also an ABI breaking change since some of the existing objects needed to be changed or removed, and new ones added. Additionally, if res_stir_shaken is enabled in menuselect, you'll need to either have the development package for libjwt v1.15.3 installed or use the --with-libjwt-bundled option with ./configure.
526 lines
12 KiB
C
526 lines
12 KiB
C
/*
|
|
* Asterisk -- An open source telephony toolkit.
|
|
*
|
|
* Copyright (C) 2023, Sangoma Technologies Corporation
|
|
*
|
|
* George Joseph <gjoseph@sangoma.com>
|
|
*
|
|
* See http://www.asterisk.org for more information about
|
|
* the Asterisk project. Please do not directly contact
|
|
* any of the maintainers of this project for assistance;
|
|
* the project provides a web site, mailing lists and IRC
|
|
* channels for your use.
|
|
*
|
|
* This program is free software, distributed under the terms of
|
|
* the GNU General Public License Version 2. See the LICENSE file
|
|
* at the top of the source tree.
|
|
*/
|
|
|
|
#include <openssl/err.h>
|
|
#include <openssl/ssl.h>
|
|
#include <openssl/evp.h>
|
|
#include <openssl/md5.h>
|
|
#include <openssl/sha.h>
|
|
#include <openssl/bio.h>
|
|
#include <openssl/obj_mac.h>
|
|
#include <openssl/x509.h>
|
|
#include <openssl/x509v3.h>
|
|
#include <openssl/x509_vfy.h>
|
|
|
|
#include "crypto_utils.h"
|
|
|
|
#include "asterisk.h"
|
|
#include "asterisk/logger.h"
|
|
#include "asterisk/module.h"
|
|
#include "asterisk/stringfields.h"
|
|
#include "asterisk/utils.h"
|
|
#include "asterisk/vector.h"
|
|
#include "asterisk/cli.h"
|
|
|
|
void __attribute__((format(printf, 5, 6)))
|
|
crypto_log_openssl(int level, char *file, int line, const char *function,
|
|
const char *fmt, ...)
|
|
{
|
|
FILE *fp;
|
|
char *buffer;
|
|
size_t length;
|
|
va_list ap;
|
|
char *tmp_fmt;
|
|
|
|
fp = open_memstream(&buffer, &length);
|
|
if (!fp) {
|
|
return;
|
|
}
|
|
|
|
va_start(ap, fmt);
|
|
if (!ast_strlen_zero(fmt)) {
|
|
size_t fmt_len = strlen(fmt);
|
|
if (fmt[fmt_len - 1] == '\n') {
|
|
tmp_fmt = ast_strdupa(fmt);
|
|
tmp_fmt[fmt_len - 1] = '\0';
|
|
fmt = tmp_fmt;
|
|
}
|
|
}
|
|
vfprintf(fp, fmt, ap);
|
|
fputs(": ", fp);
|
|
ERR_print_errors_fp(fp);
|
|
fclose(fp);
|
|
|
|
if (length) {
|
|
ast_log(level, file, line, function, "%s\n", buffer);
|
|
}
|
|
|
|
ast_std_free(buffer);
|
|
}
|
|
|
|
int crypto_register_x509_extension(const char *oid, const char *short_name,
|
|
const char *long_name)
|
|
{
|
|
int nid = 0;
|
|
|
|
if (ast_strlen_zero(oid) || ast_strlen_zero(short_name) ||
|
|
ast_strlen_zero(long_name)) {
|
|
ast_log(LOG_ERROR, "One or more of oid, short_name or long_name are NULL or empty\n");
|
|
return -1;
|
|
}
|
|
|
|
nid = OBJ_sn2nid(short_name);
|
|
if (nid != NID_undef) {
|
|
ast_log(LOG_NOTICE, "NID %d, object %s already registered\n", nid, short_name);
|
|
return nid;
|
|
}
|
|
|
|
nid = OBJ_create(oid, short_name, long_name);
|
|
if (nid == NID_undef) {
|
|
crypto_log_openssl(LOG_ERROR, "Couldn't register %s X509 extension\n", short_name);
|
|
return -1;
|
|
}
|
|
ast_log(LOG_NOTICE, "Registered object %s as NID %d\n", short_name, nid);
|
|
|
|
return nid;
|
|
}
|
|
|
|
ASN1_OCTET_STRING *crypto_get_cert_extension_data(X509 *cert,
|
|
int nid, const char *short_name)
|
|
{
|
|
int ex_idx;
|
|
X509_EXTENSION *ex;
|
|
|
|
if (nid <= 0) {
|
|
nid = OBJ_sn2nid(short_name);
|
|
if (nid == NID_undef) {
|
|
ast_log(LOG_ERROR, "Extension object for %s not found\n", short_name);
|
|
return NULL;
|
|
}
|
|
} else {
|
|
const char *tmp = OBJ_nid2sn(nid);
|
|
if (!tmp) {
|
|
ast_log(LOG_ERROR, "Extension object for NID %d not found\n", nid);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
ex_idx = X509_get_ext_by_NID(cert, nid, -1);
|
|
if (ex_idx < 0) {
|
|
ast_log(LOG_ERROR, "Extension index not found in certificate\n");
|
|
return NULL;
|
|
}
|
|
ex = X509_get_ext(cert, ex_idx);
|
|
if (!ex) {
|
|
ast_log(LOG_ERROR, "Extension not found in certificate\n");
|
|
return NULL;
|
|
}
|
|
|
|
return X509_EXTENSION_get_data(ex);
|
|
}
|
|
|
|
EVP_PKEY *crypto_load_privkey_from_file(const char *filename)
|
|
{
|
|
EVP_PKEY *key = NULL;
|
|
FILE *fp;
|
|
|
|
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;
|
|
}
|
|
|
|
key = PEM_read_PrivateKey(fp, NULL, NULL, NULL);
|
|
fclose(fp);
|
|
if (!key) {
|
|
crypto_log_openssl(LOG_ERROR, "Failed to load private key from %s\n", filename);
|
|
}
|
|
return key;
|
|
}
|
|
|
|
X509 *crypto_load_cert_from_file(const char *filename)
|
|
{
|
|
FILE *fp;
|
|
X509 *cert = 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;
|
|
}
|
|
|
|
cert = PEM_read_X509(fp, &cert, NULL, NULL);
|
|
fclose(fp);
|
|
if (!cert) {
|
|
crypto_log_openssl(LOG_ERROR, "Failed to create cert from %s\n", filename);
|
|
}
|
|
return cert;
|
|
}
|
|
|
|
X509 *crypto_load_cert_from_memory(const char *buffer, size_t size)
|
|
{
|
|
RAII_VAR(BIO *, bio, NULL, BIO_free_all);
|
|
X509 *cert = NULL;
|
|
|
|
if (ast_strlen_zero(buffer) || size <= 0) {
|
|
ast_log(LOG_ERROR, "buffer was null or empty\n");
|
|
return NULL;
|
|
}
|
|
|
|
bio = BIO_new_mem_buf(buffer, size);
|
|
if (!bio) {
|
|
crypto_log_openssl(LOG_ERROR, "Unable to create memory BIO\n");
|
|
return NULL;
|
|
}
|
|
|
|
cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
|
if (!cert) {
|
|
crypto_log_openssl(LOG_ERROR, "Failed to create cert from BIO\n");
|
|
}
|
|
return cert;
|
|
}
|
|
|
|
static EVP_PKEY *load_private_key_from_memory(const char *buffer, size_t size)
|
|
{
|
|
RAII_VAR(BIO *, bio, NULL, BIO_free_all);
|
|
EVP_PKEY *key = NULL;
|
|
|
|
if (ast_strlen_zero(buffer) || size <= 0) {
|
|
ast_log(LOG_ERROR, "buffer was null or empty\n");
|
|
return NULL;
|
|
}
|
|
|
|
bio = BIO_new_mem_buf(buffer, size);
|
|
if (!bio) {
|
|
crypto_log_openssl(LOG_ERROR, "Unable to create memory BIO\n");
|
|
return NULL;
|
|
}
|
|
|
|
key = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
|
|
|
|
return key;
|
|
}
|
|
|
|
EVP_PKEY *crypto_load_private_key_from_memory(const char *buffer, size_t size)
|
|
{
|
|
EVP_PKEY *key = load_private_key_from_memory(buffer, size);
|
|
if (!key) {
|
|
crypto_log_openssl(LOG_ERROR, "Unable to load private key from memory\n");
|
|
}
|
|
return key;
|
|
}
|
|
|
|
int crypto_has_private_key_from_memory(const char *buffer, size_t size)
|
|
{
|
|
RAII_VAR(EVP_PKEY *, key, load_private_key_from_memory(buffer, size), EVP_PKEY_free);
|
|
|
|
return key ? 1 : 0;
|
|
}
|
|
|
|
static int dump_mem_bio(BIO *bio, unsigned char **buffer)
|
|
{
|
|
char *temp_ptr;
|
|
int raw_key_len;
|
|
|
|
raw_key_len = BIO_get_mem_data(bio, &temp_ptr);
|
|
if (raw_key_len <= 0) {
|
|
crypto_log_openssl(LOG_ERROR, "Unable to extract raw public key\n");
|
|
return -1;
|
|
}
|
|
*buffer = ast_malloc(raw_key_len);
|
|
if (!*buffer) {
|
|
ast_log(LOG_ERROR, "Unable to allocate memory for raw public key\n");
|
|
return -1;
|
|
}
|
|
memcpy(*buffer, temp_ptr, raw_key_len);
|
|
|
|
return raw_key_len;
|
|
}
|
|
|
|
int crypto_extract_raw_pubkey(EVP_PKEY *key, unsigned char **buffer)
|
|
{
|
|
RAII_VAR(BIO *, bio, NULL, BIO_free_all);
|
|
|
|
bio = BIO_new(BIO_s_mem());
|
|
|
|
if (!bio || (PEM_write_bio_PUBKEY(bio, key) <= 0)) {
|
|
crypto_log_openssl(LOG_ERROR, "Unable to write pubkey to BIO\n");
|
|
return -1;
|
|
}
|
|
|
|
return dump_mem_bio(bio, buffer);
|
|
}
|
|
|
|
int crypto_get_raw_pubkey_from_cert(X509 *cert,
|
|
unsigned char **buffer)
|
|
{
|
|
RAII_VAR(BIO *, bio, NULL, BIO_free_all);
|
|
EVP_PKEY *public_key;
|
|
|
|
public_key = X509_get0_pubkey(cert);
|
|
if (!public_key) {
|
|
crypto_log_openssl(LOG_ERROR, "Unable to retrieve pubkey from cert\n");
|
|
return -1;
|
|
}
|
|
|
|
return crypto_extract_raw_pubkey(public_key, buffer);
|
|
}
|
|
|
|
int crypto_extract_raw_privkey(EVP_PKEY *key, unsigned char **buffer)
|
|
{
|
|
RAII_VAR(BIO *, bio, NULL, BIO_free_all);
|
|
|
|
bio = BIO_new(BIO_s_mem());
|
|
|
|
if (!bio || (PEM_write_bio_PrivateKey(bio, key, NULL, NULL, 0, NULL, NULL) <= 0)) {
|
|
crypto_log_openssl(LOG_ERROR, "Unable to write privkey to BIO\n");
|
|
return -1;
|
|
}
|
|
|
|
return dump_mem_bio(bio, buffer);
|
|
}
|
|
|
|
void crypto_free_cert_store(X509_STORE *store)
|
|
{
|
|
if (!store) {
|
|
return;
|
|
}
|
|
X509_STORE_free(store);
|
|
}
|
|
|
|
int crypto_lock_cert_store(X509_STORE *store)
|
|
{
|
|
if (!store) {
|
|
return -1;
|
|
}
|
|
/* lock returns 1 on success */
|
|
return X509_STORE_lock(store) == 1 ? 0 : -1;
|
|
}
|
|
|
|
int crypto_unlock_cert_store(X509_STORE *store)
|
|
{
|
|
if (!store) {
|
|
return -1;
|
|
}
|
|
/* unlock returns 1 on success */
|
|
return X509_STORE_unlock(store) == 1 ? 0 : -1;
|
|
}
|
|
|
|
X509_STORE *crypto_create_cert_store(void)
|
|
{
|
|
X509_STORE *store = X509_STORE_new();
|
|
|
|
if (!store) {
|
|
crypto_log_openssl(LOG_ERROR, "Failed to create X509_STORE\n");
|
|
return NULL;
|
|
}
|
|
|
|
return store;
|
|
}
|
|
|
|
int crypto_load_cert_store(X509_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");
|
|
return -1;
|
|
}
|
|
|
|
if (!store) {
|
|
ast_log(LOG_ERROR, "store is NULL");
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* 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 ''.
|
|
*/
|
|
if (!X509_STORE_load_locations(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"));
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int crypto_show_cli_store(X509_STORE *store, int fd)
|
|
{
|
|
STACK_OF(X509_OBJECT) *certs = NULL;
|
|
int count = 0;
|
|
int i = 0;
|
|
char subj[1024];
|
|
|
|
certs = X509_STORE_get0_objects(store);
|
|
count = sk_X509_OBJECT_num(certs);
|
|
for (i = 0; i < count ; i++) {
|
|
X509_OBJECT *o = sk_X509_OBJECT_value(certs, i);
|
|
X509 *c = X509_OBJECT_get0_X509(o);
|
|
X509_NAME_oneline(X509_get_subject_name(c), subj, 1024);
|
|
ast_cli(fd, "%s\n", subj);
|
|
}
|
|
return count;
|
|
}
|
|
int crypto_is_cert_time_valid(X509*cert, time_t reftime)
|
|
{
|
|
ASN1_STRING *notbefore;
|
|
ASN1_STRING *notafter;
|
|
|
|
if (!reftime) {
|
|
reftime = time(NULL);
|
|
}
|
|
notbefore = X509_get_notBefore(cert);
|
|
notafter = X509_get_notAfter(cert);
|
|
if (!notbefore || !notafter) {
|
|
ast_log(LOG_ERROR, "Either notbefore or notafter were not present in the cert\n");
|
|
return 0;
|
|
}
|
|
|
|
return (X509_cmp_time(notbefore, &reftime) < 0 &&
|
|
X509_cmp_time(notafter, &reftime) > 0);
|
|
}
|
|
|
|
int crypto_is_cert_trusted(X509_STORE *store, X509 *cert, const char **err_msg)
|
|
{
|
|
X509_STORE_CTX *verify_ctx = NULL;
|
|
int rc = 0;
|
|
|
|
if (!(verify_ctx = X509_STORE_CTX_new())) {
|
|
crypto_log_openssl(LOG_ERROR, "Unable to create verify_ctx\n");
|
|
return 0;
|
|
}
|
|
|
|
if (X509_STORE_CTX_init(verify_ctx, store, cert, NULL) != 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;
|
|
}
|
|
|
|
rc = X509_verify_cert(verify_ctx);
|
|
if (rc != 1 && err_msg != NULL) {
|
|
int err = X509_STORE_CTX_get_error(verify_ctx);
|
|
*err_msg = X509_verify_cert_error_string(err);
|
|
}
|
|
X509_STORE_CTX_cleanup(verify_ctx);
|
|
X509_STORE_CTX_free(verify_ctx);
|
|
|
|
return rc;
|
|
}
|
|
|
|
#define SECS_PER_DAY 86400
|
|
time_t crypto_asn_time_as_time_t(ASN1_TIME *at)
|
|
{
|
|
int pday;
|
|
int psec;
|
|
time_t rt = time(NULL);
|
|
|
|
if (!ASN1_TIME_diff(&pday, &psec, NULL, at)) {
|
|
crypto_log_openssl(LOG_ERROR, "Unable to calculate time diff\n");
|
|
return 0;
|
|
}
|
|
|
|
rt += ((pday * SECS_PER_DAY) + psec);
|
|
|
|
return rt;
|
|
}
|
|
#undef SECS_PER_DAY
|
|
|
|
char *crypto_get_cert_subject(X509 *cert, const char *short_name)
|
|
{
|
|
size_t len = 0;
|
|
RAII_VAR(char *, buffer, NULL, ast_std_free);
|
|
char *search_buff = NULL;
|
|
char *search = NULL;
|
|
size_t search_len = 0;
|
|
char *rtn = NULL;
|
|
char *line = NULL;
|
|
/*
|
|
* If short_name was supplied, we want a multiline subject
|
|
* with each component on a separate line. This makes it easier
|
|
* to iterate over the components to find the one we want.
|
|
* Otherwise, we just want the whole subject on one line.
|
|
*/
|
|
unsigned long flags =
|
|
short_name ? XN_FLAG_FN_SN | XN_FLAG_SEP_MULTILINE : XN_FLAG_ONELINE;
|
|
FILE *fp = open_memstream(&buffer, &len);
|
|
BIO *bio = fp ? BIO_new_fp(fp, BIO_CLOSE) : NULL;
|
|
X509_NAME *subject = X509_get_subject_name(cert);
|
|
int rc = 0;
|
|
|
|
if (!fp || !bio || !subject) {
|
|
return NULL;
|
|
}
|
|
|
|
rc = X509_NAME_print_ex(bio, subject, 0, flags);
|
|
BIO_free(bio);
|
|
if (rc < 0) {
|
|
return NULL;
|
|
}
|
|
|
|
if (!short_name) {
|
|
rtn = ast_malloc(len + 1);
|
|
if (rtn) {
|
|
strcpy(rtn, buffer); /* Safe */
|
|
}
|
|
return rtn;
|
|
}
|
|
|
|
search_len = strlen(short_name) + 1;
|
|
rc = ast_asprintf(&search, "%s=", short_name);
|
|
if (rc != search_len) {
|
|
return NULL;
|
|
}
|
|
|
|
search_buff = buffer;
|
|
while((line = ast_read_line_from_buffer(&search_buff))) {
|
|
if (ast_begins_with(line, search)) {
|
|
rtn = ast_malloc(strlen(line) - search_len + 1);
|
|
if (rtn) {
|
|
strcpy(rtn, line + search_len); /* Safe */
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
ast_std_free(search);
|
|
return rtn;
|
|
}
|
|
|
|
int crypto_load(void)
|
|
{
|
|
return AST_MODULE_LOAD_SUCCESS;
|
|
}
|
|
|
|
int crypto_unload(void)
|
|
{
|
|
return 0;
|
|
}
|
|
|