diff --git a/configs/samples/stir_shaken.conf.sample b/configs/samples/stir_shaken.conf.sample index 57d1634057d224e47bca1ed14fd146f25ece3f97..71acad23c4d3d2c700b0579230b25728d9824660 100644 --- a/configs/samples/stir_shaken.conf.sample +++ b/configs/samples/stir_shaken.conf.sample @@ -47,3 +47,9 @@ ; ; URL to the public key ;public_key_url=http://mycompany.com/alice.pub +; +; Must have an attestation of A, B, or C +;attestation=C +; +; The origination identifier for the certificate +;origid=MyAsterisk diff --git a/include/asterisk/res_stir_shaken.h b/include/asterisk/res_stir_shaken.h index 997054d4d7c20882a1db2a69def85d14e6be9b16..cad9282fc439f697da6aa47b81c1a78199389b9b 100644 --- a/include/asterisk/res_stir_shaken.h +++ b/include/asterisk/res_stir_shaken.h @@ -21,6 +21,10 @@ #include <openssl/evp.h> #include <openssl/pem.h> +#define STIR_SHAKEN_ENCRYPTION_ALGORITHM "ES256" +#define STIR_SHAKEN_PPT "shaken" +#define STIR_SHAKEN_TYPE "passport" + enum ast_stir_shaken_verification_result { AST_STIR_SHAKEN_VERIFY_NOT_PRESENT, /*! No STIR/SHAKEN information was available */ AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED, /*! Signature verification failed */ @@ -32,6 +36,24 @@ struct ast_stir_shaken_payload; struct ast_json; +/*! + * \brief Retrieve the value for 'signature' from an ast_stir_shaken_payload + * + * \param payload The payload + * + * \retval The signature + */ +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 + * + * \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); + /*! * \brief Retrieve the value for 'signature_timeout' from 'general' config object * diff --git a/include/asterisk/utils.h b/include/asterisk/utils.h index da14eb6e70480705d74f1179f941a2f14e2321dc..f6280ebdfe5404c6e67bc008ce27f3fd1eaf1ab0 100644 --- a/include/asterisk/utils.h +++ b/include/asterisk/utils.h @@ -239,6 +239,19 @@ int ast_base64encode_full(char *dst, const unsigned char *src, int srclen, int m */ int ast_base64encode(char *dst, const unsigned char *src, int srclen, int max); +/*! + * \brief Same as ast_base64encode, but does hte math for you and returns + * an encoded string + * + * \note The returned string will need to be freed later + * + * \param src The source buffer + * + * \retval NULL on failure + * \retval Encoded string on success + */ +char *ast_base64encode_string(const char *src); + /*! * \brief Decode data from base64 * \param dst the destination buffer diff --git a/main/utils.c b/main/utils.c index 59880fde625cb1604a937298c5c1dc5a5d1aedf6..0b6c649342cc802841af018365fff4403f124531 100644 --- a/main/utils.c +++ b/main/utils.c @@ -398,6 +398,24 @@ int ast_base64encode(char *dst, const unsigned char *src, int srclen, int max) return ast_base64encode_full(dst, src, srclen, max, 0); } +/*! \brief Encode to BASE64 and return encoded string */ +char *ast_base64encode_string(const char *src) +{ + size_t encoded_len; + char *encoded_string; + + if (ast_strlen_zero(src)) { + return NULL; + } + + encoded_len = ((strlen(src) * 4 / 3 + 3) & ~3) + 1; + encoded_string = ast_calloc(1, encoded_len); + + ast_base64encode(encoded_string, (const unsigned char *)src, strlen(src), encoded_len); + + return encoded_string; +} + static void base64_init(void) { int x; diff --git a/res/res_pjsip_stir_shaken.c b/res/res_pjsip_stir_shaken.c index 68665988dbba615276757242604b10f149c79ec8..3620579d84b6a650985bcbc994679871935161cd 100644 --- a/res/res_pjsip_stir_shaken.c +++ b/res/res_pjsip_stir_shaken.c @@ -194,10 +194,102 @@ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_r return 0; } +static void add_identity_header(const struct ast_sip_session *session, pjsip_tx_data *tdata) +{ + static const pj_str_t identity_str = { "Identity", 8 }; + pjsip_generic_string_hdr *identity_hdr; + pj_str_t identity_val; + pjsip_fromto_hdr *old_identity; + char *signature; + char *public_key_url; + struct ast_json *header; + struct ast_json *payload; + char *dumped_string; + RAII_VAR(struct ast_json *, json, NULL, ast_json_free); + RAII_VAR(struct ast_stir_shaken_payload *, ss_payload, NULL, ast_stir_shaken_payload_free); + RAII_VAR(char *, encoded_header, NULL, ast_free); + RAII_VAR(char *, encoded_payload, NULL, ast_free); + RAII_VAR(char *, combined_str, NULL, ast_free); + size_t combined_size; + + old_identity = pjsip_msg_find_hdr_by_name(tdata->msg, &identity_str, NULL); + if (old_identity) { + return; + } + + /* x5u (public key URL), attestation, and origid will be added by ast_stir_shaken_sign */ + json = ast_json_pack("{s: {s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg", "ES256", "ppt", "shaken", "typ", "passport", + "payload", "orig", "tn", session->id.number.str); + if (!json) { + ast_log(LOG_ERROR, "Failed to allocate memory for STIR/SHAKEN JSON\n"); + return; + } + + ss_payload = ast_stir_shaken_sign(json); + if (!ss_payload) { + ast_log(LOG_ERROR, "Failed to allocate memory for STIR/SHAKEN payload\n"); + return; + } + + header = ast_json_object_get(json, "header"); + dumped_string = ast_json_dump_string(header); + encoded_header = ast_base64encode_string(dumped_string); + ast_json_free(dumped_string); + if (!encoded_header) { + ast_log(LOG_ERROR, "Failed to encode STIR/SHAKEN header\n"); + return; + } + + payload = ast_json_object_get(json, "payload"); + dumped_string = ast_json_dump_string(payload); + encoded_payload = ast_base64encode_string(dumped_string); + ast_json_free(dumped_string); + if (!encoded_payload) { + ast_log(LOG_ERROR, "Failed to encode STIR/SHAKEN payload\n"); + return; + } + + signature = (char *)ast_stir_shaken_payload_get_signature(ss_payload); + public_key_url = ast_stir_shaken_payload_get_public_key_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 + */ + combined_size = strlen(encoded_header) + 1 + strlen(encoded_payload) + 1 + + strlen(signature) + strlen(";info=<>alg=;ppt=") + strlen(public_key_url) + + strlen(STIR_SHAKEN_ENCRYPTION_ALGORITHM) + strlen(STIR_SHAKEN_PPT) + 1; + combined_str = ast_calloc(1, combined_size); + if (!combined_str) { + ast_log(LOG_ERROR, "Failed to allocate memory for STIR/SHAKEN identity string\n"); + 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); + + identity_val = pj_str(combined_str); + identity_hdr = pjsip_generic_string_hdr_create(tdata->pool, &identity_str, &identity_val); + if (!identity_hdr) { + ast_log(LOG_ERROR, "Failed to create STIR/SHAKEN Identity header\n"); + return; + } + + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *)identity_hdr); +} + +static void stir_shaken_outgoing_request(struct ast_sip_session *session, pjsip_tx_data *tdata) +{ + if (ast_strlen_zero(session->id.number.str) && session->id.number.valid) { + return; + } + + add_identity_header(session, tdata); +} + static struct ast_sip_session_supplement stir_shaken_supplement = { .method = "INVITE", .priority = AST_SIP_SUPPLEMENT_PRIORITY_CHANNEL + 1, /* Run AFTER channel creation */ .incoming_request = stir_shaken_incoming_request, + .outgoing_request = stir_shaken_outgoing_request, }; static int unload_module(void) diff --git a/res/res_stir_shaken.c b/res/res_stir_shaken.c index 5183c7e957c2f4bb67d546bc3c3010f66a147deb..632fd1b38f13bb9746288520623cd80ff904f8f8 100644 --- a/res/res_stir_shaken.c +++ b/res/res_stir_shaken.c @@ -99,6 +99,12 @@ Must be a valid http, or https, URL. </para></description> </configOption> + <configOption name="attestation"> + <synopsis>Attestation level</synopsis> + </configOption> + <configOption name="origid" default=""> + <synopsis>The origination ID</synopsis> + </configOption> <configOption name="caller_id_number" default=""> <synopsis>The caller ID number to match on.</synopsis> </configOption> @@ -136,10 +142,6 @@ </function> ***/ -#define STIR_SHAKEN_ENCRYPTION_ALGORITHM "ES256" -#define STIR_SHAKEN_PPT "shaken" -#define STIR_SHAKEN_TYPE "passport" - static struct ast_sorcery *stir_shaken_sorcery; /* Used for AstDB entries */ @@ -184,6 +186,16 @@ void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload) ast_free(payload); } +unsigned char *ast_stir_shaken_payload_get_signature(const struct ast_stir_shaken_payload *payload) +{ + return payload ? payload->signature : NULL; +} + +char *ast_stir_shaken_payload_get_public_key_url(const struct ast_stir_shaken_payload *payload) +{ + return payload ? payload->public_key_url : NULL; +} + unsigned int ast_stir_shaken_get_signature_timeout(void) { return ast_stir_shaken_signature_timeout(stir_shaken_general_get()); @@ -1020,6 +1032,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 *caller_id_num; const char *header; const char *payload; @@ -1049,22 +1062,19 @@ struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json) goto cleanup; } - if (stir_shaken_add_x5u(json, stir_shaken_certificate_get_public_key_url(cert))) { + 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"); goto cleanup; } + ss_payload->public_key_url = ast_strdup(public_key_url); - /* TODO: This is just a placeholder for adding 'attest', 'iat', and - * 'origid' to the payload. Later, additional logic will need to be - * added to determine what these values actually are, but the functions - * themselves are ready to go. - */ - if (stir_shaken_add_attest(json, "B")) { + if (stir_shaken_add_attest(json, stir_shaken_certificate_get_attestation(cert))) { ast_log(LOG_ERROR, "Failed to add 'attest' to payload\n"); goto cleanup; } - if (stir_shaken_add_origid(json, "asterisk")) { + if (stir_shaken_add_origid(json, stir_shaken_certificate_get_origid(cert))) { ast_log(LOG_ERROR, "Failed to add 'origid' to payload\n"); goto cleanup; } diff --git a/res/res_stir_shaken/certificate.c b/res/res_stir_shaken/certificate.c index 73b5ce107cf9578e23effe3f292af9bf79b8d0e8..1a1447e5acdf4eb208a4066d7ae1253beb1ba583 100644 --- a/res/res_stir_shaken/certificate.c +++ b/res/res_stir_shaken/certificate.c @@ -38,6 +38,10 @@ struct stir_shaken_certificate { AST_STRING_FIELD(public_key_url); /*! The caller ID number associated with the certificate */ AST_STRING_FIELD(caller_id_number); + /*! The attestation level for this certificate */ + AST_STRING_FIELD(attestation); + /*! The origination ID for this certificate */ + AST_STRING_FIELD(origid); ); /*! The private key for the certificate */ EVP_PKEY *private_key; @@ -93,20 +97,22 @@ struct stir_shaken_certificate *stir_shaken_certificate_get_by_caller_id_number( const char *stir_shaken_certificate_get_public_key_url(struct stir_shaken_certificate *cert) { - if (!cert) { - return NULL; - } + return cert ? cert->public_key_url : NULL; +} - return cert->public_key_url; +const char *stir_shaken_certificate_get_attestation(struct stir_shaken_certificate *cert) +{ + return cert ? cert->attestation : NULL; } -EVP_PKEY *stir_shaken_certificate_get_private_key(struct stir_shaken_certificate *cert) +const char *stir_shaken_certificate_get_origid(struct stir_shaken_certificate *cert) { - if (!cert) { - return NULL; - } + return cert ? cert->origid : NULL; +} - return cert->private_key; +EVP_PKEY *stir_shaken_certificate_get_private_key(struct stir_shaken_certificate *cert) +{ + return cert ? cert->private_key : NULL; } static int stir_shaken_certificate_apply(const struct ast_sorcery *sorcery, void *obj) @@ -114,11 +120,16 @@ static int stir_shaken_certificate_apply(const struct ast_sorcery *sorcery, void EVP_PKEY *private_key; struct stir_shaken_certificate *cert = obj; - if (strlen(cert->caller_id_number) == 0) { + if (ast_strlen_zero(cert->caller_id_number)) { ast_log(LOG_ERROR, "Caller ID must be present\n"); return -1; } + if (ast_strlen_zero(cert->attestation)) { + ast_log(LOG_ERROR, "Attestation must be present\n"); + return -1; + } + private_key = stir_shaken_read_key(cert->path, 1); if (!private_key) { return -1; @@ -244,6 +255,28 @@ static int public_key_url_to_str(const void *obj, const intptr_t *args, char **b return 0; } +static int on_load_attestation(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct stir_shaken_certificate *cfg = obj; + + if (strcmp(var->value, "A") && strcmp(var->value, "B") && strcmp(var->value, "C")) { + ast_log(LOG_ERROR, "stir/shaken - attestation level must be A, B, or C (object=%s)\n", + ast_sorcery_object_get_id(cfg)); + return -1; + } + + return ast_string_field_set(cfg, attestation, var->value); +} + +static int attestation_to_str(const void *obj, const intptr_t *args, char **buf) +{ + const struct stir_shaken_certificate *cfg = obj; + + *buf = ast_strdup(cfg->attestation); + + return 0; +} + #ifdef TEST_FRAMEWORK /* Name for test certificaate */ @@ -343,6 +376,9 @@ int stir_shaken_certificate_load(void) 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, "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)); ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "caller_id_number", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct stir_shaken_certificate, caller_id_number)); ast_cli_register_multiple(stir_shaken_certificate_cli, diff --git a/res/res_stir_shaken/certificate.h b/res/res_stir_shaken/certificate.h index ff303180bff560329b90734bab87490103aabb18..6eeb36bec82d41523b5d4492b994efee93354912 100644 --- a/res/res_stir_shaken/certificate.h +++ b/res/res_stir_shaken/certificate.h @@ -44,6 +44,26 @@ struct stir_shaken_certificate *stir_shaken_certificate_get_by_caller_id_number( */ const char *stir_shaken_certificate_get_public_key_url(struct stir_shaken_certificate *cert); +/*! + * \brief Get the attestation level associated with a certificate + * + * \param cert The certificate + * + * \retval NULL on failure + * \retval The attestation on success + */ +const char *stir_shaken_certificate_get_attestation(struct stir_shaken_certificate *cert); + +/*! + * \brief Get the origination ID associated with a certificate + * + * \param cert The certificate + * + * \retval NULL on failure + * \retval The origid on success + */ +const char *stir_shaken_certificate_get_origid(struct stir_shaken_certificate *cert); + /*! * \brief Get the private key associated with a certificate *