[Asterisk-code-review] res_stir_shaken: Implemented signing of JSON payload. (asterisk[master])

Friendly Automation asteriskteam at digium.com
Fri Apr 3 11:08:30 CDT 2020


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

Change subject: res_stir_shaken: Implemented signing of JSON payload.
......................................................................

res_stir_shaken: Implemented signing of JSON payload.

This change provides functions that take in a JSON payload, verify that
the contents contain all the mandatory fields and required values (if
any), and signs the payload with the private key. Four fields are added
to the payload: x5u, attest, iat, and origid. As of now, these are just
placeholder values that will be set to actual values once the logic is
implemented for what to do when an actual payload is received, but the
functions to add these values have all been implemented and are ready to
use. Upon successful signing and the addition of those four values, a
ast_stir_shaken_payload is returned, containing other useful information
such as the algorithm and signature.

Change-Id: I74fa41c0640ab2a64a1a80110155bd7062f13393
---
M include/asterisk/res_stir_shaken.h
M res/res_stir_shaken.c
M res/res_stir_shaken/certificate.c
M res/res_stir_shaken/certificate.h
4 files changed, 421 insertions(+), 17 deletions(-)

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



diff --git a/include/asterisk/res_stir_shaken.h b/include/asterisk/res_stir_shaken.h
index 0c589a9..16f0139 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>
 
+struct ast_stir_shaken_payload;
+
+struct ast_json;
+
 /*!
  * \brief Retrieve the stir/shaken sorcery context
  *
@@ -29,12 +33,15 @@
 struct ast_sorcery *ast_stir_shaken_sorcery(void);
 
 /*!
- * \brief Get the private key associated with a caller id
- *
- * \param caller_id_number The caller id used to look up the private key
- *
- * \retval The private key
+ * \brief Free a STIR/SHAKEN payload
  */
-EVP_PKEY *ast_stir_shaken_get_private_key(const char *caller_id_number);
+void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload);
+
+/*!
+ * \brief Sign a JSON STIR/SHAKEN payload
+ *
+ * \note This function will automatically add the "attest", "iat", and "origid" fields.
+ */
+struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json);
 
 #endif /* _RES_STIR_SHAKEN_H */
diff --git a/res/res_stir_shaken.c b/res/res_stir_shaken.c
index a6656d0..cb4cc82 100644
--- a/res/res_stir_shaken.c
+++ b/res/res_stir_shaken.c
@@ -24,6 +24,8 @@
 
 #include "asterisk/module.h"
 #include "asterisk/sorcery.h"
+#include "asterisk/time.h"
+#include "asterisk/json.h"
 
 #include "asterisk/res_stir_shaken.h"
 #include "res_stir_shaken/stir_shaken.h"
@@ -31,16 +33,386 @@
 #include "res_stir_shaken/store.h"
 #include "res_stir_shaken/certificate.h"
 
+#define STIR_SHAKEN_ENCRYPTION_ALGORITHM "ES256"
+#define STIR_SHAKEN_PPT "shaken"
+#define STIR_SHAKEN_TYPE "passport"
+
 static struct ast_sorcery *stir_shaken_sorcery;
 
+struct ast_stir_shaken_payload {
+	/*! The JWT header */
+	struct ast_json *header;
+	/*! The JWT payload */
+	struct ast_json *payload;
+	/*! Signature for the payload */
+	unsigned char *signature;
+	/*! The algorithm used */
+	char *algorithm;
+	/*! THe URL to the public key for the certificate */
+	char *public_key_url;
+};
+
 struct ast_sorcery *ast_stir_shaken_sorcery(void)
 {
 	return stir_shaken_sorcery;
 }
 
-EVP_PKEY *ast_stir_shaken_get_private_key(const char *caller_id_number)
+void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload)
 {
-	return stir_shaken_certificate_get_private_key(caller_id_number);
+	if (!payload) {
+		return;
+	}
+
+	ast_json_unref(payload->header);
+	ast_json_unref(payload->payload);
+	ast_free(payload->algorithm);
+	ast_free(payload->public_key_url);
+	ast_free(payload->signature);
+
+	ast_free(payload);
+}
+
+/*!
+ * \brief Verifies the necessary contents are in the JSON and returns a
+ * ast_stir_shaken_payload with the extracted values.
+ *
+ * \param json The JSON to verify
+ *
+ * \return ast_stir_shaken_payload on success
+ * \return NULL on failure
+ */
+static struct ast_stir_shaken_payload *stir_shaken_verify_json(struct ast_json *json)
+{
+	struct ast_stir_shaken_payload *payload;
+	struct ast_json *obj;
+	const char *val;
+
+	payload = ast_calloc(1, sizeof(*payload));
+	if (!payload) {
+		ast_log(LOG_ERROR, "Failed to allocate STIR_SHAKEN payload\n");
+		goto cleanup;
+	}
+
+	/* Look through the header first */
+	obj = ast_json_object_get(json, "header");
+	if (!obj) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have the required field 'header'\n");
+		goto cleanup;
+	}
+
+	payload->header = ast_json_deep_copy(obj);
+	if (!payload->header) {
+		ast_log(LOG_ERROR, "STIR_SHAKEN payload failed to copy 'header'\n");
+		goto cleanup;
+	}
+
+	/* Check the ppt value for "shaken" */
+	val = ast_json_string_get(ast_json_object_get(obj, "ppt"));
+	if (ast_strlen_zero(val)) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have the required field 'ppt'\n");
+		goto cleanup;
+	}
+	if (strcmp(val, STIR_SHAKEN_PPT)) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN JWT field 'ppt' did not have "
+			"required value '%s' (was '%s')\n", STIR_SHAKEN_PPT, val);
+		goto cleanup;
+	}
+
+	/* Check the typ value for "passport" */
+	val = ast_json_string_get(ast_json_object_get(obj, "typ"));
+	if (ast_strlen_zero(val)) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have the required field 'typ'\n");
+		goto cleanup;
+	}
+	if (strcmp(val, STIR_SHAKEN_TYPE)) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN JWT field 'typ' did not have "
+			"required value '%s' (was '%s')\n", STIR_SHAKEN_TYPE, val);
+		goto cleanup;
+	}
+
+	/* Check the alg value for "ES256" */
+	val = ast_json_string_get(ast_json_object_get(obj, "alg"));
+	if (ast_strlen_zero(val)) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have required field 'alg'\n");
+		goto cleanup;
+	}
+	if (strcmp(val, STIR_SHAKEN_ENCRYPTION_ALGORITHM)) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN JWT field 'alg' did not have "
+			"required value '%s' (was '%s')\n", STIR_SHAKEN_ENCRYPTION_ALGORITHM, val);
+		goto cleanup;
+	}
+
+	payload->algorithm = ast_strdup(val);
+	if (!payload->algorithm) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN payload failed to copy 'algorithm'\n");
+		goto cleanup;
+	}
+
+	/* Now let's check the payload section */
+	obj = ast_json_object_get(json, "payload");
+	if (!obj) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN payload JWT did not have required field 'payload'\n");
+		goto cleanup;
+	}
+
+	/* Check the orig tn value for not NULL */
+	val = ast_json_string_get(ast_json_object_get(ast_json_object_get(obj, "orig"), "tn"));
+	if (ast_strlen_zero(val)) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have required field 'orig->tn'\n");
+		goto cleanup;
+	}
+
+	/* Payload seems sane. Copy it and return on success */
+	payload->payload = ast_json_deep_copy(obj);
+	if (!payload->payload) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN payload failed to copy 'payload'\n");
+		goto cleanup;
+	}
+
+	return payload;
+
+cleanup:
+	ast_stir_shaken_payload_free(payload);
+	return NULL;
+}
+
+/*!
+ * \brief Signs the payload and returns the signature.
+ *
+ * \param json_str The string representation of the JSON
+ * \param private_key The private key used to sign the payload
+ *
+ * \retval signature on success
+ * \retval NULL on failure
+ */
+static unsigned char *stir_shaken_sign(char *json_str, EVP_PKEY *private_key)
+{
+	EVP_MD_CTX *mdctx = NULL;
+	int ret = 0;
+	unsigned char *encoded_signature = NULL;
+	unsigned char *signature = NULL;
+	size_t encoded_length = 0;
+	size_t signature_length = 0;
+
+	mdctx = EVP_MD_CTX_create();
+	if (!mdctx) {
+		ast_log(LOG_ERROR, "Failed to create Message Digest Context\n");
+		goto cleanup;
+	}
+
+	ret = EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, private_key);
+	if (ret != 1) {
+		ast_log(LOG_ERROR, "Failed to initialize Message Digest Context\n");
+		goto cleanup;
+	}
+
+	ret = EVP_DigestSignUpdate(mdctx, json_str, strlen(json_str));
+	if (ret != 1) {
+		ast_log(LOG_ERROR, "Failed to update Message Digest Context\n");
+		goto cleanup;
+	}
+
+	ret = EVP_DigestSignFinal(mdctx, NULL, &signature_length);
+	if (ret != 1) {
+		ast_log(LOG_ERROR, "Failed initial phase of Message Digest Context signing\n");
+		goto cleanup;
+	}
+
+	signature = ast_calloc(1, sizeof(unsigned char) * signature_length);
+	if (!signature) {
+		ast_log(LOG_ERROR, "Failed to allocate space for signature\n");
+		goto cleanup;
+	}
+
+	ret = EVP_DigestSignFinal(mdctx, signature, &signature_length);
+	if (ret != 1) {
+		ast_log(LOG_ERROR, "Failed final phase of Message Digest Context signing\n");
+		goto cleanup;
+	}
+
+	/* There are 6 bits to 1 base64 digit, so in order to get the size of the base64 encoded
+	 * signature, we need to multiply by the number of bits in a byte and divide by 6. Since
+	 * there's rounding when doing base64 conversions, add 3 bytes, just in case, and account
+	 * for padding. Add another byte for the NULL-terminator so we don't lose data.
+	 */
+	encoded_length = ((signature_length * 4 / 3 + 3) & ~3) + 1;
+	encoded_signature = ast_calloc(1, encoded_length);
+	if (!encoded_signature) {
+		ast_log(LOG_ERROR, "Failed to allocate space for encoded signature\n");
+		goto cleanup;
+	}
+
+	ast_base64encode((char *)encoded_signature, signature, signature_length, encoded_length);
+
+cleanup:
+	if (mdctx) {
+		EVP_MD_CTX_destroy(mdctx);
+	}
+	ast_free(signature);
+
+	return encoded_signature;
+}
+
+/*!
+ * \brief Adds the 'x5u' (public key URL) field to the JWT.
+ *
+ * \param json The JWT
+ * \param x5u The public key URL
+ *
+ * \retval 0 on success
+ * \retval -1 on failure
+ */
+static int stir_shaken_add_x5u(struct ast_json *json, const char *x5u)
+{
+	struct ast_json *value;
+
+	value = ast_json_string_create(x5u);
+	if (!value) {
+		return -1;
+	}
+
+	return ast_json_object_set(ast_json_object_get(json, "header"), "x5u", value);
+}
+
+/*!
+ * \brief Adds the 'attest' field to the JWT.
+ *
+ * \param json The JWT
+ * \param attest The value to set attest to
+ *
+ * \retval 0 on success
+ * \retval -1 on failure
+ */
+static int stir_shaken_add_attest(struct ast_json *json, const char *attest)
+{
+	struct ast_json *value;
+
+	value = ast_json_string_create(attest);
+	if (!value) {
+		return -1;
+	}
+
+	return ast_json_object_set(ast_json_object_get(json, "payload"), "attest", value);
+}
+
+/*!
+ * \brief Adds the 'origid' field to the JWT.
+ *
+ * \param json The JWT
+ * \param origid The value to set origid to
+ *
+ * \retval 0 on success
+ * \retval -1 on failure
+ */
+static int stir_shaken_add_origid(struct ast_json *json, const char *origid)
+{
+	struct ast_json *value;
+
+	value = ast_json_string_create(origid);
+	if (!origid) {
+		return -1;
+	}
+
+	return ast_json_object_set(ast_json_object_get(json, "payload"), "origid", value);
+}
+
+/*!
+ * \brief Adds the 'iat' field to the JWT.
+ *
+ * \param json The JWT
+ *
+ * \retval 0 on success
+ * \retval -1 on failure
+ */
+static int stir_shaken_add_iat(struct ast_json *json)
+{
+	struct ast_json *value;
+	struct timeval tv;
+	int timestamp;
+
+	tv = ast_tvnow();
+	timestamp = tv.tv_sec + tv.tv_usec / 1000;
+	value = ast_json_integer_create(timestamp);
+
+	return ast_json_object_set(ast_json_object_get(json, "payload"), "iat", value);
+}
+
+struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json)
+{
+	struct ast_stir_shaken_payload *payload;
+	unsigned char *signature;
+	const char *caller_id_num;
+	char *json_str = NULL;
+	struct stir_shaken_certificate *cert = NULL;
+
+	payload = stir_shaken_verify_json(json);
+	if (!payload) {
+		return NULL;
+	}
+
+	/* From the payload section of the JSON, get the orig section, and then get
+	 * the value of tn. This will be the caller ID number */
+	caller_id_num = ast_json_string_get(ast_json_object_get(ast_json_object_get(
+			ast_json_object_get(json, "payload"), "orig"), "tn"));
+	if (!caller_id_num) {
+		ast_log(LOG_ERROR, "Failed to get caller ID number from JWT\n");
+		goto cleanup;
+	}
+
+	cert = stir_shaken_certificate_get_by_caller_id_number(caller_id_num);
+	if (!cert) {
+		ast_log(LOG_ERROR, "Failed to retrieve certificate for caller ID "
+			"'%s'\n", caller_id_num);
+		goto cleanup;
+	}
+
+	if (stir_shaken_add_x5u(json, stir_shaken_certificate_get_public_key_url(cert))) {
+		ast_log(LOG_ERROR, "Failed to add 'x5u' (public key URL) to payload\n");
+		goto cleanup;
+	}
+
+	/* 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")) {
+		ast_log(LOG_ERROR, "Failed to add 'attest' to payload\n");
+		goto cleanup;
+	}
+
+	if (stir_shaken_add_origid(json, "asterisk")) {
+		ast_log(LOG_ERROR, "Failed to add 'origid' to payload\n");
+		goto cleanup;
+	}
+
+	if (stir_shaken_add_iat(json)) {
+		ast_log(LOG_ERROR, "Failed to add 'iat' to payload\n");
+		goto cleanup;
+	}
+
+	json_str = ast_json_dump_string(json);
+	if (!json_str) {
+		ast_log(LOG_ERROR, "Failed to convert JSON to string\n");
+		goto cleanup;
+	}
+
+	signature = stir_shaken_sign(json_str, stir_shaken_certificate_get_private_key(cert));
+	if (!signature) {
+		goto cleanup;
+	}
+
+	payload->signature = signature;
+	ao2_cleanup(cert);
+	ast_json_free(json_str);
+
+	return payload;
+
+cleanup:
+	ao2_cleanup(cert);
+	ast_stir_shaken_payload_free(payload);
+	ast_json_free(json_str);
+	return NULL;
 }
 
 static int reload_module(void)
diff --git a/res/res_stir_shaken/certificate.c b/res/res_stir_shaken/certificate.c
index 799cea1..812fc1e 100644
--- a/res/res_stir_shaken/certificate.c
+++ b/res/res_stir_shaken/certificate.c
@@ -79,23 +79,34 @@
 	return cfg;
 }
 
-EVP_PKEY *stir_shaken_certificate_get_private_key(const char *caller_id_number)
+struct stir_shaken_certificate *stir_shaken_certificate_get_by_caller_id_number(const char *caller_id_number)
 {
-	struct stir_shaken_certificate *cert;
 	struct ast_variable fields = {
 		.name = "caller_id_number",
 		.value = caller_id_number,
 		.next = NULL,
 	};
 
-	cert = ast_sorcery_retrieve_by_fields(ast_stir_shaken_sorcery(),
+	return ast_sorcery_retrieve_by_fields(ast_stir_shaken_sorcery(),
 		"certificate", AST_RETRIEVE_FLAG_DEFAULT, &fields);
+}
 
-	if (cert) {
-		return cert->private_key;
+const char *stir_shaken_certificate_get_public_key_url(struct stir_shaken_certificate *cert)
+{
+	if (!cert) {
+		return NULL;
 	}
 
-	return NULL;
+	return cert->public_key_url;
+}
+
+EVP_PKEY *stir_shaken_certificate_get_private_key(struct stir_shaken_certificate *cert)
+{
+	if (!cert) {
+		return NULL;
+	}
+
+	return cert->private_key;
 }
 
 static int stir_shaken_certificate_apply(const struct ast_sorcery *sorcery, void *obj)
diff --git a/res/res_stir_shaken/certificate.h b/res/res_stir_shaken/certificate.h
index 9d6ec73..fda3bf1 100644
--- a/res/res_stir_shaken/certificate.h
+++ b/res/res_stir_shaken/certificate.h
@@ -22,15 +22,29 @@
 
 struct ast_sorcery;
 
+struct stir_shaken_certificate;
+
+struct stir_shaken_certificate *stir_shaken_certificate_get_by_caller_id_number(const char *caller_id_number);
+
 /*!
- * \brief Get the private key associated with a caller id
+ * \brief Get the public key URL associated with a certificate
  *
- * \param caller_id_number The caller id used to look up the private key
+ * \param cert The certificate to get the public key URL from
+ *
+ * \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);
+
+/*!
+ * \brief Get the private key associated with a certificate
+ *
+ * \param cert The certificate to get the private key from
  *
  * \retval NULL on failure
  * \retval The private key on success
  */
-EVP_PKEY *stir_shaken_certificate_get_private_key(const char *caller_id_number);
+EVP_PKEY *stir_shaken_certificate_get_private_key(struct stir_shaken_certificate *cert);
 
 /*!
  * \brief Load time initialization for the stir/shaken 'certificate' configuration

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

Gerrit-Project: asterisk
Gerrit-Branch: master
Gerrit-Change-Id: I74fa41c0640ab2a64a1a80110155bd7062f13393
Gerrit-Change-Number: 14031
Gerrit-PatchSet: 5
Gerrit-Owner: Benjamin Keith Ford <bford at digium.com>
Gerrit-Reviewer: Friendly Automation
Gerrit-Reviewer: George Joseph <gjoseph at digium.com>
Gerrit-Reviewer: Joshua Colp <jcolp at sangoma.com>
Gerrit-Reviewer: Kevin Harwell <kharwell at digium.com>
Gerrit-CC: Corey Farrell <git at cfware.com>
Gerrit-MessageType: merged
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20200403/b44fb00e/attachment-0001.html>


More information about the asterisk-code-review mailing list