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

Benjamin Keith Ford asteriskteam at digium.com
Thu Mar 26 13:38:49 CDT 2020


Benjamin Keith Ford has uploaded this change for review. ( 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. Three fields are added
to the payload: 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 three values, a
ast_stir_shaken_payload is returned, containing other useful information
such as the algorithm, signature, and public_key_url.

Change-Id: I74fa41c0640ab2a64a1a80110155bd7062f13393
---
M include/asterisk/res_stir_shaken.h
M res/res_stir_shaken.c
2 files changed, 363 insertions(+), 0 deletions(-)



  git pull ssh://gerrit.asterisk.org:29418/asterisk refs/changes/31/14031/1

diff --git a/include/asterisk/res_stir_shaken.h b/include/asterisk/res_stir_shaken.h
index 0c589a9..1fff9c5 100644
--- a/include/asterisk/res_stir_shaken.h
+++ b/include/asterisk/res_stir_shaken.h
@@ -18,9 +18,24 @@
 #ifndef _RES_STIR_SHAKEN_H
 #define _RES_STIR_SHAKEN_H
 
+#include "asterisk/json.h"
+
 #include <openssl/evp.h>
 #include <openssl/pem.h>
 
+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;
+};
+
 /*!
  * \brief Retrieve the stir/shaken sorcery context
  *
@@ -37,4 +52,16 @@
  */
 EVP_PKEY *ast_stir_shaken_get_private_key(const char *caller_id_number);
 
+/*!
+ * \brief Free a STIR/SHAKEN payload
+ */
+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..8c12398 100644
--- a/res/res_stir_shaken.c
+++ b/res/res_stir_shaken.c
@@ -24,6 +24,7 @@
 
 #include "asterisk/module.h"
 #include "asterisk/sorcery.h"
+#include "asterisk/time.h"
 
 #include "asterisk/res_stir_shaken.h"
 #include "res_stir_shaken/stir_shaken.h"
@@ -31,6 +32,10 @@
 #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_sorcery *ast_stir_shaken_sorcery(void)
@@ -38,11 +43,342 @@
 	return stir_shaken_sorcery;
 }
 
+void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload)
+{
+	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);
+}
+
 EVP_PKEY *ast_stir_shaken_get_private_key(const char *caller_id_number)
 {
 	return stir_shaken_certificate_get_private_key(caller_id_number);
 }
 
+/*!
+ * \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 (!val || strlen(val) == 0) {
+		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'\n", STIR_SHAKEN_PPT);
+		goto cleanup;
+	}
+
+	/* Check the typ value for "passport" */
+	val = ast_json_string_get(ast_json_object_get(obj, "typ"));
+	if (!val || strlen(val) == 0) {
+		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'\n", STIR_SHAKEN_TYPE);
+		goto cleanup;
+	}
+
+	/* Check the alg value for "ES256" */
+	val = ast_json_string_get(ast_json_object_get(obj, "alg"));
+	if (!val || strlen(val) == 0) {
+		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'\n", STIR_SHAKEN_ENCRYPTION_ALGORITHM);
+		goto cleanup;
+	}
+
+	payload->algorithm = ast_strdup(val);
+	if (!payload->algorithm) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN payload failed to copy 'algorithm'\n");
+		goto cleanup;
+	}
+
+	/* Check the x5u value for a URL */
+	val = ast_json_string_get(ast_json_object_get(obj, "x5u"));
+	if (!val || strlen(val) == 0) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have required field 'x5u' (public key URL)\n");
+		goto cleanup;
+	}
+
+	payload->public_key_url = ast_strdup(val);
+	if (!payload->public_key_url) {
+		ast_log(LOG_ERROR, "STIR/SHAKEN payload failed to copy 'x5u' (public key URL)\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 (!val || strlen(val) == 0) {
+		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 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;
+	}
+
+	encoded_signature = ast_calloc(1, sizeof(unsigned char) * (signature_length + 1));
+	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, signature_length + 1);
+
+cleanup:
+	if (mdctx) {
+		EVP_MD_CTX_destroy(mdctx);
+	}
+	ast_free(signature);
+
+	return encoded_signature;
+}
+
+/*!
+ * \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;
+	EVP_PKEY *private_key;
+
+	payload = stir_shaken_verify_json(json);
+	if (!payload) {
+		return NULL;
+	}
+
+	/* From the payload section of the HSON, 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"));
+	private_key = stir_shaken_certificate_get_private_key(caller_id_num);
+	if (!private_key) {
+		ast_log(LOG_ERROR, "Failed to get private key to sign payload\n");
+		ast_stir_shaken_payload_free(payload);
+		return NULL;
+	}
+
+	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, private_key);
+	if (!signature) {
+		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;
+	}
+
+	payload->signature = signature;
+	ast_json_free(json_str);
+
+	return payload;
+
+cleanup:
+	ast_stir_shaken_payload_free(payload);
+	ast_json_free(json_str);
+	return NULL;
+}
+
 static int reload_module(void)
 {
 	if (stir_shaken_sorcery) {

-- 
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: 1
Gerrit-Owner: Benjamin Keith Ford <bford at digium.com>
Gerrit-MessageType: newchange
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20200326/88ca37af/attachment-0001.html>


More information about the asterisk-code-review mailing list