From 12741171022220a24786556eaf97a0b90ddf7b24 Mon Sep 17 00:00:00 2001 From: Ben Ford <bford@digium.com> Date: Tue, 2 Jun 2020 09:04:23 -0500 Subject: [PATCH] res_stir_shaken: Add outbound INVITE support. Integrated STIR/SHAKEN support with outgoing INVITEs. When an INVITE is sent, the caller ID will be checked to see if there is a certificate that corresponds to it. If so, that information will be retrieved and an Identity header will be added to the SIP message. The format is: header.payload.signature;info=<public_key_url>alg=ES256;ppt=shaken Header, payload, and signature are all BASE64 encoded. The public key URL is retrieved from the certificate. Currently the algorithm and ppt are ES256 and shaken, respectively. This message is signed and can be used for verification on the receiving end. Two new configuration options have been added to the certificate object: attestation and origid. The attestation is required and must be A, B, or C. origid is the origination identifier. A new utility function has been added as well that takes a string, allocates space, BASE64 encodes it, then returns it, eliminating the need to calculate the size yourself. Change-Id: I1f84d6a5839cb2ed152ef4255b380cfc2de662b4 --- configs/samples/stir_shaken.conf.sample | 6 ++ include/asterisk/res_stir_shaken.h | 22 ++++++ include/asterisk/utils.h | 13 ++++ main/utils.c | 18 +++++ res/res_pjsip_stir_shaken.c | 92 +++++++++++++++++++++++++ res/res_stir_shaken.c | 34 +++++---- res/res_stir_shaken/certificate.c | 56 ++++++++++++--- res/res_stir_shaken/certificate.h | 20 ++++++ 8 files changed, 239 insertions(+), 22 deletions(-) diff --git a/configs/samples/stir_shaken.conf.sample b/configs/samples/stir_shaken.conf.sample index 57d1634057..71acad23c4 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 997054d4d7..cad9282fc4 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 da14eb6e70..f6280ebdfe 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 59880fde62..0b6c649342 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 68665988db..3620579d84 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 5183c7e957..632fd1b38f 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 73b5ce107c..1a1447e5ac 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 ff303180bf..6eeb36bec8 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 * -- GitLab