P1sec / pycrate

A Python library to ease the development of encoders and decoders for various protocols and file formats; contains ASN.1 and CSN.1 compilers.
GNU Lesser General Public License v2.1
381 stars 132 forks source link

Is it possible to decode containers within 3GPP messages while decoding the message? #206

Closed elrandira closed 1 year ago

elrandira commented 2 years ago

Hi,

I am integrating your solution to decode 3GPP protocols (RRC, NAS, NGAP). I just found out that when I decode a RRC message for example, if a container is present, it won't be decoded but instead displayed as a PDU.

Is there a way to automatically decode containers as part of their parent messages/PDU? At least when it is the same protocol, this would exclude NAS container of RRC messages for example (but that could be customised depending on your answer).

Here's my call on your API

from binascii import unhexlify
from pycrate_asn1dir.RRCNR import NR_RRC_Definitions
from typing import Any, Dict

def decode_rrc(pdu: str, channel: str) -> Dict[str, Any]:
  """
      Decode a RRC PDU from a specific channel (mandatory from 3GPP).
      Allowed channel values are 'BCCH-BCH', 'BCCH-DL-SCH', 'DL-CCCH', 'DL-DCCH', 'PCCH', 
      'UL-CCCH', 'UL-CCCH1' and 'UL-DCCH'

      Transfer syntax for RRC PDUs is derived from their ASN.1 definitions by use of Packed
      Encoding Rules, unaligned specified by 3GPP 38.331 §8.1

  :param
      pdu (str): Packet Data Unit to decode

      channel (str): channel providing the PDU

  :return
      dict[str, Any]: JSON representation of the decoded PDU

  """
  _parsers_name = ['BCCH-BCH', 'BCCH-DL-SCH', 'DL-CCCH', 'DL-DCCH', 'PCCH', 'UL-CCCH', 'UL-CCCH1', 'UL-DCCH']
  if channel not in _parsers_name:
      raise Exception(f'Only channels supported are: {_parsers_name}')

  # Just in case
  pdu = pdu.replace(' ','')

  # get parser from channel parameter
  _msgParser = getattr(NR_RRC_Definitions, f"{channel}-Message".replace('-','_'))
  _msgParser.from_uper(unhexlify(pdu))
  return _msgParser._to_jval()    

a ueCapability information message will return this for example:

{
  "message": {
    "c1": {
      "ueCapabilityInformation": {
        "rrc-TransactionIdentifier": 0,
        "criticalExtensions": {
          "ueCapabilityInformation": {
            "ue-CapabilityRAT-ContainerList": [
              {
                "rat-Type": "nr",
                "ue-CapabilityRAT-Container": "e1a01000574f7a03020002c0a02410010180931600098350304d9061cf9863ca160704c020311c00a83626eb04610d04083a4022cb64c550d0e7c471e819f604f8c73e618f28581c0e1e0b8002f800e000be0003901c006ca0000a000000108389a00000000404d0501a018103639f0fd02c03800281c0c0381802a03800301f01801000000008000010040000100200000c01000008008000050040000300dca9ac0cac0dac8dac14ac08ac15a0282000012ca48080000cb29202000052ca4a080001cb29202000092ca48080002cb29200ba551540e2e95555238aa0b1540a28085401a81c0e050381c0e00000001002004628009500"
              }
            ]
          }
        }
      }
    }
  }
}

when I call:

decode_rrc("4882203df868040015d3de80c08000b028090400406024c5800260d40c13641873e618f28581c130080c47002a0d89bac1184341020e9008b2d931543439f11c7a067d813e31cf9863ca1607038782e000be0038002f8000e407001b2800028000000420e2680000000101341406806040d8e7c3f40b00e000a070300e0600a80e000c07c06004000000020000040100000400800003004000020020000140100000c0372a6b032b036b236b052b022b05680a0800004b29202000032ca480800014b29282000072ca480800024b292020000b2ca4802e95455038ba555548e2a82c55028a0215006a07038140e07038000000040080118a00254000", "UL-DCCH")

Thanks for your help,

p1-bmu commented 2 years ago

Is there a way to automatically decode containers as part of their parent messages/PDU? Unfortunately no. For each container, you need to read the 3gpp spec, and see what structure is required for further decoding. Most of the container content correspond to ASN.1-defined structures, but not all (i.e. GSM and GPRS are not using ASN.1). Sometimes, the content of the container is even contextual... There are some issues already related to this, see for example:

If you wish to decode everything in a single pass, you will need to build enhanced ASN.1 modules gathering RRC3G, RRCLTE and RRCNR (and maybe more), and use table constraint and evolved ASN.1 features (which are not used in RRC specifications).

elrandira commented 2 years ago

Thanks for your feedback, much appreciated.

elrandira commented 2 years ago

partially answering my own question for people that might need it:

As I am working with PDU decoded in JSON, I can use the excellent jsonpath-ng to search through the message using JSONPath to look for PDU to decode and then decode them.

For example, with the RRC PDUs in the UeCapability message. the 3GPP 38.331 indeed doesn't decode these capability containers (called ue-CapabilityRAT-Container) directly inside the message. Each container is defined in the ASN.1 as an octet string and later in the ASN.1 definition the container is described but no link between these two are defined.

my function above becomes:

from binascii import unhexlify
from jsonpath_ng.ext.parser import parse as jp_parse
from pycrate_asn1dir.RRCNR import NR_RRC_Definitions
from pycrate_asn1dir.RRCLTE import EUTRA_RRC_Definitions

#for typing
from typing import Any, Optional, Union, Dict, List, Tuple
from pycrate_asn1rt.asnobj import ASN1Obj
jsonObj = Dict[str, Any]

SUB_PDUS_RRC: List[Tuple[str, ASN1Obj, str]] = [
    # jsonPath, parser, method
    ('$..ue-CapabilityRAT-ContainerList[?(@.rat-Type == "nr")].ue-CapabilityRAT-Container', NR_RRC_Definitions.UE_NR_Capability, "from_uper"),
    ('$..ue-CapabilityRAT-ContainerList[?(@.rat-Type == "eutra")].ue-CapabilityRAT-Container', EUTRA_RRC_Definitions.UE_EUTRA_Capability, "from_uper"),
]

def decode_rrc(pdu: str, channel: str) -> jsonObj:
    """
        Decode a RRC PDU from a specific channel (mandatory from 3GPP).
        Allowed channel values are 'BCCH-BCH', 'BCCH-DL-SCH', 'DL-CCCH', 'DL-DCCH', 'PCCH', 
        'UL-CCCH', 'UL-CCCH1' and 'UL-DCCH'

        Transfer syntax for RRC PDUs is derived from their ASN.1 definitions by use of Packed
        Encoding Rules, unaligned specified by 3GPP 38.331 §8.1

    :param
        pdu (str): Packet Data Unit to decode

        channel (str): channel providing the PDU

    :return
        dict[str, Any]: JSON representation of the decoded PDU

    """
    _parsers_name = ['BCCH-BCH', 'BCCH-DL-SCH', 'DL-CCCH', 'DL-DCCH', 'PCCH', 'UL-CCCH', 'UL-CCCH1', 'UL-DCCH']
    if channel not in _parsers_name:
        raise Exception(f'Only channels supported are: {_parsers_name}')

    # Just in case
    pdu = pdu.replace(' ','').replace('\n', '')

    # get parser from channel parameter
    _msgParser = getattr(NR_RRC_Definitions, f"{channel}-Message".replace('-','_'))
    buff = unhexlify(pdu)
    _msgParser.from_uper(buff)
    decoded= _msgParser._to_jval()

    #Attempt decoding some PDUs inside the message
    _decode_rrc_sub_pdus(decoded)

    return decoded

def _decode_rrc_sub_pdus(message: jsonObj):
    #search for specific IEs
    for jp, parser, method in SUB_PDUS_RRC:
        # apply jsonPath on jsonObj
        rv= jp_parse(jp).find(message)
        if len(rv) > 0:
            # TODO: managed possible multiple elements of rv
            sub_pdu=rv[0].value
            # decode sub_pdu
            getattr(parser, method)(unhexlify(sub_pdu))
            # retrieve sub_pdu in json format
            sub_dec_pdu=parser._to_jval()
            # it might containt itself a new sub_PDU
            _decode_rrc_sub_pdus(sub_dec_pdu)
            # update with decoded value
            # TODO: dangerous if more that one element in rv
            jp_parse(jp).update(message, sub_dec_pdu)

It is a bit dirty but it gives the proper result and can still be improved. The downside is that list such as SUB_PDUS_RRC must be manually filled in.

p1-bmu commented 2 years ago

Looks good to me. I wasn't aware of jsonpath_ng but the syntax it supports seems to make it quite powerful. Thanks for your feedback.