STIR/SHAKEN: Fix certificate type and storage.

During OpenSIPit, we found out that the public certificates must be of
type X.509. When reading in public keys, we use the corresponding X.509
functions now.

We also discovered that we needed a better naming scheme for the
certificates since certificates with the same name would cause issues
(overwriting certs, etc.). Now when we download a public certificate, we
get the serial number from it and use that as the name of the cached
certificate.

The configuration option public_key_url in stir_shaken.conf has also
been renamed to public_cert_url, which better describes what the option
is for.

https://wiki.asterisk.org/wiki/display/AST/OpenSIPit+2021

Change-Id: Ia00b20835f5f976e3603797f2f2fb19672d8114d
This commit is contained in:
Ben Ford 2021-04-21 11:12:55 -05:00 committed by George Joseph
parent 40bdfff73b
commit 5e6508b56f
12 changed files with 376 additions and 163 deletions

View File

@ -2,6 +2,29 @@
; This file is used by the res_stir_shaken module to configure parameters
; used for STIR/SHAKEN.
;
; There are 2 sides to STIR/SHAKEN: attestation and verification.
;
; Attestation is done on outgoing calls and makes use out of the certificate
; objects. The cert located at path will be used to sign, and the cert
; located at public_cert_url will be placed in the Identity header to let the
; remote side know where to download the public cert from. These 2 certs must
; match; that is, the cert located at public_cert_url must be the public cert
; derived from the private cert located at path.
;
; Verification is done on incoming calls and doesn't rely on cert objects
; defined in this file.
;
; The general section applies to all STIR/SHAKEN operations. However,
; cache_max_size, curl_timeout, and signature_timeout only apply to the
; verification side.
;
; It's important to note that downloaded certificates are stored in
; <ast_config_AST_DATA_DIR>/keys/stir_shaken, which is usually
; /etc/asterisk/keys/stir_shaken, but may be changed depending on where your
; config directory is.
;
; Visit the wiki page:
; https://wiki.asterisk.org/wiki/display/AST/STIR+and+SHAKEN
;
; [general]
;
@ -33,9 +56,11 @@
; Path to a directory containing certificates
;path=/etc/asterisk/stir
;
; URL to the public key(s). Must contain variable '${CERTIFICATE}' used for
; substitution
;public_key_url=http://mycompany.com/${CERTIFICATE}.pub
; URL to the public certificate(s). Must contain variable '${CERTIFICATE}' used for
; substitution. '${CERTIFICATE}' will be replaced by the names of the files located
; at path.
; This will be put in the Identity header when signing.
;public_cert_url=http://mycompany.com/${CERTIFICATE}.pem
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
@ -45,11 +70,13 @@
; type must be "certificate"
;type=certificate
;
; File path to a certificate
;path=/etc/asterisk/stir/alice.crt
; File path to a certificate. This can be RSA or ECDSA, but eventually only ECDSA will be supported.
;path=/etc/asterisk/stir/alice.pem
;
; URL to the public key
;public_key_url=http://mycompany.com/alice.pub
; URL to the public certificate. Must be of type X509 and be derived from the
; certificate located at path.
; This will be put in the identity header when signing.
;public_cert_url=http://mycompany.com/alice.pem
;
; The caller ID number to match on
;caller_id_number=1234567

View File

@ -0,0 +1,6 @@
Subject: STIR/SHAKEN
The configuration option public_key_url in stir_shaken.conf
has been renamed to public_cert_url to better fit what it
contains. Only the name has changed - functionality is the
same.

View File

@ -43,13 +43,13 @@ struct ast_json;
unsigned char *ast_stir_shaken_payload_get_signature(const struct ast_stir_shaken_payload *payload);
/*!
* \brief Retrieve the value for 'public_key_url' from an ast_stir_shaken_payload
* \brief Retrieve the value for 'public_cert_url' from an ast_stir_shaken_payload
*
* \param payload The payload
*
* \retval The public key URL
*/
char *ast_stir_shaken_payload_get_public_key_url(const struct ast_stir_shaken_payload *payload);
char *ast_stir_shaken_payload_get_public_cert_url(const struct ast_stir_shaken_payload *payload);
/*!
* \brief Retrieve the value for 'signature_timeout' from 'general' config object
@ -79,13 +79,13 @@ int ast_stir_shaken_add_verification(struct ast_channel *chan, const char *ident
* \param payload The payload section
* \param signature The payload signature
* \param algorithm The signature algorithm
* \param public_key_url The public key URL
* \param public_cert_url The public key URL
*
* \retval ast_stir_shaken_payload on success
* \retval NULL on failure
*/
struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const char *payload, const char *signature,
const char *algorithm, const char *public_key_url);
const char *algorithm, const char *public_cert_url);
/*!
* \brief Retrieve the stir/shaken sorcery context

View File

@ -130,7 +130,7 @@ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_r
RAII_VAR(char *, payload, NULL, ast_free);
char *signature;
char *algorithm;
char *public_key_url;
char *public_cert_url;
char *attestation;
int mismatch = 0;
struct ast_stir_shaken_payload *ss_payload;
@ -168,8 +168,8 @@ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_r
/* Trim "info=<" to get public key URL */
strtok_r(identity_hdr_val, "<", &identity_hdr_val);
public_key_url = strtok_r(identity_hdr_val, ">", &identity_hdr_val);
if (ast_strlen_zero(public_key_url)) {
public_cert_url = strtok_r(identity_hdr_val, ">", &identity_hdr_val);
if (ast_strlen_zero(public_cert_url)) {
ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED);
return 0;
}
@ -182,7 +182,7 @@ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_r
attestation = get_attestation_from_payload(payload);
ss_payload = ast_stir_shaken_verify(header, payload, signature, algorithm, public_key_url);
ss_payload = ast_stir_shaken_verify(header, payload, signature, algorithm, public_cert_url);
if (!ss_payload) {
ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED);
return 0;
@ -209,7 +209,7 @@ static void add_identity_header(const struct ast_sip_session *session, pjsip_tx_
pj_str_t identity_val;
pjsip_fromto_hdr *old_identity;
char *signature;
char *public_key_url;
char *public_cert_url;
struct ast_json *header;
struct ast_json *payload;
char *dumped_string;
@ -258,13 +258,13 @@ static void add_identity_header(const struct ast_sip_session *session, pjsip_tx_
}
signature = (char *)ast_stir_shaken_payload_get_signature(ss_payload);
public_key_url = ast_stir_shaken_payload_get_public_key_url(ss_payload);
public_cert_url = ast_stir_shaken_payload_get_public_cert_url(ss_payload);
/* The format for the identity header:
* header.payload.signature;info=<public_key_url>alg=STIR_SHAKEN_ENCRYPTION_ALGORITHM;ppt=STIR_SHAKEN_PPT
* header.payload.signature;info=<public_cert_url>alg=STIR_SHAKEN_ENCRYPTION_ALGORITHM;ppt=STIR_SHAKEN_PPT
*/
combined_size = strlen(encoded_header) + 1 + strlen(encoded_payload) + 1
+ strlen(signature) + strlen(";info=<>alg=;ppt=") + strlen(public_key_url)
+ strlen(signature) + strlen(";info=<>alg=;ppt=") + strlen(public_cert_url)
+ strlen(STIR_SHAKEN_ENCRYPTION_ALGORITHM) + strlen(STIR_SHAKEN_PPT) + 1;
combined_str = ast_calloc(1, combined_size);
if (!combined_str) {
@ -272,7 +272,7 @@ static void add_identity_header(const struct ast_sip_session *session, pjsip_tx_
return;
}
snprintf(combined_str, combined_size, "%s.%s.%s;info=<%s>alg=%s;ppt=%s", encoded_header,
encoded_payload, signature, public_key_url, STIR_SHAKEN_ENCRYPTION_ALGORITHM, STIR_SHAKEN_PPT);
encoded_payload, signature, public_cert_url, STIR_SHAKEN_ENCRYPTION_ALGORITHM, STIR_SHAKEN_PPT);
identity_val = pj_str(combined_str);
identity_hdr = pjsip_generic_string_hdr_create(tdata->pool, &identity_str, &identity_val);

View File

@ -79,8 +79,8 @@
<configOption name="path" default="">
<synopsis>Path to a directory containing certificates</synopsis>
</configOption>
<configOption name="public_key_url" default="">
<synopsis>URL to the public key(s)</synopsis>
<configOption name="public_cert_url" default="">
<synopsis>URL to the public certificate(s)</synopsis>
<description><para>
Must be a valid http, or https, URL. The URL must also contain the ${CERTIFICATE} variable, which is used for public key name substitution.
For example: http://mycompany.com/${CERTIFICATE}.pub
@ -95,8 +95,8 @@
<configOption name="path" default="">
<synopsis>File path to a certificate</synopsis>
</configOption>
<configOption name="public_key_url" default="">
<synopsis>URL to the public key</synopsis>
<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>
@ -169,8 +169,8 @@ struct ast_stir_shaken_payload {
unsigned char *signature;
/*! The algorithm used */
char *algorithm;
/*! THe URL to the public key for the certificate */
char *public_key_url;
/*! THe URL to the public certificate */
char *public_cert_url;
};
struct ast_sorcery *ast_stir_shaken_sorcery(void)
@ -187,7 +187,7 @@ void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload)
ast_json_unref(payload->header);
ast_json_unref(payload->payload);
ast_free(payload->algorithm);
ast_free(payload->public_key_url);
ast_free(payload->public_cert_url);
ast_free(payload->signature);
ast_free(payload);
@ -198,9 +198,9 @@ unsigned char *ast_stir_shaken_payload_get_signature(const struct ast_stir_shake
return payload ? payload->signature : NULL;
}
char *ast_stir_shaken_payload_get_public_key_url(const struct ast_stir_shaken_payload *payload)
char *ast_stir_shaken_payload_get_public_cert_url(const struct ast_stir_shaken_payload *payload)
{
return payload ? payload->public_key_url : NULL;
return payload ? payload->public_cert_url : NULL;
}
unsigned int ast_stir_shaken_get_signature_timeout(void)
@ -349,17 +349,17 @@ int ast_stir_shaken_add_verification(struct ast_channel *chan, const char *ident
* \brief Sets the expiration for the public key based on the provided fields.
* If Cache-Control is present, use it. Otherwise, use Expires.
*
* \param hash The hash for the public key URL
* \param public_cert_url The URL to the public certificate
* \param data The CURL callback data containing expiration data
*/
static void set_public_key_expiration(const char *public_key_url, const struct curl_cb_data *data)
static void set_public_key_expiration(const char *public_cert_url, const struct curl_cb_data *data)
{
char time_buf[32];
char *value;
struct timeval actual_expires = ast_tvnow();
char hash[41];
ast_sha1_hash(hash, public_key_url);
ast_sha1_hash(hash, public_cert_url);
value = curl_cb_data_get_cache_control(data);
if (!ast_strlen_zero(value)) {
@ -400,19 +400,19 @@ static void set_public_key_expiration(const char *public_key_url, const struct c
/*!
* \brief Check to see if the public key is expired
*
* \param public_key_url The public key URL
* \param public_cert_url The public cert URL
*
* \retval 1 if expired
* \retval 0 if not expired
*/
static int public_key_is_expired(const char *public_key_url)
static int public_key_is_expired(const char *public_cert_url)
{
struct timeval current_time = ast_tvnow();
struct timeval expires = { .tv_sec = 0, .tv_usec = 0 };
char expiration[32];
char hash[41];
ast_sha1_hash(hash, public_key_url);
ast_sha1_hash(hash, public_cert_url);
ast_db_get(hash, "expiration", expiration, sizeof(expiration));
if (ast_strlen_zero(expiration)) {
@ -429,17 +429,17 @@ static int public_key_is_expired(const char *public_key_url)
/*!
* \brief Returns the path to the downloaded file for the provided URL
*
* \param public_key_url The public key URL
* \param public_cert_url The public cert URL
*
* \retval Empty string if not present in AstDB
* \retval The file path if present in AstDB
*/
static char *get_path_to_public_key(const char *public_key_url)
static char *get_path_to_public_key(const char *public_cert_url)
{
char hash[41];
char file_path[MAX_PATH_LEN];
ast_sha1_hash(hash, public_key_url);
ast_sha1_hash(hash, public_cert_url);
ast_db_get(hash, "path", file_path, sizeof(file_path));
@ -453,30 +453,30 @@ static char *get_path_to_public_key(const char *public_key_url)
/*!
* \brief Add the public key details and file path to AstDB
*
* \param public_key_url The public key URL
* \param public_cert_url The public cert URL
* \param filepath The path to the file
*/
static void add_public_key_to_astdb(const char *public_key_url, const char *filepath)
static void add_public_key_to_astdb(const char *public_cert_url, const char *filepath)
{
char hash[41];
ast_sha1_hash(hash, public_key_url);
ast_sha1_hash(hash, public_cert_url);
ast_db_put(AST_DB_FAMILY, public_key_url, hash);
ast_db_put(AST_DB_FAMILY, public_cert_url, hash);
ast_db_put(hash, "path", filepath);
}
/*!
* \brief Remove the public key details and associated information from AstDB
*
* \param public_key_url The public key URL
* \param public_cert_url The public cert URL
*/
static void remove_public_key_from_astdb(const char *public_key_url)
static void remove_public_key_from_astdb(const char *public_cert_url)
{
char hash[41];
char filepath[MAX_PATH_LEN];
ast_sha1_hash(hash, public_key_url);
ast_sha1_hash(hash, public_cert_url);
/* Remove this public key from storage */
ast_db_get(hash, "path", filepath, sizeof(filepath));
@ -484,7 +484,7 @@ static void remove_public_key_from_astdb(const char *public_key_url)
/* Remove the actual file from the system */
remove(filepath);
ast_db_del(AST_DB_FAMILY, public_key_url);
ast_db_del(AST_DB_FAMILY, public_cert_url);
ast_db_deltree(hash, NULL);
}
@ -554,77 +554,87 @@ static int stir_shaken_verify_signature(const char *msg, const char *signature,
}
/*!
* \brief CURL the file located at public_key_url to the specified path
* \brief CURL the file located at public_cert_url to the specified path
*
* \param public_key_url The public key URL
* \note filename will need to be freed by the caller
*
* \param public_cert_url The public cert URL
* \param path The path to download the file to
*
* \retval -1 on failure
* \retval 0 on success
* \retval NULL on failure
* \retval full path filename on success
*/
static int run_curl(const char *public_key_url, const char *path)
static char *run_curl(const char *public_cert_url, const char *path)
{
struct curl_cb_data *data;
char *filename;
data = curl_cb_data_create();
if (!data) {
ast_log(LOG_ERROR, "Failed to create CURL callback data\n");
return -1;
return NULL;
}
if (curl_public_key(public_key_url, path, data)) {
ast_log(LOG_ERROR, "Could not retrieve public key for '%s'\n", public_key_url);
filename = curl_public_key(public_cert_url, path, data);
if (!filename) {
ast_log(LOG_ERROR, "Could not retrieve public key for '%s'\n", public_cert_url);
curl_cb_data_free(data);
return -1;
return NULL;
}
set_public_key_expiration(public_key_url, data);
set_public_key_expiration(public_cert_url, data);
curl_cb_data_free(data);
return 0;
return filename;
}
/*!
* \brief Downloads the public key from public_key_url. If curl is non-zero, that signals
* \brief Downloads the public cert from public_cert_url. If curl is non-zero, that signals
* CURL has already been run, and we should bail here. The entry is added to AstDB as well.
*
* \param public_key_url The public key URL
* \note filename will need to be freed by the caller
*
* \param public_cert_url The public cert URL
* \param path The path to download the file to
* \param curl Flag signaling if we have run CURL or not
*
* \retval -1 on failure
* \retval 0 on success
* \retval NULL on failure
* \retval full path filename on success
*/
static int curl_and_check_expiration(const char *public_key_url, const char *path, int *curl)
static char *curl_and_check_expiration(const char *public_cert_url, const char *path, int *curl)
{
char *filename;
if (curl) {
ast_log(LOG_ERROR, "Already downloaded public key '%s'\n", path);
return -1;
return NULL;
}
if (run_curl(public_key_url, path)) {
return -1;
filename = run_curl(public_cert_url, path);
if (!filename) {
return NULL;
}
if (public_key_is_expired(public_key_url)) {
if (public_key_is_expired(public_cert_url)) {
ast_log(LOG_ERROR, "Newly downloaded public key '%s' is expired\n", path);
return -1;
ast_free(filename);
return NULL;
}
*curl = 1;
add_public_key_to_astdb(public_key_url, path);
add_public_key_to_astdb(public_cert_url, filename);
return 0;
return filename;
}
struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const char *payload, const char *signature,
const char *algorithm, const char *public_key_url)
const char *algorithm, const char *public_cert_url)
{
struct ast_stir_shaken_payload *ret_payload;
EVP_PKEY *public_key;
char *filename;
int curl = 0;
RAII_VAR(char *, file_path, NULL, ast_free);
RAII_VAR(char *, dir_path, NULL, ast_free);
RAII_VAR(char *, combined_str, NULL, ast_free);
size_t combined_size;
@ -648,41 +658,39 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const
return NULL;
}
if (ast_strlen_zero(public_key_url)) {
ast_log(LOG_ERROR, "'public_key_url' is required for STIR/SHAKEN verification\n");
if (ast_strlen_zero(public_cert_url)) {
ast_log(LOG_ERROR, "'public_cert_url' is required for STIR/SHAKEN verification\n");
return NULL;
}
/* Check to see if we have already downloaded this public key. The reason we
/* Check to see if we have already downloaded this public cert. The reason we
* store the file path is because:
*
* 1. If, for some reason, the default directory changes, we still know where
* to look for the files we already have.
*
* 2. In the future, if we want to add a way to store the keys in multiple
* 2. In the future, if we want to add a way to store the certs in multiple
* {configurable) directories, we already have the storage mechanism in place.
* The only thing that would be left to do is pull from the configuration.
*/
file_path = get_path_to_public_key(public_key_url);
file_path = get_path_to_public_key(public_cert_url);
if (ast_asprintf(&dir_path, "%s/keys/%s", ast_config_AST_DATA_DIR, STIR_SHAKEN_DIR_NAME) < 0) {
return NULL;
}
/* If we don't have an entry in AstDB, CURL from the provided URL */
if (ast_strlen_zero(file_path)) {
/* Remove this entry from the database, since we will be
* downloading a new file anyways.
*/
remove_public_key_from_astdb(public_key_url);
remove_public_key_from_astdb(public_cert_url);
/* Go ahead and free file_path, in case anything was allocated above */
ast_free(file_path);
/* Set up the default path */
filename = basename(public_key_url);
if (ast_asprintf(&file_path, "%s/keys/%s/%s", ast_config_AST_DATA_DIR, STIR_SHAKEN_DIR_NAME, filename) < 0) {
return NULL;
}
/* Download to the default path */
if (run_curl(public_key_url, file_path)) {
file_path = run_curl(public_cert_url, dir_path);
if (!file_path) {
return NULL;
}
@ -692,18 +700,20 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const
/* We should have a successful download at this point, so
* add an entry to the database.
*/
add_public_key_to_astdb(public_key_url, file_path);
add_public_key_to_astdb(public_cert_url, file_path);
}
/* Check to see if the key we downloaded (or already had) is expired */
if (public_key_is_expired(public_key_url)) {
/* Check to see if the cert we downloaded (or already had) is expired */
if (public_key_is_expired(public_cert_url)) {
ast_debug(3, "Public key '%s' is expired\n", public_key_url);
ast_debug(3, "Public cert '%s' is expired\n", public_cert_url);
remove_public_key_from_astdb(public_key_url);
remove_public_key_from_astdb(public_cert_url);
/* If this fails, then there's nothing we can do */
if (curl_and_check_expiration(public_key_url, file_path, &curl)) {
ast_free(file_path);
file_path = curl_and_check_expiration(public_cert_url, dir_path, &curl);
if (!file_path) {
return NULL;
}
}
@ -715,16 +725,18 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const
ast_debug(3, "Failed first read of public key file '%s'\n", file_path);
remove_public_key_from_astdb(public_key_url);
remove_public_key_from_astdb(public_cert_url);
if (curl_and_check_expiration(public_key_url, file_path, &curl)) {
ast_free(file_path);
file_path = curl_and_check_expiration(public_cert_url, dir_path, &curl);
if (!file_path) {
return NULL;
}
public_key = stir_shaken_read_key(file_path, 0);
if (!public_key) {
ast_log(LOG_ERROR, "Failed to read public key from '%s'\n", file_path);
remove_public_key_from_astdb(public_key_url);
remove_public_key_from_astdb(public_cert_url);
return NULL;
}
}
@ -769,7 +781,7 @@ struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const
ret_payload->signature = (unsigned char *)ast_strdup(signature);
ret_payload->algorithm = ast_strdup(algorithm);
ret_payload->public_key_url = ast_strdup(public_key_url);
ret_payload->public_cert_url = ast_strdup(public_cert_url);
return ret_payload;
}
@ -1043,7 +1055,7 @@ struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json)
{
struct ast_stir_shaken_payload *ss_payload;
unsigned char *signature;
const char *public_key_url;
const char *public_cert_url;
const char *caller_id_num;
const char *header;
const char *payload;
@ -1073,12 +1085,12 @@ struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json)
goto cleanup;
}
public_key_url = stir_shaken_certificate_get_public_key_url(cert);
if (stir_shaken_add_x5u(json, public_key_url)) {
ast_log(LOG_ERROR, "Failed to add 'x5u' (public key URL) to payload\n");
public_cert_url = stir_shaken_certificate_get_public_cert_url(cert);
if (stir_shaken_add_x5u(json, public_cert_url)) {
ast_log(LOG_ERROR, "Failed to add 'x5u' (public cert URL) to payload\n");
goto cleanup;
}
ss_payload->public_key_url = ast_strdup(public_key_url);
ss_payload->public_cert_url = ast_strdup(public_cert_url);
if (stir_shaken_add_attest(json, stir_shaken_certificate_get_attestation(cert))) {
ast_log(LOG_ERROR, "Failed to add 'attest' to payload\n");
@ -1253,14 +1265,14 @@ static struct ast_custom_function stir_shaken_function = {
#ifdef TEST_FRAMEWORK
static void test_stir_shaken_add_fake_astdb_entry(const char *public_key_url, const char *file_path)
static void test_stir_shaken_add_fake_astdb_entry(const char *public_cert_url, const char *file_path)
{
struct timeval expires = ast_tvnow();
char time_buf[32];
char hash[41];
ast_sha1_hash(hash, public_key_url);
add_public_key_to_astdb(public_key_url, file_path);
ast_sha1_hash(hash, public_cert_url);
add_public_key_to_astdb(public_cert_url, file_path);
snprintf(time_buf, sizeof(time_buf), "%30lu", expires.tv_sec + 300);
ast_db_put(hash, "expiration", time_buf);
@ -1283,15 +1295,23 @@ static int test_stir_shaken_write_temp_key(char *file_path, int private)
char *type = private ? "private" : "public";
char *private_data =
"-----BEGIN EC PRIVATE KEY-----\n"
"MHcCAQEEIFkNGlrmRky2j7wmjGBGoPFBsyEQELmEYN02BiiG508noAoGCCqGSM49\n"
"AwEHoUQDQgAECwCaeAYwVG/FAnEnkwaucz6o047iSWq3cJBBUc0n2ZlUDr5VywAz\n"
"MZ86EthIqF3CGZjhLHn0xRITXYwfqTtWBw==\n"
"MHcCAQEEIC+xv2GKNTDd81vJM8rwGAGNqgklKKxz9Qejn+pcRPC1oAoGCCqGSM49\n"
"AwEHoUQDQgAEq12QXu8lH295ZMZ4udKy5VV8wVgE4qSOnkdofn3hEDsh6QTKTZg9\n"
"W6PncYAVnmOFRL4cTGRbmAIShN4naZk2Yg==\n"
"-----END EC PRIVATE KEY-----";
char *public_data =
"-----BEGIN PUBLIC KEY-----\n"
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECwCaeAYwVG/FAnEnkwaucz6o047i\n"
"SWq3cJBBUc0n2ZlUDr5VywAzMZ86EthIqF3CGZjhLHn0xRITXYwfqTtWBw==\n"
"-----END PUBLIC KEY-----";
"-----BEGIN CERTIFICATE-----\n"
"MIIBzDCCAXGgAwIBAgIUXDt6EC0OixT1iRSSPV3jB/zQAlQwCgYIKoZIzj0EAwIw\n"
"RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu\n"
"dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA0MTMwNjM3MjRaFw0yMzA3MTcw\n"
"NjM3MjRaMGoxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJWQTESMBAGA1UEBwwJU29t\n"
"ZXdoZXJlMRowGAYDVQQKDBFBY21lVGVsZWNvbSwgSW5jLjENMAsGA1UECwwEVk9J\n"
"UDEPMA0GA1UEAwwGU0hBS0VOMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq12Q\n"
"Xu8lH295ZMZ4udKy5VV8wVgE4qSOnkdofn3hEDsh6QTKTZg9W6PncYAVnmOFRL4c\n"
"TGRbmAIShN4naZk2YqMaMBgwFgYIKwYBBQUHARoECjAIoAYWBDEwMDEwCgYIKoZI\n"
"zj0EAwIDSQAwRgIhAMa9Ky38DgVaIgVm9Mgws/qN3zxjMQXfxEExAbDwyq/WAiEA\n"
"zbC29mvtSulwbvQJ4fBdFU84cFC3Ctu1QrCeFOiZHc4=\n"
"-----END CERTIFICATE-----";
fd = mkstemp(file_path);
if (fd < 0) {
@ -1302,6 +1322,7 @@ static int test_stir_shaken_write_temp_key(char *file_path, int private)
file = fdopen(fd, "w");
if (!file) {
ast_log(LOG_ERROR, "Failed to create temp %s key file: %s\n", type, strerror(errno));
close(fd);
return -1;
}
@ -1478,7 +1499,7 @@ AST_TEST_DEFINE(test_stir_shaken_sign)
AST_TEST_DEFINE(test_stir_shaken_verify)
{
char *caller_id_number = "1234567";
char *public_key_url = "http://testing123";
char *public_cert_url = "http://testing123";
char *header;
char *payload;
struct ast_json *tmp_json;
@ -1511,7 +1532,7 @@ AST_TEST_DEFINE(test_stir_shaken_verify)
/* Get the signature */
json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg",
STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE,
"x5u", public_key_url, "payload", "orig", "tn", caller_id_number);
"x5u", public_cert_url, "payload", "orig", "tn", caller_id_number);
signed_payload = ast_stir_shaken_sign(json);
if (!signed_payload) {
ast_test_status_update(test, "Failed to sign a valid JWT\n");
@ -1527,7 +1548,7 @@ AST_TEST_DEFINE(test_stir_shaken_verify)
/* Test empty header parameter */
returned_payload = ast_stir_shaken_verify("", payload, (const char *)signed_payload->signature,
STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url);
STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_cert_url);
if (returned_payload) {
ast_test_status_update(test, "Verified a signature with missing 'header'\n");
test_stir_shaken_cleanup_cert(caller_id_number);
@ -1536,7 +1557,7 @@ AST_TEST_DEFINE(test_stir_shaken_verify)
/* Test empty payload parameter */
returned_payload = ast_stir_shaken_verify(header, "", (const char *)signed_payload->signature,
STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url);
STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_cert_url);
if (returned_payload) {
ast_test_status_update(test, "Verified a signature with missing 'payload'\n");
test_stir_shaken_cleanup_cert(caller_id_number);
@ -1545,7 +1566,7 @@ AST_TEST_DEFINE(test_stir_shaken_verify)
/* Test empty signature parameter */
returned_payload = ast_stir_shaken_verify(header, payload, "",
STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url);
STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_cert_url);
if (returned_payload) {
ast_test_status_update(test, "Verified a signature with missing 'signature'\n");
test_stir_shaken_cleanup_cert(caller_id_number);
@ -1554,7 +1575,7 @@ AST_TEST_DEFINE(test_stir_shaken_verify)
/* Test empty algorithm parameter */
returned_payload = ast_stir_shaken_verify(header, payload, (const char *)signed_payload->signature,
"", public_key_url);
"", public_cert_url);
if (returned_payload) {
ast_test_status_update(test, "Verified a signature with missing 'algorithm'\n");
test_stir_shaken_cleanup_cert(caller_id_number);
@ -1571,19 +1592,19 @@ AST_TEST_DEFINE(test_stir_shaken_verify)
}
/* Trick the function into thinking we've already downloaded the key */
test_stir_shaken_add_fake_astdb_entry(public_key_url, public_path);
test_stir_shaken_add_fake_astdb_entry(public_cert_url, public_path);
/* Verify a valid signature */
returned_payload = ast_stir_shaken_verify(header, payload, (const char *)signed_payload->signature,
STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url);
STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_cert_url);
if (!returned_payload) {
ast_test_status_update(test, "Failed to verify a valid signature\n");
remove_public_key_from_astdb(public_key_url);
remove_public_key_from_astdb(public_cert_url);
test_stir_shaken_cleanup_cert(caller_id_number);
return AST_TEST_FAIL;
}
remove_public_key_from_astdb(public_key_url);
remove_public_key_from_astdb(public_cert_url);
test_stir_shaken_cleanup_cert(caller_id_number);

View File

@ -34,8 +34,8 @@ struct stir_shaken_certificate {
AST_DECLARE_STRING_FIELDS(
/*! Path to a directory containing certificates */
AST_STRING_FIELD(path);
/*! URL to the public key */
AST_STRING_FIELD(public_key_url);
/*! URL to the public certificate */
AST_STRING_FIELD(public_cert_url);
/*! The caller ID number associated with the certificate */
AST_STRING_FIELD(caller_id_number);
/*! The attestation level for this certificate */
@ -95,9 +95,9 @@ struct stir_shaken_certificate *stir_shaken_certificate_get_by_caller_id_number(
"certificate", AST_RETRIEVE_FLAG_DEFAULT, &fields);
}
const char *stir_shaken_certificate_get_public_key_url(struct stir_shaken_certificate *cert)
const char *stir_shaken_certificate_get_public_cert_url(struct stir_shaken_certificate *cert)
{
return cert ? cert->public_key_url : NULL;
return cert ? cert->public_cert_url : NULL;
}
const char *stir_shaken_certificate_get_attestation(struct stir_shaken_certificate *cert)
@ -234,23 +234,23 @@ static int path_to_str(const void *obj, const intptr_t *args, char **buf)
return 0;
}
static int on_load_public_key_url(const struct aco_option *opt, struct ast_variable *var, void *obj)
static int on_load_public_cert_url(const struct aco_option *opt, struct ast_variable *var, void *obj)
{
struct stir_shaken_certificate *cfg = obj;
if (!ast_begins_with(var->value, "http")) {
ast_log(LOG_ERROR, "stir/shaken - public_key_url scheme must be 'http[s]'\n");
ast_log(LOG_ERROR, "stir/shaken - public_cert_url scheme must be 'http[s]'\n");
return -1;
}
return ast_string_field_set(cfg, public_key_url, var->value);
return ast_string_field_set(cfg, public_cert_url, var->value);
}
static int public_key_url_to_str(const void *obj, const intptr_t *args, char **buf)
static int public_cert_url_to_str(const void *obj, const intptr_t *args, char **buf)
{
const struct stir_shaken_certificate *cfg = obj;
*buf = ast_strdup(cfg->public_key_url);
*buf = ast_strdup(cfg->public_cert_url);
return 0;
}
@ -332,7 +332,7 @@ int test_stir_shaken_create_cert(const char *caller_id_number, const char *file_
}
ast_string_field_set(cert, path, file_path);
ast_string_field_set(cert, public_key_url, TEST_CONFIG_URL);
ast_string_field_set(cert, public_cert_url, TEST_CONFIG_URL);
ast_string_field_set(cert, caller_id_number, caller_id_number);
private_key = stir_shaken_read_key(cert->path, 1);
@ -374,8 +374,8 @@ int stir_shaken_certificate_load(void)
ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "type", "", OPT_NOOP_T, 0, 0);
ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "path", "",
on_load_path, path_to_str, NULL, 0, 0);
ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "public_key_url", "",
on_load_public_key_url, public_key_url_to_str, NULL, 0, 0);
ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "public_cert_url", "",
on_load_public_cert_url, public_cert_url_to_str, NULL, 0, 0);
ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "attestation", "",
on_load_attestation, attestation_to_str, NULL, 0, 0);
ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "origid", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct stir_shaken_certificate, origid));

View File

@ -42,7 +42,7 @@ struct stir_shaken_certificate *stir_shaken_certificate_get_by_caller_id_number(
* \retval NULL on failure
* \retval The public key URL on success
*/
const char *stir_shaken_certificate_get_public_key_url(struct stir_shaken_certificate *cert);
const char *stir_shaken_certificate_get_public_cert_url(struct stir_shaken_certificate *cert);
/*!
* \brief Get the attestation level associated with a certificate

View File

@ -22,8 +22,10 @@
#include "asterisk/logger.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
@ -148,33 +150,80 @@ static CURL *get_curl_instance(struct curl_cb_data *data)
return curl;
}
int curl_public_key(const char *public_key_url, const char *path, struct curl_cb_data *data)
/*!
* \brief Create a temporary file located at path
*
* \note This function assumes path does not end with a '/'
*
* \param path The directory path to create the file in
* \param filename Function allocates memory and stores full filename (including path) here
*
* \retval -1 on failure
* \retval file descriptor on success
*/
static int create_temp_file(const char *path, char **filename)
{
const char *template_name = "certXXXXXX";
int fd;
if (ast_asprintf(filename, "%s/%s", path, template_name) < 0) {
ast_log(LOG_ERROR, "Failed to set up temporary file path for CURL\n");
return -1;
}
ast_mkdir(path, 0644);
if ((fd = mkstemp(*filename)) < 0) {
ast_log(LOG_NOTICE, "Failed to create temporary file for CURL\n");
ast_free(*filename);
return -1;
}
return fd;
}
char *curl_public_key(const char *public_cert_url, const char *path, struct curl_cb_data *data)
{
FILE *public_key_file;
RAII_VAR(char *, tmp_filename, NULL, ast_free);
char *filename;
char *serial;
int fd;
long http_code;
CURL *curl;
char curl_errbuf[CURL_ERROR_SIZE + 1];
char hash[41];
ast_sha1_hash(hash, public_key_url);
curl_errbuf[CURL_ERROR_SIZE] = '\0';
public_key_file = fopen(path, "wb");
/* For now, it's fine to pass in path as is - it shouldn't end with a '/'. However,
* if we decide to change how certificates are stored in the future (configurable paths),
* then we will need to check to see if path ends with '/', copy everything up to the '/',
* and use this new variable for create_temp_file as well as for ast_asprintf below.
*/
fd = create_temp_file(path, &tmp_filename);
if (fd == -1) {
ast_log(LOG_ERROR, "Failed to get temporary file descriptor for CURL\n");
return NULL;
}
public_key_file = fdopen(fd, "wb");
if (!public_key_file) {
ast_log(LOG_ERROR, "Failed to open file '%s' to write public key from '%s': %s (%d)\n",
path, public_key_url, strerror(errno), errno);
return -1;
tmp_filename, public_cert_url, strerror(errno), errno);
close(fd);
remove(tmp_filename);
return NULL;
}
curl = get_curl_instance(data);
if (!curl) {
ast_log(LOG_ERROR, "Failed to set up CURL isntance for '%s'\n", public_key_url);
ast_log(LOG_ERROR, "Failed to set up CURL isntance for '%s'\n", public_cert_url);
fclose(public_key_file);
return -1;
remove(tmp_filename);
return NULL;
}
curl_easy_setopt(curl, CURLOPT_URL, public_key_url);
curl_easy_setopt(curl, CURLOPT_URL, public_cert_url);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, public_key_file);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
@ -182,7 +231,8 @@ int curl_public_key(const char *public_key_url, const char *path, struct curl_cb
ast_log(LOG_ERROR, "%s\n", curl_errbuf);
curl_easy_cleanup(curl);
fclose(public_key_file);
return -1;
remove(tmp_filename);
return NULL;
}
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
@ -191,9 +241,34 @@ int curl_public_key(const char *public_key_url, const char *path, struct curl_cb
fclose(public_key_file);
if (http_code / 100 != 2) {
ast_log(LOG_ERROR, "Failed to retrieve URL '%s': code %ld\n", public_key_url, http_code);
return -1;
ast_log(LOG_ERROR, "Failed to retrieve URL '%s': code %ld\n", public_cert_url, http_code);
remove(tmp_filename);
return NULL;
}
return 0;
serial = stir_shaken_get_serial_number_x509(tmp_filename);
if (!serial) {
ast_log(LOG_ERROR, "Failed to get serial from cert %s\n", tmp_filename);
remove(tmp_filename);
return NULL;
}
if (ast_asprintf(&filename, "%s/%s.pem", path, serial) < 0) {
ast_log(LOG_ERROR, "Failed to allocate memory for new filename for temporary "
"file %s after CURL\n", tmp_filename);
ast_free(serial);
remove(tmp_filename);
return NULL;
}
ast_free(serial);
if (rename(tmp_filename, filename)) {
ast_log(LOG_ERROR, "Failed to rename temporary file %s to %s after CURL\n", tmp_filename, filename);
ast_free(filename);
remove(tmp_filename);
return NULL;
}
return filename;
}

View File

@ -61,13 +61,15 @@ char *curl_cb_data_get_expires(const struct curl_cb_data *data);
/*!
* \brief CURL the public key from the provided URL to the specified path
*
* \param public_key_url The public key URL
* \note The returned string will need to be freed by the caller
*
* \param public_cert_url The public cert URL
* \param path The path to download the file to
* \param data The curl_cb_data
*
* \retval 1 on failure
* \retval 0 on success
* \retval NULL on failure
* \retval full path filename on success
*/
int curl_public_key(const char *public_key_url, const char *path, struct curl_cb_data *data);
char *curl_public_key(const char *public_cert_url, const char *path, struct curl_cb_data *data);
#endif /* _STIR_SHAKEN_CURL_H */

View File

@ -90,6 +90,7 @@ EVP_PKEY *stir_shaken_read_key(const char *path, int priv)
{
EVP_PKEY *key = NULL;
FILE *fp;
X509 *cert = NULL;
fp = fopen(path, "r");
if (!fp) {
@ -97,10 +98,24 @@ EVP_PKEY *stir_shaken_read_key(const char *path, int priv)
return NULL;
}
/* If this is to get the private key, the file will be ECDSA or RSA, with the former eventually
* replacing the latter. For the public key, the file will be X.509.
*/
if (priv) {
key = PEM_read_PrivateKey(fp, NULL, NULL, NULL);
} else {
key = PEM_read_PUBKEY(fp, NULL, NULL, NULL);
cert = PEM_read_X509(fp, NULL, NULL, NULL);
if (!cert) {
ast_log(LOG_ERROR, "Failed to read X.509 cert from file '%s'\n", path);
fclose(fp);
return NULL;
}
key = X509_get_pubkey(cert);
/* It's fine to free the cert after we get the key because they are 2
* independent objects; you don't need a X509 object to be in memory
* in order to have an EVP_PKEY, and it doesn't rely on it being there.
*/
X509_free(cert);
}
if (!key) {
@ -109,8 +124,9 @@ EVP_PKEY *stir_shaken_read_key(const char *path, int priv)
return NULL;
}
if (EVP_PKEY_id(key) != EVP_PKEY_EC) {
ast_log(LOG_ERROR, "%s key from '%s' must be of type EVP_PKEY_EC\n", priv ? "private" : "public", path);
if (EVP_PKEY_id(key) != EVP_PKEY_EC && EVP_PKEY_id(key) != EVP_PKEY_RSA) {
ast_log(LOG_ERROR, "%s key from '%s' must be of type EVP_PKEY_EC or EVP_PKEY_RSA\n",
priv ? "Private" : "Public", path);
fclose(fp);
EVP_PKEY_free(key);
return NULL;
@ -120,3 +136,57 @@ EVP_PKEY *stir_shaken_read_key(const char *path, int priv)
return key;
}
char *stir_shaken_get_serial_number_x509(const char *path)
{
FILE *fp;
X509 *cert;
ASN1_INTEGER *serial;
BIGNUM *bignum;
char *serial_hex;
fp = fopen(path, "r");
if (!fp) {
ast_log(LOG_ERROR, "Failed to open file %s\n", path);
return NULL;
}
cert = PEM_read_X509(fp, NULL, NULL, NULL);
if (!cert) {
ast_log(LOG_ERROR, "Failed to read X.509 cert from file %s\n", path);
fclose(fp);
return NULL;
}
serial = X509_get_serialNumber(cert);
if (!serial) {
ast_log(LOG_ERROR, "Failed to get serial number from certificate %s\n", path);
X509_free(cert);
fclose(fp);
return NULL;
}
bignum = ASN1_INTEGER_to_BN(serial, NULL);
if (bignum == NULL) {
ast_log(LOG_ERROR, "Failed to convert serial to bignum for certificate %s\n", path);
X509_free(cert);
fclose(fp);
return NULL;
}
/* This will return a string with memory allocated. After we get the string,
* we don't need the cert, file, or bignum references anymore, so free them
* and return the string, if BN_bn2hex was a success.
*/
serial_hex = BN_bn2hex(bignum);
X509_free(cert);
fclose(fp);
BN_free(bignum);
if (!serial_hex) {
ast_log(LOG_ERROR, "Failed to convert bignum to hex for certificate %s\n", path);
return NULL;
}
return serial_hex;
}

View File

@ -52,4 +52,16 @@ char *stir_shaken_tab_complete_name(const char *word, struct ao2_container *cont
*/
EVP_PKEY *stir_shaken_read_key(const char *path, int priv);
/*!
* \brief Gets the serial number in hex form from the X509 certificate at path
*
* \note The returned string will need to be freed by the caller
*
* \param path The full path of the X509 certificate
*
* \retval NULL on failure
* \retval serial number on success
*/
char *stir_shaken_get_serial_number_x509(const char *path);
#endif /* _STIR_SHAKEN_H */

View File

@ -36,8 +36,8 @@ struct stir_shaken_store {
AST_DECLARE_STRING_FIELDS(
/*! Path to a directory containing certificates */
AST_STRING_FIELD(path);
/*! URL to the public key */
AST_STRING_FIELD(public_key_url);
/*! URL to the public certificate */
AST_STRING_FIELD(public_cert_url);
);
};
@ -142,29 +142,29 @@ static int path_to_str(const void *obj, const intptr_t *args, char **buf)
return 0;
}
static int on_load_public_key_url(const struct aco_option *opt, struct ast_variable *var, void *obj)
static int on_load_public_cert_url(const struct aco_option *opt, struct ast_variable *var, void *obj)
{
struct stir_shaken_store *cfg = obj;
if (!ast_begins_with(var->value, "http")) {
ast_log(LOG_ERROR, "stir/shaken - public_key_url scheme must be 'http[s]'\n");
ast_log(LOG_ERROR, "stir/shaken - public_cert_url scheme must be 'http[s]'\n");
return -1;
}
if (!strstr(var->value, VARIABLE_SUBSTITUTE)) {
ast_log(LOG_ERROR, "stir/shaken - public_key_url must contain variable '%s' "
ast_log(LOG_ERROR, "stir/shaken - public_cert_url must contain variable '%s' "
"used for substitution\n", VARIABLE_SUBSTITUTE);
return -1;
}
return ast_string_field_set(cfg, public_key_url, var->value);
return ast_string_field_set(cfg, public_cert_url, var->value);
}
static int public_key_url_to_str(const void *obj, const intptr_t *args, char **buf)
static int public_cert_url_to_str(const void *obj, const intptr_t *args, char **buf)
{
const struct stir_shaken_store *cfg = obj;
*buf = ast_strdup(cfg->public_key_url);
*buf = ast_strdup(cfg->public_cert_url);
return 0;
}
@ -192,8 +192,8 @@ int stir_shaken_store_load(void)
ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "type", "", OPT_NOOP_T, 0, 0);
ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "path", "",
on_load_path, path_to_str, NULL, 0, 0);
ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "public_key_url", "",
on_load_public_key_url, public_key_url_to_str, NULL, 0, 0);
ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "public_cert_url", "",
on_load_public_cert_url, public_cert_url_to_str, NULL, 0, 0);
ast_cli_register_multiple(stir_shaken_store_cli,
ARRAY_LEN(stir_shaken_store_cli));