Merge remote-tracking branch 'GuidoCasati/issue-963' into integration_2026_w27

NAS: refactor Identity Request handler and enforce spec-compliant security checks (#217)

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
Reviewed-by: Robert Schmidt <robert.schmidt@openairinterface.org>
Reviewed-By: Rakesh Mundlamuri <rakesh.mundlamuri@openairinterface.org
This commit is contained in:
Robert Schmidt
2026-07-02 14:54:04 +02:00
4 changed files with 150 additions and 19 deletions

View File

@@ -17,13 +17,19 @@ Key 5GS NAS messages:
## OAI Implementation Status ## 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 | | Type | Message | Encoding | Decoding | Unit test |
|-------|-------------------------------------------|----------|----------|------------| |-------|-------------------------------------------|----------|----------|------------|
| 5GMM | Service Request | yes | yes | yes | | 5GMM | Service Request | yes | yes | yes |
| 5GMM | Service Accept | yes | yes | yes | | 5GMM | Service Accept | yes | yes | yes |
| 5GMM | Service Reject | 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 | Authentication Response | yes | no | no |
| 5GMM | Identity Response | yes | no | no | | 5GMM | Identity Response | yes | no | no |
| 5GMM | Security Mode Complete | 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 Request | yes | no | no |
| 5GSM | PDU Session Establishment Accept | no | yes | 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 ### Code Structure
[openair3/NAS/NR_UE/nr_nas_msg.c](../openair3/NAS/NR_UE/nr_nas_msg.c): [openair3/NAS/NR_UE/nr_nas_msg.c](../openair3/NAS/NR_UE/nr_nas_msg.c):

View File

@@ -612,6 +612,38 @@ static int fill_imeisv(FGSMobileIdentity *mi, const uicc_t *uicc)
return 19; 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) 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}; 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); int size = sizeof(fgmm_msg_header_t);
fgmm_nas_message_plain_t plain = {0}; 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); plain.header = set_mm_header(FGS_IDENTITY_RESPONSE, PLAIN_5GS_MSG);
size += sizeof(plain.header); 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; fgmm_identity_response_msg *mm_msg = &plain.mm_msg.fgs_identity_response;
if (identitytype == FGS_MOBILE_IDENTITY_SUCI) { int identity_size = nas_fill_5gs_mobile_identity(&mm_msg->fgsmobileidentity, nas, identitytype);
size += fill_suci(&mm_msg->fgsmobileidentity, uicc); if (identity_size <= 0)
} return;
size += identity_size;
// encode the message // encode the message
initialNasMsg->nas_data = malloc_or_fail(size * sizeof(*initialNasMsg->nas_data)); 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", "Received IDENTITY REQUEST for identity type: %s\n",
print_info(msg.fgsmobileidentity, fgs_identity_type_text, sizeofArray(fgs_identity_type_text))); 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) { if (mm_header.security_header_type == PLAIN_5GS_MSG && msg.fgsmobileidentity != FGS_MOBILE_IDENTITY_SUCI) {
// see 3GPP TS 24.501 4.4.4.2 // TS 24.501, 4.4.4.2: unprotected Identity Request is processable only when the requested identity is SUCI.
LOG_E(NAS, "Only SUCI mobile identity is expected in a security-unprotected request\n"); LOG_E(NAS, "Drop unprotected Identity Request: SUCI required (TS 24.501 4.4.4.2), got %d\n", msg.fgsmobileidentity);
return; 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) static void generateAuthenticationResp(nr_ue_nas_t *nas, as_nas_info_t *initialNasMsg, uint8_t *buf)

View File

@@ -77,7 +77,23 @@ You can build the tester as follows:
Start the tester with the dedicated configuration file: 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 # Limitations
- The tester is limited to a fixed flow: Initial Attach -> PDU Session -> Deregistration. - The tester is limited to a fixed flow: Initial Attach -> PDU Session -> Deregistration.

View File

@@ -11,8 +11,11 @@
#include "openair2/GNB_APP/gnb_paramdef.h" #include "openair2/GNB_APP/gnb_paramdef.h"
#include "openair3/SCTP/sctp_default_values.h" #include "openair3/SCTP/sctp_default_values.h"
#include "openair3/NAS/NR_UE/nr_nas_msg.h" #include "openair3/NAS/NR_UE/nr_nas_msg.h"
#include "openair3/UTILS/conversions.h"
#include "executables/nr-uesoftmodem.h" #include "executables/nr-uesoftmodem.h"
#include "openair3/ocp-gtpu/gtp_itf.h" #include "openair3/ocp-gtpu/gtp_itf.h"
#include "SIMULATION/TOOLS/sim.h"
#include <stdlib.h>
RAN_CONTEXT_t RC; RAN_CONTEXT_t RC;
THREAD_STRUCT thread_struct; 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; 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 // Emulate registration request to register UE in the AMF
void send_initial_ue_message(instance_t instance) 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).gNB_ue_ngap_id = 1; // Simulated UE ID
NGAP_NAS_FIRST_REQ(msg_p).plmn.mcc = mcc; plmn_id_t plmn = {.mcc = mcc, .mnc = mnc, .mnc_digit_length = mnc_len};
NGAP_NAS_FIRST_REQ(msg_p).plmn.mnc = mnc; NGAP_NAS_FIRST_REQ(msg_p).plmn = plmn;
NGAP_NAS_FIRST_REQ(msg_p).plmn.mnc_digit_length = mnc_len;
NGAP_NAS_FIRST_REQ(msg_p).nr_cell_id = gnb_id; 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) // serving network name (snn)
// Ideally snn should be filled from SIB1, but we operate at NAS/RRC level and we dont decode SIB1 // 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 = calloc(1, sizeof(plmn_id_t));
nr_ue_nas->sn_id->mcc = mcc; *nr_ue_nas->sn_id = plmn;
nr_ue_nas->sn_id->mnc = mnc; if (identity_guti) {
nr_ue_nas->sn_id->mnc_digit_length = mnc_len; 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); 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.buf = initialNasMsg.nas_data;
NGAP_NAS_FIRST_REQ(msg_p).nas_pdu.len = initialNasMsg.length; 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); 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) { if ((uniqCfg = load_configmodule(argc, argv, CONFIG_ENABLECMDLINEONLY)) == NULL) {
exit_fun("[SOFTMODEM] Error, configuration module init failed\n"); exit_fun("[SOFTMODEM] Error, configuration module init failed\n");
} }
config_get(uniqCfg, nas_sim_params, sizeofArray(nas_sim_params), NULL);
logInit(); logInit();
nas_init_nrue(NB_UE_INST); nas_init_nrue(NB_UE_INST);