Introduction
The development of mobile networks has been intrinsically linked to the evolution of user security. In previous generation architectures (2G, 3G, and 4G), subscriber identity management relied on the transmission of the IMSI (International Mobile Subscriber Identity) in plain text during certain phases of the registration protocol. This design, while functional for network interconnection, created a cryptographic vulnerability of a physical nature. The exposure of the IMSI allows the execution of passive capture attacks, commonly known as IMSI catchers. These devices operate as fake base stations, whose objective is not to decrypt encrypted traffic, but simply to intercept the identity in clear text, which enables tracking and data correlation over time and space.
The 5G platform was conceived as an architectural response to this problem. The design incorporates SUCI (Subscription Concealed Identifier) to mitigate the risk of passive identification. However, the transition from a protocol specification to an effective operational functionality is a process of great complexity, where security is not a single attribute, but an emergent function of the interaction of multiple modules: the USIM card, the terminal (UE/ME), the core network (AMF, UDM), and the radio interface.
This analysis focuses on describing the technical axioms and verification methodologies that determine if the privacy of the SUPI (Subscription Permanent Identifier) is achieved in an operational environment. The mechanisms of the card, the logic of the concealment decision, and the auditing methods that contrast the declared capability with the real behavior of the registration message will be addressed.
Fundamentals of Protocol Security
The SUPI is the unique and permanent subscriber identifier, composed of the network code and the MSIN, that is, it is equivalent to the IMSI of 2G/3G/4G networks. Its function is the anchoring of the identity in the system.
SUCI is the implementation of the concealment function. The key to this process lies in the use of public-key cryptography, specifically through the ECIES (Elliptic Curve Integrated Encryption Scheme) scheme. The architecture establishes that the MSIN must be encrypted with the operator’s public key. This configuration ensures that, even if the identity package travels over the air, decoding is unfeasible for any external entity, limiting the identification capability only to the legitimate core network.
The 3GPP specification defines different protection schemes for this transformation:
- Null Scheme (ID 0): Represents the absence of encryption. The MSIN is transmitted unencrypted, maintaining exposure.
- Profile A (ID 1) and Profile B (ID 2): These schemes implement concealment using ECIES. The success of privacy is based on the MSIN being encrypted before leaving the terminal, fulfilling the objective of mitigating IMSI catchers.
It is imperative to establish that the identity field can carry the SUCI format (indicated by Type of identity, but if the field Protection scheme Id indicates null scheme (0), the identity is considered completely exposed to passive observation. This implies that the mere addition of the SUCI container does not automatically confer privacy; protection is materialized in the execution of a non-null cryptographic scheme.
Process Management
The management of the concealment process is a software function that resides on the USIM card and is governed by the services table (EF.UST).
Functionality of the services table
The EF.UST is a functionality register that acts as a set of switches. For SUCI support, the key services are:
- Service 124 (Subscription identifier privacy support): This service is the primary decisional axiom. Its activation is the minimum requirement to declare privacy support. If this bit is disabled, the terminal operates under the presumption that concealment is not required, which leads to
null schemeas an operational result. - Service 125 (SUCI calculation by the USIM): This service defines the responsibility for the calculation. If it is active, the USIM is responsible for executing the cryptographic concealment process. If it is inactive, but service 124 is active, the responsibility for calculating the SUCI falls to the Modem/Terminal (ME).
Application Dependency
SUCI support requires more than just switch 124. The cryptographic capability must be provisioned. The file EF.SUCI_Calc_Info contains the information matrix necessary for this process: the list of available protection schemes (with their ID and priority), and the identifications of the public keys of the home network.
The protocol logic establishes that privacy is effective only if, and only if, service 124 is active, if EF.SUCI_Calc_Info contains non-null schemas, and if the corresponding network public key is available for use. If these elements are present, the terminal is instructed to encrypt; if only the files are present but the switch 124 is inactive, the concealment mandate is null. Switch 125 requires that the USIM card has the programmed functionality to generate the identifiers via software.
Audit of the Implementations
To validate the implementation of SUCI, a dual methodology is employed that compares the theoretical capacity (what the card declares it can do) with the real behavior (what the card does on the network).
Capability verification through file reading
Capacity inspection is performed using standard UICC communication protocols (ISO/IEC 7816), utilizing interface emulation tools, such as the library pySim. This approach allows for precise reading and decoding of the card’s file system records.
The Python application check_suci.py is designed to interact with the USIM using APDU commands. Its function is not only to read data, but to apply the file interpretation logic, such as the decoding of the EF.UST.
When running the script with a reader device, a formal verdict on the card’s capacity is obtained. It supports the standard options of pySim, it is important to mention that if the SIM card has a PIN code, it must be specified with the argument --pin. The script’s output, by showing the status of services 124 and 125, quantifies the privacy capacity.
The Python tool only requires the library pySim, as a virtual environment is created and installed.
$ python3 -m venv suci_env
$ source suci_env/bin/activate
$ git clone https://github.com/osmocom/pysim
$ pip install .
The tool can be executed below; this would be an execution example for a SIM card that does not support the SUCI functionality.
$ python ../check_suci.py -p 0
================================================================
SUCI Support on the (U)SIM
================================================================
EF.UST - USIM Service Table:
[✗] Service 122 5GS Mobility Management Information
[✗] Service 123 5G Security Parameters
[✗] Service 124 Subscription identifier privacy support
[✗] Service 125 SUCI calculation by the USIM
DF.5GS / EF.SUCI_Calc_Info:
[✗] DF.5GS (5FC0) present
[✗] EF.SUCI_Calc_Info (4F07) present
----------------------------------------------------------------
Verdict : NO_SUCI_SUPPORT
SUPI Privacy: NO
Summary : The USIM does NOT declare identifier privacy support (service 124 inactive): SUCI is not supported. The SUPI/IMSI may be sent unencrypted.
----------------------------------------------------------------
And in this execution, the SUCI functionality is supported, but the calculation is performed through the use of the mobile device (service 125 disabled).
================================================================
SUCI Support on the (U)SIM
================================================================
EF.UST - USIM Service Table:
[✓] Service 122 5GS Mobility Management Information
[✓] Service 123 5G Security Parameters
[✓] Service 124 Subscription identifier privacy support
[✗] Service 125 SUCI calculation by the USIM
DF.5GS / EF.SUCI_Calc_Info:
[✓] DF.5GS (5FC0) present
[✓] EF.SUCI_Calc_Info (4F07) present
Protection schemes (by priority):
- priority 0: Profile B - ECIES with secp256r1 (key_index=1)
Home network public keys:
- id 2: 65 bytes
----------------------------------------------------------------
Verdict : SUCI_ME_PROTECTED
SUPI Privacy: YES
Summary : The USIM supports SUCI with EFFECTIVE SUPI concealment (non-null scheme + network public key). SUCI calculation is handled by: ME (terminal).
----------------------------------------------------------------
Operational verification through a signaling capture
The definitive test is traffic observation. The process requires a controlled testing environment using an Android device with permissions to access diagnostic mode or a 5G modem, using the tool SCAT:
- Isolation: The radio is deactivated (AT command
AT+CFUN=0or activation of airplane mode) for deterministic state control. - Capture: The tool is initiated to dump the logs of the air interface during the registration phase (
scat -t qc -s /dev/ttyUSB0 -F capture.pcap). - Activation: The radio is reactivated (AT command
AT+CFUN=0or airplane mode deactivation), forcing the execution of the registration procedure. - Analysis: The NAS messages are examined, specifically
Registration Request. The decoding of the field5GS mobile identityreveals theProtection scheme Idused by the terminal.
The result of this capture is the most reliable operational data. If the logging process is completed, the trace analysis provides the final evidence.
5GS mobile identity
Length: 13
0... .... = Spare: 0
.000 .... = SUPI format: IMSI (0)
.... 0... = Spare: 0
.... .001 = Type of identity: SUCI (1)
Mobile Country Code (MCC): Private network (...)
Mobile Network Code (MNC): Unknown (...)
Routing indicator: 0
.... 0000 = Protection scheme Id: NULL scheme (0)
Home network public key identifier: 0
MSIN: <MSIN numérico>
Conclusion
The analysis of identifier privacy in 5G demonstrates that security is not an inherent function of the SUCI name, but rather the consequence of the successful convergence of protocol axioms, from integration into the USIM card to support in the 5G network itself.
Source code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
check_suci.py - Checks if a (U)SIM supports SUCI in 5G.
Criteria (3GPP TS 31.102 / TS 33.501), with NUANCED logic:
* EF.UST (6F38) - USIM Service Table:
- Service 124 "Subscription identifier privacy support"
-> MINIMUM indicator of SUCI support (SUPI concealment).
- Service 125 "SUCI calculation by the USIM"
-> if active, the USIM calculates the SUCI;
if NOT active (but 124 is), the ME (the terminal) calculates it.
Therefore, 124 AND 125 are NOT required for "SUCI support".
* DF.5GS (5FC0) / EF.SUCI_Calc_Info (4F07):
- Presence of the file + protection schemes and public keys.
- Distinguishes "effective privacy" (there exists a non-null scheme
-> Profile A/ECIES-Curve25519 or Profile B/ECIES-secp256r1 with key)
from the case where only the null scheme is present (the SUPI is not concealed).
Communication with the card via pySim (osmocom). Requires a compatible reader.
Note: by default, pySim's native output (the tracing of T=0 TPDU of
the T0: case #N TPDU: ... => ... SW protocol, which pySim activates
upon import by setting its logging to DEBUG) is SILENCED. It is only shown
when passing --debug.
Examples:
python3 check_suci.py --pcsc-device 0
python3 check_suci.py --pcsc-device 0 --pin 1234
python3 check_suci.py --pcsc-device 0 --json
python3 check_suci.py --pcsc-device 0 --debug # shows pySim trace
(--pcsc-device, --device/-d for series, --modem-device, etc. are provided by pySim.)
"""
import argparse
import json
import logging
import sys
from pySim.transport import init_reader, argparse_add_reader_args
from pySim.transport import NoCardError, ReaderError
from pySim.commands import SimCardCommands
from pySim.exceptions import SwMatchError
from pySim.ts_31_102 import EF_UST, EF_SUCI_Calc_Info
# pySim, upon import, executes logging.root.setLevel(DEBUG) and attaches a
# handler that prints every TPDU ("T0: case #N TPDU: ..."). We silence this
# native output by default; with --debug it is reactivated (see main()).
logging.getLogger().setLevel(logging.WARNING)
# ----------------------------------------------------------------------------
# File Constants (TS 31.102 / TS 102 221)
# ----------------------------------------------------------------------------
USIM_AID_PREFIX = "a0000000871002" # RID + USIM application code
EF_DIR_FID = "2f00"
EF_UST_FID = "6f38"
DF_5GS_FID = "5fc0"
EF_SUCI_FID = "4f07"
# Protection scheme identifiers (TS 24.501)
SCHEME_NAMES = {
0: "null scheme (SUPI unencrypted)",
1: "Profile A - ECIES with Curve25519 (X25519)",
2: "Profile B - ECIES with secp256r1",
}
# ----------------------------------------------------------------------------
# Card Layer
# ----------------------------------------------------------------------------
def open_card(opts):
"""Initializes the reader and leaves the USIM ready (CLA/sel_ctrl of UICC)."""
tp = init_reader(opts)
tp.connect()
tp.reset_card()
scc = SimCardCommands(tp)
# By default SimCardCommands uses 2G values (cla=a0, sel=0000).
# For a UICC/USIM, we must use class 00 and SELECT by FID with FCP.
scc.cla_byte = "00"
scc.sel_ctrl = "0004"
return tp, scc
def _extract_aid_from_dir_record(rec_hex):
"""Extracts the AID (tag 4F) from an EF.DIR record (application template, tag 61)."""
try:
b = bytes.fromhex(rec_hex)
except ValueError:
return None
i = 0
while i + 1 < len(b):
tag = b[i]
if tag in (0x00, 0xFF): # padding
i += 1
continue
length = b[i + 1]
value = b[i + 2:i + 2 + length]
if tag == 0x61: # Application Template
j = 0
while j + 1 < len(value):
t2 = value[j]
l2 = value[j + 1]
v2 = value[j + 2:j + 2 + l2]
if t2 == 0x4F: # AID
return v2.hex()
j += 2 + l2
i += 2 + length
return None
def _fcp_file_size(fcp_hex):
"""Extracts the body size of a transparent EF (tag 80 inside the
FCP 62 template), tolerating padding bytes before tag 62. Returns None if
it cannot be determined. Does not use the strict pySim parser."""
try:
b = bytes.fromhex(fcp_hex or "")
except (ValueError, TypeError):
return None
i = 0
while i < len(b) and b[i] != 0x62: # skip padding (00/FF...) until tag 62
i += 1
if i + 1 >= len(b):
return None
ln = b[i + 1]
j = i + 2
if ln == 0x81: # length in BER format of 2+ bytes
ln = b[j]; j += 1
elif ln == 0x82:
ln = (b[j] << 8) | b[j + 1]; j += 2
template = b[j:j + ln]
k = 0
while k + 1 < len(template): # traverse TLVs looking for 80 (File size)
tag = template[k]
l = template[k + 1]
val = template[k + 2:k + 2 + l]
if tag == 0x80 and val:
return int.from_bytes(val, "big")
k += 2 + l
return None
def _read_binary_raw(scc):
"""READ BINARY from offset 0 without relying on FCP. Reads in blocks up to
256 bytes and handles the T=0 '6CXX' case (card indicates length)."""
out = ""
offset = 0
while offset <= 0xFFFF:
# Le=00 -> 256
data, sw = scc.send_apdu(scc.cla_byte + "b0%04x00" % offset)
sw = sw.lower()
if sw.startswith("6c"):
# Card specifies length (n)
n = int(sw[2:4], 16)
data, sw = scc.send_apdu(scc.cla_byte + "b0%04x%02x" % (offset, n))
sw = sw.lower()
if sw != "9000" or not data:
break
out += data
nbytes = len(data) // 2
if nbytes < 256: # last block
break
offset += nbytes
return out or None
def read_transparent_ef(scc, fid):
"""Selects a transparent EF and returns its content (hex), robustly
handling non-standard FCP responses."""
fcp, _ = scc.select_file(fid) # Raw FCP (select_file does NOT parse it)
size = _fcp_file_size(fcp)
if size:
try:
raw, _ = scc.read_binary(fid, length=size) # Explicit length: ignores FCP
if raw:
return raw
except SwMatchError:
pass
return _read_binary_raw(scc) # fallback: file is already selected
def find_usim_aid(scc):
"""Reads EF.DIR and returns the USIM AID, or None if not found or if
the card returns a non-standard FCP (in which case, a partial AID is used)."""
try:
scc.select_file(EF_DIR_FID)
for rec_no in range(1, 16):
try:
data, _ = scc.read_record(EF_DIR_FID, rec_no)
except SwMatchError:
break
if not data:
continue
aid = _extract_aid_from_dir_record(data.lower())
if aid and aid.startswith(USIM_AID_PREFIX):
return aid
except Exception:
# EF.DIR absent or non-standard FCP -> fallback: known partial AID
return None
return None
def select_usim(scc, forced_aid=None):
"""Selects ADF.USIM. Returns the AID used. Raises SwMatchError if not found."""
if forced_aid:
aid = forced_aid.lower().replace(" ", "")
else:
aid = find_usim_aid(scc) or USIM_AID_PREFIX # fallback: partial AID
scc.select_adf(aid)
return aid
def read_ust(scc):
"""Reads and decodes EF.UST. Returns (dict_services, raw_hex)."""
raw = read_transparent_ef(scc, EF_UST_FID)
return EF_UST().decode_hex(raw), raw
def read_suci_calc_info(scc):
"""Returns (df5gs_present, suci_present, decoded|None, raw|None)."""
try:
scc.select_file(DF_5GS_FID)
except SwMatchError:
return False, False, None, None
try:
scc.select_file(EF_SUCI_FID)
except SwMatchError:
return True, False, None, None
raw = read_transparent_ef(scc, EF_SUCI_FID)
try:
dec = EF_SUCI_Calc_Info().decode_hex(raw)
except Exception:
dec = None
return True, True, dec, raw
# ----------------------------------------------------------------------------
# Analysis (pure functions, testable without card)
# ----------------------------------------------------------------------------
def service_active(ust_dec, n):
"""True if service n is active in the dict returned by EF_UST.decode_hex."""
entry = ust_dec.get(n)
if entry is None:
entry = ust_dec.get(str(n))
return bool(entry and entry.get("activated"))
def _scheme_to_int(v):
"""Normalizes the scheme identifier (can come as int or name)."""
if isinstance(v, int):
return v
return {"null": 0, "A": 1, "B": 2}.get(str(v), -1)
def analyze(ust_dec, suci_dec, df5gs_present, suci_present):
"""Applies the verdict logic and returns a structured dict."""
s122 = service_active(ust_dec, 122)
s123 = service_active(ust_dec, 123)
s124 = service_active(ust_dec, 124)
s125 = service_active(ust_dec, 125)
# Schemes and keys from EF.SUCI_Calc_Info
schemes = []
if suci_dec:
for e in suci_dec.get("prot_scheme_id_list", []):
sid = _scheme_to_int(e.get("identifier"))
schemes.append({
"priority": e.get("priority"),
"id": sid,
"key_index": e.get("key_index"),
"name": SCHEME_NAMES.get(sid, "unknown (%s)" % sid),
})
pubkeys = []
if suci_dec:
for k in suci_dec.get("hnet_pubkey_list", []):
pk = k.get("hnet_pubkey")
length = len(pk) if isinstance(pk, (bytes, bytearray)) else None
pubkeys.append({"id": k.get("hnet_pubkey_identifier"), "length_bytes": length})
non_null = [s for s in schemes if s["id"] not in (0, -1)]
has_key = len(pubkeys) > 0
file_protects = bool(non_null) and has_key # the file offers real concealment
# ---- Verdict (nuanced logic) ----
if not s124:
verdict = "NO_SUCI_SUPPORT"
privacy = False
summary = ("The USIM does NOT declare identifier privacy support "
"(service 124 inactive): SUCI is not supported. The SUPI/IMSI "
"may be sent unencrypted.")
else:
calc_by = "USIM" if s125 else "ME (terminal)"
if suci_present:
if file_protects:
verdict = ("SUCI_USIM_PROTECTED" if s125 else "SUCI_ME_PROTECTED")
privacy = True
summary = ("The USIM supports SUCI with EFFECTIVE SUPI concealment "
"(non-null scheme + network public key). "
"SUCI calculation is handled by: %s." % calc_by)
else:
verdict = "SUCI_ONLY_NULL"
privacy = False
summary = ("The USIM supports SUCI (service 124), but EF.SUCI_Calc_Info "
"only offers the null scheme or lacks a public key: "
"the SUPI is NOT effectively concealed. "
"Calculation handled by: %s." % calc_by)
else:
# 124 active but no legible EF.SUCI_Calc_Info
if s125:
verdict = "SUCI_USIM_UNDETERMINABLE"
privacy = None
summary = ("The USIM supports SUCI and calculates it itself (service 125). "
"EF.SUCI_Calc_Info is not present/legible, so the "
"scheme used is internal and cannot be verified from the "
"file system.")
else:
verdict = "SUCI_SUPPORTED_WITHOUT_CALC_INFO"
privacy = False
summary = ("The USIM declares SUCI support (service 124) and the calculation "
"falls to the ME, but EF.SUCI_Calc_Info is missing: the terminal "
"would not have schemes/keys -> likely null scheme.")
return {
"services": {"122": s122, "123": s123, "124": s124, "125": s125},
"df_5gs_present": df5gs_present,
"suci_calc_info_present": suci_present,
"schemes": schemes,
"pubkeys": pubkeys,
"effective_privacy": privacy,
"verdict": verdict,
"summary": summary,
}
# ----------------------------------------------------------------------------
# Presentation
# ----------------------------------------------------------------------------
def _mark(b):
return "\u2713" if b else "\u2717" # checks / cross
def print_human(result):
s = result["services"]
print("=" * 64)
print(" SUCI Support on the (U)SIM")
print("=" * 64)
print("\n EF.UST - USIM Service Table:")
print(" [%s] Service 122 5GS Mobility Management Information" % _mark(s["122"]))
print(" [%s] Service 123 5G Security Parameters" % _mark(s["123"]))
print(" [%s] Service 124 Subscription identifier privacy support" % _mark(s["124"]))
print(" [%s] Service 125 SUCI calculation by the USIM" % _mark(s["125"]))
print("\n DF.5GS / EF.SUCI_Calc_Info:")
print(" [%s] DF.5GS (5FC0) present" % _mark(result["df_5gs_present"]))
print(" [%s] EF.SUCI_Calc_Info (4F07) present" % _mark(result["suci_calc_info_present"]))
if result["schemes"]:
print("\n Protection schemes (by priority):")
for e in result["schemes"]:
print(" - priority %s: %s (key_index=%s)"
% (e["priority"], e["name"], e["key_index"]))
if result["pubkeys"]:
print("\n Home network public keys:")
for k in result["pubkeys"]:
print(" - id %s: %s bytes" % (k["id"], k["length_bytes"]))
pe = result["effective_privacy"]
pe_txt = {True: "YES", False: "NO", None: "undeterminable"}[pe]
print("\n" + "-" * 64)
print(" Verdict : %s" % result["verdict"])
print(" SUPI Privacy: %s" % pe_txt)
print(" Summary : %s" % result["summary"])
print("-" * 64)
# ----------------------------------------------------------------------------
# Main
# ----------------------------------------------------------------------------
def build_parser():
p = argparse.ArgumentParser(
description="Checks SUCI (5G) support on a (U)SIM using pySim.")
argparse_add_reader_args(p) # --pcsc-device, --device, --modem-device, --apdu-trace...
p.add_argument("--pin", help="PIN1/CHV1, if EF.UST requires verification.")
p.add_argument("--aid", help="Force ADF.USIM AID (hex), skips EF.DIR reading.")
p.add_argument("--json", action="store_true", help="JSON output (machine readable).")
p.add_argument("--debug", action="store_true",
help="Shows pySim native output (TPDU trace 'T0: ...'), "
"which is silenced by default.")
return p
def main(argv=None):
opts = build_parser().parse_args(argv)
if opts.debug:
# Reactivate pySim native output (TPDU trace "T0: ...").
logging.getLogger().setLevel(logging.DEBUG)
# 1) Connection
try:
tp, scc = open_card(opts)
except (NoCardError, ReaderError) as e:
print("ERROR: could not access the card/reader: %s" % e, file=sys.stderr)
return 2
except Exception as e:
print("ERROR initializing reader: %s" % e, file=sys.stderr)
return 2
try:
# 2) Select ADF.USIM
try:
aid = select_usim(scc, forced_aid=opts.aid)
except SwMatchError:
print("ERROR: could not select ADF.USIM. Is this a USIM card?",
file=sys.stderr)
return 3
# 3) PIN (optional)
if opts.pin:
try:
scc.verify_chv(1, opts.pin.encode("ascii"))
except SwMatchError as e:
print("WARNING: PIN1 verification failed (%s). Continuing anyway."
% e, file=sys.stderr)
# 4) EF.UST
try:
ust_dec, _ = read_ust(scc)
except SwMatchError as e:
print("ERROR reading EF.UST (%s). Probably requires PIN1: use --pin."
% e, file=sys.stderr)
return 4
# 5) DF.5GS / EF.SUCI_Calc_Info
df5gs, suci_present, suci_dec, _ = read_suci_calc_info(scc)
# 6) Analysis + Output
result = analyze(ust_dec, suci_dec, df5gs, suci_present)
result["aid_usim"] = aid
if opts.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print_human(result)
# Exit code: 0 if SUCI support, 1 if not.
return 0 if result["services"]["124"] else 1
finally:
try:
tp.disconnect()
except Exception:
pass
if __name__ == "__main__":
sys.exit(main())