add osmo_mobile_identity API
Implement better API around 3GPP TS 24.008 Mobile Identity coding.
struct osmo_mobile_identity is a decoded representation of the raw Mobile
Identity, with a string representation as well as dedicated raw uint32_t TMSI.
The aim is to remove all uncertainty about decoded buffer sizes / data types.
I have patches ready for all osmo programs, completely replacing the Mobile
Identity coding with this new API. Hence deprecate the old MI API.
New API functions provide properly size-checking implementations of:
- decoding a raw MI from a bunch of MI octets;
- locating and decoding MI from a full 3GPP TS 24.008 Complete Layer 3 msgb;
- encoding to a buffer;
- encoding to the end of a msgb.
Other than the old gsm48_generate_mid(), omit a TLV tag and length from
encoding. Many callers manually stripped the tag and value after calling
gsm48_generate_mid(). The aim is to leave writing a TL to the caller entirely,
especially since some callers need to use a TvL, i.e. support a variable-size
length of 8 or 16 bit.
New validity checks so far not implemented anywhere else:
- stricter validation of number of digits of IMSI, IMEI, IMEI-SV MI.
- stricter on filler nibbles to be 0xf.
Rationale:
While implementing osmo-bsc's MSC pooling feature in osmo-bsc, this API will be
used to reduce the number of times a Mobile Identity is extracted from a raw
RSL message.
Extracting the Mobile Identity from messages has numerous duplicate
implementations across our code with various levels of specialization.
https://xkcd.com/927/
To name a few:
- libosmocore: gsm48_mi_to_string(), osmo_mi_name_buf()
- osmo-bsc: extract_sub()
- osmo-msc: mm_rx_loc_upd_req(), cm_serv_reuse_conn(), gsm48_rx_mm_serv_req(),
vlr_proc_acc_req()
We have existing functions to produce a human readable string from a Mobile
Identity, more or less awkward:
- gsm48_mi_to_string() decodes a TMSI as a decimal number. These days we use
hexadecimal TMSI everywhere.
- osmo_mi_name_buf() decodes the BCD digits from a raw MI every time, so we'd
need to pass around the raw message bytes. Also, osmo_mi_name_buf() has the
wrong signature, it should return a length like snprintf().
- osmo-bsc's extract_sub() first uses gsm48_mi_to_string() which encodes the
raw uint32_t TMSI to a string, and then calls strtoul() via
tmsi_from_string() to code those back to a raw uint32_t.
Each of the above implementations employ their own size overflow checks, each
invoke osmo_bcd2str() and implement their own TMSI osmo_load32be() handling.
Too much code dup, let's hope that each and every one is correct.
In osmo-bsc, I am now implementing MSC pooling, and need to extract NRI bits
from a TMSI Mobile Identity. Since none of the above functions are general
enough to be re-used, I found myself again copy-pasting Mobile Identity code:
locating the MI in a 24.008 message with proper size checks, decoding MI
octets.
This time I would like it to become a generally re-usable API.
Change-Id: Ic3f969e739654c1e8c387aedeeba5cce07fe2307
diff --git a/src/gb/gprs_bssgp.c b/src/gb/gprs_bssgp.c
index 38794c2..2784d0a 100644
--- a/src/gb/gprs_bssgp.c
+++ b/src/gb/gprs_bssgp.c
@@ -1170,19 +1170,20 @@
/* IMSI */
if (dup->imsi && strlen(dup->imsi)) {
- uint8_t mi[GSM48_MID_MAX_SIZE];
-/* gsm48_generate_mid_from_imsi() is guaranteed to never return more than 11,
- * but somehow gcc (8.2) is not smart enough to figure this out and claims that
- * the memcpy in msgb_tvlv_put() below will cause and out-of-bounds access up to
- * mi[131], which is wrong */
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Warray-bounds"
- int imsi_len = gsm48_generate_mid_from_imsi(mi, dup->imsi);
- OSMO_ASSERT(imsi_len <= GSM48_MID_MAX_SIZE);
- if (imsi_len > 2)
- msgb_tvlv_push(msg, BSSGP_IE_IMSI,
- imsi_len-2, mi+2);
-#pragma GCC diagnostic pop
+ struct osmo_mobile_identity mi = { .type = GSM_MI_TYPE_IMSI, };
+ OSMO_STRLCPY_ARRAY(mi.imsi, dup->imsi);
+ msgb_tvl_put(msg, BSSGP_IE_IMSI, osmo_mobile_identity_encoded_len(&mi, NULL));
+ if (osmo_mobile_identity_encode_msgb(msg, &mi, false) <= 0) {
+ if (log_check_level(DBSSGP, LOGL_NOTICE)) {
+ char strbuf[64];
+ osmo_mobile_identity_to_str_buf(strbuf, sizeof(strbuf), &mi);
+ LOGP(DBSSGP, LOGL_ERROR,
+ "NSEI=%u/BVCI=%u Cannot encode Mobile Identity %s\n",
+ nsei, bvci, strbuf);
+ }
+ msgb_free(msg);
+ return -EINVAL;
+ }
}
/* DRX parameters */
@@ -1227,12 +1228,8 @@
struct bssgp_normal_hdr *bgph =
(struct bssgp_normal_hdr *) msgb_put(msg, sizeof(*bgph));
uint16_t drx_params = osmo_htons(pinfo->drx_params);
- uint8_t mi[GSM48_MID_MAX_SIZE];
- int imsi_len = gsm48_generate_mid_from_imsi(mi, pinfo->imsi);
struct gsm48_ra_id ra;
-
- if (imsi_len < 2)
- return -EINVAL;
+ struct osmo_mobile_identity mi;
msgb_nsei(msg) = nsei;
msgb_bvci(msg) = ns_bvci;
@@ -1241,16 +1238,23 @@
bgph->pdu_type = BSSGP_PDUT_PAGING_PS;
else
bgph->pdu_type = BSSGP_PDUT_PAGING_CS;
+
/* IMSI */
-/* gsm48_generate_mid_from_imsi() is guaranteed to never return more than 11,
- * but somehow gcc (8.2) is not smart enough to figure this out and claims that
- * the memcpy in msgb_tvlv_put() below will cause and out-of-bounds access up to
- * mi[131], which is wrong */
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Warray-bounds"
- OSMO_ASSERT(imsi_len <= GSM48_MID_MAX_SIZE);
- msgb_tvlv_put(msg, BSSGP_IE_IMSI, imsi_len-2, mi+2);
-#pragma GCC diagnostic pop
+ mi = (struct osmo_mobile_identity){ .type = GSM_MI_TYPE_IMSI, };
+ OSMO_STRLCPY_ARRAY(mi.imsi, pinfo->imsi);
+ msgb_tvl_put(msg, BSSGP_IE_IMSI, osmo_mobile_identity_encoded_len(&mi, NULL));
+ if (osmo_mobile_identity_encode_msgb(msg, &mi, false) <= 0) {
+ if (log_check_level(DBSSGP, LOGL_NOTICE)) {
+ char strbuf[64];
+ osmo_mobile_identity_to_str_buf(strbuf, sizeof(strbuf), &mi);
+ LOGP(DBSSGP, LOGL_ERROR,
+ "NSEI=%u/BVCI=%u Cannot encode Mobile Identity %s\n",
+ nsei, ns_bvci, strbuf);
+ }
+ msgb_free(msg);
+ return -EINVAL;
+ }
+
/* DRX Parameters */
msgb_tvlv_put(msg, BSSGP_IE_DRX_PARAMS, 2,
(uint8_t *) &drx_params);
diff --git a/src/gb/gprs_bssgp_bss.c b/src/gb/gprs_bssgp_bss.c
index 5c9d11c..9e9cefc 100644
--- a/src/gb/gprs_bssgp_bss.c
+++ b/src/gb/gprs_bssgp_bss.c
@@ -178,22 +178,17 @@
const char *imsi)
{
struct msgb *msg = common_tx_radio_status(bctx);
- uint8_t mi[GSM48_MID_MAX_SIZE];
- int imsi_len = gsm48_generate_mid_from_imsi(mi, imsi);
+ struct osmo_mobile_identity mi = { .type = GSM_MI_TYPE_IMSI, };
+ OSMO_STRLCPY_ARRAY(mi.imsi, imsi);
if (!msg)
return -ENOMEM;
-/* gsm48_generate_mid_from_imsi() is guaranteed to never return more than 11,
- * but somehow gcc (8.2) is not smart enough to figure this out and claims that
- * the memcpy in msgb_tvlv_put() below will cause and out-of-bounds access up to
- * mi[131], which is wrong */
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Warray-bounds"
- OSMO_ASSERT(imsi_len <= GSM48_MID_MAX_SIZE);
- /* strip the MI type and length values (2 bytes) */
- if (imsi_len > 2)
- msgb_tvlv_put(msg, BSSGP_IE_IMSI, imsi_len-2, mi+2);
-#pragma GCC diagnostic pop
+
+ msgb_tvl_put(msg, BSSGP_IE_IMSI, osmo_mobile_identity_encoded_len(&mi, NULL));
+ if (osmo_mobile_identity_encode_msgb(msg, &mi, false) <= 0) {
+ msgb_free(msg);
+ return -EINVAL;
+ }
LOGPC(DBSSGP, LOGL_NOTICE, "IMSI=%s ", imsi);
return common_tx_radio_status2(msg, cause);
@@ -486,6 +481,7 @@
struct tlv_parsed tp;
uint8_t ra[6];
int rc, data_len;
+ struct osmo_mobile_identity mi;
memset(ra, 0, sizeof(ra));
@@ -510,9 +506,11 @@
goto err_mand_ie;
if (!pinfo->imsi)
pinfo->imsi = talloc_zero_size(pinfo, GSM_IMSI_LENGTH);
- gsm48_mi_to_string(pinfo->imsi, GSM_IMSI_LENGTH,
- TLVP_VAL(&tp, BSSGP_IE_IMSI),
- TLVP_LEN(&tp, BSSGP_IE_IMSI));
+ if (osmo_mobile_identity_decode(&mi, TLVP_VAL(&tp, BSSGP_IE_IMSI), TLVP_LEN(&tp, BSSGP_IE_IMSI), false))
+ goto err_mand_ie;
+ if (mi.type != GSM_MI_TYPE_IMSI)
+ goto err_mand_ie;
+ osmo_talloc_replace_string(pinfo, &pinfo->imsi, mi.imsi);
/* DRX Parameters */
if (!TLVP_PRESENT(&tp, BSSGP_IE_DRX_PARAMS))
diff --git a/src/gsm/gsm0808.c b/src/gsm/gsm0808.c
index 9fdf379..6560d4b 100644
--- a/src/gsm/gsm0808.c
+++ b/src/gsm/gsm0808.c
@@ -730,9 +730,10 @@
const uint8_t *chan_needed)
{
struct msgb *msg;
- uint8_t mid_buf[GSM48_MI_SIZE + 2];
- int mid_len;
+ struct osmo_mobile_identity mi;
uint32_t tmsi_sw;
+ uint8_t *l;
+ int rc;
/* Mandatory elements! */
OSMO_ASSERT(imsi);
@@ -750,8 +751,15 @@
msgb_v_put(msg, BSS_MAP_MSG_PAGING);
/* mandatory IMSI 3.2.2.6 */
- mid_len = gsm48_generate_mid_from_imsi(mid_buf, imsi);
- msgb_tlv_put(msg, GSM0808_IE_IMSI, mid_len - 2, mid_buf + 2);
+ mi = (struct osmo_mobile_identity){ .type = GSM_MI_TYPE_IMSI, };
+ OSMO_STRLCPY_ARRAY(mi.imsi, imsi);
+ l = msgb_tl_put(msg, GSM0808_IE_IMSI);
+ rc = osmo_mobile_identity_encode_msgb(msg, &mi, false);
+ if (rc <= 0) {
+ msgb_free(msg);
+ return NULL;
+ }
+ *l = rc;
/* TMSI 3.2.2.7 */
if (tmsi) {
@@ -967,9 +975,17 @@
/* IMSI 3.2.2.6 */
if (params->imsi) {
- uint8_t mid_buf[GSM48_MI_SIZE + 2];
- int mid_len = gsm48_generate_mid_from_imsi(mid_buf, params->imsi);
- msgb_tlv_put(msg, GSM0808_IE_IMSI, mid_len - 2, mid_buf + 2);
+ uint8_t *l;
+ int rc;
+ struct osmo_mobile_identity mi = { .type = GSM_MI_TYPE_IMSI, };
+ OSMO_STRLCPY_ARRAY(mi.imsi, params->imsi);
+ l = msgb_tl_put(msg, GSM0808_IE_IMSI);
+ rc = osmo_mobile_identity_encode_msgb(msg, &mi, false);
+ if (rc <= 0) {
+ msgb_free(msg);
+ return NULL;
+ }
+ *l = rc;
}
if (params->aoip_transport_layer)
diff --git a/src/gsm/gsm29118.c b/src/gsm/gsm29118.c
index 2c02b2f..5c72b4d 100644
--- a/src/gsm/gsm29118.c
+++ b/src/gsm/gsm29118.c
@@ -219,12 +219,14 @@
/* Encode IMSI from string representation and append to SGSaAP msg */
static void msgb_sgsap_imsi_put(struct msgb *msg, const char *imsi)
{
- uint8_t buf[16];
- uint8_t len;
- /* encoding is just like TS 04.08 */
- len = gsm48_generate_mid_from_imsi(buf, imsi);
- /* skip first two bytes (tag+length) so we can use msgb_tlv_put */
- msgb_tlv_put(msg, SGSAP_IE_IMSI, len - 2, buf + 2);
+ int rc;
+ struct osmo_mobile_identity mi = { .type = GSM_MI_TYPE_IMSI, };
+ uint8_t *l;
+ OSMO_STRLCPY_ARRAY(mi.imsi, imsi);
+ l = msgb_tl_put(msg, SGSAP_IE_IMSI);
+ rc = osmo_mobile_identity_encode_msgb(msg, &mi, false);
+ /* This function fails to do error handling, so in case of error, leave the len == 0. */
+ *l = rc > 0? rc : 0;
}
/* Encode LAI from struct representation and append to SGSaAP msg */
diff --git a/src/gsm/gsm48.c b/src/gsm/gsm48.c
index 8d0998b..11bd361 100644
--- a/src/gsm/gsm48.c
+++ b/src/gsm/gsm48.c
@@ -45,6 +45,7 @@
#include <osmocom/gsm/protocol/gsm_04_80.h>
#include <osmocom/gsm/protocol/gsm_08_58.h>
#include <osmocom/gsm/protocol/gsm_04_08_gprs.h>
+#include <osmocom/gsm/protocol/gsm_23_003.h>
/*! \addtogroup gsm0408
* @{
@@ -458,70 +459,500 @@
return get_value_string(mi_type_names, mi);
}
-/*! Return a human readable representation of a Mobile Identity in caller-provided buffer.
+/*! Deprecated, see osmo_mobile_identity instead.
+ * Return a human readable representation of a Mobile Identity in caller-provided buffer.
* \param[out] buf caller-provided output buffer
* \param[in] buf_len size of buf in bytes
* \param[in] mi Mobile Identity buffer containing 3GPP TS 04.08 style MI type and data.
* \param[in] mi_len Length of mi.
* \return buf
*/
-char *osmo_mi_name_buf(char *buf, size_t buf_len, const uint8_t *mi, uint8_t mi_len)
+char *osmo_mi_name_buf(char *buf, size_t buf_len, const uint8_t *mi_data, uint8_t mi_len)
{
- uint8_t mi_type;
- uint32_t tmsi;
- char mi_string[GSM48_MI_SIZE];
-
- mi_type = (mi && mi_len) ? (mi[0] & GSM_MI_TYPE_MASK) : GSM_MI_TYPE_NONE;
-
- switch (mi_type) {
- case GSM_MI_TYPE_TMSI:
- /* Table 10.5.4.3, reverse generate_mid_from_tmsi */
- if (mi_len == GSM48_TMSI_LEN && mi[0] == (0xf0 | GSM_MI_TYPE_TMSI)) {
- tmsi = osmo_load32be(&mi[1]);
- snprintf(buf, buf_len, "TMSI-0x%08" PRIX32, tmsi);
- } else {
- snprintf(buf, buf_len, "TMSI-invalid");
- }
- return buf;
-
- case GSM_MI_TYPE_IMSI:
- case GSM_MI_TYPE_IMEI:
- case GSM_MI_TYPE_IMEISV:
- osmo_bcd2str(mi_string, sizeof(mi_string), mi, 1, (mi_len * 2) - (mi[0] & GSM_MI_ODD ? 0 : 1), true);
- snprintf(buf, buf_len, "%s-%s", gsm48_mi_type_name(mi_type), mi_string);
- return buf;
-
- default:
+ struct osmo_mobile_identity mi;
+ if (osmo_mobile_identity_decode(&mi, mi_data, mi_len, true)) {
snprintf(buf, buf_len, "unknown");
return buf;
}
+ osmo_mobile_identity_to_str_buf(buf, buf_len, &mi);
+ return buf;
}
-/*! Return a human readable representation of a Mobile Identity in static buffer.
+/*! Deprecated, see osmo_mobile_identity instead.
+ * Return a human readable representation of a Mobile Identity in static buffer.
* \param[in] mi Mobile Identity buffer containing 3GPP TS 04.08 style MI type and data.
* \param[in] mi_len Length of mi.
* \return A string like "IMSI-1234567", "TMSI-0x1234ABCD" or "unknown", "TMSI-invalid"...
*/
-const char *osmo_mi_name(const uint8_t *mi, uint8_t mi_len)
+const char *osmo_mi_name(const uint8_t *mi_data, uint8_t mi_len)
{
static __thread char mi_name[10 + GSM48_MI_SIZE + 1];
- return osmo_mi_name_buf(mi_name, sizeof(mi_name), mi, mi_len);
+ struct osmo_mobile_identity mi;
+ if (osmo_mobile_identity_decode(&mi, mi_data, mi_len, true) == 0)
+ osmo_mobile_identity_to_str_buf(mi_name, sizeof(mi_name), &mi);
+ else
+ snprintf(mi_name, sizeof(mi_name), "unknown");
+ return mi_name;
}
-/*! Return a human readable representation of a Mobile Identity in dynamically-allocated buffer.
+/*! Deprecated, see osmo_mobile_identity instead.
+ * Return a human readable representation of a Mobile Identity in dynamically-allocated buffer.
* \param[in] ctx talloc context from which to allocate output buffer
* \param[in] mi Mobile Identity buffer containing 3GPP TS 04.08 style MI type and data.
* \param[in] mi_len Length of mi.
* \return A string like "IMSI-1234567", "TMSI-0x1234ABCD" or "unknown", "TMSI-invalid" in a
* dynamically-allocated output buffer.
*/
-char *osmo_mi_name_c(const void *ctx, const uint8_t *mi, uint8_t mi_len)
+char *osmo_mi_name_c(const void *ctx, const uint8_t *mi_data, uint8_t mi_len)
{
- size_t buf_len = 10 + GSM48_MI_SIZE + 1;
- char *mi_name = talloc_size(ctx, buf_len);
- if (!mi_name)
- return NULL;
- return osmo_mi_name_buf(mi_name, buf_len, mi, mi_len);
+ struct osmo_mobile_identity mi;
+ if (osmo_mobile_identity_decode(&mi, mi_data, mi_len, true))
+ return talloc_strdup((void*)ctx, "unknown");
+ return osmo_mobile_identity_to_str_c((void*)ctx, &mi);
+}
+
+/*! Extract Mobile Identity from encoded bytes (3GPP TS 24.008 10.5.1.4).
+ *
+ * On failure (negative return value), mi->type == GSM_MI_TYPE_NONE, mi->string[] is all-zero and mi->tmsi ==
+ * GSM_RESERVED_TMSI.
+ *
+ * On success, mi->type reflects the decoded Mobile Identity type (GSM_MI_TYPE_IMSI, GSM_MI_TYPE_TMSI, GSM_MI_TYPE_IMEI
+ * or GSM_MI_TYPE_IMEISV).
+ *
+ * On success, mi->string always contains a human readable representation of the Mobile Identity digits: IMSI, IMEI and
+ * IMEISV as digits like "12345678", and TMSI as "0x" and 8 hexadecimal digits like "0x1234abcd".
+ *
+ * mi->tmsi contains the uint32_t TMSI value iff the extracted Mobile Identity was a TMSI, or GSM_RESERVED_TMSI
+ * otherwise.
+ *
+ * \param[out] mi Return buffer for decoded Mobile Identity.
+ * \param[in] mi_data The encoded Mobile Identity octets.
+ * \param[in] mi_len Number of octets in mi_data.
+ * \param[in] allow_hex If false, hexadecimal digits (>9) result in an error return value.
+ * \returns 0 on success, negative on error: -EBADMSG = invalid length indication or invalid data,
+ * -EINVAL = unknown Mobile Identity type.
+ */
+int osmo_mobile_identity_decode(struct osmo_mobile_identity *mi, const uint8_t *mi_data, uint8_t mi_len,
+ bool allow_hex)
+{
+ int rc;
+ int nibbles_len;
+ char *str;
+ size_t str_size;
+
+ if (!mi_data || mi_len < 1)
+ return -EBADMSG;
+
+ nibbles_len = (mi_len - 1) * 2 + ((mi_data[0] & GSM_MI_ODD) ? 1 : 0);
+
+ *mi = (struct osmo_mobile_identity){
+ .type = mi_data[0] & GSM_MI_TYPE_MASK,
+ };
+
+ /* First do length checks */
+ switch (mi->type) {
+ case GSM_MI_TYPE_TMSI:
+ mi->tmsi = GSM_RESERVED_TMSI;
+ if (nibbles_len != (GSM23003_TMSI_NUM_BYTES * 2)) {
+ rc = -EBADMSG;
+ goto return_error;
+ }
+ break;
+
+ case GSM_MI_TYPE_IMSI:
+ if (nibbles_len < GSM23003_IMSI_MIN_DIGITS || nibbles_len > GSM23003_IMSI_MAX_DIGITS) {
+ rc = -EBADMSG;
+ goto return_error;
+ }
+ str = mi->imsi;
+ str_size = sizeof(mi->imsi);
+ break;
+
+ case GSM_MI_TYPE_IMEI:
+ if (nibbles_len != GSM23003_IMEI_NUM_DIGITS && nibbles_len != GSM23003_IMEI_NUM_DIGITS_NO_CHK) {
+ rc = -EBADMSG;
+ goto return_error;
+ }
+ str = mi->imei;
+ str_size = sizeof(mi->imei);
+ break;
+
+ case GSM_MI_TYPE_IMEISV:
+ if (nibbles_len != GSM23003_IMEISV_NUM_DIGITS) {
+ rc = -EBADMSG;
+ goto return_error;
+ }
+ str = mi->imeisv;
+ str_size = sizeof(mi->imeisv);
+ break;
+
+ default:
+ rc = -EINVAL;
+ goto return_error;
+ }
+
+ /* Decode BCD digits */
+ switch (mi->type) {
+ case GSM_MI_TYPE_TMSI:
+ /* MI is a 32bit integer TMSI. Length has been checked above. */
+ if ((mi_data[0] & 0xf0) != 0xf0) {
+ /* A TMSI always has the first nibble == 0xf */
+ rc = -EBADMSG;
+ goto return_error;
+ }
+ mi->tmsi = osmo_load32be(&mi_data[1]);
+ return 0;
+
+ case GSM_MI_TYPE_IMSI:
+ case GSM_MI_TYPE_IMEI:
+ case GSM_MI_TYPE_IMEISV:
+ /* If the length is even, the last nibble (higher nibble of last octet) must be 0xf */
+ if (!(mi_data[0] & GSM_MI_ODD)
+ && ((mi_data[mi_len - 1] & 0xf0) != 0xf0)) {
+ rc = -EBADMSG;
+ goto return_error;
+ }
+ rc = osmo_bcd2str(str, str_size, mi_data, 1, 1 + nibbles_len, allow_hex);
+ /* rc checked below */
+ break;
+
+ default:
+ /* Already handled above, but as future bug paranoia: */
+ rc = -EINVAL;
+ goto return_error;
+ }
+
+ /* check mi->str printing rc */
+ if (rc < 1 || rc >= str_size) {
+ rc = -EBADMSG;
+ goto return_error;
+ }
+ return 0;
+
+return_error:
+ *mi = (struct osmo_mobile_identity){
+ .type = GSM_MI_TYPE_NONE,
+ };
+ return rc;
+}
+
+/*! Return the number of encoded Mobile Identity octets, without actually encoding.
+ * Useful to write tag-length header before encoding the MI.
+ * \param[in] mi Mobile Identity.
+ * \param[out] mi_digits If not NULL, store the number of nibbles of used MI data (i.e. strlen(mi->string) or 8 for a TMSI).
+ * \return octets that osmo_mobile_identity_encode_msgb() will write for this mi.
+ */
+int osmo_mobile_identity_encoded_len(const struct osmo_mobile_identity *mi, int *mi_digits)
+{
+ int mi_nibbles;
+ if (!mi)
+ return -EINVAL;
+ switch (mi->type) {
+ case GSM_MI_TYPE_TMSI:
+ mi_nibbles = GSM23003_TMSI_NUM_BYTES * 2;
+ break;
+ case GSM_MI_TYPE_IMSI:
+ mi_nibbles = strlen(mi->imsi);
+ if (mi_nibbles < GSM23003_IMSI_MIN_DIGITS
+ || mi_nibbles > GSM23003_IMSI_MAX_DIGITS)
+ return -EINVAL;
+ break;
+ case GSM_MI_TYPE_IMEI:
+ mi_nibbles = strlen(mi->imei);
+ if (mi_nibbles < GSM23003_IMEI_NUM_DIGITS_NO_CHK
+ || mi_nibbles > GSM23003_IMEI_NUM_DIGITS)
+ return -EINVAL;
+ break;
+ case GSM_MI_TYPE_IMEISV:
+ mi_nibbles = strlen(mi->imeisv);
+ if (mi_nibbles != GSM23003_IMEISV_NUM_DIGITS)
+ return -EINVAL;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ if (mi_digits)
+ *mi_digits = mi_nibbles;
+
+ /* one type nibble, plus the MI nibbles, plus a filler nibble to complete the last octet:
+ * mi_octets = ceil((float)(mi_nibbles + 1) / 2)
+ */
+ return (mi_nibbles + 2) / 2;
+}
+
+/*! Encode Mobile Identity from uint32_t (TMSI) or digits string (all others) (3GPP TS 24.008 10.5.1.4).
+ *
+ * \param[out] buf Return buffer for encoded Mobile Identity.
+ * \param[in] buflen sizeof(buf).
+ * \param[in] mi Mobile identity to encode.
+ * \param[in] allow_hex If false, hexadecimal digits (>9) result in an error return value.
+ * \returns Amount of bytes written to buf, or negative on error.
+ */
+int osmo_mobile_identity_encode_buf(uint8_t *buf, size_t buflen, const struct osmo_mobile_identity *mi, bool allow_hex)
+{
+ int rc;
+ int nibbles_len;
+ int mi_octets;
+ const char *mi_str;
+
+ if (!buf || !buflen)
+ return -EIO;
+
+ mi_octets = osmo_mobile_identity_encoded_len(mi, &nibbles_len);
+ if (mi_octets < 0)
+ return mi_octets;
+ if (mi_octets > buflen)
+ return -ENOSPC;
+
+ buf[0] = (mi->type & GSM_MI_TYPE_MASK) | ((nibbles_len & 1) ? GSM_MI_ODD : 0);
+
+ switch (mi->type) {
+ case GSM_MI_TYPE_TMSI:
+ buf[0] |= 0xf0;
+ osmo_store32be(mi->tmsi, &buf[1]);
+ return mi_octets;
+
+ case GSM_MI_TYPE_IMSI:
+ mi_str = mi->imsi;
+ break;
+ case GSM_MI_TYPE_IMEI:
+ mi_str = mi->imei;
+ break;
+ case GSM_MI_TYPE_IMEISV:
+ mi_str = mi->imeisv;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ rc = osmo_str2bcd(buf, buflen, mi_str, 1, -1, allow_hex);
+ if (rc != mi_octets)
+ return -EINVAL;
+ return mi_octets;
+}
+
+/*! Encode Mobile Identity type and BCD digits, appended to a msgb.
+ * Example to add a GSM48_IE_MOBILE_ID IEI with tag and length to a msgb:
+ *
+ * struct osmo_mobile_identity mi = { .type = GSM_MI_TYPE_IMSI, .tmsi = random_tmsi, };
+ * uint8_t *l = msgb_tl_put(msg, GSM48_IE_MOBILE_ID);
+ * int rc = osmo_mobile_identity_encode_msgb(msg, &mi, false);
+ * if (rc < 0)
+ * goto error;
+ * *l = rc;
+ *
+ * Example to add a BSSGP_IE_IMSI with tag and variable-size length, where the
+ * length needs to be known at the time of writing the IE tag-length header:
+ *
+ * struct osmo_mobile_identity mi = { .type = GSM_MI_TYPE_IMSI, };
+ * OSMO_STRLCPY_ARRAY(mi.imsi, pinfo->imsi);
+ * msgb_tvl_put(msg, BSSGP_IE_IMSI, osmo_mobile_identity_encoded_len(&mi, NULL));
+ * if (osmo_mobile_identity_encode_msgb(msg, &mi, false) < 0)
+ * goto error;
+ */
+int osmo_mobile_identity_encode_msgb(struct msgb *msg, const struct osmo_mobile_identity *mi, bool allow_hex)
+{
+ int rc = osmo_mobile_identity_encode_buf(msg->tail, msgb_tailroom(msg), mi, allow_hex);
+ if (rc < 0)
+ return rc;
+ msgb_put(msg, rc);
+ return rc;
+}
+
+/*! Extract Mobile Identity from a Complete Layer 3 message.
+ *
+ * Determine the Mobile Identity data and call osmo_mobile_identity_decode() to return a decoded struct
+ * osmo_mobile_identity.
+ *
+ * \param[out] mi Return buffer for decoded Mobile Identity.
+ * \param[in] msg The Complete Layer 3 message to extract from (LU, CM Service Req or Paging Resp).
+ * \returns 0 on success, negative on error: return codes as defined in osmo_mobile_identity_decode(), or
+ * -ENOTSUP = not a Complete Layer 3 message,
+ */
+int osmo_mobile_identity_decode_from_l3(struct osmo_mobile_identity *mi, struct msgb *msg, bool allow_hex)
+{
+ const struct gsm48_hdr *gh;
+ int8_t pdisc = 0;
+ uint8_t mtype = 0;
+ const struct gsm48_loc_upd_req *lu;
+ const uint8_t *cm2_buf;
+ uint8_t cm2_len;
+ const uint8_t *mi_start;
+ const struct gsm48_pag_resp *paging_response;
+ const uint8_t *mi_data;
+ uint8_t mi_len;
+ const struct gsm48_imsi_detach_ind *idi;
+
+ *mi = (struct osmo_mobile_identity){
+ .type = GSM_MI_TYPE_NONE,
+ .tmsi = GSM_RESERVED_TMSI,
+ };
+
+ if (msgb_l3len(msg) < sizeof(*gh))
+ return -EBADMSG;
+
+ gh = msgb_l3(msg);
+ pdisc = gsm48_hdr_pdisc(gh);
+ mtype = gsm48_hdr_msg_type(gh);
+
+ switch (pdisc) {
+ case GSM48_PDISC_MM:
+
+ switch (mtype) {
+ case GSM48_MT_MM_LOC_UPD_REQUEST:
+ /* First make sure that lu-> can be dereferenced */
+ if (msgb_l3len(msg) < sizeof(*gh) + sizeof(*lu))
+ return -EBADMSG;
+
+ /* Now we know there is enough msgb data to read a lu->mi_len, so also check that */
+ lu = (struct gsm48_loc_upd_req*)gh->data;
+ if (msgb_l3len(msg) < sizeof(*gh) + sizeof(*lu) + lu->mi_len)
+ return -EBADMSG;
+ mi_data = lu->mi;
+ mi_len = lu->mi_len;
+ goto got_mi;
+
+ case GSM48_MT_MM_CM_SERV_REQ:
+ case GSM48_MT_MM_CM_REEST_REQ:
+ /* Unfortunately in Phase1 the Classmark2 length is variable, so we cannot
+ * just use gsm48_service_request struct, and need to parse it manually. */
+ if (msgb_l3len(msg) < sizeof(*gh) + 2)
+ return -EBADMSG;
+
+ cm2_len = gh->data[1];
+ cm2_buf = gh->data + 2;
+ goto got_cm2;
+
+ case GSM48_MT_MM_IMSI_DETACH_IND:
+ if (msgb_l3len(msg) < sizeof(*gh) + sizeof(*idi))
+ return -EBADMSG;
+ idi = (struct gsm48_imsi_detach_ind*) gh->data;
+ mi_data = idi->mi;
+ mi_len = idi->mi_len;
+ goto got_mi;
+
+ case GSM48_MT_MM_ID_RESP:
+ if (msgb_l3len(msg) < sizeof(*gh) + 2)
+ return -EBADMSG;
+ mi_data = gh->data+1;
+ mi_len = gh->data[0];
+ goto got_mi;
+
+ default:
+ break;
+ }
+ break;
+
+ case GSM48_PDISC_RR:
+
+ switch (mtype) {
+ case GSM48_MT_RR_PAG_RESP:
+ if (msgb_l3len(msg) < sizeof(*gh) + sizeof(*paging_response))
+ return -EBADMSG;
+ paging_response = (struct gsm48_pag_resp*)gh->data;
+ cm2_len = paging_response->cm2_len;
+ cm2_buf = (uint8_t*)&paging_response->cm2;
+ goto got_cm2;
+
+ default:
+ break;
+ }
+ break;
+ }
+
+ return -ENOTSUP;
+
+got_cm2:
+ /* MI (Mobile Identity) LV follows the Classmark2 */
+
+ /* There must be at least a mi_len byte after the CM2 */
+ if (cm2_buf + cm2_len + 1 > msg->tail)
+ return -EBADMSG;
+
+ mi_start = cm2_buf + cm2_len;
+ mi_len = mi_start[0];
+ mi_data = mi_start + 1;
+
+got_mi:
+ /* mi_data points at the start of the Mobile Identity coding of mi_len bytes */
+ if (mi_data + mi_len > msg->tail)
+ return -EBADMSG;
+
+ return osmo_mobile_identity_decode(mi, mi_data, mi_len, allow_hex);
+}
+
+/*! Return a human readable representation of a struct osmo_mobile_identity.
+ * Write a string like "IMSI-1234567", "TMSI-0x1234ABCD" or "NONE", "NULL".
+ * \param[out] buf String buffer to write to.
+ * \param[in] buflen sizeof(buf).
+ * \param[in] mi Decoded Mobile Identity data.
+ * \return the strlen() of the string written when buflen is sufficiently large, like snprintf().
+ */
+int osmo_mobile_identity_to_str_buf(char *buf, size_t buflen, const struct osmo_mobile_identity *mi)
+{
+ struct osmo_strbuf sb = { .buf = buf, .len = buflen };
+ if (!mi)
+ return snprintf(buf, buflen, "NULL");
+ OSMO_STRBUF_PRINTF(sb, "%s", gsm48_mi_type_name(mi->type));
+ switch (mi->type) {
+ case GSM_MI_TYPE_TMSI:
+ OSMO_STRBUF_PRINTF(sb, "-0x%08" PRIX32, mi->tmsi);
+ break;
+ case GSM_MI_TYPE_IMSI:
+ OSMO_STRBUF_PRINTF(sb, "-%s", mi->imsi);
+ break;
+ case GSM_MI_TYPE_IMEI:
+ OSMO_STRBUF_PRINTF(sb, "-%s", mi->imei);
+ break;
+ case GSM_MI_TYPE_IMEISV:
+ OSMO_STRBUF_PRINTF(sb, "-%s", mi->imeisv);
+ break;
+ default:
+ break;
+ }
+ return sb.chars_needed;
+}
+
+/*! Like osmo_mobile_identity_to_str_buf(), but return the string in a talloc buffer.
+ * \param[in] ctx Talloc context to allocate from.
+ * \param[in] mi Decoded Mobile Identity data.
+ * \return a string like "IMSI-1234567", "TMSI-0x1234ABCD" or "NONE", "NULL".
+ */
+char *osmo_mobile_identity_to_str_c(void *ctx, const struct osmo_mobile_identity *mi)
+{
+ OSMO_NAME_C_IMPL(ctx, 32, "ERROR", osmo_mobile_identity_to_str_buf, mi)
+}
+
+/*! Compare two osmo_mobile_identity structs, returning typical cmp() result.
+ * \param[in] a Left side osmo_mobile_identity.
+ * \param[in] b Right side osmo_mobile_identity.
+ * \returns 0 if both are equal, -1 if a < b, 1 if a > b.
+ */
+int osmo_mobile_identity_cmp(const struct osmo_mobile_identity *a, const struct osmo_mobile_identity *b)
+{
+ int cmp;
+ if (a == b)
+ return 0;
+ if (!a)
+ return -1;
+ if (!b)
+ return 1;
+ cmp = OSMO_CMP(a->type, b->type);
+ if (cmp)
+ return cmp;
+ switch (a->type) {
+ case GSM_MI_TYPE_TMSI:
+ return OSMO_CMP(a->tmsi, b->tmsi);
+ case GSM_MI_TYPE_IMSI:
+ return strncmp(a->imsi, b->imsi, sizeof(a->imsi));
+ case GSM_MI_TYPE_IMEI:
+ return strncmp(a->imei, b->imei, sizeof(a->imei));
+ case GSM_MI_TYPE_IMEISV:
+ return strncmp(a->imeisv, b->imeisv, sizeof(a->imeisv));
+ default:
+ /* No known type, but both have the same type. */
+ return 0;
+ }
}
/*! Checks is particular message is cipherable in A/Gb mode according to
@@ -676,64 +1107,76 @@
}
}
-/*! Generate TS 04.08 Mobile ID from TMSI
+static int legacy_compat_generate_mid(uint8_t *buf, const struct osmo_mobile_identity *mi)
+{
+ int rc;
+ buf[0] = GSM48_IE_MOBILE_ID;
+ rc = osmo_mobile_identity_encode_buf(buf + 2, GSM48_MID_MAX_SIZE - 2, mi, false);
+ if (rc <= 0)
+ return 0;
+ OSMO_ASSERT(rc <= 9);
+ buf[1] = rc;
+ return 2 + rc;
+}
+
+/*! Deprecated, see osmo_mobile_identity instead.
+ * Generate TS 04.08 Mobile ID from TMSI
* \param[out] buf Caller-provided output buffer (7 bytes)
* \param[in] tmsi TMSI to be encoded
* \returns number of byes encoded (always 7) */
int gsm48_generate_mid_from_tmsi(uint8_t *buf, uint32_t tmsi)
{
- uint32_t tmsi_be = osmo_htonl(tmsi);
-
- buf[0] = GSM48_IE_MOBILE_ID;
- buf[1] = GSM48_TMSI_LEN;
- buf[2] = 0xf0 | GSM_MI_TYPE_TMSI;
- memcpy(&buf[3], &tmsi_be, sizeof(tmsi_be));
-
- return 7;
+ struct osmo_mobile_identity mi = {
+ .type = GSM_MI_TYPE_TMSI,
+ .tmsi = tmsi,
+ };
+ return legacy_compat_generate_mid(buf, &mi);
}
-/*! Generate TS 24.008 §10.5.1.4 Mobile ID of BCD type from ASCII string
+/*! Deprecated, see osmo_mobile_identity instead.
+ * Generate TS 24.008 §10.5.1.4 Mobile ID of BCD type from ASCII string
* \param[out] buf Caller-provided output buffer of at least GSM48_MID_MAX_SIZE bytes
* \param[in] id Identity to be encoded
* \param[in] mi_type Type of identity (e.g. GSM_MI_TYPE_IMSI, IMEI, IMEISV)
* \returns number of bytes used in \a buf */
uint8_t gsm48_generate_mid(uint8_t *buf, const char *id, uint8_t mi_type)
{
- uint8_t length = strnlen(id, 16), i, off = 0, odd = (length & 1) == 1;
- /* maximum length == 16 (IMEISV) */
-
- buf[0] = GSM48_IE_MOBILE_ID;
- buf[2] = osmo_char2bcd(id[0]) << 4 | (mi_type & GSM_MI_TYPE_MASK) | (odd << 3);
-
- /* if the length is even we will fill half of the last octet */
- buf[1] = (length + (odd ? 1 : 2)) >> 1;
- /* buf[1] maximum = 18/2 = 9 */
- OSMO_ASSERT(buf[1] <= 9);
-
- for (i = 1; i < buf[1]; ++i) {
- uint8_t upper, lower = osmo_char2bcd(id[++off]);
- if (!odd && off + 1 == length)
- upper = 0x0f;
- else
- upper = osmo_char2bcd(id[++off]) & 0x0f;
-
- buf[2 + i] = (upper << 4) | lower;
+ struct osmo_mobile_identity mi = { .type = mi_type };
+ switch (mi_type) {
+ case GSM_MI_TYPE_TMSI:
+ mi.tmsi = strtoul(id, NULL, 10);
+ break;
+ case GSM_MI_TYPE_IMSI:
+ OSMO_STRLCPY_ARRAY(mi.imsi, id);
+ break;
+ case GSM_MI_TYPE_IMEI:
+ OSMO_STRLCPY_ARRAY(mi.imei, id);
+ break;
+ case GSM_MI_TYPE_IMEISV:
+ OSMO_STRLCPY_ARRAY(mi.imeisv, id);
+ break;
+ default:
+ return 0;
}
-
- /* maximum return value: 2 + 9 = 11 */
- return 2 + buf[1];
+ return legacy_compat_generate_mid(buf, &mi);
}
-/*! Generate TS 04.08 Mobile ID from IMSI
+/*! Deprecated, see osmo_mobile_identity instead.
+ * Generate TS 04.08 Mobile ID from IMSI
* \param[out] buf Caller-provided output buffer
* \param[in] imsi IMSI to be encoded
* \returns number of bytes used in \a buf */
int gsm48_generate_mid_from_imsi(uint8_t *buf, const char *imsi)
{
- return gsm48_generate_mid(buf, imsi, GSM_MI_TYPE_IMSI);
+ struct osmo_mobile_identity mi = {
+ .type = GSM_MI_TYPE_IMSI,
+ };
+ OSMO_STRLCPY_ARRAY(mi.imsi, imsi);
+ return legacy_compat_generate_mid(buf, &mi);
}
-/*! Convert TS 04.08 Mobile Identity (10.5.1.4) to string.
+/*! Deprecated, see osmo_mobile_identity instead.
+ * Convert TS 04.08 Mobile Identity (10.5.1.4) to string.
* This function does not validate the Mobile Identity digits, i.e. digits > 9 are returned as 'A'-'F'.
* \param[out] string Caller-provided buffer for output
* \param[in] str_len Length of \a string in bytes
diff --git a/src/gsm/libosmogsm.map b/src/gsm/libosmogsm.map
index ac9aeb2..742cec3 100644
--- a/src/gsm/libosmogsm.map
+++ b/src/gsm/libosmogsm.map
@@ -363,6 +363,14 @@
gsm48_generate_mid_from_imsi;
gsm48_generate_mid_from_tmsi;
gsm48_mi_to_string;
+osmo_mobile_identity_to_str_buf;
+osmo_mobile_identity_to_str_c;
+osmo_mobile_identity_cmp;
+osmo_mobile_identity_decode;
+osmo_mobile_identity_decode_from_l3;
+osmo_mobile_identity_encoded_len;
+osmo_mobile_identity_encode_buf;
+osmo_mobile_identity_encode_msgb;
gsm48_mm_att_tlvdef;
gsm48_number_of_paging_subchannels;
gsm48_parse_ra;
diff --git a/src/utils.c b/src/utils.c
index 18e105f..3c4a8c9 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -175,6 +175,65 @@
return OSMO_MAX(0, end_nibble - start_nibble);
}
+/*! Convert string to BCD.
+ * The given nibble offsets are interpreted in BCD order, i.e. nibble 0 is bcd[0] & 0x0f, nibble 1 is bcd[0] & 0xf0, nibble
+ * 3 is bcd[1] & 0x0f, etc..
+ * \param[out] dst Output BCD buffer.
+ * \param[in] dst_size sizeof() the output string buffer.
+ * \param[in] digits String containing decimal or hexadecimal digits in upper or lower case.
+ * \param[in] start_nibble Offset to start from, in nibbles, typically 1 to skip the first (MI type) nibble.
+ * \param[in] end_nibble Negative to write all digits found in str, followed by 0xf nibbles to fill any started octet.
+ * If >= 0, stop before this offset in nibbles, e.g. to get default behavior, pass
+ * start_nibble + strlen(str) + ((start_nibble + strlen(str)) & 1? 1 : 0) + 1.
+ * \param[in] allow_hex If false, return error if there are hexadecimal digits (A-F). If true, write those to
+ * BCD.
+ * \returns The buffer size in octets that is used to place all bcd digits (including the skipped nibbles
+ * from 'start_nibble' and rounded up to full octets); -EINVAL on invalid digits;
+ * -ENOMEM if dst is NULL, if dst_size is too small to contain all nibbles, or if start_nibble is negative.
+ */
+int osmo_str2bcd(uint8_t *dst, size_t dst_size, const char *digits, int start_nibble, int end_nibble, bool allow_hex)
+{
+ const char *digit = digits;
+ int nibble_i;
+
+ if (!dst || !dst_size || start_nibble < 0)
+ return -ENOMEM;
+
+ if (end_nibble < 0) {
+ end_nibble = start_nibble + strlen(digits);
+ /* If the last octet is not complete, add another filler nibble */
+ if (end_nibble & 1)
+ end_nibble++;
+ }
+ if ((end_nibble / 2) > dst_size)
+ return -ENOMEM;
+
+ for (nibble_i = start_nibble; nibble_i < end_nibble; nibble_i++) {
+ uint8_t nibble = 0xf;
+ int octet = nibble_i >> 1;
+ if (*digit) {
+ char c = *digit;
+ digit++;
+ if (c >= '0' && c <= '9')
+ nibble = c - '0';
+ else if (allow_hex && c >= 'A' && c <= 'F')
+ nibble = 0xa + (c - 'A');
+ else if (allow_hex && c >= 'a' && c <= 'f')
+ nibble = 0xa + (c - 'a');
+ else
+ return -EINVAL;
+ }
+ nibble &= 0xf;
+ if ((nibble_i & 1))
+ dst[octet] = (nibble << 4) | (dst[octet] & 0x0f);
+ else
+ dst[octet] = (dst[octet] & 0xf0) | nibble;
+ }
+
+ /* floor(float(end_nibble) / 2) */
+ return end_nibble / 2;
+}
+
/*! Parse a string containing hexadecimal digits
* \param[in] str string containing ASCII encoded hexadecimal digits
* \param[out] b output buffer