From 0564d1228049d27ee4a0216662512e3f64a8a85e Mon Sep 17 00:00:00 2001
From: Ben Ford <bford@digium.com>
Date: Mon, 26 Apr 2021 17:00:11 -0500
Subject: [PATCH] STIR/SHAKEN: Switch to base64 URL encoding.

STIR/SHAKEN encodes using base64 URL format. Currently, we just use
base64. New functions have been added that convert to and from base64
encoding.

The origid field should also be an UUID. This means there's no reason to
have it as an option in stir_shaken.conf, as we can simply generate one
when creating the Identity header.

https://wiki.asterisk.org/wiki/display/AST/OpenSIPit+2021

Change-Id: Icf094a2a54e87db91d6b12244c9f5ba4fc2e0b8c
---
 configs/samples/stir_shaken.conf.sample    |   3 -
 doc/UPGRADE-staging/stir_shaken_origid.txt |   8 ++
 include/asterisk/utils.h                   |  60 ++++++++++
 main/utils.c                               | 129 +++++++++++++++++++++
 res/res_pjsip_stir_shaken.c                |   8 +-
 res/res_stir_shaken.c                      |  34 +++---
 res/res_stir_shaken/certificate.c          |   8 --
 res/res_stir_shaken/certificate.h          |  10 --
 8 files changed, 214 insertions(+), 46 deletions(-)
 create mode 100644 doc/UPGRADE-staging/stir_shaken_origid.txt

diff --git a/configs/samples/stir_shaken.conf.sample b/configs/samples/stir_shaken.conf.sample
index 1bd260641b..c39bc97265 100644
--- a/configs/samples/stir_shaken.conf.sample
+++ b/configs/samples/stir_shaken.conf.sample
@@ -83,6 +83,3 @@
 ;
 ; Must have an attestation of A, B, or C
 ;attestation=C
-;
-; The origination identifier for the certificate
-;origid=MyAsterisk
diff --git a/doc/UPGRADE-staging/stir_shaken_origid.txt b/doc/UPGRADE-staging/stir_shaken_origid.txt
new file mode 100644
index 0000000000..f0b897757f
--- /dev/null
+++ b/doc/UPGRADE-staging/stir_shaken_origid.txt
@@ -0,0 +1,8 @@
+Subject: STIR/SHAKEN
+
+STIR/SHAKEN originally needed an origid to be specified in
+stir_shaken.conf under the certificate config object in
+order to work. Now, one is automatically created by
+generating a UUID, as recommended by RFC8588. Any origid
+you have in your stir_shaken.conf will need to be removed
+for the module to read in certificates.
diff --git a/include/asterisk/utils.h b/include/asterisk/utils.h
index 0ee11ee5d5..08120bf220 100644
--- a/include/asterisk/utils.h
+++ b/include/asterisk/utils.h
@@ -276,6 +276,66 @@ int ast_base64decode(unsigned char *dst, const char *src, int max);
  */
 char *ast_base64decode_string(const char *src);
 
+/*!
+ * \brief Decode data from base64 URL
+ *
+ * \param dst The destination buffer
+ * \param src The source buffer
+ * \param max The maximum number of bytes to write into the destination
+ *            buffer. Note that this function will not ensure that the
+ *            destination buffer is NULL terminated. So, in general,
+ *            this parameter should be sizeof(dst) - 1
+ */
+int ast_base64url_decode(unsigned char *dst, const char *src, int max);
+
+/*!
+ * \brief Same as ast_base64encode_full but for base64 URL
+ *
+ * \param dst The destination buffer
+ * \param src The source buffer
+ * \param srclen The number of bytes present in the source buffer
+ * \param max The maximum number of bytes to write into the destination
+ *            buffer, *including* the terminating NULL character.
+ * \param linebreaks Set to 1 if there should be linebreaks inserted
+ *                   in the result
+ */
+int ast_base64url_encode_full(char *dst, const unsigned char *src, int srclen, int max, int linebreaks);
+
+/*!
+ * \brief Encode data in base64 URL
+ *
+ * \param dst The destination buffer
+ * \param src The source data to be encoded
+ * \param srclen The number of bytes present in the source buffer
+ * \param max The maximum number of bytes to write into the destination
+ *            buffer, including the terminating NULL character
+ */
+int ast_base64url_encode(char *dst, const unsigned char *src, int srclen, int max);
+
+/*!
+ * \brief Decode string from base64 URL
+ *
+ * \note The returned string will need to be freed later
+ *
+ * \param src The source buffer
+ *
+ * \retval NULL on failure
+ * \retval Decoded string on success
+ */
+char *ast_base64url_decode_string(const char *src);
+
+/*!
+ * \brief Encode string in base64 URL
+ *
+ * \note The returned string will need to be freed later
+ *
+ * \param src The source data to be encoded
+ *
+ * \retval NULL on failure
+ * \retval Encoded string on success
+ */
+char *ast_base64url_encode_string(const char *src);
+
 #define AST_URI_ALPHANUM     (1 << 0)
 #define AST_URI_MARK         (1 << 1)
 #define AST_URI_UNRESERVED   (AST_URI_ALPHANUM | AST_URI_MARK)
diff --git a/main/utils.c b/main/utils.c
index 827ee2e57a..c6e71d9fd2 100644
--- a/main/utils.c
+++ b/main/utils.c
@@ -70,8 +70,15 @@
 #define AST_API_MODULE
 #include "asterisk/alertpipe.h"
 
+/* These arrays are global static variables because they are only modified
+ * once - in base64_init. The only purpose they have is to serve as a dictionary
+ * for encoding and decoding base64 and base64 URL, so there's no harm in
+ * accessing these arrays in multiple threads.
+ */
 static char base64[64];
+static char base64url[64];
 static char b2a[256];
+static char b2a_url[256];
 
 AST_THREADSTORAGE(inet_ntoa_buf);
 
@@ -417,28 +424,150 @@ char *ast_base64encode_string(const char *src)
 	return encoded_string;
 }
 
+int ast_base64url_decode(unsigned char *dst, const char *src, int max)
+{
+	int cnt = 0;
+	unsigned int byte = 0;
+	unsigned int bits = 0;
+
+	while (*src && (cnt < max)) {
+		byte <<= 6;
+		byte |= (b2a_url[(int)(*src)]) & 0x3f;
+		bits += 6;
+		src++;
+		if (bits >= 8) {
+			bits -= 8;
+			*dst = (byte >> bits) & 0xff;
+			dst++;
+			cnt++;
+		}
+	}
+	return cnt;
+}
+
+char *ast_base64url_decode_string(const char *src)
+{
+	size_t decoded_len;
+	unsigned char *decoded_string;
+
+	if (ast_strlen_zero(src)) {
+		return NULL;
+	}
+
+	decoded_len = strlen(src) * 3 / 4;
+	decoded_string = ast_malloc(decoded_len + 1);
+	if (!decoded_string) {
+		return NULL;
+	}
+
+	ast_base64url_decode(decoded_string, src, decoded_len);
+	decoded_string[decoded_len] = '\0';
+
+	return (char *)decoded_string;
+}
+
+int ast_base64url_encode_full(char *dst, const unsigned char *src, int srclen, int max, int linebreaks)
+{
+	int cnt = 0;
+	int col = 0;
+	unsigned int byte = 0;
+	int bits = 0;
+	int cntin = 0;
+
+	max--;
+	while ((cntin < srclen) && (cnt < max)) {
+		byte <<= 8;
+		byte |= *(src++);
+		bits += 8;
+		cntin++;
+		if ((bits == 24) && (cnt + 4 <= max)) {
+			*dst++ = base64url[(byte >> 18) & 0x3f];
+			*dst++ = base64url[(byte >> 12) & 0x3f];
+			*dst++ = base64url[(byte >> 6) & 0x3f];
+			*dst++ = base64url[(byte) & 0x3f];
+			cnt += 4;
+			col += 4;
+			bits = 0;
+			byte = 0;
+		}
+		if (linebreaks && (cnt < max) && (col == 64)) {
+			*dst++ = '\n';
+			cnt++;
+			col = 0;
+		}
+	}
+	if (bits && (cnt + 4 <= max)) {
+		byte <<= 24 - bits;
+		*dst++ = base64url[(byte >> 18) & 0x3f];
+		*dst++ = base64url[(byte >> 12) & 0x3f];
+		if (bits == 16) {
+			*dst++ = base64url[(byte >> 6) & 0x3f];
+		}
+		cnt += 4;
+	}
+	if (linebreaks && (cnt < max)) {
+		*dst++ = '\n';
+		cnt++;
+	}
+	*dst = '\0';
+	return cnt;
+}
+
+int ast_base64url_encode(char *dst, const unsigned char *src, int srclen, int max)
+{
+	return ast_base64url_encode_full(dst, src, srclen, max, 0);
+}
+
+char *ast_base64url_encode_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_malloc(encoded_len);
+
+	ast_base64url_encode(encoded_string, (const unsigned char *)src, strlen(src), encoded_len);
+
+	return encoded_string;
+}
+
 static void base64_init(void)
 {
 	int x;
 	memset(b2a, -1, sizeof(b2a));
+	memset(b2a_url, -1, sizeof(b2a_url));
 	/* Initialize base-64 Conversion table */
 	for (x = 0; x < 26; x++) {
 		/* A-Z */
 		base64[x] = 'A' + x;
+		base64url[x] = 'A' + x;
 		b2a['A' + x] = x;
+		b2a_url['A' + x] = x;
 		/* a-z */
 		base64[x + 26] = 'a' + x;
+		base64url[x + 26] = 'a' + x;
 		b2a['a' + x] = x + 26;
+		b2a_url['a' + x] = x + 26;
 		/* 0-9 */
 		if (x < 10) {
 			base64[x + 52] = '0' + x;
+			base64url[x + 52] = '0' + x;
 			b2a['0' + x] = x + 52;
+			b2a_url['0' + x] = x + 52;
 		}
 	}
 	base64[62] = '+';
 	base64[63] = '/';
+	base64url[62] = '-';
+	base64url[63] = '_';
 	b2a[(int)'+'] = 62;
 	b2a[(int)'/'] = 63;
+	b2a_url[(int)'-'] = 62;
+	b2a_url[(int)'_'] = 63;
 }
 
 const struct ast_flags ast_uri_http = {AST_URI_UNRESERVED};
diff --git a/res/res_pjsip_stir_shaken.c b/res/res_pjsip_stir_shaken.c
index 351d7ccf29..a90b821e55 100644
--- a/res/res_pjsip_stir_shaken.c
+++ b/res/res_pjsip_stir_shaken.c
@@ -146,14 +146,14 @@ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_r
 	}
 
 	encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val);
-	header = ast_base64decode_string(encoded_val);
+	header = ast_base64url_decode_string(encoded_val);
 	if (ast_strlen_zero(header)) {
 		ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED);
 		return 0;
 	}
 
 	encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val);
-	payload = ast_base64decode_string(encoded_val);
+	payload = ast_base64url_decode_string(encoded_val);
 	if (ast_strlen_zero(payload)) {
 		ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED);
 		return 0;
@@ -241,7 +241,7 @@ static void add_identity_header(const struct ast_sip_session *session, pjsip_tx_
 
 	header = ast_json_object_get(json, "header");
 	dumped_string = ast_json_dump_string(header);
-	encoded_header = ast_base64encode_string(dumped_string);
+	encoded_header = ast_base64url_encode_string(dumped_string);
 	ast_json_free(dumped_string);
 	if (!encoded_header) {
 		ast_log(LOG_ERROR, "Failed to encode STIR/SHAKEN header\n");
@@ -250,7 +250,7 @@ static void add_identity_header(const struct ast_sip_session *session, pjsip_tx_
 
 	payload = ast_json_object_get(json, "payload");
 	dumped_string = ast_json_dump_string(payload);
-	encoded_payload = ast_base64encode_string(dumped_string);
+	encoded_payload = ast_base64url_encode_string(dumped_string);
 	ast_json_free(dumped_string);
 	if (!encoded_payload) {
 		ast_log(LOG_ERROR, "Failed to encode STIR/SHAKEN payload\n");
diff --git a/res/res_stir_shaken.c b/res/res_stir_shaken.c
index f8eb97fe4f..dbc2de08c4 100644
--- a/res/res_stir_shaken.c
+++ b/res/res_stir_shaken.c
@@ -104,9 +104,6 @@
 				<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>
@@ -503,7 +500,7 @@ static int stir_shaken_verify_signature(const char *msg, const char *signature,
 	EVP_MD_CTX *mdctx = NULL;
 	int ret = 0;
 	unsigned char *decoded_signature;
-	size_t signature_length, decoded_signature_length, padding = 0;
+	size_t signature_length, decoded_signature_length;
 
 	mdctx = EVP_MD_CTX_create();
 	if (!mdctx) {
@@ -525,19 +522,12 @@ static int stir_shaken_verify_signature(const char *msg, const char *signature,
 		return -1;
 	}
 
-	/* We need to decode the signature from base64 to bytes. Make sure we have
+	/* We need to decode the signature from base64 URL to bytes. Make sure we have
 	 * at least enough characters for this check */
 	signature_length = strlen(signature);
-	if (signature_length > 2 && signature[signature_length - 1] == '=') {
-		padding++;
-		if (signature[signature_length - 2] == '=') {
-			padding++;
-		}
-	}
-
-	decoded_signature_length = (signature_length / 4 * 3) - padding;
+	decoded_signature_length = (signature_length * 3 / 4);
 	decoded_signature = ast_calloc(1, decoded_signature_length);
-	ast_base64decode(decoded_signature, signature, decoded_signature_length);
+	ast_base64url_decode(decoded_signature, signature, decoded_signature_length);
 
 	ret = EVP_DigestVerifyFinal(mdctx, decoded_signature, decoded_signature_length);
 	if (ret != 1) {
@@ -944,7 +934,7 @@ static unsigned char *stir_shaken_sign(char *json_str, EVP_PKEY *private_key)
 		goto cleanup;
 	}
 
-	/* There are 6 bits to 1 base64 digit, so in order to get the size of the base64 encoded
+	/* There are 6 bits to 1 base64 URL 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.
@@ -956,7 +946,7 @@ static unsigned char *stir_shaken_sign(char *json_str, EVP_PKEY *private_key)
 		goto cleanup;
 	}
 
-	ast_base64encode((char *)encoded_signature, signature, signature_length, encoded_length);
+	ast_base64url_encode((char *)encoded_signature, signature, signature_length, encoded_length);
 
 cleanup:
 	if (mdctx) {
@@ -1013,20 +1003,22 @@ static int stir_shaken_add_attest(struct ast_json *json, const char *attest)
  * \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)
+static int stir_shaken_add_origid(struct ast_json *json)
 {
 	struct ast_json *value;
+	char uuid_str[AST_UUID_STR_LEN];
 
-	value = ast_json_string_create(origid);
-	if (!origid) {
+	ast_uuid_generate_str(uuid_str, sizeof(uuid_str));
+	if (strlen(uuid_str) != (AST_UUID_STR_LEN - 1)) {
 		return -1;
 	}
 
+	value = ast_json_string_create(uuid_str);
+
 	return ast_json_object_set(ast_json_object_get(json, "payload"), "origid", value);
 }
 
@@ -1097,7 +1089,7 @@ struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json)
 		goto cleanup;
 	}
 
-	if (stir_shaken_add_origid(json, stir_shaken_certificate_get_origid(cert))) {
+	if (stir_shaken_add_origid(json)) {
 		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 f4103f96ec..df4f38b8f7 100644
--- a/res/res_stir_shaken/certificate.c
+++ b/res/res_stir_shaken/certificate.c
@@ -40,8 +40,6 @@ struct stir_shaken_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;
@@ -105,11 +103,6 @@ const char *stir_shaken_certificate_get_attestation(struct stir_shaken_certifica
 	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)
 {
 	return cert ? cert->private_key : NULL;
@@ -378,7 +371,6 @@ int stir_shaken_certificate_load(void)
 		on_load_public_cert_url, public_cert_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 9574d46795..c95cba56b6 100644
--- a/res/res_stir_shaken/certificate.h
+++ b/res/res_stir_shaken/certificate.h
@@ -54,16 +54,6 @@ const char *stir_shaken_certificate_get_public_cert_url(struct stir_shaken_certi
  */
 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