diff --git a/configs/samples/stir_shaken.conf.sample b/configs/samples/stir_shaken.conf.sample index 957fd14df7..1bd260641b 100644 --- a/configs/samples/stir_shaken.conf.sample +++ b/configs/samples/stir_shaken.conf.sample @@ -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 +; /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 diff --git a/doc/UPGRADE-staging/stir-shaken-public-key-url.txt b/doc/UPGRADE-staging/stir-shaken-public-key-url.txt new file mode 100644 index 0000000000..094bccfe72 --- /dev/null +++ b/doc/UPGRADE-staging/stir-shaken-public-key-url.txt @@ -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. diff --git a/include/asterisk/res_stir_shaken.h b/include/asterisk/res_stir_shaken.h index 34d58a3068..5175907bbd 100644 --- a/include/asterisk/res_stir_shaken.h +++ b/include/asterisk/res_stir_shaken.h @@ -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 diff --git a/res/res_pjsip_stir_shaken.c b/res/res_pjsip_stir_shaken.c index 8c1c70f053..351d7ccf29 100644 --- a/res/res_pjsip_stir_shaken.c +++ b/res/res_pjsip_stir_shaken.c @@ -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=alg=STIR_SHAKEN_ENCRYPTION_ALGORITHM;ppt=STIR_SHAKEN_PPT + * header.payload.signature;info=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); diff --git a/res/res_stir_shaken.c b/res/res_stir_shaken.c index a9f861baa1..f8eb97fe4f 100644 --- a/res/res_stir_shaken.c +++ b/res/res_stir_shaken.c @@ -79,8 +79,8 @@ Path to a directory containing certificates - - URL to the public key(s) + + URL to the public certificate(s) 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 @@ File path to a certificate - - URL to the public key + + URL to the public certificate Must be a valid http, or https, URL. @@ -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); diff --git a/res/res_stir_shaken/certificate.c b/res/res_stir_shaken/certificate.c index 1a1447e5ac..f4103f96ec 100644 --- a/res/res_stir_shaken/certificate.c +++ b/res/res_stir_shaken/certificate.c @@ -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)); diff --git a/res/res_stir_shaken/certificate.h b/res/res_stir_shaken/certificate.h index 6eeb36bec8..9574d46795 100644 --- a/res/res_stir_shaken/certificate.h +++ b/res/res_stir_shaken/certificate.h @@ -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 diff --git a/res/res_stir_shaken/curl.c b/res/res_stir_shaken/curl.c index ab29e3d833..2030f46386 100644 --- a/res/res_stir_shaken/curl.c +++ b/res/res_stir_shaken/curl.c @@ -22,8 +22,10 @@ #include "asterisk/logger.h" #include "curl.h" #include "general.h" +#include "stir_shaken.h" #include +#include /* 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; + } + + 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 0; + return filename; } diff --git a/res/res_stir_shaken/curl.h b/res/res_stir_shaken/curl.h index d587327ddf..7009d36598 100644 --- a/res/res_stir_shaken/curl.h +++ b/res/res_stir_shaken/curl.h @@ -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 */ diff --git a/res/res_stir_shaken/stir_shaken.c b/res/res_stir_shaken/stir_shaken.c index 0b38732141..b580773c31 100644 --- a/res/res_stir_shaken/stir_shaken.c +++ b/res/res_stir_shaken/stir_shaken.c @@ -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; +} diff --git a/res/res_stir_shaken/stir_shaken.h b/res/res_stir_shaken/stir_shaken.h index a49050e539..90df4e97e2 100644 --- a/res/res_stir_shaken/stir_shaken.h +++ b/res/res_stir_shaken/stir_shaken.h @@ -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 */ diff --git a/res/res_stir_shaken/store.c b/res/res_stir_shaken/store.c index 99a50383e7..30bc63aed0 100644 --- a/res/res_stir_shaken/store.c +++ b/res/res_stir_shaken/store.c @@ -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));