From 0abc46b708cc705478a294ec6eea1ed44e0785cd Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Tue, 28 Apr 2026 16:33:49 +0200 Subject: [PATCH 1/3] NAS: refactor Identity Request handler and enforce spec-compliant security checks Align UE Identity Request handling with TS 24.501 by checking the NAS security header and rejecting unprotected non-SUCI requests. Also, refactor Identity Response identity selection based on the requested identity types, add to a helper and handle unavailable identities in the UE context. Closes #82 Signed-off-by: Guido Casati --- openair3/NAS/NR_UE/nr_nas_msg.c | 55 +++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/openair3/NAS/NR_UE/nr_nas_msg.c b/openair3/NAS/NR_UE/nr_nas_msg.c index d4eb64d4de..7ef3cbeba0 100644 --- a/openair3/NAS/NR_UE/nr_nas_msg.c +++ b/openair3/NAS/NR_UE/nr_nas_msg.c @@ -612,6 +612,38 @@ static int fill_imeisv(FGSMobileIdentity *mi, const uicc_t *uicc) return 19; } +/** @brief Fill requested 5GS Mobile Identity (TS 24.501, 9.11.3.4) according to identity type. + * + * @param mobile_identity Pointer to the mobile identity structure to fill + * @param nas Pointer to the UE NAS context + * @param identitytype Type of identity to fill + * @return Encoded identity size in bytes, 0 on failure + */ +static int nas_fill_5gs_mobile_identity(FGSMobileIdentity *mobile_identity, const nr_ue_nas_t *nas, uint8_t identitytype) +{ + switch (identitytype) { + case FGS_MOBILE_IDENTITY_SUCI: + return fill_suci(mobile_identity, nas->uicc); + case FGS_MOBILE_IDENTITY_5G_GUTI: + if (!nas->guti) { + LOG_W(NAS, "Cannot build Identity Response with 5G-GUTI: UE has no valid GUTI\n"); + return 0; + } + return fill_guti(mobile_identity, nas->guti); + case FGS_MOBILE_IDENTITY_IMEISV: + return fill_imeisv(mobile_identity, nas->uicc); + case FGS_MOBILE_IDENTITY_5GS_TMSI: + if (!nas->guti) { + LOG_W(NAS, "Cannot build Identity Response with 5G-S-TMSI: UE has no valid GUTI\n"); + return 0; + } + return fill_fgstmsi(&mobile_identity->stmsi, nas->guti); + default: + LOG_W(NAS, "Cannot build Identity Response: requested identity type %d is unsupported\n", identitytype); + return 0; + } +} + void transferRES(uint8_t ck[16], uint8_t ik[16], uint8_t *input, uint8_t rand[16], uint8_t *output, plmn_id_t *plmn_id) { uint8_t S[100] = {0}; @@ -1116,7 +1148,8 @@ void generateServiceRequest(as_nas_info_t *initialNasMsg, nr_ue_nas_t *nas) } } -static void generateIdentityResponse(as_nas_info_t *initialNasMsg, const uint8_t identitytype, uicc_t *uicc) +/** @brief Build Identity Response according to requested identity type (8.2.22 of 3GPP TS 24.501) */ +static void generateIdentityResponse(const nr_ue_nas_t *nas, as_nas_info_t *initialNasMsg, uint8_t identitytype) { int size = sizeof(fgmm_msg_header_t); fgmm_nas_message_plain_t plain = {0}; @@ -1125,11 +1158,13 @@ static void generateIdentityResponse(as_nas_info_t *initialNasMsg, const uint8_t plain.header = set_mm_header(FGS_IDENTITY_RESPONSE, PLAIN_5GS_MSG); size += sizeof(plain.header); - // set identity response + /** Build Identity Response according to requested identity type: if identity + * is unavailable in UE context (e.g., missing 5G-GUTI), return */ fgmm_identity_response_msg *mm_msg = &plain.mm_msg.fgs_identity_response; - if (identitytype == FGS_MOBILE_IDENTITY_SUCI) { - size += fill_suci(&mm_msg->fgsmobileidentity, uicc); - } + int identity_size = nas_fill_5gs_mobile_identity(&mm_msg->fgsmobileidentity, nas, identitytype); + if (identity_size <= 0) + return; + size += identity_size; // encode the message initialNasMsg->nas_data = malloc_or_fail(size * sizeof(*initialNasMsg->nas_data)); @@ -1161,13 +1196,15 @@ static void handle_identity_request(as_nas_info_t *initialNasMsg, nr_ue_nas_t *n "Received IDENTITY REQUEST for identity type: %s\n", print_info(msg.fgsmobileidentity, fgs_identity_type_text, sizeofArray(fgs_identity_type_text))); - if (mm_header.message_type == NAS_SECURITY_UNPROTECTED && msg.fgsmobileidentity != FGS_MOBILE_IDENTITY_SUCI) { - // see 3GPP TS 24.501 4.4.4.2 - LOG_E(NAS, "Only SUCI mobile identity is expected in a security-unprotected request\n"); + if (mm_header.security_header_type == PLAIN_5GS_MSG && msg.fgsmobileidentity != FGS_MOBILE_IDENTITY_SUCI) { + // TS 24.501, 4.4.4.2: unprotected Identity Request is processable only when the requested identity is SUCI. + LOG_E(NAS, "Drop unprotected Identity Request: SUCI required (TS 24.501 4.4.4.2), got %d\n", msg.fgsmobileidentity); return; } - generateIdentityResponse(initialNasMsg, msg.fgsmobileidentity, nas->uicc); + generateIdentityResponse(nas, initialNasMsg, msg.fgsmobileidentity); + if (initialNasMsg->length <= 0) + LOG_W(NAS, "Dropped Identity Response: unable to build Identity Response for identity type %d\n", msg.fgsmobileidentity); } static void generateAuthenticationResp(nr_ue_nas_t *nas, as_nas_info_t *initialNasMsg, uint8_t *buf) From b61d41a440e6c51a63a9d5326da3c546864b653e Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Wed, 1 Jul 2026 17:36:26 +0200 Subject: [PATCH 2/3] tests: add --identity-guti to nr-ue-nas-simulator Add an optional Identity Request test to the NAS/NGAP simulator. With --identity-guti, the Initial UE Message carries a random 5G-GUTI plus matching NGAP 5G-S-TMSI and GUAMI so the AMF requests SUCI per TS 23.502 registration step 6. The simulator then continues the usual attach, PDU session, and deregistration flow. Changes: - nr-ue-nas-simulator: seed_unknown_guti(), identity_request BOOLPARAM, NGAP ue_identity fill in send_initial_ue_message() - Update README Signed-off-by: Guido Casati --- tests/nr-ue-nas-simulator/README.md | 18 ++++- .../nr-ue-nas-simulator/nr-ue-nas-simulator.c | 71 ++++++++++++++++--- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/tests/nr-ue-nas-simulator/README.md b/tests/nr-ue-nas-simulator/README.md index 471472158a..0bf707940e 100644 --- a/tests/nr-ue-nas-simulator/README.md +++ b/tests/nr-ue-nas-simulator/README.md @@ -77,7 +77,23 @@ You can build the tester as follows: Start the tester with the dedicated configuration file: - openairinterface5g/build$ LD_LIBRARY_PATH=. ./tests/nr-ue-nas-simulator/nr-ue-nas-simulator-test -O ../tests/nr-ue-nas-simulator/test.conf + openairinterface5g/build$ ./tests/nr-ue-nas-simulator/nr-ue-nas-simulator-test -O ../tests/nr-ue-nas-simulator/test.conf + +# Testing NAS procedures + +## Registration with unknown 5G-GUTI + +Optional `--identity-guti` or `identity-guti = 1` in the config file: +Registration Request is sent with an unknown 5G-GUTI so the AMF sends Identity +Request (SUCI) per TS 23.502 step 6, the simulator then continues the normal +attach flow. From `cmake_targets/ran_build/build`: + +```bash + ./tests/nr-ue-nas-simulator/nr-ue-nas-simulator-test \ + -O ../tests/nr-ue-nas-simulator/test.conf --identity-guti +``` + +Look for `Unknown-GUTI test:` then `IDENTITY REQUEST` / `IDENTITY RESPONSE` in NAS logs. # Limitations - The tester is limited to a fixed flow: Initial Attach -> PDU Session -> Deregistration. diff --git a/tests/nr-ue-nas-simulator/nr-ue-nas-simulator.c b/tests/nr-ue-nas-simulator/nr-ue-nas-simulator.c index b5ee18e301..2c0e7d0c97 100644 --- a/tests/nr-ue-nas-simulator/nr-ue-nas-simulator.c +++ b/tests/nr-ue-nas-simulator/nr-ue-nas-simulator.c @@ -11,8 +11,11 @@ #include "openair2/GNB_APP/gnb_paramdef.h" #include "openair3/SCTP/sctp_default_values.h" #include "openair3/NAS/NR_UE/nr_nas_msg.h" +#include "openair3/UTILS/conversions.h" #include "executables/nr-uesoftmodem.h" #include "openair3/ocp-gtpu/gtp_itf.h" +#include "SIMULATION/TOOLS/sim.h" +#include RAN_CONTEXT_t RC; THREAD_STRUCT thread_struct; @@ -75,6 +78,46 @@ int nr_rlc_get_available_tx_space(const rnti_t rntiP, const logical_chan_id_t ch configmodule_interface_t *uniqCfg = NULL; +static int identity_guti = 0; + +static paramdef_t nas_sim_params[] = { + BOOLPARAM("identity-guti", + "Registration with random 5G-GUTI to trigger AMF Identity Request (SUCI)\n", + PARAMFLAG_BOOL, + &identity_guti, + 0), +}; + +/** Seed a 5G-GUTI unknown to the AMF (TS 23.502 step 6: AMF should request SUCI) */ +static void seed_unknown_guti(nr_ue_nas_t *nas, const plmn_id_t *plmn) +{ + nas->guti = calloc_or_fail(1, sizeof(*nas->guti)); + Guti5GSMobileIdentity_t *guti = nas->guti; + guti->typeofidentity = FGS_MOBILE_IDENTITY_5G_GUTI; + guti->mccdigit1 = MCC_HUNDREDS(plmn->mcc); + guti->mccdigit2 = MCC_MNC_DECIMAL(plmn->mcc); + guti->mccdigit3 = MCC_MNC_DIGIT(plmn->mcc); + guti->mncdigit1 = MNC_HUNDREDS(plmn->mnc, plmn->mnc_digit_length); + guti->mncdigit2 = MCC_MNC_DECIMAL(plmn->mnc); + guti->mncdigit3 = MCC_MNC_DIGIT(plmn->mnc); + /* OAI CN5G served GUAMI */ + guti->amfregionid = 0x01; + guti->amfsetid = 0x001; + guti->amfpointer = 0x01; + fill_random(&guti->tmsi, sizeof(guti->tmsi)); + LOG_I(NAS, + "Unknown-GUTI test: Registration Request will use 5G-GUTI MCC=%u%u%u MNC=%u%u AMFRI=%u AMFSI=%u AMFPT=%u TMSI=0x%08x\n", + guti->mccdigit1, + guti->mccdigit2, + guti->mccdigit3, + guti->mncdigit1, + guti->mncdigit2, + guti->amfregionid, + guti->amfsetid, + guti->amfpointer, + guti->tmsi); +} + // Emulate registration request to register UE in the AMF void send_initial_ue_message(instance_t instance) { @@ -82,9 +125,8 @@ void send_initial_ue_message(instance_t instance) NGAP_NAS_FIRST_REQ(msg_p).gNB_ue_ngap_id = 1; // Simulated UE ID - NGAP_NAS_FIRST_REQ(msg_p).plmn.mcc = mcc; - NGAP_NAS_FIRST_REQ(msg_p).plmn.mnc = mnc; - NGAP_NAS_FIRST_REQ(msg_p).plmn.mnc_digit_length = mnc_len; + plmn_id_t plmn = {.mcc = mcc, .mnc = mnc, .mnc_digit_length = mnc_len}; + NGAP_NAS_FIRST_REQ(msg_p).plmn = plmn; NGAP_NAS_FIRST_REQ(msg_p).nr_cell_id = gnb_id; @@ -95,16 +137,27 @@ void send_initial_ue_message(instance_t instance) // serving network name (snn) // Ideally snn should be filled from SIB1, but we operate at NAS/RRC level and we dont decode SIB1 nr_ue_nas->sn_id = calloc(1, sizeof(plmn_id_t)); - nr_ue_nas->sn_id->mcc = mcc; - nr_ue_nas->sn_id->mnc = mnc; - nr_ue_nas->sn_id->mnc_digit_length = mnc_len; + *nr_ue_nas->sn_id = plmn; + if (identity_guti) { + seed_unknown_guti(nr_ue_nas, &plmn); + + ngap_ue_identity_t *ue_identity = &NGAP_NAS_FIRST_REQ(msg_p).ue_identity; + const Guti5GSMobileIdentity_t *guti = nr_ue_nas->guti; + + ue_identity->presenceMask = NGAP_UE_IDENTITIES_FiveG_s_tmsi | NGAP_UE_IDENTITIES_guami; + ue_identity->s_tmsi.amf_set_id = guti->amfsetid; + ue_identity->s_tmsi.amf_pointer = guti->amfpointer; + ue_identity->s_tmsi.m_tmsi = guti->tmsi; + ue_identity->guami.plmn = plmn; + ue_identity->guami.amf_region_id = guti->amfregionid; + ue_identity->guami.amf_set_id = guti->amfsetid; + ue_identity->guami.amf_pointer = guti->amfpointer; + } generateRegistrationRequest(&initialNasMsg, nr_ue_nas, false); NGAP_NAS_FIRST_REQ(msg_p).nas_pdu.buf = initialNasMsg.nas_data; NGAP_NAS_FIRST_REQ(msg_p).nas_pdu.len = initialNasMsg.length; - NGAP_NAS_FIRST_REQ(msg_p).ue_identity.presenceMask = 0; - itti_send_msg_to_task(TASK_NGAP, instance, msg_p); } @@ -349,6 +402,8 @@ int main(int argc, char **argv) if ((uniqCfg = load_configmodule(argc, argv, CONFIG_ENABLECMDLINEONLY)) == NULL) { exit_fun("[SOFTMODEM] Error, configuration module init failed\n"); } + config_get(uniqCfg, nas_sim_params, sizeofArray(nas_sim_params), NULL); + logInit(); nas_init_nrue(NB_UE_INST); From 3226e5bb3488646f674dd14ba933cc9b8442e7fd Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Wed, 1 Jul 2026 19:08:21 +0200 Subject: [PATCH 3/3] doc (NAS): update doc/5Gnas.md Signed-off-by: Guido Casati --- doc/5Gnas.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/doc/5Gnas.md b/doc/5Gnas.md index 2f7bfb080f..4d5521403e 100644 --- a/doc/5Gnas.md +++ b/doc/5Gnas.md @@ -17,13 +17,19 @@ Key 5GS NAS messages: ## OAI Implementation Status -The following table lists implemented NAS messages and whether there is an encoder or decoder function, and if a corresponding unit test exists. +The following table lists NAS messages with dedicated `encode_*` / `decode_*` codecs under +`openair3/NAS/NR_UE/5GS/` (`5GMM/MSG`, `5GSM/MSG`). Unit test entries refer to +[`nas_lib_test.c`](../openair3/NAS/NR_UE/5GS/tests/nas_lib_test.c), most are encode/decode round-trips. | Type | Message | Encoding | Decoding | Unit test | |-------|-------------------------------------------|----------|----------|------------| | 5GMM | Service Request | yes | yes | yes | | 5GMM | Service Accept | yes | yes | yes | | 5GMM | Service Reject | yes | yes | yes | +| 5GMM | Authentication Failure | yes | yes | yes | +| 5GMM | Authentication Reject | yes | yes | yes | +| 5GMM | Security Mode Reject | yes | yes | yes | +| 5GMM | Identity Request | no | yes | no | | 5GMM | Authentication Response | yes | no | no | | 5GMM | Identity Response | yes | no | no | | 5GMM | Security Mode Complete | yes | no | no | @@ -38,6 +44,23 @@ The following table lists implemented NAS messages and whether there is an encod | 5GSM | PDU Session Establishment Request | yes | no | no | | 5GSM | PDU Session Establishment Accept | no | yes | no | +### Runtime-handled messages + +These network-originated messages are handled in [`nr_nas_msg.c`](../openair3/NAS/NR_UE/nr_nas_msg.c): + +* Authentication Request +* Security Mode Command +* Downlink NAS Transport +* Deregistration Accept (UE originating) +* Registration Reject +* PDU Session Establishment Reject + +## Integration testing + +End-to-end attach can be tested with the [NR UE NAS simulator](../tests/nr-ue-nas-simulator/README.md), +which runs the OAI UE NAS stack and gNB NGAP against the AMF, forwarding NAS PDUs without PHY/MAC/RLC. +Use `nas_lib_test` for isolated codec round-trips. + ### Code Structure [openair3/NAS/NR_UE/nr_nas_msg.c](../openair3/NAS/NR_UE/nr_nas_msg.c):