From 7b4e5b1390cad9087e5d8f7b8b3344b9e2c677fa Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Wed, 1 Apr 2020 13:13:50 +0200 Subject: [PATCH] VTY: alloc/dealloc/show imsi pseudo Create VTY commands and related database functions to get, allocate and deallocate pseudonymous IMSIs. As this is a proof of concept, the code to retrieve the next pseudonymous IMSI has no performance optimizations and might not use a good entropy source (I did not verify). Related: OS#4476 Change-Id: Ia93ee58d5e03c801eb774b9483a9a4417d031959 --- include/osmocom/hlr/Makefile.am | 1 + include/osmocom/hlr/db.h | 4 + include/osmocom/hlr/imsi_pseudo.h | 19 ++++ include/osmocom/hlr/logging.h | 1 + src/Makefile.am | 1 + src/db.c | 17 ++++ src/db_imsi_pseudo.c | 143 ++++++++++++++++++++++++++++++ src/hlr_vty_subscr.c | 96 ++++++++++++++++++++ src/logging.c | 6 ++ tests/Makefile.am | 11 +++ tests/imsi_pseudo/imsi_pseudo.vty | 42 +++++++++ tests/test_nodes.vty | 1 + tests/test_subscriber.vty | 3 + 13 files changed, 345 insertions(+) create mode 100644 include/osmocom/hlr/imsi_pseudo.h create mode 100644 src/db_imsi_pseudo.c create mode 100644 tests/imsi_pseudo/imsi_pseudo.vty diff --git a/include/osmocom/hlr/Makefile.am b/include/osmocom/hlr/Makefile.am index aceda4a6..876c97fa 100644 --- a/include/osmocom/hlr/Makefile.am +++ b/include/osmocom/hlr/Makefile.am @@ -9,6 +9,7 @@ noinst_HEADERS = \ hlr_ussd.h \ hlr_vty.h \ hlr_vty_subscr.h \ + imsi_pseudo.h \ logging.h \ lu_fsm.h \ mslookup_server.h \ diff --git a/include/osmocom/hlr/db.h b/include/osmocom/hlr/db.h index ca336a0f..abb73573 100644 --- a/include/osmocom/hlr/db.h +++ b/include/osmocom/hlr/db.h @@ -33,6 +33,10 @@ enum stmt_idx { DB_STMT_SET_LAST_LU_SEEN_PS, DB_STMT_EXISTS_BY_IMSI, DB_STMT_EXISTS_BY_MSISDN, + DB_STMT_PSEUDO_BY_ID, + DB_STMT_PSEUDO_INSERT, + DB_STMT_PSEUDO_DELETE, + DB_STMT_PSEUDO_NEXT, _NUM_DB_STMT }; diff --git a/include/osmocom/hlr/imsi_pseudo.h b/include/osmocom/hlr/imsi_pseudo.h new file mode 100644 index 00000000..e1118c39 --- /dev/null +++ b/include/osmocom/hlr/imsi_pseudo.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include + +#define LOGPSEUDO(id, level, fmt, args ...) LOGP(DPSEUDO, level, "subscriber_id='%" PRId64 "': " fmt, id, ## args) + +struct imsi_pseudo_data { + int alloc_count; /* 0: none, 1: only current is allocated, 2: current and previous are allocated */ + int64_t i; /* current imsi_pseudo_i */ + char current[GSM23003_IMSI_MAX_DIGITS+1]; + char previous[GSM23003_IMSI_MAX_DIGITS+1]; +}; + +int db_get_imsi_pseudo_data(struct db_context *dbc, int64_t subscr_id, struct imsi_pseudo_data *data); +int db_alloc_imsi_pseudo(struct db_context *dbc, int64_t subscr_id, const char *imsi_pseudo, int64_t imsi_pseudo_i); +int db_dealloc_imsi_pseudo(struct db_context *dbc, const char *imsi_pseudo); +int db_get_imsi_pseudo_next(struct db_context *dbc, char *imsi_pseudo); diff --git a/include/osmocom/hlr/logging.h b/include/osmocom/hlr/logging.h index a8081af6..5be21a5f 100644 --- a/include/osmocom/hlr/logging.h +++ b/include/osmocom/hlr/logging.h @@ -8,6 +8,7 @@ enum { DGSUP, DAUC, DSS, + DPSEUDO, DMSLOOKUP, DLU, DDGSM, diff --git a/src/Makefile.am b/src/Makefile.am index 09e9101d..49b50f4a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -44,6 +44,7 @@ osmo_hlr_SOURCES = \ db.c \ db_auc.c \ db_hlr.c \ + db_imsi_pseudo.c \ gsup_router.c \ gsup_server.c \ hlr.c \ diff --git a/src/db.c b/src/db.c index 325b63a6..2949d2f3 100644 --- a/src/db.c +++ b/src/db.c @@ -85,6 +85,23 @@ static const char *stmt_sql[] = { [DB_STMT_SET_LAST_LU_SEEN_PS] = "UPDATE subscriber SET last_lu_seen_ps = datetime($val, 'unixepoch') WHERE id = $subscriber_id", [DB_STMT_EXISTS_BY_IMSI] = "SELECT 1 FROM subscriber WHERE imsi = $imsi", [DB_STMT_EXISTS_BY_MSISDN] = "SELECT 1 FROM subscriber WHERE msisdn = $msisdn", + [DB_STMT_PSEUDO_BY_ID] = + "SELECT imsi_pseudo, imsi_pseudo_i" + " FROM subscriber_imsi_pseudo" + " WHERE subscriber_id = $subscriber_id" + " ORDER BY imsi_pseudo_i DESC", + [DB_STMT_PSEUDO_INSERT] = + "INSERT INTO subscriber_imsi_pseudo (subscriber_id, imsi_pseudo, imsi_pseudo_i)" + " VALUES($subscriber_id, $imsi_pseudo, $imsi_pseudo_i)", + [DB_STMT_PSEUDO_DELETE] = + "DELETE FROM subscriber_imsi_pseudo WHERE imsi_pseudo = $imsi_pseudo", + [DB_STMT_PSEUDO_NEXT] = /* Proof of concept! Verify performance and entropy usage before using in real world! */ + "SELECT imsi" + " FROM subscriber" + " LEFT JOIN subscriber_imsi_pseudo ON imsi = imsi_pseudo" + " WHERE imsi_pseudo IS NULL" + " ORDER BY RANDOM()" + " LIMIT 1", }; static void sql3_error_log_cb(void *arg, int err_code, const char *msg) diff --git a/src/db_imsi_pseudo.c b/src/db_imsi_pseudo.c new file mode 100644 index 00000000..d1c09ab4 --- /dev/null +++ b/src/db_imsi_pseudo.c @@ -0,0 +1,143 @@ +/* (C) 2020 by sysmocom s.f.m.c. GmbH + * + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +#include +#include + +#include + +#include +#include +#include + +int db_get_imsi_pseudo_data(struct db_context *dbc, int64_t subscr_id, struct imsi_pseudo_data *data) +{ + sqlite3_stmt *stmt = dbc->stmt[DB_STMT_PSEUDO_BY_ID]; + int i, rc, ret = 0; + + memset(data, 0, sizeof(*data)); + + if (!db_bind_int64(stmt, "$subscriber_id", subscr_id)) + return -EIO; + + /* Retrieve up to two allocated pseudo IMSIs in three sqlite3 steps */ + for (i = 0; i < 3; i++) { + rc = sqlite3_step(stmt); + switch (rc) { + case SQLITE_ROW: + data->alloc_count = i + 1; + switch (i) { + case 0: + /* First entry is always current (ORDER BY in SQL statement) */ + copy_sqlite3_text_to_buf(data->current, stmt, 0); + data->i = sqlite3_column_int(stmt, 1); + break; + case 1: + copy_sqlite3_text_to_buf(data->previous, stmt, 0); + break; + case 2: + LOGPSEUDO(subscr_id, LOGL_ERROR, "more than two pseudonymous IMSI allocated\n"); + ret = -EINVAL; + goto out; + } + break; + case SQLITE_DONE: + goto out; + default: + LOGPSEUDO(subscr_id, LOGL_ERROR, "error executing SQL: %d\n", rc); + ret = -EIO; + goto out; + } + } +out: + db_remove_reset(stmt); + return ret; +} + +int db_alloc_imsi_pseudo(struct db_context *dbc, int64_t subscr_id, const char *imsi_pseudo, int64_t imsi_pseudo_i) +{ + sqlite3_stmt *stmt = dbc->stmt[DB_STMT_PSEUDO_INSERT]; + int rc, ret = 0; + + if (!db_bind_int64(stmt, "$subscriber_id", subscr_id)) + return -EIO; + if (!db_bind_text(stmt, "$imsi_pseudo", imsi_pseudo)) + return -EIO; + if (!db_bind_int64(stmt, "$imsi_pseudo_i", imsi_pseudo_i)) + return -EIO; + + rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) { + LOGPSEUDO(subscr_id, LOGL_ERROR, "imsi_pseudo='%s', imsi_pseudo_i='%" PRId64 "': SQL error during" + " allocate: %d\n", imsi_pseudo, imsi_pseudo_i, rc); + ret = -EIO; + } + db_remove_reset(stmt); + return ret; +} + +int db_dealloc_imsi_pseudo(struct db_context *dbc, const char *imsi_pseudo) +{ + sqlite3_stmt *stmt = dbc->stmt[DB_STMT_PSEUDO_DELETE]; + int rc, ret = 0; + + if (!db_bind_text(stmt, "$imsi_pseudo", imsi_pseudo)) + return -EIO; + + rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) { + LOGP(DPSEUDO, LOGL_ERROR, "imsi_pseudo='%s': SQL error during deallocate: %d\n", imsi_pseudo, rc); + ret = -EIO; + } + + db_remove_reset(stmt); + return ret; +} + +/*! Get the next random free pseudo IMSI. + * \param[in] dbc database context. + * \param[out] imsi_pseudo buffer with length GSM23003_IMSI_MAX_DIGITS+1. + * \returns 0: success, -1: no next IMSI available, -2: SQL error. */ +int db_get_imsi_pseudo_next(struct db_context *dbc, char *imsi_pseudo) +{ + sqlite3_stmt *stmt = dbc->stmt[DB_STMT_PSEUDO_NEXT]; + const char *imsi; + int rc, ret = 0; + + rc = sqlite3_step(stmt); + switch (rc) { + case SQLITE_ROW: + /* Can't use copy_sqlite3_text_to_buf, as it assumes the wrong size for imsi_pseudo */ + imsi = (const char *)sqlite3_column_text(stmt, 0); + osmo_strlcpy(imsi_pseudo, imsi, GSM23003_IMSI_MAX_DIGITS+1); + break; + case SQLITE_DONE: + LOGP(DPSEUDO, LOGL_ERROR, "failed to get next pseudonymous IMSI: all IMSIs are already allocated as" + " pseudo IMSI\n"); + ret = -1; + break; + default: + LOGP(DPSEUDO, LOGL_ERROR, "failed to get next pseudonymous IMSI, SQL error: %d\n", rc); + ret = -2; + break; + } + + db_remove_reset(stmt); + return ret; +} diff --git a/src/hlr_vty_subscr.c b/src/hlr_vty_subscr.c index a9262bab..58094fb1 100644 --- a/src/hlr_vty_subscr.c +++ b/src/hlr_vty_subscr.c @@ -31,6 +31,7 @@ #include #include #include +#include struct vty; @@ -69,6 +70,18 @@ static void dump_last_lu_seen(struct vty *vty, const char *domain_label, time_t } } +static void dump_imsi_pseudo(struct vty *vty, struct hlr_subscriber *subscr) +{ + struct imsi_pseudo_data data; + + if (db_get_imsi_pseudo_data(g_hlr->dbc, subscr->id, &data) != 0) + return; + if (data.alloc_count >= 1) + vty_out(vty, " Pseudonymous IMSI (current): %s, i: %" PRId64 "%s", data.current, data.i, VTY_NEWLINE); + if (data.alloc_count == 2) + vty_out(vty, " Pseudonymous IMSI (previous): %s%s", data.previous, VTY_NEWLINE); +} + static void subscr_dump_full_vty(struct vty *vty, struct hlr_subscriber *subscr) { int rc; @@ -78,6 +91,7 @@ static void subscr_dump_full_vty(struct vty *vty, struct hlr_subscriber *subscr) vty_out(vty, " ID: %"PRIu64"%s", subscr->id, VTY_NEWLINE); vty_out(vty, " IMSI: %s%s", *subscr->imsi ? subscr->imsi : "none", VTY_NEWLINE); + dump_imsi_pseudo(vty, subscr); vty_out(vty, " MSISDN: %s%s", *subscr->msisdn ? subscr->msisdn : "none", VTY_NEWLINE); if (*subscr->imei) { @@ -203,6 +217,7 @@ static int get_subscr_by_argv(struct vty *vty, const char *type, const char *id, #define SUBSCR_UPDATE SUBSCR "update " #define SUBSCR_UPDATE_HELP SUBSCR_HELP "Set or update subscriber data\n" #define SUBSCR_MSISDN_HELP "Set MSISDN (phone number) of the subscriber\n" +#define SUBSCR_IMSI_PSEUDO_HELP "Allocate or deallocate pseudonymous IMSI of the subscriber\n" DEFUN(subscriber_show, subscriber_show_cmd, @@ -625,6 +640,85 @@ DEFUN(subscriber_nam, return CMD_SUCCESS; } +DEFUN(subscriber_imsi_pseudo_alloc, + subscriber_imsi_pseudo_alloc_cmd, + SUBSCR_UPDATE "imsi-pseudo alloc", + SUBSCR_UPDATE_HELP + SUBSCR_IMSI_PSEUDO_HELP + "Allocate a new pseudonymous IMSI (max. 2)\n") +{ + struct hlr_subscriber subscr; + struct imsi_pseudo_data pseudo; + const char *id_type = argv[0]; + const char *id = argv[1]; + char imsi_pseudo[GSM23003_IMSI_MAX_DIGITS+1]; + int rc; + + if (get_subscr_by_argv(vty, id_type, id, &subscr)) + return CMD_WARNING; + + if (db_get_imsi_pseudo_data(g_hlr->dbc, subscr.id, &pseudo) != 0) + return CMD_WARNING; + + if (pseudo.alloc_count == 2) { + vty_out(vty, "%% Error: subscriber already has two pseudonymous IMSI allocated%s", VTY_NEWLINE); + return CMD_WARNING; + } + + rc = db_get_imsi_pseudo_next(g_hlr->dbc, imsi_pseudo); + switch (rc) { + case -1: + vty_out(vty, "%% Error: all IMSIs are already allocated as pseudonymous IMSI%s", VTY_NEWLINE); + return CMD_WARNING; + case -2: + vty_out(vty, "%% Error: failed to get next pseudonymous IMSI (SQL error)%s", VTY_NEWLINE); + return CMD_WARNING; + } + + if (db_alloc_imsi_pseudo(g_hlr->dbc, subscr.id, imsi_pseudo, pseudo.i + 1) != 0) { + vty_out(vty, "%% Error: failed to allocate pseudonymous IMSI '%s'%s", imsi_pseudo, VTY_NEWLINE); + return CMD_WARNING; + } + + vty_out(vty, "%% New pseudonymous IMSI allocated: %s%s", imsi_pseudo, VTY_NEWLINE); + return CMD_SUCCESS; +} + +DEFUN(subscriber_imsi_pseudo_dealloc, + subscriber_imsi_pseudo_dealloc_cmd, + SUBSCR_UPDATE "imsi-pseudo dealloc IMSI_PSEUDO", + SUBSCR_UPDATE_HELP + SUBSCR_IMSI_PSEUDO_HELP + "Deallocate a pseudonymous IMSI\n" + "Pseudonymous IMSI to deallocate\n") +{ + struct hlr_subscriber subscr; + struct imsi_pseudo_data pseudo; + const char *id_type = argv[0]; + const char *id = argv[1]; + const char *imsi_pseudo = argv[2]; + + if (get_subscr_by_argv(vty, id_type, id, &subscr)) + return CMD_WARNING; + + if (db_get_imsi_pseudo_data(g_hlr->dbc, subscr.id, &pseudo) != 0) + return CMD_WARNING; + + if ((pseudo.alloc_count >= 1 && strcmp(imsi_pseudo, pseudo.current) == 0) || + (pseudo.alloc_count == 2 && strcmp(imsi_pseudo, pseudo.previous) == 0)) { + /* Pseudonymous IMSI is allocated for given subscriber */ + if (db_dealloc_imsi_pseudo(g_hlr->dbc, imsi_pseudo) != 0) { + vty_out(vty, "Failed to deallocate pseudonymous IMSI%s", VTY_NEWLINE); + return CMD_WARNING; + } + } else { + vty_out(vty, "%% Error: pseudonymous IMSI '%s' is not allocated to given subscriber%s", imsi_pseudo, + VTY_NEWLINE); + return CMD_WARNING; + } + + return CMD_SUCCESS; +} void hlr_vty_subscriber_init(void) { @@ -639,4 +733,6 @@ void hlr_vty_subscriber_init(void) install_element(ENABLE_NODE, &subscriber_aud3g_cmd); install_element(ENABLE_NODE, &subscriber_imei_cmd); install_element(ENABLE_NODE, &subscriber_nam_cmd); + install_element(ENABLE_NODE, &subscriber_imsi_pseudo_alloc_cmd); + install_element(ENABLE_NODE, &subscriber_imsi_pseudo_dealloc_cmd); } diff --git a/src/logging.c b/src/logging.c index eab0510c..6302ba2d 100644 --- a/src/logging.c +++ b/src/logging.c @@ -25,6 +25,12 @@ const struct log_info_cat hlr_log_info_cat[] = { .color = "\033[1;34m", .enabled = 1, .loglevel = LOGL_NOTICE, }, + [DPSEUDO] = { + .name = "DPSEUDO", + .description = "IMSI Pseudonymization", + .color = "\033[1;36m", + .enabled = 1, .loglevel = LOGL_NOTICE, + }, [DMSLOOKUP] = { .name = "DMSLOOKUP", .description = "Mobile Subscriber Lookup", diff --git a/tests/Makefile.am b/tests/Makefile.am index 9015494b..e3588ba2 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -45,6 +45,7 @@ if ENABLE_EXT_TESTS python-tests: # don't run vty and ctrl tests concurrently so that the ports don't conflict $(MAKE) vty-test + $(MAKE) vty-test-imsi-pseudo $(MAKE) ctrl-test $(MAKE) db-upgrade-equivalence-test else @@ -68,6 +69,16 @@ vty-test: $(U) $(srcdir)/$(VTY_TEST) -rm -f $(VTY_TEST_DB) $(VTY_TEST_DB)-* +# IMSI pseudon VTY tests: don't share the DB with other VTY tests, so we can have deterministic "random" pseudo IMSIs +vty-test-imsi-pseudo: + -rm -f $(VTY_TEST_DB) + sqlite3 $(VTY_TEST_DB) < $(top_srcdir)/sql/hlr.sql + osmo_verify_transcript_vty.py -v \ + -n OsmoHLR -p 4258 \ + -r "$(top_builddir)/src/osmo-hlr -c $(top_srcdir)/doc/examples/osmo-hlr.cfg -l $(VTY_TEST_DB)" \ + $(U) $(srcdir)/imsi_pseudo/imsi_pseudo.vty + -rm -f $(VTY_TEST_DB) $(VTY_TEST_DB)-* + CTRL_TEST_DB = hlr_ctrl_test.db # To update the CTRL script from current application behavior, diff --git a/tests/imsi_pseudo/imsi_pseudo.vty b/tests/imsi_pseudo/imsi_pseudo.vty new file mode 100644 index 00000000..4709edbd --- /dev/null +++ b/tests/imsi_pseudo/imsi_pseudo.vty @@ -0,0 +1,42 @@ +OsmoHLR> enable +OsmoHLR# subscriber imsi 111111111111111 create +... +OsmoHLR# subscriber id 1 update imsi-pseudo alloc +% New pseudonymous IMSI allocated: 111111111111111 +OsmoHLR# subscriber imsi 222222222222222 create +... +OsmoHLR# subscriber id 1 update imsi-pseudo alloc +% New pseudonymous IMSI allocated: 222222222222222 +OsmoHLR# subscriber id 1 update imsi-pseudo dealloc 111111111111111 +OsmoHLR# subscriber id 2 update imsi-pseudo alloc +% New pseudonymous IMSI allocated: 111111111111111 +OsmoHLR# subscriber imsi 333333333333333 create +... +OsmoHLR# subscriber id 1 update imsi-pseudo alloc +% New pseudonymous IMSI allocated: 333333333333333 +OsmoHLR# subscriber id 1 show + ID: 1 + IMSI: 111111111111111 + Pseudonymous IMSI (current): 333333333333333, i: 3 + Pseudonymous IMSI (previous): 222222222222222 + MSISDN: none +OsmoHLR# subscriber id 2 show + ID: 2 + IMSI: 222222222222222 + Pseudonymous IMSI (current): 111111111111111, i: 1 + MSISDN: none +OsmoHLR# subscriber id 3 show + ID: 3 + IMSI: 333333333333333 + MSISDN: none + +OsmoHLR# subscriber id 3 update imsi-pseudo alloc +% Error: all IMSIs are already allocated as pseudonymous IMSI + +OsmoHLR# subscriber id 1 update imsi-pseudo dealloc 123 +% Error: pseudonymous IMSI '123' is not allocated to given subscriber + +OsmoHLR# subscriber imsi 444444444444444 create +... +OsmoHLR# subscriber id 1 update imsi-pseudo alloc +% Error: subscriber already has two pseudonymous IMSI allocated diff --git a/tests/test_nodes.vty b/tests/test_nodes.vty index 4995f85d..7507c8f2 100644 --- a/tests/test_nodes.vty +++ b/tests/test_nodes.vty @@ -93,6 +93,7 @@ log stderr logging level db notice logging level auc notice logging level ss info + logging level pseudo notice logging level mslookup notice logging level lu notice logging level dgsm notice diff --git a/tests/test_subscriber.vty b/tests/test_subscriber.vty index fb5da0e8..a22641ea 100644 --- a/tests/test_subscriber.vty +++ b/tests/test_subscriber.vty @@ -13,6 +13,8 @@ OsmoHLR# list subscriber (imsi|msisdn|id|imei) IDENT update aud3g milenage k K (op|opc) OP_C [ind-bitlen] [<0-28>] subscriber (imsi|msisdn|id|imei) IDENT update imei (none|IMEI) subscriber (imsi|msisdn|id|imei) IDENT update network-access-mode (none|cs|ps|cs+ps) + subscriber (imsi|msisdn|id|imei) IDENT update imsi-pseudo alloc + subscriber (imsi|msisdn|id|imei) IDENT update imsi-pseudo dealloc IMSI_PSEUDO show mslookup services OsmoHLR# subscriber? @@ -132,6 +134,7 @@ OsmoHLR# subscriber imsi 123456789023000 update ? aud3g Set UMTS authentication data (3G, and 2G with UMTS AKA) imei Set IMEI of the subscriber (normally populated from MSC, no need to set this manually) network-access-mode Set Network Access Mode (NAM) of the subscriber + imsi-pseudo Allocate or deallocate pseudonymous IMSI of the subscriber OsmoHLR# subscriber imsi 123456789023000 update msisdn ? none Remove MSISDN (phone number)