mirror of
				https://github.com/asterisk/asterisk.git
				synced 2025-10-31 02:37:10 +00:00 
			
		
		
		
	Put checks in place to limit how much we will actually download, as well as a check for the data we receive at the start to ensure it begins with what we would expect a certificate to begin with. ASTERISK-29872 Change-Id: Ifd3c6b8bd52b8b6192a04166ccce4fc8a8000b46
		
			
				
	
	
		
			296 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			296 lines
		
	
	
		
			7.3 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 "curl.h"
 | |
| #include "general.h"
 | |
| #include "stir_shaken.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_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);
 | |
| }
 | |
| 
 | |
| 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;
 | |
| }
 | |
| 
 | |
| char *curl_public_key(const char *public_cert_url, const char *path, struct curl_cb_data *data)
 | |
| {
 | |
| 	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;
 | |
| 
 | |
| 	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;
 | |
| 	}
 | |
| 
 | |
| 	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);
 | |
| 
 | |
| 	if (curl_easy_perform(curl)) {
 | |
| 		ast_log(LOG_ERROR, "%s\n", curl_errbuf);
 | |
| 		curl_easy_cleanup(curl);
 | |
| 		ast_free(buf);
 | |
| 		return NULL;
 | |
| 	}
 | |
| 
 | |
| 	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;
 | |
| }
 |