]> git.ipfire.org Git - thirdparty/asterisk.git/commitdiff
res_stir_shaken: Add outbound INVITE support.
authorBen Ford <bford@digium.com>
Tue, 2 Jun 2020 14:04:23 +0000 (09:04 -0500)
committerFriendly Automation <jenkins2@gerrit.asterisk.org>
Thu, 18 Jun 2020 22:45:27 +0000 (17:45 -0500)
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
include/asterisk/res_stir_shaken.h
include/asterisk/utils.h
main/utils.c
res/res_pjsip_stir_shaken.c
res/res_stir_shaken.c
res/res_stir_shaken/certificate.c
res/res_stir_shaken/certificate.h

index 57d1634057d224e47bca1ed14fd146f25ece3f97..71acad23c4d3d2c700b0579230b25728d9824660 100644 (file)
@@ -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
index 997054d4d7c20882a1db2a69def85d14e6be9b16..cad9282fc439f697da6aa47b81c1a78199389b9b 100644 (file)
 #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
  *
index da14eb6e70480705d74f1179f941a2f14e2321dc..f6280ebdfe5404c6e67bc008ce27f3fd1eaf1ab0 100644 (file)
@@ -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
index 59880fde625cb1604a937298c5c1dc5a5d1aedf6..0b6c649342cc802841af018365fff4403f124531 100644 (file)
@@ -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;
index 68665988dbba615276757242604b10f149c79ec8..3620579d84b6a650985bcbc994679871935161cd 100644 (file)
@@ -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)
index 5183c7e957c2f4bb67d546bc3c3010f66a147deb..632fd1b38f13bb9746288520623cd80ff904f8f8 100644 (file)
                                         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>
        </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;
        }
index 73b5ce107cf9578e23effe3f292af9bf79b8d0e8..1a1447e5acdf4eb208a4066d7ae1253beb1ba583 100644 (file)
@@ -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,
index ff303180bff560329b90734bab87490103aabb18..6eeb36bec82d41523b5d4492b994efee93354912 100644 (file)
@@ -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
  *