From 493772354172d418ae8d79118090e75cd8c1be80 Mon Sep 17 00:00:00 2001 From: Evangelos Ribeiro Tzaras Date: Thu, 14 Apr 2022 17:09:57 +0200 Subject: [PATCH] sip: Add srtp-utilities These utilities aid in generating and parsing SDP crypto attributes to be used during the offer/answer negotiation. --- plugins/sip/calls-srtp-utils.c | 693 +++++++++++++++++++++++++++++++++ plugins/sip/calls-srtp-utils.h | 106 +++++ plugins/sip/meson.build | 1 + tests/meson.build | 15 + tests/test-srtp.c | 236 +++++++++++ 5 files changed, 1051 insertions(+) create mode 100644 plugins/sip/calls-srtp-utils.c create mode 100644 plugins/sip/calls-srtp-utils.h create mode 100644 tests/test-srtp.c diff --git a/plugins/sip/calls-srtp-utils.c b/plugins/sip/calls-srtp-utils.c new file mode 100644 index 0000000..a7a87e6 --- /dev/null +++ b/plugins/sip/calls-srtp-utils.c @@ -0,0 +1,693 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * This file is part of Calls. + * + * Calls is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calls is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Calls. If not, see . + * + * Author: Evangelos Ribeiro Tzaras + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include "calls-srtp-utils.h" +#include "util.h" + +#include +#include + +/** + * SECTION:srtp-utils + * @short_description: SRTP utilities for SDP parsing + * @Title: CallsSrtpUtils + * + * Utilities for parsing and generating the crypto attribute + * in SDP for SRTP use based on RFC 4568. + * + * Note that limitations of libsrtp are taken into account when checking validity + * of the parsed attribute. These are: + * A maximum of 16 keys, + * key derivation rate must be 0, + * lifetimes other than 2^48 (we actually ignore the specified lifetimes) + */ + + +/* The default used in libsrtp. No API to change this. See https://github.com/cisco/libsrtp/issues/588 */ +#define SRTP_DEFAULT_LIFETIME_POW2 48 +#define SRTP_MAX_LIFETIME_POW2 48 + +/* The default used in libsrtp (and GstSrtpEnc/GstSrtpDec) */ +#define SRTP_DEFAULT_WINDOW_SIZE 128 + +const char * srtp_crypto_suites[] = { + "AES_CM_128_HMAC_SHA1_32", /* RFC 4568 */ + "AES_CM_128_HMAC_SHA1_80", /* RFC 4568 */ + "F8_128_HMAC_SHA1_32", /* RFC 4568 but not supported by GstSrtpEnc/GstSrtpDec */ + "AEAD_AES_128_GCM", /* RFC 7714 TODO support in the future */ + "AEAD_AES_256_GCM", /* RFC 7714 TODO support in the future */ + NULL +}; + + +static gsize +get_key_size_for_suite (calls_srtp_crypto_suite suite) +{ + switch (suite) { + case CALLS_SRTP_SUITE_AES_128_SHA1_32: + case CALLS_SRTP_SUITE_AES_128_SHA1_80: + return 30; + + case CALLS_SRTP_SUITE_UNKNOWN: + default: + return 0; + } +} + + +static gboolean +validate_crypto_attribute (calls_srtp_crypto_attribute *attr, + GError **error) +{ + guint expected_key_salt_length; + gboolean need_mki; + guint expected_mki_length = 0; + calls_srtp_crypto_key_param *key_param; + GSList *mki_list = NULL; /* for checking uniqueness of MKIs */ + GSList *key_list = NULL; /* for checking uniqueness of keys */ + + if (!attr) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Attribute is NULL"); + return FALSE; + } + + if (attr->tag <= 0 || attr->tag >= 1000000000) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Tag is not valid: %d", attr->tag); + return FALSE; + } + + switch (attr->crypto_suite) { + case CALLS_SRTP_SUITE_AES_128_SHA1_32: + case CALLS_SRTP_SUITE_AES_128_SHA1_80: + expected_key_salt_length = 30; /* 16 byte key + 14 byte salt */ + break; + + case CALLS_SRTP_SUITE_UNKNOWN: + default: + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Crypto suite unknown"); + return FALSE; + } + + /* at least one and a maximum of 16 key parameters */ + if (attr->n_key_params == 0 || attr->n_key_params > 16) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Must have between 1 and 16 keys, got %d", attr->n_key_params); + return FALSE; + } + + need_mki = attr->n_key_params > 1 || + attr->key_params[0].mki || + attr->key_params[0].mki_length; + + if (need_mki) { + expected_mki_length = attr->key_params[0].mki_length; + if (expected_mki_length == 0 || + expected_mki_length > 128) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "MKI length must be between 1 and 128, got %u", + expected_key_salt_length); + return FALSE; + } + } + + for (guint i = 0; i < attr->n_key_params; i++) { + g_autofree guchar *key_salt = NULL; + gsize key_salt_length; + + key_param = &attr->key_params[i]; + + /* must have a key */ + if (!key_param->b64_keysalt) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "No key found in parameter %d", i); + goto failed; + } + + key_salt = g_base64_decode (key_param->b64_keysalt, &key_salt_length); + + /* key must have length consistent with suite */ + if (key_salt_length != expected_key_salt_length) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Key %d has length %" G_GSIZE_FORMAT ", but expected %d", + i, key_salt_length, expected_key_salt_length); + goto failed; + } + + /* key must be unique */ + if (g_slist_find_custom (key_list, + key_param->b64_keysalt, + (GCompareFunc) g_strcmp0)) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Key %d is not unique: %s", i, key_param->b64_keysalt); + goto failed; + } + + key_list = g_slist_append (key_list, key_param->b64_keysalt); + + /* lifetime in range */ + if (key_param->lifetime_type == CALLS_SRTP_LIFETIME_AS_DECIMAL_NUMBER && + key_param->lifetime >= (1ULL << SRTP_MAX_LIFETIME_POW2)) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Lifetime of key %d out of bounds: Got %" G_GINT64_FORMAT " but maximum is 2^%d", + i, key_param->lifetime, SRTP_MAX_LIFETIME_POW2); + goto failed; + } + + if (key_param->lifetime_type == CALLS_SRTP_LIFETIME_AS_POWER_OF_TWO && + key_param->lifetime >= SRTP_MAX_LIFETIME_POW2) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Lifetime of key %d out of bounds: Got 2^%" G_GINT64_FORMAT " but maximum is 2^%d", + i, key_param->lifetime, SRTP_MAX_LIFETIME_POW2); + goto failed; + } + + /* if MKI length is set, it must be the same for all key parameters */ + if (need_mki) { + if (key_param->mki_length != expected_mki_length) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "MKI length must be the same for all keys. Key %d has length %d but expected %d", + i, key_param->mki_length, expected_mki_length); + goto failed; + } + + /* MKI must not have leading zero */ + if (key_param->mki == 0) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "No MKI set for key %d", i); + goto failed; + } + + /* MKI must be unique */ + if (g_slist_find (mki_list, GINT_TO_POINTER (key_param->mki))) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "MKI for key %d is not unique", i); + goto failed; + } + + mki_list = g_slist_append (mki_list, GINT_TO_POINTER (key_param->mki)); + } + + } + + /* check session parameters */ + + /* libsrtp does only support kdr=0 */ + if (attr->kdr != 0) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Key derivation rate must be 0, got %d", + attr->kdr); + goto failed; + } + + g_slist_free (mki_list); + g_slist_free (key_list); + + return TRUE; + +failed: + + g_slist_free (mki_list); + g_slist_free (key_list); + + return FALSE; +} + +/** + * calls_srtp_generate_key_salt: + * @length: Desired length of random data + * + * Generate random data to be used as master key and master salt of desired length @length + * + * Returns: (transfer full): Random data to be used as key and salt in SRTP + * or %NULL if failed. Free with g_free() when done. + */ +guchar * +calls_srtp_generate_key_salt (gsize length) +{ + g_autofree guchar *key_salt = NULL; + gsize n_bytes; + + g_return_val_if_fail (length > 0, NULL); + + key_salt = g_malloc (length); + + n_bytes = getrandom (key_salt, length, GRND_NONBLOCK); + if (n_bytes == -1) { + return NULL; + } + + return g_steal_pointer (&key_salt); +} + + +/** + * calls_srtp_generate_key_salt_for_suite: + * @suite: a #calls_srtp_crypto_suite + * + * Generate random data to be used as master key and master salt. + * The required length is determined by the requirements of the @suite + * + * Returns: (transfer full): Random data to be used as key and salt in SRTP + * or %NULL if failed. Free with g_free() when done. + */ +guchar * +calls_srtp_generate_key_salt_for_suite (calls_srtp_crypto_suite suite) +{ + gsize size = get_key_size_for_suite (suite); + + if (size == 0) + return NULL; + + return calls_srtp_generate_key_salt (size); +} + +/** + * calls_srtp_parse_sdp_crypto_attribute: + * @attribute: attribute line + * @error: a #GError + * + * Parse textual attribute line into structured data. + * + * Returns: (transfer full): A #calls_srtp_crypto_attribute containing + * parsed attribute data, or %NULL if parsing failed. + */ +calls_srtp_crypto_attribute * +calls_srtp_parse_sdp_crypto_attribute (const char *attribute, + GError **error) +{ + g_auto (GStrv) attr_fields = NULL; + g_auto (GStrv) key_info_strv = NULL; + guint n_attr_fields; + guint n_key_params; + char *tag_str; + gint tag; + calls_srtp_crypto_attribute *attr; + calls_srtp_crypto_suite crypto_suite; + gboolean need_mki; + gboolean attr_invalid = FALSE; + g_autofree char *err_msg = NULL; + + if (STR_IS_NULL_OR_EMPTY (attribute)) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Cannot parse null or empty strings"); + return NULL; + } + + if (!g_str_has_prefix (attribute, "a=crypto:")) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Does not look like a SDP crypto attribute: %s", + attribute); + + return NULL; + } + + attr_fields = g_strsplit (attribute, " ", -1); + n_attr_fields = g_strv_length (attr_fields); + + if (n_attr_fields < 3) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Need at least three fields in a SDP crypto attribute: %s", + attribute); + + return NULL; + } + + tag_str = &attr_fields[0][9]; /* 9 is the length of "a=crypto:" */ + + /* leading zeros MUST NOT be used */ + if (*tag_str == '0') { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Tag must not have a leading zero: %s", + tag_str); + + return NULL; + } + + tag = (int) strtol (tag_str, NULL, 10); + if (tag == 0) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Tag set to 0: %s", tag_str); + + return NULL; + } + + if (tag < 0) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Tag must be positive: %s", tag_str); + + return NULL; + } + + if (tag >= 1000000000) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Tag must have a maximum of 9 digits: %s", tag_str); + + return NULL; + } + + /* f.e. attr_fields[1] = "AES_CM_128_HMAC_SHA1_32" */ + if (g_strcmp0 (attr_fields[1], "AES_CM_128_HMAC_SHA1_32") == 0) + crypto_suite = CALLS_SRTP_SUITE_AES_128_SHA1_32; + else if (g_strcmp0 (attr_fields[1], "AES_CM_128_HMAC_SHA1_80") == 0) + crypto_suite = CALLS_SRTP_SUITE_AES_128_SHA1_80; + else + crypto_suite = CALLS_SRTP_SUITE_UNKNOWN; /* error */ + + /* key infos are split by ';' */ + key_info_strv = g_strsplit (attr_fields[2], ";", -1); + n_key_params = g_strv_length (key_info_strv); + + /* libsrtp supports a maximum of 16 master keys */ + if (n_key_params > 16) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "More than 16 keys are not supported by libsrtp"); + + return NULL; + } + + need_mki = n_key_params > 1; + + attr = calls_srtp_crypto_attribute_new (n_key_params); + + attr->tag = tag; + attr->crypto_suite = crypto_suite; + + for (guint i = 0; i < n_key_params; i++) { + char *key_info; /* srtp-key-info = key-salt ["|" lifetime] ["|" mki] */ + g_auto (GStrv) key_info_fields = NULL; + guint n_key_infos; + guint key_info_lifetime_index; + guint key_info_mki_index; + calls_srtp_crypto_key_param *key_param = &attr->key_params[i]; + + if (!g_str_has_prefix (key_info_strv[i], "inline:")) { + attr_invalid = TRUE; + err_msg = g_strdup_printf ("Key method not 'inline': %s", key_info_strv[i]); + break; + } + + key_info = &key_info_strv[i][7]; /* 7 is the length of "inline:" */ + key_info_fields = g_strsplit (key_info, "|", -1); + + n_key_infos = g_strv_length (key_info_fields); + + key_param->b64_keysalt = g_strdup (key_info_fields[0]); + + if (n_key_infos == 1) { + key_info_lifetime_index = 0; + key_info_mki_index = 0; + } else if (n_key_infos == 2) { + /* either MKI or lifetime */ + if (g_strstr_len (key_info_fields[1], -1, ":")) { + key_info_lifetime_index = 0; + key_info_mki_index = 1; + } else { + key_info_lifetime_index = 1; + key_info_mki_index = 0; + } + } else if (n_key_infos == 3) { + key_info_lifetime_index = 1; + key_info_mki_index = 2; + } else { + /* invalid */ + attr_invalid = TRUE; + err_msg = g_strdup_printf ("Unexpected number of key-info fields: %s", key_info); + break; + } + + /* lifetime type */ + if (key_info_lifetime_index) { + char *lifetime_number; + char *endptr; + if (g_str_has_prefix (key_info_fields[key_info_lifetime_index], "2^")) { + key_param->lifetime_type = CALLS_SRTP_LIFETIME_AS_POWER_OF_TWO; + lifetime_number = &key_info_fields[key_info_lifetime_index][2]; /* 2 is the length of "2^" */ + } else { + key_param->lifetime_type = CALLS_SRTP_LIFETIME_AS_DECIMAL_NUMBER; + lifetime_number = key_info_fields[key_info_lifetime_index]; + } + + if (*lifetime_number == '0') { + attr_invalid = TRUE; + err_msg = g_strdup_printf ("Leading zero in lifetime: %s", + key_info_fields[key_info_lifetime_index]); + break; + } + + key_param->lifetime = g_ascii_strtoull (lifetime_number, &endptr, 10); + if (key_param->lifetime == 0) { + attr_invalid = TRUE; + err_msg = g_strdup_printf ("Lifetime set to zero: %s", + key_info_fields[key_info_lifetime_index]); + break; + } + + if (*endptr != '\0') { + attr_invalid = TRUE; + err_msg = g_strdup_printf ("Non numeric characters in lifetime: %s", + key_info_fields[key_info_lifetime_index]); + break; + } + + /* out of bounds check will be performed during validation of the attribute */ + } + + if (need_mki && key_info_mki_index == 0) { + attr_invalid = TRUE; + err_msg = g_strdup_printf ("MKI needed, but not found: %s", key_info); + break; + } + + if (need_mki) { + g_auto (GStrv) mki_split = g_strsplit (key_info_fields[key_info_mki_index], ":", -1); + guint n_mki = g_strv_length (mki_split); + guint64 mki; + guint64 mki_length; + char *endptr; + + if (n_mki != 2) { + attr_invalid = TRUE; + err_msg = g_strdup_printf ("MKI field not separated into two fields by colon: %s", + key_info_fields[key_info_mki_index]); + break; + } + + /* no leading zero allowed */ + if (*mki_split[0] == '0') { + attr_invalid = TRUE; + err_msg = g_strdup_printf ("Leading zero in MKI: %s", mki_split[0]); + break; + } + + if (*mki_split[1] == '0') { + attr_invalid = TRUE; + err_msg = g_strdup_printf ("Leading zero in MKI length: %s", mki_split[1]); + break; + } + + mki = g_ascii_strtoull (mki_split[0], &endptr, 10); + if (mki == 0) { + attr_invalid = TRUE; + err_msg = g_strdup_printf ("MKI set to 0: %s", mki_split[0]); + break; + } + + if (*endptr != '\0') { + attr_invalid = TRUE; + err_msg = g_strdup_printf ("Non numeric characters found in MKI: %s", mki_split[0]); + break; + } + + mki_length = g_ascii_strtoull (mki_split[1], &endptr, 10); + /* number of bytes of the MKI field in the SRTP packet */ + if (mki_length == 0 || mki_length > 128) { + attr_invalid = TRUE; + err_msg = g_strdup_printf ("MKI length not between 0 and 128: %s", mki_split[1]); + break; + } + + if (*endptr != '\0') { + attr_invalid = TRUE; + err_msg = g_strdup_printf ("Non numeric characters found in MKI length: %s", mki_split[1]); + break; + } + + key_param->mki = mki; + key_param->mki_length = (guint) mki_length; + } + + } + + if (attr_invalid) { + calls_srtp_crypto_attribute_free (attr); + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, err_msg); + + return NULL; + } + + /* TODO session parameters */ + + if (!validate_crypto_attribute (attr, error)) { + calls_srtp_crypto_attribute_free (attr); + + return NULL; + + } + + return attr; +} + +/** + * calls_srtp_print_sdp_crypto_attribute: + * @attr: Structured crypto attribute + * @error: A #GError + * + * Returns: (transfer full): Textual representation of crypto attribute + * or %NULL if attribute contains invalid data. + */ +char * +calls_srtp_print_sdp_crypto_attribute (calls_srtp_crypto_attribute *attr, + GError **error) +{ + const char *crypto_suite; + GString *attr_str; + + if (!validate_crypto_attribute (attr, error)) + return NULL; + + if (attr->crypto_suite == CALLS_SRTP_SUITE_AES_128_SHA1_32) + crypto_suite = "AES_CM_128_HMAC_SHA1_32"; + else if (attr->crypto_suite == CALLS_SRTP_SUITE_AES_128_SHA1_80) + crypto_suite = "AES_CM_128_HMAC_SHA1_80"; + else + return NULL; + + attr_str = g_string_sized_new (96); /* minimal string length is 82 */ + + g_string_append_printf (attr_str, "a=crypto:%d %s ", + attr->tag, crypto_suite); + + /* key parameters */ + for (guint i = 0; i < attr->n_key_params; i++) { + calls_srtp_crypto_key_param *key_param = &attr->key_params[i]; + + g_string_append_printf (attr_str, "inline:%s", key_param->b64_keysalt); + if (key_param->lifetime_type == CALLS_SRTP_LIFETIME_AS_DECIMAL_NUMBER) + g_string_append_printf (attr_str, "|%" G_GINT64_FORMAT, key_param->lifetime); + if (key_param->lifetime_type == CALLS_SRTP_LIFETIME_AS_POWER_OF_TWO) + g_string_append_printf (attr_str, "|2^%" G_GINT64_FORMAT, key_param->lifetime); + + if (key_param->mki > 0) { + g_string_append_printf(attr_str, "|%" G_GUINT64_FORMAT ":%u", + key_param->mki, key_param->mki_length); + } + + if (i + 1 < attr->n_key_params) + g_string_append (attr_str, ";"); + } + + /* TODO session parameters */ + + return g_string_free (attr_str, FALSE); +} + +/** + * calls_srtp_crypto_attribute_new: + * @n_key_params: The number of key parameters + * + * Returns: (transfer full): A new empty #calls_srtp_crypto_attribute + * with @n_key_params key parameters allocated. Key parameters must be set either + * manually or by using calls_srtp_crypto_attribute_init_keys(). + * + * Free the attribute with calls_srtp_crypto_attribute_free() after you are done. + */ +calls_srtp_crypto_attribute * +calls_srtp_crypto_attribute_new (guint n_key_params) +{ + calls_srtp_crypto_attribute *attr; + + g_return_val_if_fail (n_key_params > 0 || n_key_params < 16, NULL); + + attr = g_new0 (calls_srtp_crypto_attribute, 1); + attr->key_params = g_new0 (calls_srtp_crypto_key_param, n_key_params); + attr->n_key_params = n_key_params; + + return attr; +} + +/** + * calls_srtp_crypto_attribute_init_keys: + * @attr: A #calls_srtp_crypto_attribute + * + * Generate key material and set sane default parameters for each + * key parameter. + */ +gboolean +calls_srtp_crypto_attribute_init_keys (calls_srtp_crypto_attribute *attr) +{ + gsize key_size; + gboolean need_mki; + + g_return_val_if_fail (attr, FALSE); + + key_size = get_key_size_for_suite (attr->crypto_suite); + if (key_size == 0) + return FALSE; + + need_mki = attr->n_key_params > 1; + + for (uint i = 0; i < attr->n_key_params; i++) { + g_autofree guchar *key = calls_srtp_generate_key_salt (key_size); + g_free (attr->key_params[i].b64_keysalt); + attr->key_params[i].b64_keysalt = g_base64_encode (key, key_size); + + if (need_mki) { + attr->key_params[i].mki = i + 1; + attr->key_params[i].mki_length = 4; + } + } + + return TRUE; +} + +/** + * calls_srtp_crypto_attribute_free: + * @attr: A #calls_srtp_crypto_attribute + * + * Frees all memory allocated for @attr. + */ +void +calls_srtp_crypto_attribute_free (calls_srtp_crypto_attribute *attr) +{ + for (guint i = 0; i < attr->n_key_params; i++) { + g_free (attr->key_params[i].b64_keysalt); + } + + g_free (attr->key_params); + g_free (attr->b64_fec_key); + g_free (attr); +} diff --git a/plugins/sip/calls-srtp-utils.h b/plugins/sip/calls-srtp-utils.h new file mode 100644 index 0000000..72d37ca --- /dev/null +++ b/plugins/sip/calls-srtp-utils.h @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * This file is part of Calls. + * + * Calls is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calls is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Calls. If not, see . + * + * Author: Evangelos Ribeiro Tzaras + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +typedef enum { + CALLS_SRTP_SUITE_UNKNOWN = 0, + CALLS_SRTP_SUITE_AES_128_SHA1_32, + CALLS_SRTP_SUITE_AES_128_SHA1_80, +} calls_srtp_crypto_suite; + + +typedef enum { + CALLS_SRTP_FEC_ORDER_UNSET = 0, + CALLS_SRTP_FEC_ORDER_FEC_SRTP, + CALLS_SRTP_FEC_ORDER_SRTP_FEC +} calls_srtp_fec_order; + + +typedef enum { + CALLS_SRTP_LIFETIME_UNSET = 0, + CALLS_SRTP_LIFETIME_AS_DECIMAL_NUMBER, + CALLS_SRTP_LIFETIME_AS_POWER_OF_TWO +} calls_srtp_lifetime_type; + + +typedef struct { + char *b64_keysalt; + calls_srtp_lifetime_type lifetime_type; + /* maximum lifetime: SRTP 2^48 packets; SRTCP 2^31 packets; libsrtp uses a hardcoded limit of 2^48 */ + guint64 lifetime; + + guint64 mki; + guint mki_length; +} calls_srtp_crypto_key_param; + + +typedef struct { + gint tag; + calls_srtp_crypto_suite crypto_suite; + + calls_srtp_crypto_key_param *key_params; + guint n_key_params; + + /** session parameters + * For more information see https://datatracker.ietf.org/doc/html/rfc4568#section-6.3 + * KDR (key derivation rate) defaults to 0; declarative parameter + * decimal integer in {1,2,...,24}, denotes a power of two. 0 means unspecified + * defaulting to a single initial key derivation + * UNENCRYPTED_SRTCP, UNENCRYPTED_SRTP; negotiated parameter + * UNAUTHENTICATED_SRTP; negotiated parameter + * FEC_ORDER; declarative parameter + * FEC_KEY separate key-params for forward error correction; declarative parameter + * WSH (Window Size Hint); declarative parameter; MAY be ignored + */ + gint kdr; + gboolean unencrypted_srtp; + gboolean unencrypted_srtcp; + gboolean unauthenticated_srtp; + calls_srtp_fec_order fec_order; /* FEC in RTP: RFC2733 */ + char *b64_fec_key; /* TODO this should also be an array of calls_srtp_crypto_key_param */ + guint window_size_hint; +} calls_srtp_crypto_attribute; + + +guchar *calls_srtp_generate_key_salt (gsize length); +guchar *calls_srtp_generate_key_salt_for_suite (calls_srtp_crypto_suite suite); +calls_srtp_crypto_attribute *calls_srtp_parse_sdp_crypto_attribute (const char *attr, + GError **error); +char *calls_srtp_print_sdp_crypto_attribute (calls_srtp_crypto_attribute *attr, + GError **error); +calls_srtp_crypto_attribute *calls_srtp_crypto_attribute_new (guint n_key_params); +gboolean calls_srtp_crypto_attribute_init_keys (calls_srtp_crypto_attribute *attr); +void calls_srtp_crypto_attribute_free (calls_srtp_crypto_attribute *attr); +char *calls_srtp_generate_crypto_for_offer (void); + + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (calls_srtp_crypto_attribute, calls_srtp_crypto_attribute_free) + + +G_END_DECLS diff --git a/plugins/sip/meson.build b/plugins/sip/meson.build index 4e97c84..9b3e1dc 100644 --- a/plugins/sip/meson.build +++ b/plugins/sip/meson.build @@ -51,6 +51,7 @@ sip_sources = files( 'calls-sip-media-manager.c', 'calls-sip-media-manager.h', 'calls-sip-media-pipeline.c', 'calls-sip-media-pipeline.h', 'calls-sip-account-widget.c', 'calls-sip-account-widget.h', + 'calls-srtp-utils.c', 'calls-srtp-utils.h', 'gst-rfc3551.c', 'gst-rfc3551.h', ] ) diff --git a/tests/meson.build b/tests/meson.build index 9171ce8..1f68d2c 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -123,6 +123,21 @@ t = executable('sip', test_sources, ) test('sip', t, env: test_env) +test_sources = [ 'test-srtp.c' ] +test_sources += sip_sources +t = executable('srtp', test_sources, + c_args : test_cflags, + link_args: test_link_args, + pie: true, + link_with : [calls_vala, libcalls], + dependencies: [calls_deps, sip_deps], + include_directories : [ + calls_includes, + sip_include, + ] + ) +test('srtp', t, env: test_env) + test_sources = [ 'test-util.c' ] t = executable('util', test_sources, c_args : test_cflags, diff --git a/tests/test-srtp.c b/tests/test-srtp.c new file mode 100644 index 0000000..1d35231 --- /dev/null +++ b/tests/test-srtp.c @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + * + * Author: Evangelos Ribeiro Tzaras + */ + +#include "calls-srtp-utils.h" + +#include + + +static void +assert_attr_eq (calls_srtp_crypto_attribute *a, + calls_srtp_crypto_attribute *b) +{ + g_assert_cmpint (a->tag, ==, b->tag); + g_assert_cmpint (a->crypto_suite, ==, b->crypto_suite); + g_assert_cmpuint (a->n_key_params, ==, b->n_key_params); + + for (guint i = 0; i < a->n_key_params; i++) { + g_assert_cmpstr (a->key_params[i].b64_keysalt, ==, + b->key_params[i].b64_keysalt); + g_assert_cmpuint (a->key_params[i].mki, ==, + b->key_params[i].mki); + g_assert_cmpuint (a->key_params[i].mki_length, ==, + b->key_params[i].mki_length); + } + + g_assert_cmpint (a->unencrypted_srtp, ==, b->unencrypted_srtp); + g_assert_cmpint (a->unauthenticated_srtp, ==, b->unauthenticated_srtp); + g_assert_cmpint (a->unencrypted_srtcp, ==, b->unencrypted_srtcp); + + g_assert_cmpint (a->kdr, ==, b->kdr); + g_assert_cmpint (a->fec_order, ==, b->fec_order); + g_assert_cmpint (a->window_size_hint, ==, b->window_size_hint); +} + + +static void +test_crypto_attribute_validity (void) +{ + g_autoptr (calls_srtp_crypto_attribute) attr = NULL; + char *attr_str; + guchar *key_salt; + char *tmp_str; + + /* single key parameter */ + + attr = calls_srtp_crypto_attribute_new (1); + + attr->tag = 1; + attr->crypto_suite = CALLS_SRTP_SUITE_UNKNOWN; + + g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL)); + + attr->crypto_suite = CALLS_SRTP_SUITE_AES_128_SHA1_32; + key_salt = calls_srtp_generate_key_salt (30); + attr->key_params[0].b64_keysalt = g_base64_encode (key_salt, 30); + g_free (key_salt); + + attr_str = calls_srtp_print_sdp_crypto_attribute (attr, NULL); + g_assert_true (attr_str); + + g_free (attr_str); + + /* tag out of bounds */ + attr->tag = 0; + g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL)); + + attr->tag = 1000000000; + g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL)); + + attr->tag = 1; + + /* lifetime out of bounds */ + attr->key_params[0].lifetime = 49; + attr->key_params[0].lifetime_type = CALLS_SRTP_LIFETIME_AS_POWER_OF_TWO; + + g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL)); + + attr->key_params[0].lifetime = 1ULL << 48; + attr->key_params[0].lifetime_type = CALLS_SRTP_LIFETIME_AS_DECIMAL_NUMBER; + + g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL)); + + attr->key_params[0].lifetime = 0; + attr->key_params[0].lifetime_type = CALLS_SRTP_LIFETIME_UNSET; + + /* MKI without length */ + attr->key_params[0].mki = 1; + attr->key_params[0].mki_length = 0; + + g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL)); + + attr->key_params[0].mki_length = 129; + + g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL)); + + attr->key_params[0].mki = 0; + attr->key_params[0].mki_length = 4; + + g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL)); + + /* missing key */ + attr->key_params[0].mki_length = 0; + g_clear_pointer (&attr->key_params[0].b64_keysalt, g_free); + + g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL)); + + /* wrong key length */ + key_salt = calls_srtp_generate_key_salt (29); + attr->key_params[0].b64_keysalt = g_base64_encode (key_salt, 29); + g_free (key_salt); + + g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL)); + + calls_srtp_crypto_attribute_free (attr); + + /* multiple key parameters */ + + attr = calls_srtp_crypto_attribute_new (4); + attr->tag = 12; + attr->crypto_suite = CALLS_SRTP_SUITE_AES_128_SHA1_80; + + calls_srtp_crypto_attribute_init_keys (attr); + attr->key_params[0].lifetime = 31; + attr->key_params[0].lifetime_type = CALLS_SRTP_LIFETIME_AS_POWER_OF_TWO; + + attr_str = calls_srtp_print_sdp_crypto_attribute (attr, NULL); + g_assert_true (attr_str); + + g_free (attr_str); + calls_srtp_crypto_attribute_free (attr); + + + /* same key */ + attr = calls_srtp_crypto_attribute_new (2); + attr->tag = 1; + tmp_str = attr->key_params[1].b64_keysalt; + attr->key_params[1].b64_keysalt = attr->key_params[0].b64_keysalt; + + g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL)); + + attr->key_params[1].b64_keysalt = tmp_str; + + /* same MKI */ + attr->key_params[0].mki = 1; + attr->key_params[1].mki = 1; + + g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL)); + + /* different MKI lengths */ + attr->key_params[1].mki = 2; + attr->key_params[0].mki_length = 1; + attr->key_params[1].mki_length = 3; + + g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL)); + + g_assert_null (calls_srtp_print_sdp_crypto_attribute (NULL, NULL)); +} + + +static void +test_parse (void) +{ + g_autoptr (calls_srtp_crypto_attribute) attr_simple = NULL; + g_autoptr (calls_srtp_crypto_attribute) attr_parsed_simple = NULL; + g_autofree char *attr_simple_str = NULL; + g_autofree char *attr_simple_str_expected = NULL; + + g_autoptr (calls_srtp_crypto_attribute) attr_multi = NULL; + g_autoptr (calls_srtp_crypto_attribute) attr_parsed_multi = NULL; + g_autofree char *attr_multi_str = NULL; + g_autofree char *attr_multi_str_expected = NULL; + + g_autofree guchar *key_salt = NULL; + + /* single key */ + + attr_simple = calls_srtp_crypto_attribute_new (1); + key_salt = calls_srtp_generate_key_salt (30); + attr_simple->tag = 1; + attr_simple->crypto_suite = CALLS_SRTP_SUITE_AES_128_SHA1_32; + attr_simple->key_params[0].b64_keysalt = g_base64_encode (key_salt, 30); + + attr_simple_str = calls_srtp_print_sdp_crypto_attribute (attr_simple, NULL); + attr_simple_str_expected = + g_strdup_printf ("a=crypto:%d AES_CM_128_HMAC_SHA1_32 inline:%s", + attr_simple->tag, + attr_simple->key_params[0].b64_keysalt); + + g_assert_cmpstr (attr_simple_str, ==, attr_simple_str_expected); + + attr_parsed_simple = calls_srtp_parse_sdp_crypto_attribute (attr_simple_str, NULL); + assert_attr_eq (attr_simple, attr_parsed_simple); + + /* multiple keys */ + + attr_multi = calls_srtp_crypto_attribute_new (2); + attr_multi->tag = 42; + attr_multi->crypto_suite = CALLS_SRTP_SUITE_AES_128_SHA1_80; + calls_srtp_crypto_attribute_init_keys (attr_multi); + + attr_multi_str = calls_srtp_print_sdp_crypto_attribute (attr_multi, NULL); + attr_multi_str_expected = + g_strdup_printf ("a=crypto:%d AES_CM_128_HMAC_SHA1_80 " + "inline:%s|%" G_GUINT64_FORMAT ":%u;" + "inline:%s|%" G_GUINT64_FORMAT ":%u", + attr_multi->tag, + attr_multi->key_params[0].b64_keysalt, + attr_multi->key_params[0].mki, + attr_multi->key_params[0].mki_length, + attr_multi->key_params[1].b64_keysalt, + attr_multi->key_params[1].mki, + attr_multi->key_params[1].mki_length); + + g_assert_cmpstr (attr_multi_str, ==, attr_multi_str_expected); + + attr_parsed_multi = calls_srtp_parse_sdp_crypto_attribute (attr_multi_str, NULL); + assert_attr_eq (attr_multi, attr_parsed_multi); +} + + +int +main (int argc, + char *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + + g_test_add_func ("/Calls/SRTP-SDP/crypto_attribute_validity", test_crypto_attribute_validity); + g_test_add_func ("/Calls/SRTP-SDP/parse", test_parse); + + return g_test_run (); +}