asterisk/res/res_stir_shaken/curl.c
Ben Ford 0724b767a3 AST-2022-002 - res_stir_shaken/curl: Add ACL checks for Identity header.
Adds a new configuration option, stir_shaken_profile, in pjsip.conf that
can be specified on a per endpoint basis. This option will reference a
stir_shaken_profile that can be configured in stir_shaken.conf. The type
of this option must be 'profile'. The stir_shaken option can be
specified on this object with the same values as before (attest, verify,
on), but it cannot be off since having the profile itself implies wanting
STIR/SHAKEN support. You can also specify an ACL from acl.conf (along
with permit and deny lines in the object itself) that will be used to
limit what interfaces Asterisk will attempt to retrieve information from
when reading the Identity header.

ASTERISK-29476

Change-Id: I87fa61f78a9ea0cd42530691a30da3c781842406
2022-04-14 16:58:17 -05:00

352 lines
8.9 KiB
C

/*
* Asterisk -- An open source telephony toolkit.
*
* Copyright (C) 2020, Sangoma Technologies Corporation
*
* Ben Ford <bford@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 "asterisk.h"
#include "asterisk/utils.h"
#include "asterisk/logger.h"
#include "asterisk/file.h"
#include "asterisk/acl.h"
#include "curl.h"
#include "general.h"
#include "stir_shaken.h"
#include "profile.h"
#include <curl/curl.h>
#include <sys/stat.h>
/* Used to check CURL headers */
#define MAX_HEADER_LENGTH 1023
/* Used to limit download size */
#define MAX_DOWNLOAD_SIZE 8192
/* Used to limit how many bytes we get from CURL per write */
#define MAX_BUF_SIZE_PER_WRITE 1024
/* Certificates should begin with this */
#define BEGIN_CERTIFICATE_STR "-----BEGIN CERTIFICATE-----"
/* CURL callback data to avoid storing useless info in AstDB */
struct curl_cb_data {
char *cache_control;
char *expires;
};
struct curl_cb_write_buf {
char buf[MAX_DOWNLOAD_SIZE + 1];
size_t size;
const char *url;
};
struct curl_cb_open_socket {
const struct ast_acl_list *acl;
curl_socket_t *sockfd;
};
struct curl_cb_data *curl_cb_data_create(void)
{
struct curl_cb_data *data;
data = ast_calloc(1, sizeof(*data));
return data;
}
void curl_cb_data_free(struct curl_cb_data *data)
{
if (!data) {
return;
}
ast_free(data->cache_control);
ast_free(data->expires);
ast_free(data);
}
static void curl_cb_open_socket_free(struct curl_cb_open_socket *data)
{
if (!data) {
return;
}
close(*data->sockfd);
/* We don't need to free the ACL since we just use a reference */
ast_free(data);
}
char *curl_cb_data_get_cache_control(const struct curl_cb_data *data)
{
if (!data) {
return NULL;
}
return data->cache_control;
}
char *curl_cb_data_get_expires(const struct curl_cb_data *data)
{
if (!data) {
return NULL;
}
return data->expires;
}
/*!
* \brief Called when a CURL request completes
*
* \param buffer, size, nitems
* \param data The curl_cb_data structure to store expiration info
*/
static size_t curl_header_callback(char *buffer, size_t size, size_t nitems, void *data)
{
struct curl_cb_data *cb_data = data;
size_t realsize;
char *header;
char *value;
realsize = size * nitems;
if (realsize > MAX_HEADER_LENGTH) {
ast_log(LOG_WARNING, "CURL header length is too large (size: '%zu' | max: '%d')\n",
realsize, MAX_HEADER_LENGTH);
return 0;
}
header = ast_alloca(realsize + 1);
memcpy(header, buffer, realsize);
header[realsize] = '\0';
value = strchr(header, ':');
if (!value) {
return realsize;
}
*value++ = '\0';
value = ast_trim_blanks(ast_skip_blanks(value));
if (!strcasecmp(header, "Cache-Control")) {
cb_data->cache_control = ast_strdup(value);
} else if (!strcasecmp(header, "Expires")) {
cb_data->expires = ast_strdup(value);
}
return realsize;
}
/*!
* \brief Prepare a CURL instance to use
*
* \param data The CURL callback data
*
* \retval NULL on failure
* \return CURL instance on success
*/
static CURL *get_curl_instance(struct curl_cb_data *data)
{
CURL *curl;
struct stir_shaken_general *cfg;
unsigned int curl_timeout;
cfg = stir_shaken_general_get();
curl_timeout = ast_stir_shaken_curl_timeout(cfg);
ao2_cleanup(cfg);
curl = curl_easy_init();
if (!curl) {
return NULL;
}
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, curl_timeout);
curl_easy_setopt(curl, CURLOPT_USERAGENT, AST_CURL_USER_AGENT);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_callback);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, data);
return curl;
}
/*!
* \brief Write callback passed to libcurl
*
* \note If this function returns anything other than the size of the data
* libcurl expected us to process, the request will cancel. That's why we return
* 0 on error, otherwise the amount of data we were given
*
* \param curl_data The data from libcurl
* \param size Always 1 according to libcurl
* \param actual_size The actual size of the data
* \param our_data The data we passed to libcurl
*
* \retval The size of the data we processed
* \retval 0 if there was an error
*/
static size_t curl_write_cb(void *curl_data, size_t size, size_t actual_size, void *our_data)
{
/* Just in case size is NOT always 1 or if it's changed in the future, let's go ahead
* and do the math for the actual size */
size_t real_size = size * actual_size;
struct curl_cb_write_buf *buf = our_data;
size_t new_size = buf->size + real_size;
if (new_size > MAX_DOWNLOAD_SIZE) {
ast_log(LOG_WARNING, "Attempted to retrieve certificate from %s failed "
"because it's size exceeds the maximum %d bytes\n", buf->url, MAX_DOWNLOAD_SIZE);
return 0;
}
memcpy(&(buf->buf[buf->size]), curl_data, real_size);
buf->size += real_size;
buf->buf[buf->size] = 0;
return real_size;
}
static curl_socket_t stir_shaken_curl_open_socket_callback(void *our_data, curlsocktype purpose, struct curl_sockaddr *address)
{
struct curl_cb_open_socket *data = our_data;
if (!ast_acl_list_is_empty((struct ast_acl_list *)data->acl)) {
struct ast_sockaddr ast_address = { {0,} };
ast_sockaddr_copy_sockaddr(&ast_address, &address->addr, address->addrlen);
if (ast_apply_acl((struct ast_acl_list *)data->acl, &ast_address, NULL) != AST_SENSE_ALLOW) {
return CURLE_COULDNT_CONNECT;
}
}
*data->sockfd = socket(address->family, address->socktype, address->protocol);
return *data->sockfd;
}
char *curl_public_key(const char *public_cert_url, const char *path, struct curl_cb_data *data, const struct ast_acl_list *acl)
{
FILE *public_key_file;
char *filename;
char *serial;
long http_code;
CURL *curl;
char curl_errbuf[CURL_ERROR_SIZE + 1];
struct curl_cb_write_buf *buf;
struct curl_cb_open_socket *open_socket_data;
curl_socket_t sockfd;
curl_errbuf[CURL_ERROR_SIZE] = '\0';
buf = ast_calloc(1, sizeof(*buf));
if (!buf) {
ast_log(LOG_ERROR, "Failed to allocate memory for CURL write buffer for %s\n", public_cert_url);
return NULL;
}
open_socket_data = ast_calloc(1, sizeof(*open_socket_data));
if (!open_socket_data) {
ast_log(LOG_ERROR, "Failed to allocate memory for open socket callback\n");
return NULL;
}
open_socket_data->acl = acl;
open_socket_data->sockfd = &sockfd;
buf->url = public_cert_url;
curl_errbuf[CURL_ERROR_SIZE] = '\0';
curl = get_curl_instance(data);
if (!curl) {
ast_log(LOG_ERROR, "Failed to set up CURL instance for '%s'\n", public_cert_url);
ast_free(buf);
return NULL;
}
curl_easy_setopt(curl, CURLOPT_URL, public_cert_url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, buf);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, MAX_BUF_SIZE_PER_WRITE);
curl_easy_setopt(curl, CURLOPT_OPENSOCKETFUNCTION, stir_shaken_curl_open_socket_callback);
curl_easy_setopt(curl, CURLOPT_OPENSOCKETDATA, open_socket_data);
if (curl_easy_perform(curl)) {
ast_log(LOG_ERROR, "%s\n", curl_errbuf);
curl_easy_cleanup(curl);
ast_free(buf);
curl_cb_open_socket_free(open_socket_data);
return NULL;
}
curl_cb_open_socket_free(open_socket_data);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
curl_easy_cleanup(curl);
if (http_code / 100 != 2) {
ast_log(LOG_ERROR, "Failed to retrieve URL '%s': code %ld\n", public_cert_url, http_code);
ast_free(buf);
return NULL;
}
if (!ast_begins_with(buf->buf, BEGIN_CERTIFICATE_STR)) {
ast_log(LOG_WARNING, "Certificate from %s does not begin with what we expect\n", public_cert_url);
ast_free(buf);
return NULL;
}
serial = stir_shaken_get_serial_number_x509(buf->buf, buf->size);
if (!serial) {
ast_log(LOG_ERROR, "Failed to get serial from CURL buffer from %s\n", public_cert_url);
ast_free(buf);
return NULL;
}
if (ast_asprintf(&filename, "%s/%s.pem", path, serial) < 0) {
ast_log(LOG_ERROR, "Failed to allocate memory for filename after CURL from %s\n", public_cert_url);
ast_free(serial);
ast_free(buf);
return NULL;
}
ast_free(serial);
public_key_file = fopen(filename, "w");
if (!public_key_file) {
ast_log(LOG_ERROR, "Failed to open file '%s' to write public key from '%s': %s (%d)\n",
filename, public_cert_url, strerror(errno), errno);
ast_free(buf);
ast_free(filename);
return NULL;
}
if (fputs(buf->buf, public_key_file) == EOF) {
ast_log(LOG_ERROR, "Failed to write string to file from URL %s\n", public_cert_url);
fclose(public_key_file);
ast_free(buf);
ast_free(filename);
return NULL;
}
fclose(public_key_file);
ast_free(buf);
return filename;
}