from enum import Enum
from functools import total_ordering
from xml.etree import ElementTree
from . import config, mapper
[docs]class TagClass(Enum):
UNIVERSAL = 0 # 0b00000000
APPLICATION = 1 # 0b01000000
CONTEXT_SPECIFIC = 2 # 0b10000000
PRIVATE = 3 # 0b11000000
[docs]class TagType(Enum):
PRIMITIVE = 0 # 0b00000000
CONSTRUCTED = 1 # 0b00100000
[docs]@total_ordering
class Tag:
"""This represents a TLV tag following the BER encoding.
It consists of the following parts:
- Tag class: bits 7-8 of the initial octet
- Tag type: bit 6 of the initial octet
- Tag number: bits 1-5 of the initial octet and bits 1-7 of subsequent octets
"""
CLASS_BITMASK = 0b11000000
TYPE_BITMASK = 0b00100000
def __init__(self, identifier: bytes, *, force_constructed: bool = False):
self.identifier = identifier
map_constructed = mapper.is_constructed(self.to_hex())
if map_constructed is not None:
force_constructed = map_constructed
self._force_constructed = force_constructed
def __repr__(self) -> str:
return self._identifier.hex()
def __str__(self) -> str:
return f"0x{self._identifier.hex()}"
def __eq__(self, other: "Tag"):
return repr(self) == repr(other)
def __lt__(self, other: "Tag"):
return repr(self) < repr(other)
@property
def identifier(self) -> bytes:
"""Return the identifier octets of this tag."""
return self._identifier
@identifier.setter
def identifier(self, identifier: bytes):
"""Set the identifier octets of this tag."""
# Strip leading zeros
identifier = identifier.lstrip(b"\x00")
self._check_tag(identifier)
self._identifier = identifier
@property
def tag_class(self) -> TagClass:
"""Return the class of this tag."""
# In the initial octet, bits 7-8 encode the class
class_bits = (self._identifier[0] & self.CLASS_BITMASK) >> 6
return TagClass(class_bits)
@property
def tag_type(self) -> TagType:
"""Return the type of this tag."""
# In the initial octet, bit 6 encodes the type
type_bits = (self._identifier[0] & self.TYPE_BITMASK) >> 5
return TagType(type_bits)
[docs] def xml_tag(self) -> str:
entry = mapper.lookup(self.to_hex())
xml_tag = ""
if entry is not None:
xml_tag = entry.get("XMLTag") or ""
return xml_tag
[docs] def is_constructed(self) -> bool:
"""Return true if the tag is constructed otherwise false."""
if self.tag_type == TagType.CONSTRUCTED or self._force_constructed:
return True
return False
[docs] def to_int(self) -> int:
"""Return an integer representing a tag."""
return int.from_bytes(self._identifier, byteorder="big")
[docs] def to_hex(self) -> str:
"""Return a hex string representing a tag."""
return "0x" + self._identifier.hex().upper()
[docs] def to_xml(self, element: ElementTree.Element) -> ElementTree.Element:
"""Return an XML element representing a tag."""
element.set("Tag", self.to_hex())
return element
[docs] @classmethod
def from_int(cls, tag: int, *, force_constructed: bool = False) -> "Tag":
"""Return the tag represented by the given integer."""
length = (tag.bit_length() + 7) // 8
return cls(
tag.to_bytes(length, byteorder="big"), force_constructed=force_constructed
)
[docs] @classmethod
def from_hex(cls, tag: str, *, force_constructed: bool = False) -> "Tag":
"""Return the tag represented by the given hex string."""
if tag.startswith("0x"):
tag = tag[2:]
return cls(bytes.fromhex(tag), force_constructed=force_constructed)
@staticmethod
def _check_tag(octets: bytes):
"""Check the tag octets against the Basic encoding rules of ASN.1 (see ISO/IEC 8825)."""
if len(octets) == 0:
raise ValueError("tag must not be empty")
if octets[0] & 0x1F == 0x1F:
if len(octets) == 1:
raise ValueError(f"tag is missing subsequent octets: {octets.hex()}")
for index, byte in enumerate(octets[1:], start=1):
if index == 1 and byte == 0x80 and config.strict_checking:
raise ValueError(
"tag has first subsequent octet where bits 7 to 1 are all "
f"zero: {octets.hex()}"
)
if index + 1 < len(octets) and byte & 0x80 != 0x80:
raise ValueError(
f"tag has subsequent octet where bit 1 is not set: {octets.hex()}"
)
if index + 1 == len(octets) and byte & 0x80 == 0x80:
raise ValueError(
f"tag is missing subsequent octets: {octets.hex()}"
)
[docs]class RootTag(Tag):
def __init__(self):
# Tag 0x30 = ASN.1 type "SEQUENCE and SEQUENCE OF"
super().__init__(b"\x30")
def __repr__(self) -> str:
return "root"
def __str__(self) -> str:
return "root"