Asterisk - The Open Source Telephony Project  18.5.0
res_pjsip_stir_shaken.c
Go to the documentation of this file.
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2020, Sangoma Technologies Corporation
5  *
6  * Ben Ford <[email protected]>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18 
19 /*** MODULEINFO
20  <depend>pjproject</depend>
21  <depend>res_pjsip</depend>
22  <depend>res_pjsip_session</depend>
23  <depend>res_stir_shaken</depend>
24  <support_level>core</support_level>
25  ***/
26 
27 #include "asterisk.h"
28 
29 #include "asterisk/res_pjsip.h"
31 #include "asterisk/module.h"
32 
34 
35 /*!
36  * \brief Get the attestation from the payload
37  *
38  * \param json_str The JSON string representation of the payload
39  *
40  * \retval Empty string on failure
41  * \retval The attestation on success
42  */
43 static char *get_attestation_from_payload(const char *json_str)
44 {
45  RAII_VAR(struct ast_json *, json, NULL, ast_json_free);
46  char *attestation;
47 
48  json = ast_json_load_string(json_str, NULL);
49  attestation = (char *)ast_json_string_get(ast_json_object_get(json, "attest"));
50 
51  if (!ast_strlen_zero(attestation)) {
52  return attestation;
53  }
54 
55  return "";
56 }
57 
58 /*!
59  * \brief Compare the caller ID from the INVITE with the one in the payload
60  *
61  * \param json_str The JSON string represntation of the payload
62  *
63  * \retval -1 on failure
64  * \retval 0 on success
65  */
66 static int compare_caller_id(char *caller_id, const char *json_str)
67 {
68  RAII_VAR(struct ast_json *, json, NULL, ast_json_free);
69  char *caller_id_other;
70 
71  json = ast_json_load_string(json_str, NULL);
72  caller_id_other = (char *)ast_json_string_get(ast_json_object_get(
73  ast_json_object_get(json, "orig"), "tn"));
74 
75  if (strcmp(caller_id, caller_id_other)) {
76  return -1;
77  }
78 
79  return 0;
80 }
81 
82 /*!
83  * \brief Compare the current timestamp with the one in the payload. If the difference
84  * is greater than the signature timeout, it's not valid anymore
85  *
86  * \param json_str The JSON string representation of the payload
87  *
88  * \retval -1 on failure
89  * \retval 0 on success
90  */
91 static int compare_timestamp(const char *json_str)
92 {
93  RAII_VAR(struct ast_json *, json, NULL, ast_json_free);
94  long int timestamp;
95  struct timeval now = ast_tvnow();
96 
97 #ifdef TEST_FRAMEWORK
98  ast_debug(3, "Ignoring STIR/SHAKEN timestamp\n");
99  return 0;
100 #endif
101 
102  json = ast_json_load_string(json_str, NULL);
103  timestamp = ast_json_integer_get(ast_json_object_get(json, "iat"));
104 
105  if (now.tv_sec - timestamp > ast_stir_shaken_get_signature_timeout()) {
106  return -1;
107  }
108 
109  return 0;
110 }
111 
112 /*!
113  * \internal
114  * \brief Session supplement callback on an incoming INVITE request
115  *
116  * When we receive an INVITE, check it for STIR/SHAKEN information and
117  * decide what to do from there
118  *
119  * \param session The session that has received an INVITE
120  * \param rdata The incoming INVITE
121  */
122 static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_rx_data *rdata)
123 {
124  static const pj_str_t identity_str = { "Identity", 8 };
125  char *identity_hdr_val;
126  char *encoded_val;
127  struct ast_channel *chan = session->channel;
128  char *caller_id = session->id.number.str;
129  RAII_VAR(char *, header, NULL, ast_free);
130  RAII_VAR(char *, payload, NULL, ast_free);
131  char *signature;
132  char *algorithm;
133  char *public_cert_url;
134  char *attestation;
135  int mismatch = 0;
136  struct ast_stir_shaken_payload *ss_payload;
137 
138  if (!session->endpoint->stir_shaken) {
139  return 0;
140  }
141 
142  identity_hdr_val = ast_sip_rdata_get_header_value(rdata, identity_str);
143  if (ast_strlen_zero(identity_hdr_val)) {
145  return 0;
146  }
147 
148  encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val);
149  header = ast_base64url_decode_string(encoded_val);
150  if (ast_strlen_zero(header)) {
152  return 0;
153  }
154 
155  encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val);
156  payload = ast_base64url_decode_string(encoded_val);
157  if (ast_strlen_zero(payload)) {
159  return 0;
160  }
161 
162  /* It's fine to leave the signature encoded */
163  signature = strtok_r(identity_hdr_val, ";", &identity_hdr_val);
164  if (ast_strlen_zero(signature)) {
166  return 0;
167  }
168 
169  /* Trim "info=<" to get public cert URL */
170  strtok_r(identity_hdr_val, "<", &identity_hdr_val);
171  public_cert_url = strtok_r(identity_hdr_val, ">", &identity_hdr_val);
172  if (ast_strlen_zero(public_cert_url)) {
174  return 0;
175  }
176 
177  /* Make sure the public URL is actually a URL */
178  if (!ast_begins_with(public_cert_url, "http")) {
180  return 0;
181  }
182 
183  algorithm = strtok_r(identity_hdr_val, ";", &identity_hdr_val);
184  if (ast_strlen_zero(algorithm)) {
186  return 0;
187  }
188 
189  attestation = get_attestation_from_payload(payload);
190 
191  ss_payload = ast_stir_shaken_verify(header, payload, signature, algorithm, public_cert_url);
192  if (!ss_payload) {
194  return 0;
195  }
196  ast_stir_shaken_payload_free(ss_payload);
197 
198  mismatch |= compare_caller_id(caller_id, payload);
199  mismatch |= compare_timestamp(payload);
200 
201  if (mismatch) {
203  return 0;
204  }
205 
207 
208  return 0;
209 }
210 
211 static int add_identity_header(const struct ast_sip_session *session, pjsip_tx_data *tdata)
212 {
213  static const pj_str_t identity_str = { "Identity", 8 };
214  pjsip_generic_string_hdr *identity_hdr;
215  pj_str_t identity_val;
216  pjsip_fromto_hdr *old_identity;
217  pjsip_fromto_hdr *to;
218  pjsip_sip_uri *uri;
219  char *signature;
220  char *public_cert_url;
221  struct ast_json *header;
222  struct ast_json *payload;
223  char *dumped_string;
224  RAII_VAR(char *, dest_tn, NULL, ast_free);
225  RAII_VAR(struct ast_json *, json, NULL, ast_json_free);
227  RAII_VAR(char *, encoded_header, NULL, ast_free);
228  RAII_VAR(char *, encoded_payload, NULL, ast_free);
229  RAII_VAR(char *, combined_str, NULL, ast_free);
230  size_t combined_size;
231 
232  old_identity = pjsip_msg_find_hdr_by_name(tdata->msg, &identity_str, NULL);
233  if (old_identity) {
234  return 0;
235  }
236 
237  to = pjsip_msg_find_hdr(tdata->msg, PJSIP_H_TO, NULL);
238  if (!to) {
239  ast_log(LOG_ERROR, "Failed to find To header while adding STIR/SHAKEN Identity header\n");
240  return -1;
241  }
242 
243  uri = pjsip_uri_get_uri(to->uri);
244  if (!uri) {
245  ast_log(LOG_ERROR, "Failed to retrieve URI from To header while adding STIR/SHAKEN Identity header\n");
246  return -1;
247  }
248 
249  dest_tn = ast_malloc(uri->user.slen + 1);
250  if (!dest_tn) {
251  ast_log(LOG_ERROR, "Failed to allocate memory for STIR/SHAKEN dest->tn\n");
252  return -1;
253  }
254 
255  ast_copy_pj_str(dest_tn, &uri->user, uri->user.slen + 1);
256 
257  /* x5u (public key URL), attestation, and origid will be added by ast_stir_shaken_sign */
258  json = ast_json_pack("{s: {s: s, s: s, s: s}, s: {s: {s: s}, s: {s: s}}}",
259  "header", "alg", "ES256", "ppt", "shaken", "typ", "passport",
260  "payload", "dest", "tn", dest_tn, "orig", "tn",
261  session->id.number.str);
262  if (!json) {
263  ast_log(LOG_ERROR, "Failed to allocate memory for STIR/SHAKEN JSON\n");
264  return -1;
265  }
266 
267  ss_payload = ast_stir_shaken_sign(json);
268  if (!ss_payload) {
269  ast_log(LOG_ERROR, "Failed to allocate memory for STIR/SHAKEN payload\n");
270  return -1;
271  }
272 
273  header = ast_json_object_get(json, "header");
274  dumped_string = ast_json_dump_string(header);
275  encoded_header = ast_base64url_encode_string(dumped_string);
276  ast_json_free(dumped_string);
277  if (!encoded_header) {
278  ast_log(LOG_ERROR, "Failed to encode STIR/SHAKEN header\n");
279  return -1;
280  }
281 
282  payload = ast_json_object_get(json, "payload");
283  dumped_string = ast_json_dump_string(payload);
284  encoded_payload = ast_base64url_encode_string(dumped_string);
285  ast_json_free(dumped_string);
286  if (!encoded_payload) {
287  ast_log(LOG_ERROR, "Failed to encode STIR/SHAKEN payload\n");
288  return -1;
289  }
290 
291  signature = (char *)ast_stir_shaken_payload_get_signature(ss_payload);
292  public_cert_url = ast_stir_shaken_payload_get_public_cert_url(ss_payload);
293 
294  /* The format for the identity header:
295  * header.payload.signature;info=<public_cert_url>alg=STIR_SHAKEN_ENCRYPTION_ALGORITHM;ppt=STIR_SHAKEN_PPT
296  */
297  combined_size = strlen(encoded_header) + 1 + strlen(encoded_payload) + 1
298  + strlen(signature) + strlen(";info=<>alg=;ppt=") + strlen(public_cert_url)
299  + strlen(STIR_SHAKEN_ENCRYPTION_ALGORITHM) + strlen(STIR_SHAKEN_PPT) + 1;
300  combined_str = ast_calloc(1, combined_size);
301  if (!combined_str) {
302  ast_log(LOG_ERROR, "Failed to allocate memory for STIR/SHAKEN identity string\n");
303  return -1;
304  }
305  snprintf(combined_str, combined_size, "%s.%s.%s;info=<%s>alg=%s;ppt=%s", encoded_header,
306  encoded_payload, signature, public_cert_url, STIR_SHAKEN_ENCRYPTION_ALGORITHM, STIR_SHAKEN_PPT);
307 
308  identity_val = pj_str(combined_str);
309  identity_hdr = pjsip_generic_string_hdr_create(tdata->pool, &identity_str, &identity_val);
310  if (!identity_hdr) {
311  ast_log(LOG_ERROR, "Failed to create STIR/SHAKEN Identity header\n");
312  return -1;
313  }
314 
315  pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *)identity_hdr);
316 
317  return 0;
318 }
319 
320 static void add_date_header(const struct ast_sip_session *session, pjsip_tx_data *tdata)
321 {
322  static const pj_str_t date_str = { "Date", 4 };
323  pjsip_fromto_hdr *old_date;
324 
325  old_date = pjsip_msg_find_hdr_by_name(tdata->msg, &date_str, NULL);
326  if (old_date) {
327  ast_debug(3, "Found old STIR/SHAKEN date header, no need to add one\n");
328  return;
329  }
330 
332 }
333 
334 static void stir_shaken_outgoing_request(struct ast_sip_session *session, pjsip_tx_data *tdata)
335 {
336  if (!session->endpoint->stir_shaken) {
337  return;
338  }
339 
340  if (ast_strlen_zero(session->id.number.str) && session->id.number.valid) {
341  return;
342  }
343 
344  /* If adding the Identity header fails for some reason, there's no point
345  * adding the Date header.
346  */
347  if ((add_identity_header(session, tdata)) != 0) {
348  return;
349  }
350  add_date_header(session, tdata);
351 }
352 
354  .method = "INVITE",
355  .priority = AST_SIP_SUPPLEMENT_PRIORITY_CHANNEL + 1, /* Run AFTER channel creation */
356  .incoming_request = stir_shaken_incoming_request,
357  .outgoing_request = stir_shaken_outgoing_request,
358 };
359 
360 static int unload_module(void)
361 {
362  ast_sip_session_unregister_supplement(&stir_shaken_supplement);
363  return 0;
364 }
365 
366 static int load_module(void)
367 {
368  ast_sip_session_register_supplement(&stir_shaken_supplement);
370 }
371 
372 #undef AST_BUILDOPT_SUM
373 #define AST_BUILDOPT_SUM ""
374 
375 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "PJSIP STIR/SHAKEN Module for Asterisk",
376  .support_level = AST_MODULE_SUPPORT_CORE,
377  .load = load_module,
378  .unload = unload_module,
379  .load_pri = AST_MODPRI_DEFAULT,
380  .requires = "res_pjsip,res_pjsip_session,res_stir_shaken",
381 );
Main Channel structure associated with a channel.
struct ast_sip_endpoint * endpoint
char * ast_base64url_decode_string(const char *src)
Decode string from base64 URL.
Definition: main/utils.c:448
char * str
Subscriber phone number (Malloced)
Definition: channel.h:292
Asterisk main include file. File version handling, generic pbx functions.
unsigned char * signature
struct ast_json * ast_json_pack(char const *format,...)
Helper for creating complex JSON values.
Definition: json.c:591
void ast_json_free(void *p)
Asterisk&#39;s custom JSON allocator. Exposed for use by unit tests.
Definition: json.c:52
static int load_module(void)
#define ast_json_dump_string(root)
Encode a JSON value to a compact string.
Definition: json.h:763
unsigned int ast_stir_shaken_get_signature_timeout(void)
Retrieve the value for &#39;signature_timeout&#39; from &#39;general&#39; config object.
static int add_identity_header(const struct ast_sip_session *session, pjsip_tx_data *tdata)
struct timeval ast_tvnow(void)
Returns current timeval. Meant to replace calls to gettimeofday().
Definition: time.h:150
char * ast_stir_shaken_payload_get_public_cert_url(const struct ast_stir_shaken_payload *payload)
Retrieve the value for &#39;public_cert_url&#39; from an ast_stir_shaken_payload.
struct ast_json * ast_json_load_string(const char *input, struct ast_json_error *error)
Parse null terminated string into a JSON object or array.
Definition: json.c:546
void ast_copy_pj_str(char *dest, const pj_str_t *src, size_t size)
Copy a pj_str_t into a standard character buffer.
Definition: res_pjsip.c:5240
void ast_sip_session_unregister_supplement(struct ast_sip_session_supplement *supplement)
Unregister a an supplement to SIP session processing.
Definition: pjsip_session.c:63
#define NULL
Definition: resample.c:96
static void add_date_header(const struct ast_sip_session *session, pjsip_tx_data *tdata)
A structure describing a SIP session.
#define ast_strlen_zero(foo)
Definition: strings.h:52
#define ast_debug(level,...)
Log a DEBUG message.
Definition: logger.h:452
#define ast_log
Definition: astobj2.c:42
struct ast_stir_shaken_payload * ast_stir_shaken_sign(struct ast_json *json)
Sign a JSON STIR/SHAKEN payload.
char * ast_sip_rdata_get_header_value(pjsip_rx_data *rdata, const pj_str_t str)
Get a specific header value from rdata.
Definition: res_pjsip.c:3543
#define RAII_VAR(vartype, varname, initval, dtor)
Declare a variable that will call a destructor function when it goes out of scope.
Definition: utils.h:911
static struct ast_mansession session
static int compare_timestamp(const char *json_str)
Compare the current timestamp with the one in the payload. If the difference is greater than the sign...
char * ast_base64url_encode_string(const char *src)
Encode string in base64 URL.
Definition: main/utils.c:521
struct ast_channel * channel
const char * ast_json_string_get(const struct ast_json *string)
Get the value of a JSON string.
Definition: json.c:273
#define ast_malloc(len)
A wrapper for malloc()
Definition: astmm.h:193
#define STIR_SHAKEN_ENCRYPTION_ALGORITHM
static struct ast_sip_session_supplement stir_shaken_supplement
static char * get_attestation_from_payload(const char *json_str)
Get the attestation from the payload.
#define LOG_ERROR
Definition: logger.h:285
struct ast_stir_shaken_payload * ast_stir_shaken_verify(const char *header, const char *payload, const char *signature, const char *algorithm, const char *public_cert_url)
Verify a JSON STIR/SHAKEN payload.
void ast_sip_add_date_header(pjsip_tx_data *tdata)
Adds a Date header to the tdata, formatted like: Date: Wed, 01 Jan 2021 14:53:01 GMT.
Definition: res_pjsip.c:3288
int ast_stir_shaken_add_verification(struct ast_channel *chan, const char *identity, const char *attestation, enum ast_stir_shaken_verification_result result)
Add a STIR/SHAKEN verification result to a channel.
#define ast_free(a)
Definition: astmm.h:182
#define ast_calloc(num, len)
A wrapper for calloc()
Definition: astmm.h:204
unsigned char * ast_stir_shaken_payload_get_signature(const struct ast_stir_shaken_payload *payload)
Retrieve the value for &#39;signature&#39; from an ast_stir_shaken_payload.
#define STIR_SHAKEN_PPT
static void stir_shaken_outgoing_request(struct ast_sip_session *session, pjsip_tx_data *tdata)
AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS|AST_MODFLAG_LOAD_ORDER, "HTTP Phone Provisioning",.support_level=AST_MODULE_SUPPORT_EXTENDED,.load=load_module,.unload=unload_module,.reload=reload,.load_pri=AST_MODPRI_CHANNEL_DEPEND,.requires="http",)
A supplement to SIP message processing.
struct ast_json * payload
struct ast_json * ast_json_object_get(struct ast_json *object, const char *key)
Get a field from a JSON object.
Definition: json.c:397
static int compare_caller_id(char *caller_id, const char *json_str)
Compare the caller ID from the INVITE with the one in the payload.
static int force_inline attribute_pure ast_begins_with(const char *str, const char *prefix)
Definition: strings.h:94
Abstract JSON element (object, array, string, int, ...).
void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload)
Free a STIR/SHAKEN payload.
static int unload_module(void)
unsigned int stir_shaken
Definition: res_pjsip.h:903
#define ASTERISK_GPL_KEY
The text the key() function should return.
Definition: module.h:46
Asterisk module definitions.
intmax_t ast_json_integer_get(const struct ast_json *integer)
Get the value from a JSON integer.
Definition: json.c:322
struct ast_party_id id
unsigned char valid
TRUE if the number information is valid/present.
Definition: channel.h:298
#define ast_sip_session_register_supplement(supplement)
static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_rx_data *rdata)
struct ast_party_number number
Subscriber phone number.
Definition: channel.h:343