diff --git a/include/asterisk/res_stir_shaken.h b/include/asterisk/res_stir_shaken.h
index 0c589a9b74b836050a3b1c0a07b341e39876f57d..16f0139ff9605ef610ebd6e6b5e0ee60a86de68e 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
+ * \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
  *
- * \retval The private key
+ * \note This function will automatically add the "attest", "iat", and "origid" fields.
  */
-EVP_PKEY *ast_stir_shaken_get_private_key(const char *caller_id_number);
+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 a6656d07e91d895a5deedc4c8d31b2a07f3c5f39..cb4cc8290194410289ef50eb1dae0dcbe6da7f58 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)
+{
+	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)
 {
-	return stir_shaken_certificate_get_private_key(caller_id_number);
+	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 799cea13af2d8f76eac8efedd0f935e14861c0a7..812fc1e6d9a21545ce844675d85391eda697fdff 100644
--- a/res/res_stir_shaken/certificate.c
+++ b/res/res_stir_shaken/certificate.c
@@ -79,23 +79,34 @@ static void *stir_shaken_certificate_alloc(const char *name)
 	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);
+}
+
+const char *stir_shaken_certificate_get_public_key_url(struct stir_shaken_certificate *cert)
+{
+	if (!cert) {
+		return NULL;
+	}
 
-	if (cert) {
-		return cert->private_key;
+	return cert->public_key_url;
+}
+
+EVP_PKEY *stir_shaken_certificate_get_private_key(struct stir_shaken_certificate *cert)
+{
+	if (!cert) {
+		return NULL;
 	}
 
-	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 9d6ec7379c3d913d5ecfc79fe2c68a1685f28d1a..fda3bf1a019ba1658a4ec333d193f452a31c72fd 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 public key URL associated with a certificate
+ *
+ * \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 caller id
+ * \brief Get the private key 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 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