Python - ASN Encode/Decode
If you are working in the protocol of cellular communication, you may be pretty familiar with a special data structure called ASN.
ASN.1 (Abstract Syntax Notation One) is a standard and notation that describes rules and structures for representing, encoding, transmitting, and decoding data in telecommunications and computer networking. ASN.1 is widely used in various fields such as telecommunication and security systems because it provides a method for data structures to be described in a manner that is independent of machine-specific encoding techniques.
Python has a special package named asn1tools that helps you work with ASN.1. It's like a translator between the ASN.1 system and Python, so you can understand and work with ASN.1 data in Python.
Major functionality of asn1tools package
This package provides several core functionalities:
Encoding and Decoding: 'asn1tools' can encode and decode ASN.1 data using various encoding rules such as BER (Basic Encoding Rules), DER (Distinguished Encoding Rules), PER (Packed Encoding Rules), and XER (XML Encoding Rules), among others. This is one of the key features of the package as it allows for the conversion of complex ASN.1 data into Python data types and vice versa.
Validation: It's possible to validate your ASN.1 data against a schema to ensure it adheres to the expected structure and data types. This is important for ensuring data integrity and preventing errors that can arise from malformed data.
Compilation: 'asn1tools' can compile ASN.1 definitions. This process involves translating the abstract syntax into a format that can be processed by a computer. Once an ASN.1 schema is compiled, it can be used to encode, decode, and validate data.
Sample Program
Writing Python script for ASN manipulation in general goes through several procedures as described below.
- Install the asn1tools package: Before you start, you need to install asn1tools. You can do this by running the command pip install asn1tools in your terminal or command prompt.
- Create your ASN.1 schema: The schema is a set of rules that describes your data structure. You need to create this schema and save it in a .asn file. The structure and rules depend on the type of data you're working with.
- Compile your schema: Once you've created your ASN.1 schema, you need to compile it using asn1tools. This means turning your schema into a form that Python can understand. You do this by using the asn1tools.compile_files('your_file.asn') function in Python, replacing 'your_file.asn' with the name of your schema file.
- Encode your data: If you have data that you want to convert into ASN.1 format, you can encode it. You use the encode function in asn1tools for this, and you specify the encoding rule you want to use (for example, 'ber' or 'per').
- Decode your data: If you have ASN.1 data that you want to convert back into Python data, you can decode it. You use the decode function in asn1tools for this, and again specify the encoding rule that was used.
- Validate your data: If you want to check that your ASN.1 data follows the rules set out in your schema, you can validate it. You use the validate function in asn1tools for this. If your data doesn't follow the rules, asn1tools will tell you.
Example 1 > Simplified MIB ASN
Following is an example for asn1tools program for 5G NR MIB message.
First you need to define ASN schema as shown in the following example. Except the first 2 lines and last one lines (i.e, DEFINITION BEGIN and END, I just copied it from 3GPP 38.331 with a little modification for pdcch-ConfigSIB1 OCTET STRING,) to make the ASN schema simpler. In next example, I am going to use the exactly same schema from 38.331 without any modification.
ASN_5G_MIB.asn
|
BCCH-BCH-Message DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
MIB ::= SEQUENCE {
systemFrameNumber BIT STRING (SIZE (6)),
subCarrierSpacingCommon ENUMERATED {scs15or60, scs30or120},
ssb-SubcarrierOffset INTEGER (0..15),
dmrs-TypeA-Position ENUMERATED {pos2, pos3},
pdcch-ConfigSIB1 OCTET STRING,
cellBarred ENUMERATED {barred, notBarred},
intraFreqReselection ENUMERATED {allowed, notAllowed},
spare BIT STRING (SIZE (1))
}
BCCH-BCH-MessageType ::= CHOICE {
mib MIB,
messageClassExtension SEQUENCE {}
}
BCCH-BCH-Message ::= SEQUENCE {
message BCCH-BCH-MessageType
}
END
|
Script |
import asn1tools
from pprint import pprint
def bytes_to_hex_string(byte_array):
return ''.join('{:02x}'.format(b) for b in byte_array)
def hex_to_bytes(hex_string):
return bytes.fromhex(hex_string)
# Compile the ASN.1 specification. Note that the ASN_5G_MIB.asn gets complied here.
spec = asn1tools.compile_files('ASN_5G_MIB.asn')
# Create a sample MIB message
message = {
'message': ('mib', {
'systemFrameNumber': (b'(', 6),
'subCarrierSpacingCommon': 'scs15or60',
'ssb-SubcarrierOffset': 5,
'dmrs-TypeA-Position': 'pos2',
'pdcch-ConfigSIB1': b'\x0c',
'cellBarred': 'notBarred',
'intraFreqReselection': 'allowed',
'spare': (b'\x00', 8)
})
}
# This is not mandatory. this is to show how to modify a specific IE after the initial definition.
message['message'][1]['subCarrierSpacingCommon'] = 'scs30or120'
message['message'][1]['ssb-SubcarrierOffset'] = 6
message['message'][1]['pdcch-ConfigSIB1'] = b'\x0e'
# Encode and Validate the message
try:
encoded_data = spec.encode('BCCH-BCH-Message', message)
print("Validation successful.")
print(f"Encoded data: {encoded_data}")
except asn1tools.EncodeError as e:
print("Validation failed: ", e)
# This is not mandatory. This is just to get the hex tring which we are more familiar with
encoded_hex_string = bytes_to_hex_string(encoded_data)
print("Encoded hex string:", encoded_hex_string)
# This is to show how to convert the human readable hex string back to byte array which is used by decode()
# function.
encoded_bytes = hex_to_bytes(encoded_hex_string)
# Decode the encoded data back to a BCCH-BCH-Message instance
decoded_data = spec.decode('BCCH-BCH-Message', encoded_bytes)
print(f"Decoded data: {decoded_data}")
pprint(decoded_data, sort_dicts=False)
# This is to show how to extract a specific IE (information Elements) from the decoded ASN.
pdcch_config = decoded_data['message'][1]['pdcch-ConfigSIB1']
print("pdcch_config = ", pdcch_config )
dmrs_TypeA_pos = decoded_data['message'][1]['dmrs-TypeA-Position']
print("dmrs-TypeA-Position = ", dmrs_TypeA_pos )
|
Result |
Validation successful.
# print(f"Encoded data: {encoded_data}")
Encoded data: b'0\x1e\xa0\x1c\xa0\x1a\x80\x02\x02(\x81\x01\x01\x82\x01\x06\x83\x01\x00\x84\x01\x0e\x85\
x01\x01\x86\x01\x00\x87\x02\x00\x00'
# print("Encoded hex string:", encoded_hex_string)
Encoded hex string: 301ea01ca01a8002022881010182010683010084010e85010186010087020000
# print(f"Decoded data: {decoded_data}")
Decoded data: {'message': ('mib', {'systemFrameNumber': (bytearray(b'('), 6), 'subCarrierSpacingCommon': 'scs30or120', 'ssb-SubcarrierOffset': 6, 'dmrs-TypeA-Position': 'pos2', 'pdcch-ConfigSIB1': b'\x0e', 'cellBarred': 'notBarred', 'intraFreqReselection': 'allowed', 'spare': (bytearray(b'\x00'), 8)})}
# pprint(decoded_data, sort_dicts=False)
{'message': ('mib',
{'systemFrameNumber': (bytearray(b'('), 6),
'subCarrierSpacingCommon': 'scs30or120',
'ssb-SubcarrierOffset': 6,
'dmrs-TypeA-Position': 'pos2',
'pdcch-ConfigSIB1': b'\x0e',
'cellBarred': 'notBarred',
'intraFreqReselection': 'allowed',
'spare': (bytearray(b'\x00'), 8)})}
# print("pdcch_config = ", pdcch_config )
pdcch_config = b'\x0e'
# print("dmrs-TypeA-Position = ", dmrs_TypeA_pos )
dmrs-TypeA-Position = pos2
|
Example 2 > Original MIB ASN
ASN_5G_RRC.asn
|
-- ASN1START
-- TAG-NR-RRC-DEFINITIONS-START
NR-RRC-Definitions DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
-- TAG-NR-RRC-DEFINITIONS-STOP
-- ASN1STOP
-- ASN1START
-- TAG-BCCH-BCH-MESSAGE-START
BCCH-BCH-Message ::= SEQUENCE {
message BCCH-BCH-MessageType
}
BCCH-BCH-MessageType ::= CHOICE {
mib MIB,
messageClassExtension SEQUENCE {}
}
-- TAG-BCCH-BCH-MESSAGE-STOP
-- ASN1STOP
-- ASN1START
-- TAG-MIB-START
MIB ::= SEQUENCE {
systemFrameNumber BIT STRING (SIZE (6)),
subCarrierSpacingCommon ENUMERATED {scs15or60, scs30or120},
ssb-SubcarrierOffset INTEGER (0..15),
dmrs-TypeA-Position ENUMERATED {pos2, pos3},
pdcch-ConfigSIB1 PDCCH-ConfigSIB1,
cellBarred ENUMERATED {barred, notBarred},
intraFreqReselection ENUMERATED {allowed, notAllowed},
spare BIT STRING (SIZE (1))
}
-- TAG-MIB-STOP
-- ASN1STOP
-- ASN1START
-- TAG-PDCCH-CONFIGSIB1-START
PDCCH-ConfigSIB1 ::= SEQUENCE {
controlResourceSetZero ControlResourceSetZero,
searchSpaceZero SearchSpaceZero
}
-- TAG-PDCCH-
-- ASN1STOP
-- ASN1START
-- TAG-CONTROLRESOURCESETZERO-START
ControlResourceSetZero ::= INTEGER (0..15)
-- TAG-CONTROLRESOURCESETZERO-STOP
-- ASN1STOP
-- ASN1START
-- TAG-SEARCHSPACEZERO-START
SearchSpaceZero ::= INTEGER (0..15)
-- TAG-SEARCHSPACEZERO-STOP
-- ASN1STOP
END
|
Script |
import asn1tools
from pprint import pprint
def bytes_to_hex_string(byte_array):
return ''.join('{:02x}'.format(b) for b in byte_array)
def hex_to_bytes(hex_string):
return bytes.fromhex(hex_string)
# Compile the ASN.1 specification
spec = asn1tools.compile_files('ASN_5G_RRC.asn')
# Create a sample MIB message
message = {
'message': ('mib', {
'systemFrameNumber': (b'(', 6),
'subCarrierSpacingCommon': 'scs15or60',
'ssb-SubcarrierOffset': 5,
'dmrs-TypeA-Position': 'pos2',
'pdcch-ConfigSIB1': {'controlResourceSetZero': 1, 'searchSpaceZero': 1},
'cellBarred': 'notBarred',
'intraFreqReselection': 'allowed',
'spare': (b'\x00', 8)
})
}
# This is not mandatory. this is to show how to modify a specific IE after the initial definition.
message['message'][1]['subCarrierSpacingCommon'] = 'scs30or120'
message['message'][1]['ssb-SubcarrierOffset'] = 6
message['message'][1]['pdcch-ConfigSIB1'] = {'controlResourceSetZero': 2, 'searchSpaceZero': 2}
# Encode and Validate the message
encoded_data = b''
try:
encoded_data = spec.encode('BCCH-BCH-Message', message)
print("Validation successful.")
print(f"Encoded data: {encoded_data}")
except asn1tools.EncodeError as e:
print("Validation failed: ", e)
# This is not mandatory. This is just to get the hex string which we are more familiar with
encoded_hex_string = bytes_to_hex_string(encoded_data)
print("Encoded hex string:", encoded_hex_string)
# This is to show how to convert the human readable hex string back to byte array which is used by decode()
# function.
encoded_bytes = hex_to_bytes(encoded_hex_string)
# Decode the encoded data back to a BCCH-BCH-Message instance
decoded_data = spec.decode('BCCH-BCH-Message', encoded_bytes)
print(f"Decoded data: {decoded_data}")
pprint(decoded_data, sort_dicts=False)
# This is to show how to extract a specific IE (information Elements) from the decoded ASN.
pdcch_config = decoded_data['message'][1]['pdcch-ConfigSIB1']
print("pdcch_config = ", pdcch_config )
dmrs_TypeA_pos = decoded_data['message'][1]['dmrs-TypeA-Position']
print("dmrs-TypeA-Position = ", dmrs_TypeA_pos )
|
Result |
Validation successful.
# print(f"Encoded data: {encoded_data}")
Encoded data: b'0#\xa0!\xa0\x1f\x80\x02\x02(\x81\x01\x01\x82\x01\x06\x83\x01\x00\xa4\x06\x80\x01\x02\x81\
x01\x02\x85\x01\x01\x86\x01\x00\x87\x02\x00\x00'
# print("Encoded hex string:", encoded_hex_string)
Encoded hex string: 3023a021a01f80020228810101820106830100a40680010281010285010186010087020000
# print(f"Decoded data: {decoded_data}")
Decoded data: {'message': ('mib', {'systemFrameNumber': (bytearray(b'('), 6), 'subCarrierSpacingCommon': 'scs30or120', 'ssb-SubcarrierOffset': 6, 'dmrs-TypeA-Position': 'pos2', 'pdcch-ConfigSIB1': {'controlResourceSetZero': 2, 'searchSpaceZero': 2}, 'cellBarred': 'notBarred', 'intraFreqReselection': 'allowed', 'spare': (bytearray(b'\x00'), 8)})}
# pprint(decoded_data, sort_dicts=False)
{'message': ('mib',
{'systemFrameNumber': (bytearray(b'('), 6),
'subCarrierSpacingCommon': 'scs30or120',
'ssb-SubcarrierOffset': 6,
'dmrs-TypeA-Position': 'pos2',
'pdcch-ConfigSIB1': {'controlResourceSetZero': 2,
'searchSpaceZero': 2},
'cellBarred': 'notBarred',
'intraFreqReselection': 'allowed',
'spare': (bytearray(b'\x00'), 8)})}
# print("pdcch_config = ", pdcch_config )
pdcch_config = {'controlResourceSetZero': 2, 'searchSpaceZero': 2}
# print("dmrs-TypeA-Position = ", dmrs_TypeA_pos )
dmrs-TypeA-Position = pos2
|
|
|