[Asterisk-code-review] res_stir_shaken: Add outbound INVITE support. (asterisk[master])

Friendly Automation asteriskteam at digium.com
Thu Jun 18 17:45:30 CDT 2020


Friendly Automation has submitted this change. ( https://gerrit.asterisk.org/c/asterisk/+/14509 )

Change subject: res_stir_shaken: Add outbound INVITE support.
......................................................................

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
---
M configs/samples/stir_shaken.conf.sample
M include/asterisk/res_stir_shaken.h
M include/asterisk/utils.h
M main/utils.c
M res/res_pjsip_stir_shaken.c
M res/res_stir_shaken.c
M res/res_stir_shaken/certificate.c
M res/res_stir_shaken/certificate.h
8 files changed, 239 insertions(+), 22 deletions(-)

Approvals:
  Joshua Colp: Looks good to me, but someone else must approve
  Kevin Harwell: Looks good to me, approved
  Friendly Automation: Approved for Submit



diff --git a/configs/samples/stir_shaken.conf.sample b/configs/samples/stir_shaken.conf.sample
index 57d1634..71acad2 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 997054d..cad9282 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 */
@@ -33,6 +37,24 @@
 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
  *
  * \retval The signature timeout
diff --git a/include/asterisk/utils.h b/include/asterisk/utils.h
index da14eb6..f6280eb 100644
--- a/include/asterisk/utils.h
+++ b/include/asterisk/utils.h
@@ -240,6 +240,19 @@
 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
  * \param src the source buffer
diff --git a/main/utils.c b/main/utils.c
index 59880fd..0b6c6493 100644
--- a/main/utils.c
+++ b/main/utils.c
@@ -398,6 +398,24 @@
 	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 6866598..3620579 100644
--- a/res/res_pjsip_stir_shaken.c
+++ b/res/res_pjsip_stir_shaken.c
@@ -194,10 +194,102 @@
 	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 5183c7e..632fd1b 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 @@
 	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 *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 @@
 		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 73b5ce1..1a1447e 100644
--- a/res/res_stir_shaken/certificate.c
+++ b/res/res_stir_shaken/certificate.c
@@ -38,6 +38,10 @@
 		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 @@
 
 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;
+}
+
+const char *stir_shaken_certificate_get_origid(struct stir_shaken_certificate *cert)
+{
+	return cert ? cert->origid : NULL;
 }
 
 EVP_PKEY *stir_shaken_certificate_get_private_key(struct stir_shaken_certificate *cert)
 {
-	if (!cert) {
-		return NULL;
-	}
-
-	return cert->private_key;
+	return cert ? cert->private_key : NULL;
 }
 
 static int stir_shaken_certificate_apply(const struct ast_sorcery *sorcery, void *obj)
@@ -114,11 +120,16 @@
 	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 @@
 	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 @@
 		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 ff30318..6eeb36b 100644
--- a/res/res_stir_shaken/certificate.h
+++ b/res/res_stir_shaken/certificate.h
@@ -45,6 +45,26 @@
 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
  *
  * \param cert The certificate to get the private key from

-- 
To view, visit https://gerrit.asterisk.org/c/asterisk/+/14509
To unsubscribe, or for help writing mail filters, visit https://gerrit.asterisk.org/settings

Gerrit-Project: asterisk
Gerrit-Branch: master
Gerrit-Change-Id: I1f84d6a5839cb2ed152ef4255b380cfc2de662b4
Gerrit-Change-Number: 14509
Gerrit-PatchSet: 4
Gerrit-Owner: Benjamin Keith Ford <bford at digium.com>
Gerrit-Reviewer: Friendly Automation
Gerrit-Reviewer: Joshua Colp <jcolp at sangoma.com>
Gerrit-Reviewer: Kevin Harwell <kharwell at digium.com>
Gerrit-MessageType: merged
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20200618/17740b65/attachment-0001.html>


More information about the asterisk-code-review mailing list