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
This commit is contained in:
Ben Ford
2020-06-02 09:04:23 -05:00
committed by Friendly Automation
parent db012e8cc6
commit 1274117102
8 changed files with 239 additions and 22 deletions

View File

@@ -47,3 +47,9 @@
; ;
; URL to the public key ; URL to the public key
;public_key_url=http://mycompany.com/alice.pub ;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

View File

@@ -21,6 +21,10 @@
#include <openssl/evp.h> #include <openssl/evp.h>
#include <openssl/pem.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 { enum ast_stir_shaken_verification_result {
AST_STIR_SHAKEN_VERIFY_NOT_PRESENT, /*! No STIR/SHAKEN information was available */ AST_STIR_SHAKEN_VERIFY_NOT_PRESENT, /*! No STIR/SHAKEN information was available */
AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED, /*! Signature verification failed */ AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED, /*! Signature verification failed */
@@ -32,6 +36,24 @@ struct ast_stir_shaken_payload;
struct ast_json; 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 * \brief Retrieve the value for 'signature_timeout' from 'general' config object
* *

View File

@@ -239,6 +239,19 @@ int ast_base64encode_full(char *dst, const unsigned char *src, int srclen, int m
*/ */
int ast_base64encode(char *dst, const unsigned char *src, int srclen, int max); 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 * \brief Decode data from base64
* \param dst the destination buffer * \param dst the destination buffer

View File

@@ -398,6 +398,24 @@ int ast_base64encode(char *dst, const unsigned char *src, int srclen, int max)
return ast_base64encode_full(dst, src, srclen, max, 0); 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) static void base64_init(void)
{ {
int x; int x;

View File

@@ -194,10 +194,102 @@ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_r
return 0; 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 = { static struct ast_sip_session_supplement stir_shaken_supplement = {
.method = "INVITE", .method = "INVITE",
.priority = AST_SIP_SUPPLEMENT_PRIORITY_CHANNEL + 1, /* Run AFTER channel creation */ .priority = AST_SIP_SUPPLEMENT_PRIORITY_CHANNEL + 1, /* Run AFTER channel creation */
.incoming_request = stir_shaken_incoming_request, .incoming_request = stir_shaken_incoming_request,
.outgoing_request = stir_shaken_outgoing_request,
}; };
static int unload_module(void) static int unload_module(void)

View File

@@ -99,6 +99,12 @@
Must be a valid http, or https, URL. Must be a valid http, or https, URL.
</para></description> </para></description>
</configOption> </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=""> <configOption name="caller_id_number" default="">
<synopsis>The caller ID number to match on.</synopsis> <synopsis>The caller ID number to match on.</synopsis>
</configOption> </configOption>
@@ -136,10 +142,6 @@
</function> </function>
***/ ***/
#define STIR_SHAKEN_ENCRYPTION_ALGORITHM "ES256"
#define STIR_SHAKEN_PPT "shaken"
#define STIR_SHAKEN_TYPE "passport"
static struct ast_sorcery *stir_shaken_sorcery; static struct ast_sorcery *stir_shaken_sorcery;
/* Used for AstDB entries */ /* Used for AstDB entries */
@@ -184,6 +186,16 @@ void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload)
ast_free(payload); 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) unsigned int ast_stir_shaken_get_signature_timeout(void)
{ {
return ast_stir_shaken_signature_timeout(stir_shaken_general_get()); return ast_stir_shaken_signature_timeout(stir_shaken_general_get());
@@ -1020,6 +1032,7 @@ struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json)
{ {
struct ast_stir_shaken_payload *ss_payload; struct ast_stir_shaken_payload *ss_payload;
unsigned char *signature; unsigned char *signature;
const char *public_key_url;
const char *caller_id_num; const char *caller_id_num;
const char *header; const char *header;
const char *payload; const char *payload;
@@ -1049,22 +1062,19 @@ struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json)
goto cleanup; 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"); ast_log(LOG_ERROR, "Failed to add 'x5u' (public key URL) to payload\n");
goto cleanup; goto cleanup;
} }
ss_payload->public_key_url = ast_strdup(public_key_url);
/* TODO: This is just a placeholder for adding 'attest', 'iat', and if (stir_shaken_add_attest(json, stir_shaken_certificate_get_attestation(cert))) {
* '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"); ast_log(LOG_ERROR, "Failed to add 'attest' to payload\n");
goto cleanup; 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"); ast_log(LOG_ERROR, "Failed to add 'origid' to payload\n");
goto cleanup; goto cleanup;
} }

View File

@@ -38,6 +38,10 @@ struct stir_shaken_certificate {
AST_STRING_FIELD(public_key_url); AST_STRING_FIELD(public_key_url);
/*! The caller ID number associated with the certificate */ /*! The caller ID number associated with the certificate */
AST_STRING_FIELD(caller_id_number); 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 */ /*! The private key for the certificate */
EVP_PKEY *private_key; EVP_PKEY *private_key;
@@ -93,20 +97,22 @@ struct stir_shaken_certificate *stir_shaken_certificate_get_by_caller_id_number(
const char *stir_shaken_certificate_get_public_key_url(struct stir_shaken_certificate *cert) const char *stir_shaken_certificate_get_public_key_url(struct stir_shaken_certificate *cert)
{ {
if (!cert) { return cert ? cert->public_key_url : NULL;
return 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) EVP_PKEY *stir_shaken_certificate_get_private_key(struct stir_shaken_certificate *cert)
{ {
if (!cert) { return cert ? cert->private_key : NULL;
return NULL;
}
return cert->private_key;
} }
static int stir_shaken_certificate_apply(const struct ast_sorcery *sorcery, void *obj) static int stir_shaken_certificate_apply(const struct ast_sorcery *sorcery, void *obj)
@@ -114,11 +120,16 @@ static int stir_shaken_certificate_apply(const struct ast_sorcery *sorcery, void
EVP_PKEY *private_key; EVP_PKEY *private_key;
struct stir_shaken_certificate *cert = obj; 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"); ast_log(LOG_ERROR, "Caller ID must be present\n");
return -1; 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); private_key = stir_shaken_read_key(cert->path, 1);
if (!private_key) { if (!private_key) {
return -1; return -1;
@@ -244,6 +255,28 @@ static int public_key_url_to_str(const void *obj, const intptr_t *args, char **b
return 0; 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 #ifdef TEST_FRAMEWORK
/* Name for test certificaate */ /* Name for test certificaate */
@@ -343,6 +376,9 @@ int stir_shaken_certificate_load(void)
on_load_path, path_to_str, NULL, 0, 0); on_load_path, path_to_str, NULL, 0, 0);
ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "public_key_url", "", 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); 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_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, ast_cli_register_multiple(stir_shaken_certificate_cli,

View File

@@ -44,6 +44,26 @@ struct stir_shaken_certificate *stir_shaken_certificate_get_by_caller_id_number(
*/ */
const char *stir_shaken_certificate_get_public_key_url(struct stir_shaken_certificate *cert); 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 * \brief Get the private key associated with a certificate
* *