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/tests/gsm0408/gsm0408_test.c b/tests/gsm0408/gsm0408_test.c
index 9617823..a86fe11 100644
--- a/tests/gsm0408/gsm0408_test.c
+++ b/tests/gsm0408/gsm0408_test.c
@@ -18,6 +18,8 @@
  *
  */
 
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+
 #include <string.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -386,8 +388,9 @@
 	{
 		.mi_type = GSM_MI_TYPE_IMSI | GSM_MI_ODD,
 		.mi_str = "423423",
-		.mi_name = "IMSI-423423",
-		.expect_mi_tlv_hex = "1704413224f3",
+		.expect_str = "",
+		.expect_rc = 1,
+		.mi_name = "unknown",
 	},
 	{
 		.mi_type = GSM_MI_TYPE_IMSI,
@@ -471,21 +474,21 @@
 		.mi_type = GSM_MI_TYPE_NONE,
 		.mi_str = "123",
 		.mi_name = "unknown",
-		.expect_mi_tlv_hex = "17021832", /* encoding invalid MI type */
+		.expect_mi_tlv_hex = "",
 		.expect_str = "",
 	},
 	{
 		.mi_type = GSM_MI_TYPE_NONE,
 		.mi_str = "1234",
 		.mi_name = "unknown",
-		.expect_mi_tlv_hex = "17031032f4", /* encoding invalid MI type */
+		.expect_mi_tlv_hex = "",
 		.expect_str = "",
 	},
 	{
 		.mi_type = GSM_MI_ODD,
 		.mi_str = "1234",
 		.mi_name = "unknown",
-		.expect_mi_tlv_hex = "17031032f4", /* encoding invalid MI type */
+		.expect_mi_tlv_hex = "",
 		.expect_str = "",
 	},
 };
@@ -522,9 +525,14 @@
 			printf("     ERROR: expected '%s'\n", t->expect_mi_tlv_hex);
 		}
 
-		/* skip the GSM48_IE_MOBILE_ID tag and length */
-		mi_buf = tlv_buf + 2;
-		mi_len = tlv_len - 2;
+		if (tlv_len) {
+			/* skip the GSM48_IE_MOBILE_ID tag and length */
+			mi_buf = tlv_buf + 2;
+			mi_len = tlv_len - 2;
+		} else {
+			mi_buf = NULL;
+			mi_len = 0;
+		}
 
 		rc = gsm48_mi_to_string(str, str_size, mi_buf, mi_len);
 		printf("  -> MI-str=%s rc=%d\n", osmo_quote_str(str, -1), rc);
@@ -611,6 +619,368 @@
 	printf("\n");
 }
 
+struct msgb *msgb_from_hex(const char *label, uint16_t size, const char *hex)
+{
+	struct msgb *msg = msgb_alloc_headroom(size, 4, label);
+	OSMO_ASSERT(msg);
+	msg->l3h = msgb_put(msg, osmo_hexparse(hex, msg->data, msgb_tailroom(msg)));
+	return msg;
+}
+
+struct mobile_identity_tc {
+	const char *label;
+	const char *compl_l3_msg;
+	int expect_rc;
+	struct osmo_mobile_identity expect_mi;
+};
+
+/* Some Complete Layer 3 messages copied from real GSM network traces. */
+struct mobile_identity_tc mobile_identity_tests[] = {
+	{
+		.label = "LU with IMSI 901700000004620",
+		.compl_l3_msg = "050802008168000130" "089910070000006402",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMSI,
+			.imsi = "901700000004620",
+		},
+	},
+	{
+		.label = "LU with TMSI 0x0980ad8a",
+		.compl_l3_msg = "05084262f224002a50" "05f40980ad8a",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_TMSI,
+			.tmsi = 0x0980ad8a,
+		},
+	},
+	{
+		.label = "LU with invalid MI type",
+		.compl_l3_msg = "050802008168000130" "089d10070000006402",
+		.expect_rc = -EINVAL,
+	},
+	{
+		.label = "LU with truncated IMSI MI",
+		.compl_l3_msg = "050802008168000130" "0899100700000064",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "LU with too short IMSI MI (12345)",
+		.compl_l3_msg = "050802008168000130" "03193254",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "LU with just long enough IMSI MI 123456",
+		.compl_l3_msg = "050802008168000130" "04113254f6",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMSI,
+			.imsi = "123456",
+		},
+	},
+	{
+		.label = "LU with max length IMSI MI 123456789012345",
+		.compl_l3_msg = "050802008168000130" "081932547698103254",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMSI,
+			.imsi = "123456789012345",
+		},
+	},
+	{
+		.label = "LU with just too long IMSI MI 1234567890123456",
+		.compl_l3_msg = "050802008168000130" "091132547698103254f6",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "LU with truncated TMSI MI",
+		.compl_l3_msg = "05084262f224002a50" "05f40980ad",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "LU with odd length TMSI",
+		.compl_l3_msg = "05084262f224002a50" "05fc0980ad8a",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "LU with too long TMSI MI",
+		.compl_l3_msg = "05084262f224002a50" "06f40980ad23",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "LU with too short TMSI",
+		.compl_l3_msg = "05084262f224002a50" "04f480ad8a",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "CM Service Request with IMSI 123456",
+		.compl_l3_msg = "052401035058a6" "04113254f6",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMSI,
+			.imsi = "123456",
+		},
+	},
+	{
+		.label = "CM Service Request with TMSI 0x5a42e404",
+		.compl_l3_msg = "052401035058a6" "05f45a42e404",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_TMSI,
+			.tmsi = 0x5a42e404,
+		},
+	},
+	{
+		.label = "CM Service Request with shorter CM2, with IMSI 123456",
+		.compl_l3_msg = "052401025058" "04113254f6",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMSI,
+			.imsi = "123456",
+		},
+	},
+	{
+		.label = "CM Service Request with longer CM2, with IMSI 123456",
+		.compl_l3_msg = "052401055058a62342" "04113254f6",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMSI,
+			.imsi = "123456",
+		},
+	},
+	{
+		.label = "CM Service Request with shorter CM2, with TMSI 0x00000000",
+		.compl_l3_msg = "052401025058" "05f400000000",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_TMSI,
+			.tmsi = 0,
+		},
+	},
+	{
+		.label = "CM Service Request with invalid MI type",
+		.compl_l3_msg = "052401035058a6" "089d10070000006402",
+		.expect_rc = -EINVAL,
+	},
+	{
+		.label = "CM Service Request with truncated IMSI MI",
+		.compl_l3_msg = "052401035058a6" "0899100700000064",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "CM Service Request with truncated TMSI MI",
+		.compl_l3_msg = "0524010150" "05f40980ad",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "CM Service Request with odd length TMSI",
+		.compl_l3_msg = "052401045058a623" "05fc0980ad8a",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "CM Service Request with too long TMSI MI",
+		.compl_l3_msg = "052401035058a6" "06f40980ad23",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "CM Service Request with too short TMSI",
+		.compl_l3_msg = "052401035058a6" "04f480ad8a",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "CM Service Reestablish Request with TMSI 0x5a42e404",
+		.compl_l3_msg = "052801035058a6" "05f45a42e404",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_TMSI,
+			.tmsi = 0x5a42e404,
+		},
+	},
+	{
+		.label = "Paging Response with IMSI 1234567",
+		.compl_l3_msg = "06270003505886" "0419325476",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMSI,
+			.imsi = "1234567",
+		},
+	},
+	{
+		.label = "Paging Response with TMSI 0xb48883de",
+		.compl_l3_msg = "06270003505886" "05f4b48883de",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_TMSI,
+			.tmsi = 0xb48883de,
+		},
+	},
+	{
+		.label = "Paging Response with TMSI, with unused nibble not 0xf",
+		.compl_l3_msg = "06270003505886" "0504b48883de",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "Paging Response with too short IMEI (1234567)",
+		.compl_l3_msg = "06270003505886" "041a325476",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "Paging Response with IMEI 123456789012345",
+		.compl_l3_msg = "06270003505886" "081a32547698103254",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMEI,
+			.imei = "123456789012345",
+		},
+	},
+	{
+		.label = "Paging Response with IMEI 12345678901234 (no Luhn checksum)",
+		.compl_l3_msg = "06270003505886" "0812325476981032f4",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMEI,
+			.imei = "12345678901234",
+		},
+	},
+	{
+		.label = "Paging Response with IMEISV 1234567890123456",
+		.compl_l3_msg = "06270003505886" "091332547698103254f6",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMEISV,
+			.imeisv = "1234567890123456",
+		},
+	},
+	{
+		.label = "Paging Response with too short IMEISV 123456789012345",
+		.compl_l3_msg = "06270003505886" "081b32547698103254",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "Paging Response with too long IMEISV 12345678901234567",
+		.compl_l3_msg = "06270003505886" "091b3254769810325476",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "Paging Response with IMSI 123456789012345 and flipped ODD bit",
+		.compl_l3_msg = "06270003505886" "081132547698103254",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "IMSI-Detach with IMSI 901700000004620",
+		.compl_l3_msg = "050130" "089910070000006402",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMSI,
+			.imsi = "901700000004620",
+		},
+	},
+	{
+		.label = "IMSI-Detach with TMSI 0x0980ad8a",
+		.compl_l3_msg = "050130" "05f40980ad8a",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_TMSI,
+			.tmsi = 0x0980ad8a,
+		},
+	},
+	{
+		.label = "IMSI-Detach with invalid MI type",
+		.compl_l3_msg = "050130" "089d10070000006402",
+		.expect_rc = -EINVAL,
+	},
+	{
+		.label = "IMSI-Detach with truncated IMSI MI",
+		.compl_l3_msg = "050130" "0899100700000064",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "IMSI-Detach with too short IMSI MI (12345)",
+		.compl_l3_msg = "050130" "03193254",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "IMSI-Detach with just long enough IMSI MI 123456",
+		.compl_l3_msg = "050130" "04113254f6",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMSI,
+			.imsi = "123456",
+		},
+	},
+	{
+		.label = "IMSI-Detach with max length IMSI MI 123456789012345",
+		.compl_l3_msg = "050130" "081932547698103254",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMSI,
+			.imsi = "123456789012345",
+		},
+	},
+	{
+		.label = "IMSI-Detach with just too long IMSI MI 1234567890123456",
+		.compl_l3_msg = "050130" "091132547698103254f6",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "IMSI-Detach with truncated TMSI MI",
+		.compl_l3_msg = "050130" "05f40980ad",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "IMSI-Detach with odd length TMSI",
+		.compl_l3_msg = "050130" "05fc0980ad8a",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "IMSI-Detach with too long TMSI MI",
+		.compl_l3_msg = "050130" "06f40980ad23",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "IMSI-Detach with too short TMSI",
+		.compl_l3_msg = "050130" "04f480ad8a",
+		.expect_rc = -EBADMSG,
+	},
+	{
+		.label = "Identity Response with IMSI 901700000004620",
+		.compl_l3_msg = "0519" "089910070000006402",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMSI,
+			.imsi = "901700000004620",
+		},
+	},
+	{
+		.label = "Identity Response with IMEI 123456789012345",
+		.compl_l3_msg = "0519" "081a32547698103254",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMEI,
+			.imei = "123456789012345",
+		},
+	},
+	{
+		.label = "Identity Response with IMEISV 9876543210987654",
+		.compl_l3_msg = "0519" "099378563412907856f4",
+		.expect_mi = {
+			.type = GSM_MI_TYPE_IMEISV,
+			.imeisv = "9876543210987654",
+		},
+	},
+};
+
+void test_struct_mobile_identity()
+{
+	struct mobile_identity_tc *t;
+	printf("%s()\n", __func__);
+	for (t = mobile_identity_tests; (t - mobile_identity_tests) < ARRAY_SIZE(mobile_identity_tests); t++) {
+		struct osmo_mobile_identity mi;
+		struct msgb *msg;
+		int rc;
+		memset(&mi, 0xff, sizeof(mi));
+
+		msg = msgb_from_hex(t->label, 1024, t->compl_l3_msg);
+		rc = osmo_mobile_identity_decode_from_l3(&mi, msg, false);
+		msgb_free(msg);
+
+		printf("%s: rc = %d", t->label, rc);
+		if (!rc) {
+			printf(", mi = %s", osmo_mobile_identity_to_str_c(OTC_SELECT, &mi));
+		}
+
+		if (rc == t->expect_rc
+		    && ((rc != 0) || !osmo_mobile_identity_cmp(&mi, &t->expect_mi))) {
+			printf(" ok");
+		} else {
+			printf("  ERROR: Expected rc = %d", t->expect_rc);
+			if (!t->expect_rc)
+				printf(", mi = %s", osmo_mobile_identity_to_str_c(OTC_SELECT, &t->expect_mi));
+		}
+		printf("\n");
+	}
+	printf("\n");
+}
+
 static const struct bcd_number_test {
 	/* Human-readable test name */
 	const char *test_name;
@@ -1182,6 +1552,7 @@
 	test_mid_from_imsi();
 	test_mid_encode_decode();
 	test_mid_decode_zero_length();
+	test_struct_mobile_identity();
 	test_bcd_number_encode_decode();
 	test_ra_cap();
 	test_lai_encode_decode();
diff --git a/tests/gsm0408/gsm0408_test.ok b/tests/gsm0408/gsm0408_test.ok
index d343869..f8de54a 100644
--- a/tests/gsm0408/gsm0408_test.ok
+++ b/tests/gsm0408/gsm0408_test.ok
@@ -17,9 +17,9 @@
   -> MI-str="423423" rc=7
   -> MI-name="IMSI-423423"
 - unknown 0x9 423423
-  -> MI-TLV-hex='1704413224f3'
-  -> MI-str="423423" rc=7
-  -> MI-name="IMSI-423423"
+  -> MI-TLV-hex=''
+  -> MI-str="" rc=1
+  -> MI-name="unknown"
 - IMSI 4234235
   -> MI-TLV-hex='170449322453'
   -> MI-str="4234235" rc=8
@@ -65,15 +65,15 @@
   -> MI-str="3054" rc=9
   -> MI-name="TMSI-0x12345678"
 - NONE 123
-  -> MI-TLV-hex='17021832'
+  -> MI-TLV-hex=''
   -> MI-str="" rc=1
   -> MI-name="unknown"
 - NONE 1234
-  -> MI-TLV-hex='17031032f4'
+  -> MI-TLV-hex=''
   -> MI-str="" rc=1
   -> MI-name="unknown"
 - unknown 0x8 1234
-  -> MI-TLV-hex='17031032f4'
+  -> MI-TLV-hex=''
   -> MI-str="" rc=1
   -> MI-name="unknown"
 
@@ -139,6 +139,57 @@
     rc=1
     returned empty string
 
+test_struct_mobile_identity()
+LU with IMSI 901700000004620: rc = 0, mi = IMSI-901700000004620 ok
+LU with TMSI 0x0980ad8a: rc = 0, mi = TMSI-0x0980AD8A ok
+LU with invalid MI type: rc = -22 ok
+LU with truncated IMSI MI: rc = -74 ok
+LU with too short IMSI MI (12345): rc = -74 ok
+LU with just long enough IMSI MI 123456: rc = 0, mi = IMSI-123456 ok
+LU with max length IMSI MI 123456789012345: rc = 0, mi = IMSI-123456789012345 ok
+LU with just too long IMSI MI 1234567890123456: rc = -74 ok
+LU with truncated TMSI MI: rc = -74 ok
+LU with odd length TMSI: rc = -74 ok
+LU with too long TMSI MI: rc = -74 ok
+LU with too short TMSI: rc = -74 ok
+CM Service Request with IMSI 123456: rc = 0, mi = IMSI-123456 ok
+CM Service Request with TMSI 0x5a42e404: rc = 0, mi = TMSI-0x5A42E404 ok
+CM Service Request with shorter CM2, with IMSI 123456: rc = 0, mi = IMSI-123456 ok
+CM Service Request with longer CM2, with IMSI 123456: rc = 0, mi = IMSI-123456 ok
+CM Service Request with shorter CM2, with TMSI 0x00000000: rc = 0, mi = TMSI-0x00000000 ok
+CM Service Request with invalid MI type: rc = -22 ok
+CM Service Request with truncated IMSI MI: rc = -74 ok
+CM Service Request with truncated TMSI MI: rc = -74 ok
+CM Service Request with odd length TMSI: rc = -74 ok
+CM Service Request with too long TMSI MI: rc = -74 ok
+CM Service Request with too short TMSI: rc = -74 ok
+CM Service Reestablish Request with TMSI 0x5a42e404: rc = 0, mi = TMSI-0x5A42E404 ok
+Paging Response with IMSI 1234567: rc = 0, mi = IMSI-1234567 ok
+Paging Response with TMSI 0xb48883de: rc = 0, mi = TMSI-0xB48883DE ok
+Paging Response with TMSI, with unused nibble not 0xf: rc = -74 ok
+Paging Response with too short IMEI (1234567): rc = -74 ok
+Paging Response with IMEI 123456789012345: rc = 0, mi = IMEI-123456789012345 ok
+Paging Response with IMEI 12345678901234 (no Luhn checksum): rc = 0, mi = IMEI-12345678901234 ok
+Paging Response with IMEISV 1234567890123456: rc = 0, mi = IMEI-SV-1234567890123456 ok
+Paging Response with too short IMEISV 123456789012345: rc = -74 ok
+Paging Response with too long IMEISV 12345678901234567: rc = -74 ok
+Paging Response with IMSI 123456789012345 and flipped ODD bit: rc = -74 ok
+IMSI-Detach with IMSI 901700000004620: rc = 0, mi = IMSI-901700000004620 ok
+IMSI-Detach with TMSI 0x0980ad8a: rc = 0, mi = TMSI-0x0980AD8A ok
+IMSI-Detach with invalid MI type: rc = -22 ok
+IMSI-Detach with truncated IMSI MI: rc = -74 ok
+IMSI-Detach with too short IMSI MI (12345): rc = -74 ok
+IMSI-Detach with just long enough IMSI MI 123456: rc = 0, mi = IMSI-123456 ok
+IMSI-Detach with max length IMSI MI 123456789012345: rc = 0, mi = IMSI-123456789012345 ok
+IMSI-Detach with just too long IMSI MI 1234567890123456: rc = -74 ok
+IMSI-Detach with truncated TMSI MI: rc = -74 ok
+IMSI-Detach with odd length TMSI: rc = -74 ok
+IMSI-Detach with too long TMSI MI: rc = -74 ok
+IMSI-Detach with too short TMSI: rc = -74 ok
+Identity Response with IMSI 901700000004620: rc = 0, mi = IMSI-901700000004620 ok
+Identity Response with IMEI 123456789012345: rc = 0, mi = IMEI-123456789012345 ok
+Identity Response with IMEISV 9876543210987654: rc = 0, mi = IMEI-SV-9876543210987654 ok
+
 BSD number encoding / decoding test
 - Running test: regular 9-digit MSISDN
   - Encoding ASCII (buffer limit=0) '123456789'...
diff --git a/tests/utils/utils_test.c b/tests/utils/utils_test.c
index e87cb22..e15cf5f 100644
--- a/tests/utils/utils_test.c
+++ b/tests/utils/utils_test.c
@@ -487,6 +487,7 @@
 {
 	int i;
 	uint8_t bcd[64];
+	uint8_t bcd2[64];
 	int rc;
 
 	printf("\nTesting bcd to string conversion\n");
@@ -511,6 +512,12 @@
 			printf("    ERROR: expected rc=%d\n", t->expect_rc);
 		if (strcmp(str, t->expect_str))
 			printf("    ERROR: expected result %s\n", osmo_quote_str(t->expect_str, -1));
+
+		memset(bcd2, 0xff, sizeof(bcd2));
+		rc = osmo_str2bcd(bcd2, sizeof(bcd2), str, t->start_nibble, -1, t->allow_hex);
+		printf("osmo_str2bcd(start_nibble=%d) -> rc=%d\n", t->start_nibble, rc);
+		if (rc > 0)
+			printf(" = %s\n", osmo_hexdump(bcd2, rc));
 	}
 
 	printf("- zero output buffer\n");
diff --git a/tests/utils/utils_test.ok b/tests/utils/utils_test.ok
index baa708e..cbab72a 100644
--- a/tests/utils/utils_test.ok
+++ b/tests/utils/utils_test.ok
@@ -181,27 +181,41 @@
 - BCD-input='1a 32 54 76 98 f0' nibbles=[1..11[ str_size=64
   rc=10
   -> "1234567890"
+osmo_str2bcd(start_nibble=1) -> rc=6
+ = 1f 32 54 76 98 f0 
 - BCD-input='1a 32 a4 cb 9d f0' nibbles=[1..11[ str_size=64
   rc=-22
   -> "1234ABCD90"
+osmo_str2bcd(start_nibble=1) -> rc=-22
 - BCD-input='1a 32 a4 cb 9d f0' nibbles=[1..11[ str_size=64
   rc=10
   -> "1234ABCD90"
+osmo_str2bcd(start_nibble=1) -> rc=6
+ = 1f 32 a4 cb 9d f0 
 - BCD-input='1a 32 54 76 98 f0' nibbles=[1..12[ str_size=64
   rc=-22
   -> "1234567890F"
+osmo_str2bcd(start_nibble=1) -> rc=-22
 - BCD-input='1a 32 54 76 98 f0' nibbles=[1..12[ str_size=64
   rc=11
   -> "1234567890F"
+osmo_str2bcd(start_nibble=1) -> rc=6
+ = 1f 32 54 76 98 f0 
 - BCD-input='1a 32 54 76 98 f0' nibbles=[0..12[ str_size=64
   rc=12
   -> "A1234567890F"
+osmo_str2bcd(start_nibble=0) -> rc=6
+ = 1a 32 54 76 98 f0 
 - BCD-input='1a 32 54 76 98 f0' nibbles=[1..12[ str_size=5
   rc=11
   -> "1234"
+osmo_str2bcd(start_nibble=1) -> rc=3
+ = 1f 32 f4 
 - BCD-input='' nibbles=[1..1[ str_size=64
   rc=0
   -> ""
+osmo_str2bcd(start_nibble=1) -> rc=1
+ = ff 
 - zero output buffer
   bcd2str(NULL, ...) -> -12
   bcd2str(dst, 0, ...) -> -12