From a984bc5337ca87d872d47a66255a2a8dfda69240 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Sun, 19 Jul 2026 20:58:33 +0200 Subject: [PATCH 01/46] contrib: add ASN.1 OER and UPER codecs Move OER/UPER codec implementations to scapy.contrib and wire asn1fields for OER/PER using the pluggable tagging/kwargs hooks. AI-Assisted: yes (Cursor) --- .config/codespell_ignore.txt | 5 + scapy/asn1/ber.py | 3 + scapy/asn1fields.py | 677 +++++++++-- scapy/contrib/oer.py | 825 ++++++++++++++ scapy/contrib/uper.py | 1369 +++++++++++++++++++++++ test/scapy/layers/asn1.uts | 355 ++++++ test/scapy/layers/asn1_build_tests.py | 185 +++ test/scapy/layers/asn1_coverage.py | 890 +++++++++++++++ test/scapy/layers/asn1_dissect_tests.py | 280 +++++ test/scapy/layers/ber_codec.py | 275 +++++ test/scapy/layers/ber_packets.py | 184 +++ test/scapy/layers/oer_fuzz.py | 106 ++ test/scapy/layers/oer_iop.py | 160 +++ test/scapy/layers/oer_packets.py | 209 ++++ test/scapy/layers/uper_asn1scc_iop.py | 190 ++++ test/scapy/layers/uper_codec.py | 174 +++ test/scapy/layers/uper_fuzz.py | 133 +++ test/scapy/layers/uper_helpers.py | 122 ++ test/scapy/layers/uper_iop.py | 189 ++++ test/scapy/layers/uper_packets.py | 543 +++++++++ 20 files changed, 6789 insertions(+), 85 deletions(-) create mode 100644 scapy/contrib/oer.py create mode 100644 scapy/contrib/uper.py create mode 100644 test/scapy/layers/asn1_build_tests.py create mode 100644 test/scapy/layers/asn1_coverage.py create mode 100644 test/scapy/layers/asn1_dissect_tests.py create mode 100644 test/scapy/layers/ber_codec.py create mode 100644 test/scapy/layers/ber_packets.py create mode 100644 test/scapy/layers/oer_fuzz.py create mode 100644 test/scapy/layers/oer_iop.py create mode 100644 test/scapy/layers/oer_packets.py create mode 100644 test/scapy/layers/uper_asn1scc_iop.py create mode 100644 test/scapy/layers/uper_codec.py create mode 100644 test/scapy/layers/uper_fuzz.py create mode 100644 test/scapy/layers/uper_helpers.py create mode 100644 test/scapy/layers/uper_iop.py create mode 100644 test/scapy/layers/uper_packets.py diff --git a/.config/codespell_ignore.txt b/.config/codespell_ignore.txt index e5d65af4708..98e33583f15 100644 --- a/.config/codespell_ignore.txt +++ b/.config/codespell_ignore.txt @@ -54,3 +54,8 @@ wan wanna webp widgits +uper +UPER +uPER +acn +ACN diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index 2f675964992..cb9503d2f23 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -297,6 +297,9 @@ def __new__(cls, class BERcodec_Object(Generic[_K], metaclass=BERcodec_metaclass): codec = ASN1_Codecs.BER tag = ASN1_Class_UNIVERSAL.ANY + skip_tagging = False + tagging_enc = staticmethod(BER_tagging_enc) + tagging_dec = staticmethod(BER_tagging_dec) @classmethod def asn1_object(cls, val): diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 0d92161153e..d5e4f932fb3 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -9,14 +9,30 @@ """ import copy - from functools import reduce +from typing import ( + Any, + AnyStr, + Callable, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, + TYPE_CHECKING, +) +from scapy import packet from scapy.asn1.asn1 import ( ASN1_BIT_STRING, ASN1_BOOLEAN, ASN1_Class, ASN1_Class_UNIVERSAL, + ASN1_Codecs, ASN1_Decoding_Error, ASN1_Error, ASN1_INTEGER, @@ -40,28 +56,22 @@ RandField, ) -from scapy import packet - -from typing import ( - Any, - AnyStr, - Callable, - Dict, - Generic, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, - cast, - TYPE_CHECKING, -) - if TYPE_CHECKING: from scapy.asn1packet import ASN1_Packet +def _oer(): + # type: () -> Any + from scapy.contrib import oer as _m + return _m + + +def _uper(): + # type: () -> Any + from scapy.contrib import uper as _m + return _m + + class ASN1F_badsequence(Exception): pass @@ -92,6 +102,10 @@ def __init__(self, explicit_tag=None, # type: Optional[int] flexible_tag=False, # type: Optional[bool] size_len=None, # type: Optional[int] + oer_unsigned=False, # type: Optional[bool] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + uper_enum_values=None, # type: Optional[List[int]] ): # type: (...) -> None if context is not None: @@ -104,6 +118,10 @@ def __init__(self, else: self.default = self.ASN1_tag.asn1_object(default) # type: ignore self.size_len = size_len + self.oer_unsigned = oer_unsigned + self.uper_min = uper_min + self.uper_max = uper_max + self.uper_enum_values = uper_enum_values self.flexible_tag = flexible_tag if (implicit_tag is not None) and (explicit_tag is not None): err_msg = "field cannot be both implicitly and explicitly tagged" @@ -113,6 +131,7 @@ def __init__(self, # network_tag gets useful for ASN1F_CHOICE self.network_tag = int(implicit_tag or explicit_tag or self.ASN1_tag) self.owners = [] # type: List[Type[ASN1_Packet]] + self._uper_kwargs_cache = None # type: Optional[Dict[str, Any]] def register_owner(self, cls): # type: (Type[ASN1_Packet]) -> None @@ -156,14 +175,24 @@ def _apply_tagging_dec(self, s, pkt, hidden_tag=None, **kwargs): def _codec_kwargs(self, pkt): # type: (ASN1_Packet) -> Dict[str, Any] - # OER/UPER need extra constraints (oer_unsigned, uper_min/max, …) on - # every enc/dec call; override this instead of hardcoding BER size_len. - return {"size_len": self.size_len} + # OER/UPER need extra constraints on every enc/dec call. + if pkt.ASN1_codec == ASN1_Codecs.PER: + return self._uper_codec_kwargs() + kwargs = {"size_len": self.size_len} # type: Dict[str, Any] + if pkt.ASN1_codec == ASN1_Codecs.OER: + kwargs["size_len"] = self.size_len or 0 + if self.oer_unsigned: + kwargs["oer_unsigned"] = self.oer_unsigned + return kwargs def _use_object_enc(self, pkt, item): # type: (ASN1_Packet, ASN1_Object[Any]) -> bool - # BER/LDAP: item.enc() when size_len is unset. UPER must override to - # False so constrained integers go through codec.enc(**kwargs). + # BER/LDAP: item.enc() when size_len is unset. PER/OER constraints + # must go through codec.enc(**kwargs). + if pkt.ASN1_codec == ASN1_Codecs.PER: + return False + if pkt.ASN1_codec == ASN1_Codecs.OER: + return self.size_len is None and not self.oer_unsigned return self.size_len is None def _encode_item(self, pkt, item): @@ -186,7 +215,7 @@ def _encode_item(self, pkt, item): item = item.val elif hasattr(item, "self_build"): # Packet values (e.g. ASN1F_STRING_PacketField) must still go through - # the BER type codec so the universal tag/length are applied. + # the type codec so the universal tag/length are applied. item = item.self_build() codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) return codec.enc(item, **self._codec_kwargs(pkt)) @@ -218,6 +247,20 @@ def m2i(self, pkt, s): dec = codec.safedec if self.flexible_tag else codec.dec return dec(s, context=self.context, **self._codec_kwargs(pkt)) # type: ignore + def m2i_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, Any) -> _A + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + return cast( + _A, + codec.dec_from_decoder( # type: ignore[attr-defined] + dec, **self._codec_kwargs(pkt), + ), + ) + + def dissect_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, Any) -> None + self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) + def i2m(self, pkt, x): # type: (ASN1_Packet, Union[bytes, _I, _A]) -> bytes if x is None: @@ -297,6 +340,54 @@ def copy(self): # type: () -> ASN1F_field[_I, _A] return copy.copy(self) + def _uper_codec_kwargs(self, size_len=None): + # type: (Optional[int]) -> Dict[str, Any] + # These kwargs only depend on attributes set once at __init__ time, + # so the common (no override) case is cached to avoid rebuilding the + # dict on every field access during build/dissect. + if size_len is None and self._uper_kwargs_cache is not None: + return self._uper_kwargs_cache + kwargs = { + "size_len": (self.size_len if size_len is None else size_len) or 0, + "oer_unsigned": self.oer_unsigned, + "uper_min": self.uper_min, + "uper_max": self.uper_max, + } # type: Dict[str, Any] + if ( + getattr(self, "uper_extensible", False) and + self.ASN1_tag == ASN1_Class_UNIVERSAL.INTEGER + ): + kwargs["uper_extensible"] = True + if self.uper_enum_values is not None: + kwargs["uper_enum_values"] = self.uper_enum_values + if size_len is None: + self._uper_kwargs_cache = kwargs + return kwargs + + def _uper_encode_into(self, enc, pkt, value=None): + # type: (Any, ASN1_Packet, Any) -> None + if value is None: + value = getattr(pkt, self.name) + if value is None: + return + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + if isinstance(value, ASN1_Object): + if (self.ASN1_tag == ASN1_Class_UNIVERSAL.ANY or + value.tag == ASN1_Class_UNIVERSAL.RAW or + value.tag == ASN1_Class_UNIVERSAL.ERROR or + self.ASN1_tag == value.tag): + raw = value.val + else: + raise ASN1_Error( + "Encoding Error: got %r instead of an %r for field [%s]" % + (value, self.ASN1_tag, self.name) + ) + else: + raw = value + codec.encode_into( # type: ignore[attr-defined] + enc, raw, **self._codec_kwargs(pkt), + ) + ############################ # Simple ASN1 Fields # @@ -313,9 +404,32 @@ def randval(self): class ASN1F_INTEGER(ASN1F_field[int, ASN1_INTEGER]): ASN1_tag = ASN1_Class_UNIVERSAL.INTEGER + def __init__(self, + name, # type: str + default, # type: Optional[Union[int, ASN1_INTEGER]] + context=None, # type: Optional[Type[ASN1_Class]] + implicit_tag=None, # type: Optional[int] + explicit_tag=None, # type: Optional[int] + flexible_tag=False, # type: Optional[bool] + size_len=None, # type: Optional[int] + oer_unsigned=False, # type: Optional[bool] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + uper_extensible=False, # type: bool + ): + # type: (...) -> None + super(ASN1F_INTEGER, self).__init__( + name, cast(Optional[ASN1_INTEGER], default), context=context, + implicit_tag=implicit_tag, explicit_tag=explicit_tag, + flexible_tag=flexible_tag, size_len=size_len, + oer_unsigned=oer_unsigned, uper_min=uper_min, + uper_max=uper_max, + ) + self.uper_extensible = uper_extensible + def randval(self): # type: () -> RandNum - return RandNum(-2**64, 2**64 - 1) + return RandNum(-2 ** 64, 2 ** 64 - 1) class ASN1F_enum_INTEGER(ASN1F_INTEGER): @@ -344,6 +458,7 @@ def __init__(self, for k in keys: i2s[k] = enum[k] s2i[enum[k]] = k + self.uper_enum_values = list(keys) def i2m(self, pkt, # type: ASN1_Packet @@ -378,12 +493,16 @@ def __init__(self, context=None, # type: Optional[Any] implicit_tag=None, # type: Optional[int] explicit_tag=None, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] ): # type: (...) -> None super(ASN1F_BIT_STRING, self).__init__( name, None, context=context, implicit_tag=implicit_tag, - explicit_tag=explicit_tag + explicit_tag=explicit_tag, + uper_min=uper_min, + uper_max=uper_max, ) if isinstance(default, (bytes, str)): self.default = ASN1_BIT_STRING(default, @@ -493,13 +612,18 @@ class ASN1F_SEQUENCE(ASN1F_field[List[Any], List[Any]]): def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None + uper_extensible = kwargs.pop("uper_extensible", False) name = "dummy_seq_name" default = [field.default for field in seq] super(ASN1F_SEQUENCE, self).__init__( name, default, **kwargs ) + self.uper_extensible = uper_extensible self.seq = seq self.islist = len(seq) > 1 + self._optionals = tuple( + f for f in seq if isinstance(f, (ASN1F_optional, ASN1F_DEFAULT)) + ) def __repr__(self): # type: () -> str @@ -514,6 +638,46 @@ def get_fields_list(self): return reduce(lambda x, y: x + y.get_fields_list(), self.seq, []) + def _dissect_sequence_children(self, pkt, s): + # type: (Any, bytes) -> bytes + if len(s) == 0: + for obj in self.seq: + obj.set_val(pkt, None) + return s + for obj in self.seq: + try: + s = obj.dissect(pkt, s) + except ASN1F_badsequence: + break + return s + + def _m2i_oer(self, pkt, s): + # type: (Any, bytes) -> Tuple[Any, bytes] + s = self._apply_tagging_dec(s, pkt, _fname=pkt.name) + s = self._dissect_sequence_children(pkt, s) + return [], s + + def _m2i_per(self, pkt, s): + # type: (Any, bytes) -> Tuple[Any, bytes] + dec = _uper().UPER_Decoder(s) + self._uper_dissect_from_decoder(pkt, dec) + if _uper().UPER_has_unexpected_remainder(dec): + raise _uper().UPER_Decoding_Error( + "unexpected remainder", + remaining=dec.remaining(), + ) + return [], b"" + + def _m2i_ber(self, pkt, s): + # type: (Any, bytes) -> Tuple[Any, bytes] + s = self._apply_tagging_dec(s, pkt, _fname=pkt.name) + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + i, s, remain = codec.check_type_check_len(s) + s = self._dissect_sequence_children(pkt, s) + if len(s) > 0: + raise BER_Decoding_Error("unexpected remainder", remaining=s) + return [], remain + def m2i(self, pkt, s): # type: (Any, bytes) -> Tuple[Any, bytes] """ @@ -524,24 +688,36 @@ def m2i(self, pkt, s): Thus m2i returns an empty list (along with the proper remainder). It is discarded by dissect() and should not be missed elsewhere. """ - s = self._apply_tagging_dec(s, pkt, _fname=pkt.name) - codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - i, s, remain = codec.check_type_check_len(s) - if len(s) == 0: - for obj in self.seq: - obj.set_val(pkt, None) - else: - for obj in self.seq: - try: - s = obj.dissect(pkt, s) - except ASN1F_badsequence: - break - if len(s) > 0: - raise BER_Decoding_Error( - "unexpected remainder in %s" % pkt.name, - remaining=s, + if pkt.ASN1_codec == ASN1_Codecs.OER: + return self._m2i_oer(pkt, s) + if pkt.ASN1_codec == ASN1_Codecs.PER: + return self._m2i_per(pkt, s) + return self._m2i_ber(pkt, s) + + def _uper_dissect_from_decoder(self, pkt, dec): + # type: (Any, Any) -> None + if self.uper_extensible: + if dec.read_bit(): + raise _uper().UPER_Decoding_Error( + "ASN1F_SEQUENCE: extension additions are not supported" ) - return [], remain + presence = [dec.read_bit() for _ in self._optionals] + opt_idx = 0 + for obj in self.seq: + if isinstance(obj, (ASN1F_optional, ASN1F_DEFAULT)): + if not presence[opt_idx]: + obj.set_absent(pkt) + opt_idx += 1 + continue + opt_idx += 1 + try: + obj.dissect_from_decoder(pkt, dec) + except ASN1F_badsequence: + break + + def dissect_from_decoder(self, pkt, dec): + # type: (Any, Any) -> None + self._uper_dissect_from_decoder(pkt, dec) def dissect(self, pkt, s): # type: (Any, bytes) -> bytes @@ -550,10 +726,25 @@ def dissect(self, pkt, s): def build(self, pkt): # type: (ASN1_Packet) -> bytes + if pkt.ASN1_codec == ASN1_Codecs.PER: + enc = _uper().UPER_Encoder() + self._uper_encode_into(enc, pkt) + return super(ASN1F_SEQUENCE, self).i2m(pkt, enc.as_bytes()) s = reduce(lambda x, y: x + y.build(pkt), self.seq, b"") return super(ASN1F_SEQUENCE, self).i2m(pkt, s) + def _uper_encode_into(self, enc, pkt, value=None): + # type: (Any, ASN1_Packet, Optional[Any]) -> None + if self.uper_extensible: + enc.append_bit(0) + for opt in self._optionals: + enc.append_bit(0 if opt.is_empty(pkt) else 1) + for obj in self.seq: + if isinstance(obj, (ASN1F_optional, ASN1F_DEFAULT)) and obj.is_empty(pkt): + continue + obj._uper_encode_into(enc, pkt) + class ASN1F_SET(ASN1F_SEQUENCE): ASN1_tag = ASN1_Class_UNIVERSAL.SET @@ -568,7 +759,7 @@ class ASN1F_SET(ASN1F_SEQUENCE): class ASN1F_SEQUENCE_OF(ASN1F_field[List[_SEQ_T], - List[ASN1_Object[Any]]]): +List[ASN1_Object[Any]]]): """ Two types are allowed as cls: ASN1_Packet, ASN1F_field """ @@ -582,6 +773,9 @@ def __init__(self, context=None, # type: Optional[Any] implicit_tag=None, # type: Optional[Any] explicit_tag=None, # type: Optional[Any] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + uper_extensible=False, # type: bool ): # type: (...) -> None if isinstance(cls, type) and issubclass(cls, ASN1F_field) or \ @@ -604,6 +798,28 @@ def __init__(self, implicit_tag=implicit_tag, explicit_tag=explicit_tag ) self.default = default + self.uper_min = uper_min + self.uper_max = uper_max + self.uper_extensible = uper_extensible + + def _uper_count_enc(self, enc, count): + # type: (Any, int) -> None + if self.uper_min is not None and self.uper_max is not None: + _uper().UPER_constrained_int_enc(count, self.uper_min, self.uper_max, enc=enc) + else: + enc.append_length_determinant(count) + + def _uper_count_dec(self, dec): + # type: (Any) -> int + if self.uper_min is not None and self.uper_max is not None: + size = self.uper_max - self.uper_min + return cast( + int, + dec.read_non_negative_binary_integer( + _uper().UPER_bits_for_range(size), + ) + self.uper_min, + ) + return cast(int, dec.read_length_determinant()) def is_empty(self, pkt, # type: ASN1_Packet @@ -611,11 +827,90 @@ def is_empty(self, # type: (...) -> bool return ASN1F_field.is_empty(self, pkt) + def _extract_packet_from_decoder(self, dec, pkt): + # type: (Any, ASN1_Packet) -> Tuple[Any, bytes] + if self.holds_packets: + p = self.cls() + p.add_underlayer(pkt) + p.ASN1_root.dissect_from_decoder(p, dec) + return p, b"" + return self.fld.m2i_from_decoder(pkt, dec), b"" + + def m2i_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, Any) -> List[Any] + if self.uper_extensible and dec.read_bit(): + count = dec.read_length_determinant() + else: + count = self._uper_count_dec(dec) + lst = [] + for _ in range(count): + item, _ = self._extract_packet_from_decoder(dec, pkt) + lst.append(item) + return lst + + def _uper_encode_into(self, enc, pkt, value=None): + # type: (Any, ASN1_Packet, Any) -> None + if value is None: + value = getattr(pkt, self.name) + if value is None: + self._uper_count_enc(enc, 0) + return + count = len(value) + if self.uper_extensible: + if ( + self.uper_min is not None and self.uper_max is not None and + self.uper_min <= count <= self.uper_max + ): + enc.append_bit(0) + else: + enc.append_bit(1) + enc.append_length_determinant(count) + for item in value: + if self.holds_packets: + cast("ASN1_Packet", item).ASN1_root._uper_encode_into( + enc, item, + ) + else: + self.fld._uper_encode_into(enc, pkt, item) + return + self._uper_count_enc(enc, count) + for item in value: + if self.holds_packets: + cast("ASN1_Packet", item).ASN1_root._uper_encode_into( + enc, item, + ) + else: + self.fld._uper_encode_into(enc, pkt, item) + def m2i(self, pkt, # type: ASN1_Packet s, # type: bytes ): # type: (...) -> Tuple[List[Any], bytes] + if pkt.ASN1_codec == ASN1_Codecs.OER: + s = self._apply_tagging_dec(s, pkt) + count, s = _oer().OER_unsigned_integer_dec(s) + lst = [] + for _ in range(count): + c, s = self._extract_packet(s, pkt) # type: ignore + if c: + lst.append(c) + return lst, s + if pkt.ASN1_codec == ASN1_Codecs.PER: + dec = _uper().UPER_Decoder(s) + if self.uper_extensible and dec.read_bit(): + count = dec.read_length_determinant() + else: + count = self._uper_count_dec(dec) + lst = [] + for _ in range(count): + c, _ = self._extract_packet_from_decoder(dec, pkt) + if c: + lst.append(c) + if _uper().UPER_has_unexpected_remainder(dec): + raise _uper().UPER_Decoding_Error("unexpected remainder", + remaining=dec.remaining()) + return lst, b"" s = self._apply_tagging_dec(s, pkt) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) i, s, remain = codec.check_type_check_len(s) @@ -639,12 +934,27 @@ def build(self, pkt): s = cast(Union[List[_SEQ_T], bytes], val) elif val is None: s = b"" - elif self.holds_packets: - s = b"".join(bytes(i) for i in val) + if pkt.ASN1_codec == ASN1_Codecs.OER: + s = _oer().OER_unsigned_integer_enc(0) + elif pkt.ASN1_codec == ASN1_Codecs.PER: + enc = _uper().UPER_Encoder() + enc.append_length_determinant(0) + s = enc.as_bytes() else: - # BER: element fields may carry implicit/explicit tags; i2m - # matches m2i()/fld.m2i(). (Packet elements use bytes() above.) - s = b"".join(self.fld.i2m(pkt, i) for i in val) + if pkt.ASN1_codec == ASN1_Codecs.PER: + enc = _uper().UPER_Encoder() + self._uper_encode_into(enc, pkt, val) + s = enc.as_bytes() + elif self.holds_packets: + s = b"".join(bytes(i) for i in val) + if pkt.ASN1_codec == ASN1_Codecs.OER: + s = _oer().OER_unsigned_integer_enc(len(val)) + s + else: + # BER/OER: element fields may carry implicit/explicit tags; + # i2m matches m2i()/fld.m2i(). + s = b"".join(self.fld.i2m(pkt, i) for i in val) + if pkt.ASN1_codec == ASN1_Codecs.OER: + s = _oer().OER_unsigned_integer_enc(len(val)) + s return self.i2m(pkt, s) def i2repr(self, pkt, x): @@ -690,6 +1000,7 @@ class ASN1F_optional(ASN1F_element): """ ASN.1 field that is optional. """ + def __init__(self, field): # type: (ASN1F_field[Any, Any]) -> None field.flexible_tag = False @@ -715,6 +1026,10 @@ def dissect(self, pkt, s): self._field.set_val(pkt, None) return s + def dissect_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, Any) -> None + return self._field.dissect_from_decoder(pkt, dec) + def build(self, pkt): # type: (ASN1_Packet) -> bytes if self._field.is_empty(pkt): @@ -729,12 +1044,57 @@ def i2repr(self, pkt, x): # type: (ASN1_Packet, Any) -> str return self._field.i2repr(pkt, x) + def set_val(self, pkt, val): + # type: (ASN1_Packet, Any) -> None + self._field.set_val(pkt, val) + + def set_absent(self, pkt): + # type: (ASN1_Packet) -> None + self.set_val(pkt, None) + + def is_empty(self, pkt): + # type: (ASN1_Packet) -> bool + # Delegate to the wrapped field (e.g. SEQUENCE checks children). + return self._field.is_empty(pkt) + + def _uper_encode_into(self, enc, pkt, value=None): + # type: (Any, ASN1_Packet, Optional[Any]) -> None + self._field._uper_encode_into(enc, pkt, value) + + +class ASN1F_DEFAULT(ASN1F_optional): + """ + ASN.1 field with a DEFAULT value (PER presence bit). + """ + + def __init__(self, field, default): + # type: (ASN1F_field[Any, Any], Any) -> None + super(ASN1F_DEFAULT, self).__init__(field) + self._default = default + + def is_empty(self, pkt): + # type: (ASN1_Packet) -> bool + val = getattr(pkt, self._field.name, None) + if val is None: + return True + if isinstance(val, ASN1_Object): + val = val.val + default = self._default + if isinstance(default, ASN1_Object): + default = default.val + return bool(val == default) + + def set_absent(self, pkt): + # type: (ASN1_Packet) -> None + self.set_val(pkt, self._default) + class ASN1F_omit(ASN1F_field[None, None]): """ ASN.1 field that is not specified. This is simply omitted on the network. This is different from ASN1F_NULL which has a network representation. """ + def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[None, bytes] return None, s @@ -761,6 +1121,7 @@ def __init__(self, name, default, *args, **kwargs): if "implicit_tag" in kwargs: err_msg = "ASN1F_CHOICE has been called with an implicit_tag" raise ASN1_Error(err_msg) + uper_extensible = kwargs.pop("uper_extensible", False) self.implicit_tag = None for kwarg in ["context", "explicit_tag"]: setattr(self, kwarg, kwargs.get(kwarg)) @@ -768,9 +1129,12 @@ def __init__(self, name, default, *args, **kwargs): name, None, context=self.context, explicit_tag=self.explicit_tag ) + self.uper_extensible = uper_extensible self.default = default self.current_choice = None self.choices = {} # type: Dict[int, _CHOICE_T] + self.choice_order = [] # type: List[int] + self.choice_list = [] # type: List[_CHOICE_T] self.pktchoices = {} for p in args: if hasattr(p, "ASN1_root"): @@ -778,21 +1142,75 @@ def __init__(self, name, default, *args, **kwargs): # should be ASN1_Packet if hasattr(p.ASN1_root, "choices"): root = cast(ASN1F_CHOICE, p.ASN1_root) - for k, v in root.choices.items(): - # ASN1F_CHOICE recursion - self.choices[k] = v + for k in root.choice_order: + self._register_choice(k, root.choices[k]) else: - self.choices[p.ASN1_root.network_tag] = p + self._register_choice(p.ASN1_root.network_tag, p) elif hasattr(p, "ASN1_tag"): if isinstance(p, type): # should be ASN1F_field class - self.choices[int(p.ASN1_tag)] = p + self._register_choice(int(p.ASN1_tag), p) else: # should be ASN1F_PACKET instance - self.choices[p.network_tag] = p + self._register_choice(p.network_tag, p) self.pktchoices[hash(p.cls)] = (p.implicit_tag, p.explicit_tag) # noqa: E501 else: raise ASN1_Error("ASN1F_CHOICE: no tag found for one field") + self._tag_to_index = { + tag: idx for idx, tag in enumerate(self.choice_order) + } + + def _register_choice(self, tag, choice): + # type: (int, _CHOICE_T) -> None + self.choices[tag] = choice + self.choice_order.append(tag) + self.choice_list.append(choice) + + def _dissect_choice_payload(self, pkt, choice, payload): + # type: (ASN1_Packet, _CHOICE_T, bytes) -> Tuple[ASN1_Object[Any], bytes] + if hasattr(choice, "ASN1_root"): + return self.extract_packet(choice, payload, _underlayer=pkt) # type: ignore + if isinstance(choice, type): + return choice(self.name, b"").m2i(pkt, payload) + return choice.m2i(pkt, payload) + + def _m2i_oer(self, pkt, s): + # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] + s = self._apply_tagging_dec(s, pkt) + tag, payload = _oer().OER_id_dec(s) + return self._m2i_tagged(pkt, tag, payload) + + def _m2i_per(self, pkt, s): + # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] + dec = _uper().UPER_Decoder(s) + val = self.m2i_from_decoder(pkt, dec) + if _uper().UPER_has_unexpected_remainder(dec): + raise _uper().UPER_Decoding_Error( + "unexpected remainder", + remaining=dec.remaining(), + ) + return val, b"" + + def _m2i_ber(self, pkt, s): + # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] + s = self._apply_tagging_dec(s, pkt) + tag, _ = BER_id_dec(s) + return self._m2i_tagged(pkt, tag, s) + + def _m2i_tagged(self, pkt, tag, payload): + # type: (ASN1_Packet, int, bytes) -> Tuple[ASN1_Object[Any], bytes] + if tag in self.choices: + choice = self.choices[tag] + elif self.flexible_tag: + choice = ASN1F_field + else: + raise ASN1_Error( + "ASN1F_CHOICE: unexpected field in '%s' " + "(tag %s not in possible tags %s)" % ( + self.name, tag, list(self.choices.keys()) + ) + ) + return self._dissect_choice_payload(pkt, choice, payload) def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] @@ -802,33 +1220,92 @@ def m2i(self, pkt, s): """ if len(s) == 0: raise ASN1_Error("ASN1F_CHOICE: got empty string") - s = self._apply_tagging_dec(s, pkt) - tag, _ = BER_id_dec(s) - if tag in self.choices: - choice = self.choices[tag] - else: - if self.flexible_tag: - choice = ASN1F_field - else: - raise ASN1_Error( - "ASN1F_CHOICE: unexpected field in '%s' " - "(tag %s not in possible tags %s)" % ( - self.name, tag, list(self.choices.keys()) - ) + if pkt.ASN1_codec == ASN1_Codecs.OER: + return self._m2i_oer(pkt, s) + if pkt.ASN1_codec == ASN1_Codecs.PER: + return self._m2i_per(pkt, s) + return self._m2i_ber(pkt, s) + + def _choice_tag_for(self, x): + # type: (Any) -> Optional[int] + index = self._choice_index_for(x) + return None if index is None else self.choice_order[index] + + def _choice_index_for(self, x): + # type: (Any) -> Optional[int] + for index, choice in enumerate(self.choice_list): + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + if isinstance(x, choice): + return index + elif hasattr(choice, "ASN1_tag"): + if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + return index + return None + + def _choice_for_index(self, index): + # type: (int) -> _CHOICE_T + return self.choice_list[index] + + def m2i_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, Any) -> ASN1_Object[Any] + if self.uper_extensible: + if dec.read_bit(): + raise _uper().UPER_Decoding_Error( + "ASN1F_CHOICE: extension additions are not supported" ) + if len(self.choice_order) > 1: + index, _ = _uper().UPER_choice_index_dec(b"", len(self.choice_order), dec=dec) + else: + index = 0 + if index >= len(self.choice_order): + raise ASN1_Error( + "ASN1F_CHOICE: unexpected index %s in '%s'" % + (index, self.name) + ) + choice = self._choice_for_index(index) + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + pkt_cls = cast("Type[ASN1_Packet]", choice) + p = pkt_cls() + p.add_underlayer(pkt) + p.ASN1_root.dissect_from_decoder(p, dec) + return cast(ASN1_Object[Any], p) + if isinstance(choice, type): + return cast( + ASN1_Object[Any], + choice(self.name, b"").m2i_from_decoder(pkt, dec), + ) + return cast(ASN1_Object[Any], choice.m2i_from_decoder(pkt, dec)) + + def _uper_encode_into(self, enc, pkt, value=None): + # type: (Any, ASN1_Packet, Any) -> None + if value is None: + value = getattr(pkt, self.name) + index = self._choice_index_for(value) + if index is None: + raise ASN1_Error( + "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % + self.name + ) + if self.uper_extensible: + enc.append_bit(0) + if len(self.choice_order) > 1: + _uper().UPER_choice_index_enc(index, len(self.choice_order), enc=enc) + choice = self._choice_for_index(index) if hasattr(choice, "ASN1_root"): - # we don't want to import ASN1_Packet in this module... - return self.extract_packet(choice, s, _underlayer=pkt) # type: ignore + cast("ASN1_Packet", value).ASN1_root._uper_encode_into(enc, value) elif isinstance(choice, type): - return choice(self.name, b"").m2i(pkt, s) + choice(self.name, b"")._uper_encode_into(enc, pkt, value) else: - # XXX check properly if this is an ASN1F_PACKET - return choice.m2i(pkt, s) + choice._uper_encode_into(enc, pkt, value) def i2m(self, pkt, x): # type: (ASN1_Packet, Any) -> bytes if x is None: s = b"" + elif pkt.ASN1_codec == ASN1_Codecs.PER: + enc = _uper().UPER_Encoder() + self._uper_encode_into(enc, pkt, x) + s = enc.as_bytes() else: # Use the packet codec for ASN1_Object values; bytes(x) would # follow conf.ASN1_default_codec instead. @@ -836,7 +1313,11 @@ def i2m(self, pkt, x): s = x.enc(pkt.ASN1_codec) else: s = bytes(x) - if hash(type(x)) in self.pktchoices: + if pkt.ASN1_codec == ASN1_Codecs.OER: + alt_tag = self._choice_tag_for(x) + if alt_tag is not None: + s = _oer().OER_tag_enc(alt_tag & 0x3f, alt_tag & 0xc0) + s + elif hash(type(x)) in self.pktchoices: imp, exp = self.pktchoices[hash(type(x))] s = self._tagging_enc( pkt, s, @@ -886,18 +1367,39 @@ def __init__(self, self.network_tag = 16 | 0x20 # 16 + CONSTRUCTED self.default = default + def _resolve_cls(self, pkt): + # type: (ASN1_Packet) -> Type[ASN1_Packet] + if self.next_cls_cb: + return self.next_cls_cb(pkt) or self.cls + return self.cls + + def m2i_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, Any) -> Optional[ASN1_Packet] + cls = self._resolve_cls(pkt) + p = cls() + p.add_underlayer(pkt) + p.ASN1_root.dissect_from_decoder(p, dec) + return p + + def _uper_encode_into(self, enc, pkt, value=None): + # type: (Any, ASN1_Packet, Any) -> None + if value is None: + value = getattr(pkt, self.name) + if value is None: + return + if isinstance(value, ASN1_Object): + value = value.val + cast("ASN1_Packet", value).ASN1_root._uper_encode_into(enc, value) + def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[Any, bytes] - if self.next_cls_cb: - cls = self.next_cls_cb(pkt) or self.cls - else: - cls = self.cls + cls = self._resolve_cls(pkt) if not hasattr(cls, "ASN1_root"): # A normal Packet (!= ASN1) return self.extract_packet(cls, s, _underlayer=pkt) s = self._apply_tagging_dec( s, pkt, - hidden_tag=cls.ASN1_root.ASN1_tag, # noqa: E501 + hidden_tag=cls.ASN1_root.ASN1_tag, _fname=self.name, ) if not s: @@ -911,6 +1413,10 @@ def i2m(self, # type: (...) -> bytes if x is None: s = b"" + elif pkt.ASN1_codec == ASN1_Codecs.PER: + enc = _uper().UPER_Encoder() + self._uper_encode_into(enc, pkt, x) + s = enc.as_bytes() elif isinstance(x, bytes): s = x elif isinstance(x, ASN1_Object): @@ -979,10 +1485,7 @@ def m2i(self, pkt, s): # type: ignore else: return None, bit_string.val_readable if len(s) > 0: - raise BER_Decoding_Error( - "unexpected remainder in %s" % pkt.name, - remaining=s, - ) + raise BER_Decoding_Error("unexpected remainder", remaining=s) return p, remain def i2m(self, pkt, x): # type: ignore @@ -1003,6 +1506,8 @@ def __init__(self, context=None, # type: Optional[Any] implicit_tag=None, # type: Optional[int] explicit_tag=None, # type: Optional[Any] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] ): # type: (...) -> None self.mapping = mapping @@ -1011,7 +1516,9 @@ def __init__(self, default_readable=False, context=context, implicit_tag=implicit_tag, - explicit_tag=explicit_tag + explicit_tag=explicit_tag, + uper_min=uper_min, + uper_max=uper_max, ) def any2i(self, pkt, x): diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py new file mode 100644 index 00000000000..ab68b2b0e83 --- /dev/null +++ b/scapy/contrib/oer.py @@ -0,0 +1,825 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Octet Encoding Rules (OER) for ASN.1 + +Basic-OER as specified in ITU-T X.696 | ISO/IEC 8825-7. +""" + +import struct + +from scapy.error import warning +from scapy.compat import chb, orb, bytes_encode +from scapy.utils import binrepr, inet_aton, inet_ntoa +from scapy.asn1.ber import BER_num_dec, BER_num_enc +from scapy.asn1.asn1 import ( + ASN1Tag, + ASN1_BADTAG, + ASN1_BadTag_Decoding_Error, + ASN1_Class, + ASN1_Class_UNIVERSAL, + ASN1_Codecs, + ASN1_DECODING_ERROR, + ASN1_Decoding_Error, + ASN1_Encoding_Error, + ASN1_Error, + ASN1_Object, + _ASN1_ERROR, +) + +from typing import ( + Any, + AnyStr, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +################## +# OER encoding # +################## + + +class OER_Exception(Exception): + pass + + +class OER_Encoding_Error(ASN1_Encoding_Error): + def __init__(self, + msg, # type: str + encoded=None, # type: Optional[Union['OERcodec_Object[Any]', str]] + remaining=b"" # type: bytes + ): + # type: (...) -> None + Exception.__init__(self, msg) + self.remaining = remaining + self.encoded = encoded + + def __str__(self): + # type: () -> str + s = Exception.__str__(self) + if isinstance(self.encoded, ASN1_Object): + s += "\n### Already encoded ###\n%s" % self.encoded.strshow() + else: + s += "\n### Already encoded ###\n%r" % self.encoded + s += "\n### Remaining ###\n%r" % self.remaining + return s + + +class OER_Decoding_Error(ASN1_Decoding_Error): + def __init__(self, + msg, # type: str + decoded=None, # type: Optional[Any] + remaining=b"" # type: bytes + ): + # type: (...) -> None + Exception.__init__(self, msg) + self.remaining = remaining + self.decoded = decoded + + def __str__(self): + # type: () -> str + s = Exception.__str__(self) + if isinstance(self.decoded, ASN1_Object): + s += "\n### Already decoded ###\n%s" % self.decoded.strshow() + else: + s += "\n### Already decoded ###\n%r" % self.decoded + s += "\n### Remaining ###\n%r" % self.remaining + return s + + +class OER_BadTag_Decoding_Error(OER_Decoding_Error, + ASN1_BadTag_Decoding_Error): + pass + + +# OER tag classes (bits 8-7 of the first identifier octet) +OER_CLASS_UNIVERSAL = 0x00 +OER_CLASS_APPLICATION = 0x40 +OER_CLASS_CONTEXT = 0x80 +OER_CLASS_PRIVATE = 0xc0 + + +def OER_len_enc(ll): + # type: (int) -> bytes + if ll < 128: + return chb(ll) + encoded = [] + value = ll + while value > 0: + encoded.insert(0, value & 0xff) + value >>= 8 + if len(encoded) > 127: + raise OER_Exception( + "OER_len_enc: Length too long (%i) to be encoded" % len(encoded) + ) + return chb(0x80 | len(encoded)) + bytes(encoded) + + +def OER_len_dec(s): + # type: (bytes) -> Tuple[int, bytes] + if not s: + raise OER_Decoding_Error("OER_len_dec: got empty string", remaining=s) + tmp_len = orb(s[0]) + if not tmp_len & 0x80: + return tmp_len, s[1:] + tmp_len &= 0x7f + if len(s) <= tmp_len: + raise OER_Decoding_Error( + "OER_len_dec: Got %i bytes while expecting %i" % + (len(s) - 1, tmp_len), + remaining=s + ) + ll = 0 + for c in s[1:tmp_len + 1]: + ll <<= 8 + ll |= orb(c) + return ll, s[tmp_len + 1:] + + +def OER_signed_integer_enc(i): + # type: (int) -> bytes + if i < 0: + number_of_bits = i.bit_length() + number_of_bytes = (number_of_bits + 7) // 8 + value = (1 << (8 * number_of_bytes)) + i + if (value & (1 << (8 * number_of_bytes - 1))) == 0: + value |= (0xff << (8 * number_of_bytes)) + number_of_bytes += 1 + elif i > 0: + number_of_bits = i.bit_length() + number_of_bytes = (number_of_bits + 7) // 8 + if number_of_bits == (8 * number_of_bytes): + number_of_bytes += 1 + value = i + else: + number_of_bytes = 1 + value = 0 + return OER_len_enc(number_of_bytes) + value.to_bytes(number_of_bytes, "big") + + +def OER_signed_integer_dec(s): + # type: (bytes) -> Tuple[int, bytes] + number_of_bytes, s = OER_len_dec(s) + if len(s) < number_of_bytes: + raise OER_Decoding_Error( + "OER_signed_integer_dec: Got %i bytes while expecting %i" % + (len(s), number_of_bytes), + remaining=s + ) + value = int.from_bytes(s[:number_of_bytes], "big") + number_of_bits = 8 * number_of_bytes + if value & (1 << (number_of_bits - 1)): + value -= (1 << number_of_bits) - 1 + value -= 1 + return value, s[number_of_bytes:] + + +def OER_unsigned_integer_enc(i): + # type: (int) -> bytes + number_of_bits = max(i.bit_length(), 1) + number_of_bytes = (number_of_bits + 7) // 8 + return OER_len_enc(number_of_bytes) + i.to_bytes(number_of_bytes, "big") + + +def OER_unsigned_integer_dec(s): + # type: (bytes) -> Tuple[int, bytes] + number_of_bytes, s = OER_len_dec(s) + if len(s) < number_of_bytes: + raise OER_Decoding_Error( + "OER_unsigned_integer_dec: Got %i bytes while expecting %i" % + (len(s), number_of_bytes), + remaining=s + ) + value = int.from_bytes(s[:number_of_bytes], "big") + return value, s[number_of_bytes:] + + +def OER_fixed_integer_enc(i, length, signed=True): + # type: (int, int, bool) -> bytes + fmt = {1: ">b", 2: ">h", 4: ">i", 8: ">q"} if signed else { + 1: ">B", 2: ">H", 4: ">I", 8: ">Q" + } + try: + return struct.pack(fmt[length], i) + except KeyError: + raise OER_Encoding_Error( + "OER_fixed_integer_enc: invalid length %i" % length + ) + + +def OER_fixed_integer_dec(s, length, signed=True): + # type: (bytes, int, bool) -> Tuple[int, bytes] + if len(s) < length: + raise OER_Decoding_Error( + "OER_fixed_integer_dec: Got %i bytes while expecting %i" % + (len(s), length), + remaining=s + ) + fmt = {1: ">b", 2: ">h", 4: ">i", 8: ">q"} if signed else { + 1: ">B", 2: ">H", 4: ">I", 8: ">Q" + } + try: + return struct.unpack(fmt[length], s[:length])[0], s[length:] + except KeyError: + raise OER_Decoding_Error( + "OER_fixed_integer_dec: invalid length %i" % length, + remaining=s + ) + + +def OER_enumerated_enc(i): + # type: (int) -> bytes + if 0 <= i <= 127: + return chb(i) + body = OER_signed_integer_enc(i)[1:] + return chb(0x80 | len(body)) + body + + +def OER_enumerated_dec(s): + # type: (bytes) -> Tuple[int, bytes] + if not s: + raise OER_Decoding_Error("OER_enumerated_dec: got empty string", + remaining=s) + first = orb(s[0]) + if not (first & 0x80): + return first, s[1:] + length = first & 0x7f + if len(s) < length + 1: + raise OER_Decoding_Error( + "OER_enumerated_dec: Got %i bytes while expecting %i" % + (len(s) - 1, length), + remaining=s + ) + value = int.from_bytes(s[1:length + 1], "big", signed=True) + return value, s[length + 1:] + + +def OER_tag_enc(n, tag_class=OER_CLASS_CONTEXT): + # type: (int, int) -> bytes + if n < 63: + return chb(tag_class | n) + tag = bytearray([tag_class | 0x3f]) + encoded = [] + value = n + while value > 0: + encoded.append(0x80 | (value & 0x7f)) + value >>= 7 + encoded[0] &= 0x7f + encoded.reverse() + tag.extend(encoded) + return bytes(tag) + + +def OER_tag_dec(s): + # type: (bytes) -> Tuple[int, int, bytes] + if not s: + raise OER_Decoding_Error("OER_tag_dec: got empty string", remaining=s) + first = orb(s[0]) + tag_class = first & 0xc0 + tag_number = first & 0x3f + if tag_number != 0x3f: + return tag_class, tag_number, s[1:] + tag_number = 0 + i = 1 + while i < len(s): + c = orb(s[i]) + tag_number <<= 7 + tag_number |= c & 0x7f + i += 1 + if not (c & 0x80): + break + else: + raise OER_Decoding_Error("OER_tag_dec: unfinished tag", remaining=s) + return tag_class, tag_number, s[i:] + + +def OER_id_dec(s): + # type: (bytes) -> Tuple[int, bytes] + tag_class, tag_number, remainder = OER_tag_dec(s) + return tag_class | tag_number, remainder + + +def OER_tagging_dec(s, # type: bytes + hidden_tag=None, # type: Optional[int | ASN1Tag] + implicit_tag=None, # type: Optional[int] + explicit_tag=None, # type: Optional[int] + safe=False, # type: Optional[bool] + _fname="", # type: str + ): + # type: (...) -> Tuple[Optional[int], bytes] + # OER does not use implicit tagging. Explicit tags are encoded as choice + # alternatives (tag + value). + real_tag = None + if explicit_tag is not None and len(s) > 0: + err_msg = ( + "OER_tagging_dec: observed tag 0x%.02x does not " + "match expected tag 0x%.02x (%s)" + ) + tag_class, tag_number, remainder = OER_tag_dec(s) + observed = tag_class | tag_number + if observed != explicit_tag: + if not safe: + raise OER_Decoding_Error( + err_msg % (observed, explicit_tag, _fname), + remaining=s) + real_tag = observed + s = remainder + return real_tag, s + + +def OER_tagging_enc(s, implicit_tag=None, explicit_tag=None): + # type: (bytes, Optional[int], Optional[int]) -> bytes + if explicit_tag is not None: + return OER_tag_enc(explicit_tag & 0x3f, explicit_tag & 0xc0) + s + return s + + +class OERcodec_metaclass(type): + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type['OERcodec_Object[Any]'] + c = cast('Type[OERcodec_Object[Any]]', + super(OERcodec_metaclass, cls).__new__(cls, name, bases, dct)) + try: + c.tag.register(c.codec, c) + except Exception: + warning("Error registering %r for %r" % (c.tag, c.codec)) + return c + + +_K = TypeVar('_K') + + +class OERcodec_Object(Generic[_K], metaclass=OERcodec_metaclass): + codec = ASN1_Codecs.OER + tag = ASN1_Class_UNIVERSAL.ANY + @classmethod + def asn1_object(cls, val): + # type: (_K) -> ASN1_Object[_K] + return cls.tag.asn1_object(val) + + @classmethod + def check_string(cls, s): + # type: (bytes) -> None + if not s: + raise OER_Decoding_Error( + "%s: Got empty object while expecting %r" % + (cls.__name__, cls.tag), remaining=s + ) + + @classmethod + def check_type(cls, s): + # type: (bytes) -> bytes + cls.check_string(s) + return s + + @classmethod + def check_type_get_len(cls, s): + # type: (bytes) -> Tuple[int, bytes] + cls.check_string(s) + return len(s), s + + @classmethod + def check_type_check_len(cls, s): + # type: (bytes) -> Tuple[int, bytes, bytes] + cls.check_string(s) + return len(s), s, b"" + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[Any], bytes] + raise OER_Decoding_Error( + "%s: Cannot decode unknown OER type without context" % + cls.__name__, remaining=s + ) + + @classmethod + def dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] + if not safe: + return cls.do_dec(s, context, safe, size_len, oer_unsigned) + try: + return cls.do_dec(s, context, safe, size_len, oer_unsigned) + except OER_BadTag_Decoding_Error as e: + o, remain = OERcodec_Object.dec( + e.remaining, context, safe, size_len, oer_unsigned + ) + return ASN1_BADTAG(o), remain + except OER_Decoding_Error as e: + return ASN1_DECODING_ERROR(s, exc=e), b"" + except ASN1_Error as e: + return ASN1_DECODING_ERROR(s, exc=e), b"" + + @classmethod + def safedec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] + return cls.dec( + s, context, safe=True, + size_len=size_len, oer_unsigned=oer_unsigned, + ) + + @classmethod + def enc(cls, s, size_len=0, **_kwargs): + # type: (_K, Optional[int]) -> bytes + if isinstance(s, (str, bytes)): + return OERcodec_STRING.enc(s, size_len=size_len) + else: + try: + return OERcodec_INTEGER.enc(int(s), size_len=size_len) # type: ignore + except TypeError: + raise TypeError("Trying to encode an invalid value !") + + +ASN1_Codecs.OER.register_stem(OERcodec_Object) +ASN1_Codecs.OER.register_tagging(OER_tagging_enc, OER_tagging_dec) + + +########################## +# OERcodec objects # +########################## + +class OERcodec_INTEGER(OERcodec_Object[int]): + tag = ASN1_Class_UNIVERSAL.INTEGER + + @classmethod + def enc(cls, i, size_len=0, **_kwargs): + # type: (int, Optional[int]) -> bytes + if size_len in (1, 2, 4, 8): + if i >= 0: + if size_len == 1 and 0 <= i <= 255: + return OER_fixed_integer_enc(i, 1, signed=False) + if size_len == 2 and 0 <= i <= 65535: + return OER_fixed_integer_enc(i, 2, signed=False) + if size_len == 4 and 0 <= i <= 4294967295: + return OER_fixed_integer_enc(i, 4, signed=False) + if size_len == 8 and 0 <= i <= 18446744073709551615: + return OER_fixed_integer_enc(i, 8, signed=False) + return OER_fixed_integer_enc(i, size_len, signed=True) + return OER_signed_integer_enc(i) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + if size_len in (1, 2, 4, 8): + x, t = OER_fixed_integer_dec( + s, size_len, signed=not oer_unsigned + ) + return cls.asn1_object(x), t + if oer_unsigned: + x, t = OER_unsigned_integer_dec(s) + else: + x, t = OER_signed_integer_dec(s) + return cls.asn1_object(x), t + + +class OERcodec_BOOLEAN(OERcodec_Object[int]): + tag = ASN1_Class_UNIVERSAL.BOOLEAN + + @classmethod + def enc(cls, i, size_len=0, **_kwargs): + # type: (int, Optional[int]) -> bytes + return chb(0xff if i else 0x00) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + cls.check_string(s) + return cls.asn1_object(0 if orb(s[0]) == 0 else 1), s[1:] + + +class OERcodec_BIT_STRING(OERcodec_Object[str]): + tag = ASN1_Class_UNIVERSAL.BIT_STRING + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[str], bytes] + length, s = OER_len_dec(s) + if length == 0: + return cls.tag.asn1_object(""), s + if len(s) < length: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), length), + remaining=s + ) + unused_bits = orb(s[0]) + if safe and unused_bits > 7: + raise OER_Decoding_Error( + "OERcodec_BIT_STRING: too many unused_bits advertised", + remaining=s + ) + fs = "".join(binrepr(orb(x)).zfill(8) for x in s[1:length]) + if unused_bits > 0: + fs = fs[:-unused_bits] + return cls.tag.asn1_object(fs), s[length:] + + @classmethod + def enc(cls, _s, size_len=0, **_kwargs): + # type: (AnyStr, Optional[int]) -> bytes + s = bytes_encode(_s) + if len(s) % 8 == 0: + unused_bits = 0 + else: + unused_bits = 8 - len(s) % 8 + s += b"0" * unused_bits + data = b"".join(chb(int(b"".join(chb(y) for y in x), 2)) + for x in zip(*[iter(s)] * 8)) + body = chb(unused_bits) + data + return OER_len_enc(len(body)) + body + + +class OERcodec_STRING(OERcodec_Object[str]): + tag = ASN1_Class_UNIVERSAL.STRING + + @classmethod + def enc(cls, _s, size_len=0, **_kwargs): + # type: (Union[str, bytes], Optional[int]) -> bytes + s = bytes_encode(_s) + if size_len and size_len == len(s): + return s + return OER_len_enc(len(s)) + s + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[Any], bytes] + if size_len and size_len not in (1, 2, 4, 8): + if len(s) < size_len: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % + (cls.__name__, len(s), size_len), + remaining=s + ) + return cls.tag.asn1_object(s[:size_len]), s[size_len:] + length, s = OER_len_dec(s) + if len(s) < length: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), length), + remaining=s + ) + return cls.tag.asn1_object(s[:length]), s[length:] + + +class OERcodec_NULL(OERcodec_Object[None]): + tag = ASN1_Class_UNIVERSAL.NULL + + @classmethod + def enc(cls, i, size_len=0, **_kwargs): + # type: (Any, Optional[int]) -> bytes + return b"" + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[None], bytes] + return cls.asn1_object(None), s + + +class OERcodec_OID(OERcodec_Object[bytes]): + tag = ASN1_Class_UNIVERSAL.OID + + @classmethod + def enc(cls, _oid, size_len=0, **_kwargs): + # type: (AnyStr, Optional[int]) -> bytes + oid = bytes_encode(_oid) + if oid: + lst = [int(x) for x in oid.strip(b".").split(b".")] + else: + lst = list() + if len(lst) >= 2: + lst[1] += 40 * lst[0] + del lst[0] + body = b"".join(BER_num_enc(k) for k in lst) + return OER_len_enc(len(body)) + body + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[bytes], bytes] + length, s = OER_len_dec(s) + if len(s) < length: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), length), + remaining=s + ) + content, t = s[:length], s[length:] + lst = [] + while content: + val, content = BER_num_dec(content) + lst.append(val) + if len(lst) > 0: + lst.insert(0, lst[0] // 40) + lst[1] %= 40 + return ( + cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)), + t, + ) + + +class OERcodec_ENUMERATED(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.ENUMERATED + + @classmethod + def enc(cls, i, size_len=0, **_kwargs): + # type: (int, Optional[int]) -> bytes + return OER_enumerated_enc(i) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + x, t = OER_enumerated_dec(s) + return cls.asn1_object(x), t + + +class OERcodec_UTF8_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTF8_STRING + + +class OERcodec_NUMERIC_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.NUMERIC_STRING + + +class OERcodec_PRINTABLE_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING + + +class OERcodec_T61_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.T61_STRING + + +class OERcodec_VIDEOTEX_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.VIDEOTEX_STRING + + +class OERcodec_IA5_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.IA5_STRING + + +class OERcodec_GENERAL_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERAL_STRING + + +class OERcodec_UTC_TIME(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTC_TIME + + +class OERcodec_GENERALIZED_TIME(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME + + +class OERcodec_ISO646_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.ISO646_STRING + + +class OERcodec_UNIVERSAL_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UNIVERSAL_STRING + + +class OERcodec_BMP_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.BMP_STRING + + +class OERcodec_SEQUENCE(OERcodec_Object[Union[bytes, List['OERcodec_Object[Any]']]]): + tag = ASN1_Class_UNIVERSAL.SEQUENCE + + @classmethod + def enc(cls, _ll, size_len=0, **_kwargs): + # type: (Union[bytes, List[OERcodec_Object[Any]]], Optional[int]) -> bytes + if isinstance(_ll, bytes): + return _ll + return b"".join(x.enc(cls.codec) for x in _ll) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[Union[bytes, List[Any]]], bytes] + raise OER_Decoding_Error( + "OERcodec_SEQUENCE: decoding requires schema-defined field order", + remaining=s + ) + + +class OERcodec_SET(OERcodec_SEQUENCE): + tag = ASN1_Class_UNIVERSAL.SET + + +class OERcodec_IPADDRESS(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.IPADDRESS + + @classmethod + def enc(cls, ipaddr_ascii, size_len=0, **_kwargs): # type: ignore + # type: (str, Optional[int]) -> bytes + try: + s = inet_aton(ipaddr_ascii) + except Exception: + raise OER_Encoding_Error("IPv4 address could not be encoded") + if size_len == len(s): + return s + return OER_len_enc(len(s)) + s + + @classmethod + def do_dec(cls, s, context=None, safe=False, + size_len=0, oer_unsigned=False): + # type: (bytes, Optional[Any], bool, Optional[int], bool) -> Tuple[ASN1_Object[str], bytes] # noqa: E501 + if size_len == 4: + raw, remain = s[:4], s[4:] + else: + length, remain = OER_len_dec(s) + if len(remain) < length: + raise OER_Decoding_Error("IP address could not be decoded", + remaining=s) + raw, remain = remain[:length], remain[length:] + try: + ipaddr_ascii = inet_ntoa(raw) + except Exception: + raise OER_Decoding_Error("IP address could not be decoded", + remaining=s) + return cls.asn1_object(ipaddr_ascii), remain + + +class OERcodec_COUNTER32(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER32 + + +class OERcodec_COUNTER64(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER64 + + +class OERcodec_GAUGE32(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.GAUGE32 + + +class OERcodec_TIME_TICKS(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.TIME_TICKS diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py new file mode 100644 index 00000000000..1af22cd4e7a --- /dev/null +++ b/scapy/contrib/uper.py @@ -0,0 +1,1369 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Unaligned Packed Encoding Rules (UPER) for ASN.1 + +As specified in ITU-T X.691 | ISO/IEC 8825-2. + +UPER is registered on ``ASN1_Codecs.PER``. Schema-driven encoding and decoding +(``ASN1F_SEQUENCE``, ``ASN1F_CHOICE``, ``ASN1F_SEQUENCE_OF``, +``ASN1F_ENUMERATED``) is supported for common field types. Not supported yet: +explicit/implicit tagging, SET, extension markers, +``ASN1F_CHOICE``/``ASN1F_SEQUENCE_OF`` with nested ``ASN1_Packet`` +alternatives, REAL, and PER-visible character string permuted alphabets. +""" + +import binascii + +from scapy.error import warning +from scapy.compat import orb, bytes_encode +from scapy.utils import binrepr, inet_aton, inet_ntoa +from scapy.asn1.ber import BER_num_dec, BER_num_enc +from scapy.asn1.asn1 import ( + ASN1_BADTAG, + ASN1_BadTag_Decoding_Error, + ASN1_Class, + ASN1_Class_UNIVERSAL, + ASN1_Codecs, + ASN1_DECODING_ERROR, + ASN1_Decoding_Error, + ASN1_Encoding_Error, + ASN1_Error, + ASN1_Object, + _ASN1_ERROR, +) + +from typing import ( + Any, + AnyStr, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, +) + + +################### +# UPER encoding # +################### + + +class UPER_Encoding_Error(ASN1_Encoding_Error): + def __init__(self, + msg, # type: str + encoded=None, # type: Optional[Union['UPERcodec_Object[Any]', str]] + remaining=b"" # type: bytes + ): + # type: (...) -> None + Exception.__init__(self, msg) + self.remaining = remaining + self.encoded = encoded + + def __str__(self): + # type: () -> str + s = Exception.__str__(self) + if isinstance(self.encoded, ASN1_Object): + s += "\n### Already encoded ###\n%s" % self.encoded.strshow() + else: + s += "\n### Already encoded ###\n%r" % self.encoded + s += "\n### Remaining ###\n%r" % self.remaining + return s + + +class UPER_Decoding_Error(ASN1_Decoding_Error): + def __init__(self, + msg, # type: str + decoded=None, # type: Optional[Any] + remaining=b"" # type: bytes + ): + # type: (...) -> None + Exception.__init__(self, msg) + self.remaining = remaining + self.decoded = decoded + + def __str__(self): + # type: () -> str + s = Exception.__str__(self) + if isinstance(self.decoded, ASN1_Object): + s += "\n### Already decoded ###\n%s" % self.decoded.strshow() + else: + s += "\n### Already decoded ###\n%r" % self.decoded + s += "\n### Remaining ###\n%r" % self.remaining + return s + + +class UPER_BadTag_Decoding_Error(UPER_Decoding_Error, + ASN1_BadTag_Decoding_Error): + pass + + +def UPER_bits_for_range(size): + # type: (int) -> int + if size <= 0: + return 0 + return size.bit_length() + + +class UPER_Encoder(object): + def __init__(self): + # type: () -> None + self.number_of_bits = 0 + self.value = 0 + self.chunks_number_of_bits = 0 + self.chunks = [] # type: List[List[int]] + + def number_of_bytes(self): + # type: () -> int + return (self.chunks_number_of_bits + self.number_of_bits + 7) // 8 + + def align_always(self): + # type: () -> None + width = 8 * self.number_of_bytes() + width -= self.chunks_number_of_bits + width -= self.number_of_bits + if width: + self.number_of_bits += width + self.value <<= width + + def append_bit(self, bit): + # type: (int) -> None + self.number_of_bits += 1 + self.value <<= 1 + self.value |= 1 if bit else 0 + + def append_bits(self, data, number_of_bits): + # type: (bytes, int) -> None + if number_of_bits == 0: + return + value = int.from_bytes(data, "big") + value >>= (8 * len(data) - number_of_bits) + self.append_non_negative_binary_integer(value, number_of_bits) + + def append_non_negative_binary_integer(self, value, number_of_bits): + # type: (int, int) -> None + if number_of_bits == 0: + return + if self.number_of_bits > 4096: + self.chunks.append([self.value, self.number_of_bits]) + self.chunks_number_of_bits += self.number_of_bits + self.number_of_bits = 0 + self.value = 0 + self.number_of_bits += number_of_bits + self.value <<= number_of_bits + self.value |= value & ((1 << number_of_bits) - 1) + + def append_bytes(self, data): + # type: (bytes) -> None + self.append_bits(data, 8 * len(data)) + + def append_length_determinant(self, length): + # type: (int) -> int + if length < 128: + encoded = bytes([length]) + elif length < 16384: + encoded = bytes([(0x80 | (length >> 8)), (length & 0xff)]) + elif length < 32768: + encoded = b"\xc1" + length = 16384 + elif length < 49152: + encoded = b"\xc2" + length = 32768 + elif length < 65536: + encoded = b"\xc3" + length = 49152 + else: + encoded = b"\xc4" + length = 65536 + self.append_bytes(encoded) + return length + + def append_unconstrained_whole_number(self, value): + # type: (int) -> None + number_of_bits = 0 if value == 0 else value.bit_length() + if value < 0: + number_of_bytes = (number_of_bits + 7) // 8 + enc = (1 << (8 * number_of_bytes)) + value + if enc & (1 << (8 * number_of_bytes - 1)) == 0: + enc |= (0xff << (8 * number_of_bytes)) + number_of_bytes += 1 + elif value > 0: + number_of_bytes = (number_of_bits + 7) // 8 + if number_of_bits == 8 * number_of_bytes: + number_of_bytes += 1 + enc = value + else: + number_of_bytes = 1 + enc = 0 + self.append_length_determinant(number_of_bytes) + self.append_non_negative_binary_integer(enc, 8 * number_of_bytes) + + def as_bytes(self): + # type: () -> bytes + value = 0 + number_of_bits = 0 + for chunk_value, chunk_number_of_bits in self.chunks: + value <<= chunk_number_of_bits + value |= chunk_value + number_of_bits += chunk_number_of_bits + value <<= self.number_of_bits + value |= self.value + number_of_bits += self.number_of_bits + if number_of_bits == 0: + return b"" + number_of_alignment_bits = (8 - (number_of_bits % 8)) % 8 + value <<= number_of_alignment_bits + number_of_bits += number_of_alignment_bits + value |= (0x80 << number_of_bits) + hexval = hex(value)[4:].rstrip("L") + if len(hexval) % 2: + hexval = "0" + hexval + return binascii.unhexlify(hexval) + + +def _uper_significant_bit_count(data): + # type: (bytes) -> int + if not data: + return 0 + total = 8 * len(data) + bits = int.from_bytes(data, "big") + end = total + while end > 0 and ((bits >> (total - end)) & 1) == 0: + end -= 1 + trimmed = total - end + if trimmed > 0 and trimmed <= 8: + return end + return total + + +def _uper_per_bits_to_bytes(bit_value, number_of_bits): + # type: (int, int) -> bytes + if number_of_bits == 0: + return b"" + bitstr = format(bit_value, "0%db" % number_of_bits) + value = "10000000" + bitstr + number_of_alignment_bits = (8 - (number_of_bits % 8)) + if number_of_alignment_bits != 8: + value += "0" * number_of_alignment_bits + hexval = hex(int(value, 2))[4:].rstrip("L") + if len(hexval) % 2: + hexval = "0" + hexval + return binascii.unhexlify(hexval) + + +def UPER_append_encoded(enc, data): + # type: (UPER_Encoder, bytes) -> None + if not data: + return + nbits = _uper_significant_bit_count(data) + if nbits == 0: + return + total = 8 * len(data) + bits = int.from_bytes(data, "big") + shift = total - nbits + value = (bits >> shift) & ((1 << nbits) - 1) + enc.append_non_negative_binary_integer(value, nbits) + + +def UPER_join_encodings(*parts): + # type: (*bytes) -> bytes + enc = UPER_Encoder() + for part in parts: + UPER_append_encoded(enc, part) + return enc.as_bytes() + + +def UPER_optional_presence_enc(bits, enc=None): + # type: (List[int], Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + for bit in bits: + enc.append_bit(bit) + return enc.as_bytes() if standalone else b"" + + +def UPER_count_enc(count, enc=None): + # type: (int, Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + enc.append_length_determinant(count) + return enc.as_bytes() if standalone else b"" + + +def UPER_has_unexpected_remainder(dec): + # type: (UPER_Decoder) -> bool + if dec.number_of_bits == 0: + return False + mask = (1 << dec.number_of_bits) - 1 + return (dec._bits & mask) != 0 + + +def UPER_count_dec(s, dec=None): + # type: (bytes, Optional[UPER_Decoder]) -> Tuple[int, bytes] + standalone = dec is None + if dec is None: + dec = UPER_Decoder(s) + count = dec.read_length_determinant() + if standalone: + return count, dec.remaining() + return count, b"" + + +class UPER_Decoder(object): + def __init__(self, encoded): + # type: (bytes) -> None + self.total_number_of_bits = 8 * len(encoded) + self.number_of_bits = self.total_number_of_bits + if encoded: + self._bits = int.from_bytes(encoded, "big") + else: + self._bits = 0 + + def _read_offset(self): + # type: () -> int + return self.total_number_of_bits - self.number_of_bits + + def _read_bits_int(self, number_of_bits): + # type: (int) -> int + if number_of_bits == 0: + return 0 + consumed = self._read_offset() + shift = self.total_number_of_bits - consumed - number_of_bits + mask = (1 << number_of_bits) - 1 + return (self._bits >> shift) & mask + + def read_bit(self): + # type: () -> int + if self.number_of_bits == 0: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + bit = self._read_bits_int(1) + self.number_of_bits -= 1 + return bit + + def read_bits(self, number_of_bits): + # type: (int) -> bytes + if number_of_bits > self.number_of_bits: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + if number_of_bits == 0: + return b"" + value = self._read_bits_int(number_of_bits) + self.number_of_bits -= number_of_bits + return _uper_per_bits_to_bytes(value, number_of_bits) + + def remaining(self): + # type: () -> bytes + if self.number_of_bits == 0: + return b"" + value = self._read_bits_int(self.number_of_bits) + return _uper_per_bits_to_bytes(value, self.number_of_bits) + + def read_bytes(self, number_of_bytes): + # type: (int) -> bytes + return self.read_bits(8 * number_of_bytes) + + def read_non_negative_binary_integer(self, number_of_bits): + # type: (int) -> int + if number_of_bits > self.number_of_bits: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + if number_of_bits == 0: + return 0 + value = self._read_bits_int(number_of_bits) + self.number_of_bits -= number_of_bits + return value + + def align_always(self): + # type: () -> None + consumed = self.total_number_of_bits - self.number_of_bits + width = (8 - (consumed % 8)) % 8 + if width: + if width > self.number_of_bits: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + self.number_of_bits -= width + + def read_length_determinant(self): + # type: () -> int + value = self.read_non_negative_binary_integer(8) + if (value & 0x80) == 0x00: + return value + if (value & 0xc0) == 0x80: + return ((value & 0x7f) << 8) | self.read_non_negative_binary_integer(8) + mapping = {0xc1: 16384, 0xc2: 32768, 0xc3: 49152, 0xc4: 65536} + if value in mapping: + return mapping[value] + raise UPER_Decoding_Error( + "UPER_Decoder: bad length determinant 0x%02x" % value + ) + + def read_unconstrained_whole_number(self): + # type: () -> int + number_of_bytes = self.read_length_determinant() + enc = self.read_non_negative_binary_integer(8 * number_of_bytes) + sign_bit = 1 << (8 * number_of_bytes - 1) + if enc & sign_bit: + return enc - (1 << (8 * number_of_bytes)) + return enc + + def consume_input(self): + # type: () -> None + self.number_of_bits = 0 + + +def UPER_constrained_int_enc(value, minimum, maximum, enc=None): + # type: (int, int, int, Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + size = maximum - minimum + enc.append_non_negative_binary_integer( + value - minimum, UPER_bits_for_range(size) + ) + return enc.as_bytes() if standalone else b"" + + +def UPER_constrained_int_dec(s, minimum, maximum): + # type: (bytes, int, int) -> Tuple[int, bytes] + dec = UPER_Decoder(s) + size = maximum - minimum + value = dec.read_non_negative_binary_integer(UPER_bits_for_range(size)) + dec.consume_input() + return value + minimum, b"" + + +def UPER_constrained_int_dec_from_decoder(dec, minimum, maximum): + # type: (UPER_Decoder, int, int) -> int + size = maximum - minimum + value = dec.read_non_negative_binary_integer(UPER_bits_for_range(size)) + return value + minimum + + +def UPER_unconstrained_int_enc(value, enc=None): + # type: (int, Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + enc.append_unconstrained_whole_number(value) + return enc.as_bytes() if standalone else b"" + + +def UPER_unconstrained_int_dec(s): + # type: (bytes) -> Tuple[int, bytes] + dec = UPER_Decoder(s) + value = dec.read_unconstrained_whole_number() + remain = dec.remaining() + return value, remain + + +def UPER_boolean_enc(value, enc=None): + # type: (int, Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + enc.append_bit(1 if value else 0) + return enc.as_bytes() if standalone else b"" + + +def UPER_boolean_dec(s): + # type: (bytes) -> Tuple[int, bytes] + dec = UPER_Decoder(s) + value = dec.read_bit() + dec.consume_input() + return value, b"" + + +def UPER_octet_string_enc(data, minimum=None, maximum=None, enc=None): + # type: (bytes, Optional[int], Optional[int], Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + if minimum is not None and maximum is not None and minimum == maximum: + enc.append_bytes(data) + elif minimum is not None and maximum is not None: + enc.append_non_negative_binary_integer( + len(data) - minimum, + UPER_bits_for_range(maximum - minimum), + ) + enc.append_bytes(data) + else: + enc.append_length_determinant(len(data)) + enc.append_bytes(data) + return enc.as_bytes() if standalone else b"" + + +def UPER_octet_string_dec(s, minimum=None, maximum=None, dec=None): + # type: (bytes, Optional[int], Optional[int], Optional[UPER_Decoder]) -> Tuple[bytes, bytes] # noqa: E501 + standalone = dec is None + if dec is None: + dec = UPER_Decoder(s) + if minimum is not None and maximum is not None and minimum == maximum: + length = minimum + elif minimum is not None and maximum is not None: + length = minimum + dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + else: + length = dec.read_length_determinant() + data = dec.read_bytes(length) + if standalone: + return data, dec.remaining() + return data, b"" + + +def UPER_choice_index_enc(index, number_of_choices, enc=None): + # type: (int, int, Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + enc.append_non_negative_binary_integer( + index, UPER_bits_for_range(number_of_choices - 1) + ) + return enc.as_bytes() if standalone else b"" + + +def UPER_choice_index_dec(s, number_of_choices, dec=None): + # type: (bytes, int, Optional[UPER_Decoder]) -> Tuple[int, bytes] + standalone = dec is None + if dec is None: + dec = UPER_Decoder(s) + index = dec.read_non_negative_binary_integer( + UPER_bits_for_range(number_of_choices - 1) + ) + if standalone: + return index, dec.remaining() + return index, b"" + + +class UPERcodec_metaclass(type): + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type['UPERcodec_Object[Any]'] + c = cast('Type[UPERcodec_Object[Any]]', + super(UPERcodec_metaclass, cls).__new__(cls, name, bases, dct)) + try: + c.tag.register(c.codec, c) + except Exception: + warning("Error registering %r for %r" % (c.tag, c.codec)) + return c + + +_K = TypeVar('_K') + + +class UPERcodec_Object(Generic[_K], metaclass=UPERcodec_metaclass): + codec = ASN1_Codecs.PER + tag = ASN1_Class_UNIVERSAL.ANY + + @classmethod + def asn1_object(cls, val): + # type: (_K) -> ASN1_Object[_K] + return cls.tag.asn1_object(val) + + @classmethod + def check_string(cls, s): + # type: (bytes) -> None + if not s and cls.tag != ASN1_Class_UNIVERSAL.NULL: + raise UPER_Decoding_Error( + "%s: Got empty object while expecting %r" % + (cls.__name__, cls.tag), remaining=s + ) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_enum_values=None, # type: Optional[List[int]] + ): + # type: (...) -> Tuple[ASN1_Object[Any], bytes] + raise UPER_Decoding_Error( + "%s: Cannot decode unknown UPER type without context" % + cls.__name__, remaining=s + ) + + @classmethod + def dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_enum_values=None, # type: Optional[List[int]] + ): + # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] + dec_kwargs = {} # type: Dict[str, Any] + if uper_enum_values is not None: + dec_kwargs["uper_enum_values"] = uper_enum_values + if not safe: + return cls.do_dec( + s, context, safe, size_len, uper_min, uper_max, oer_unsigned, + **dec_kwargs + ) + try: + return cls.do_dec( + s, context, safe, size_len, uper_min, uper_max, oer_unsigned, + **dec_kwargs + ) + except UPER_BadTag_Decoding_Error as e: + o, remain = UPERcodec_Object.dec( + e.remaining, context, safe, size_len, uper_min, uper_max, + oer_unsigned, uper_enum_values=uper_enum_values, + ) + return ASN1_BADTAG(o), remain + except UPER_Decoding_Error as e: + return ASN1_DECODING_ERROR(s, exc=e), b"" + except ASN1_Error as e: + return ASN1_DECODING_ERROR(s, exc=e), b"" + + @classmethod + def safedec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_enum_values=None, # type: Optional[List[int]] + ): + # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] + return cls.dec( + s, context, safe=True, + size_len=size_len, uper_min=uper_min, uper_max=uper_max, + oer_unsigned=oer_unsigned, uper_enum_values=uper_enum_values, + ) + + @classmethod + def enc(cls, s, size_len=0, uper_min=None, uper_max=None, **_kwargs): + # type: (_K, Optional[int], Optional[int], Optional[int]) -> bytes + if isinstance(s, (str, bytes)): + return UPERcodec_STRING.enc(s, size_len=size_len, + uper_min=uper_min, uper_max=uper_max) + else: + try: + return UPERcodec_INTEGER.enc( + int(s), + size_len=size_len, + uper_min=uper_min, + uper_max=uper_max, + ) + except Exception: + raise UPER_Encoding_Error( + "Cannot encode value %r for %s" % (s, cls.__name__), + encoded=s + ) + + +def _uper_enc_via_encode_into(cls, *args, **kwargs): + # type: (Type[UPERcodec_Object[Any]], *Any, **Any) -> bytes + enc = UPER_Encoder() + cls.encode_into(enc, *args, **kwargs) + return enc.as_bytes() + + + +def UPER_tagging_enc(s, **kwargs): + # type: (bytes, **Any) -> bytes + # UPER has no BER-style TLV tagging. + return s + + +def UPER_tagging_dec(s, **kwargs): + # type: (bytes, **Any) -> Tuple[Optional[int], bytes] + return None, s + + +ASN1_Codecs.PER.register_stem(UPERcodec_Object) +ASN1_Codecs.PER.register_tagging(UPER_tagging_enc, UPER_tagging_dec) + + +######################### +# UPERcodec objects # +######################### + + +def _uper_int_range(size_len, uper_min, uper_max, oer_unsigned=False): + # type: (Optional[int], Optional[int], Optional[int], bool) -> Tuple[Optional[int], Optional[int]] # noqa: E501 + if uper_min is not None or uper_max is not None: + return uper_min, uper_max + if size_len in (1, 2, 4, 8) and oer_unsigned: + return 0, (256 ** size_len) - 1 + return None, None + + +class UPERcodec_INTEGER(UPERcodec_Object[int]): + tag = ASN1_Class_UNIVERSAL.INTEGER + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + i, # type: int + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_extensible=False, # type: bool + ): + # type: (...) -> None + minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) + if uper_extensible and minimum is not None and maximum is not None: + if minimum <= i <= maximum: + enc.append_bit(0) + else: + enc.append_bit(1) + UPER_unconstrained_int_enc(i, enc=enc) + return + if minimum is not None and maximum is not None: + UPER_constrained_int_enc(i, minimum, maximum, enc=enc) + else: + UPER_unconstrained_int_enc(i, enc=enc) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_extensible=False, # type: bool + ): + # type: (...) -> ASN1_Object[int] + minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) + if uper_extensible and minimum is not None and maximum is not None: + if dec.read_bit(): + value = dec.read_unconstrained_whole_number() + return cls.asn1_object(value) + if minimum is not None and maximum is not None: + value = UPER_constrained_int_dec_from_decoder(dec, minimum, maximum) + else: + value = dec.read_unconstrained_whole_number() + return cls.asn1_object(value) + + @classmethod + def enc(cls, i, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False, **_kwargs): + # type: (int, Optional[int], Optional[int], Optional[int], bool) -> bytes + return _uper_enc_via_encode_into( + cls, i, size_len, uper_min, uper_max, oer_unsigned, + ) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) + if minimum is not None and maximum is not None: + x, t = UPER_constrained_int_dec(s, minimum, maximum) + else: + x, t = UPER_unconstrained_int_dec(s) + return cls.asn1_object(x), t + + +class UPERcodec_BOOLEAN(UPERcodec_Object[int]): + tag = ASN1_Class_UNIVERSAL.BOOLEAN + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + i, # type: int + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> None + UPER_boolean_enc(i, enc=enc) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> ASN1_Object[int] + return cls.asn1_object(dec.read_bit()) + + @classmethod + def enc(cls, i, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False, **_kwargs): + # type: (int, Optional[int], Optional[int], Optional[int], bool) -> bytes + return _uper_enc_via_encode_into( + cls, i, size_len, uper_min, uper_max, oer_unsigned, + ) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + x, t = UPER_boolean_dec(s) + return cls.asn1_object(x), t + + +def _uper_bytes_to_bitstr(data, nbits): + # type: (bytes, int) -> str + bitstr = "".join(binrepr(orb(x)).zfill(8) for x in data) + return bitstr[:nbits] + + +def _uper_bit_string_parts(_s): + # type: (Any) -> Tuple[bytes, int] + if isinstance(_s, tuple) and len(_s) == 2: + data, nbits = _s + return bytes_encode(data), nbits + if isinstance(_s, str) and _s and all(c in "01" for c in _s): + nbits = len(_s) + padded = _s + "0" * ((8 - nbits % 8) % 8) + data = int(padded or "0", 2).to_bytes( + max(1, len(padded) // 8), "big" + ) + return data, nbits + s = bytes_encode(_s) + return s, 8 * len(s) + + +class UPERcodec_BIT_STRING(UPERcodec_Object[str]): + tag = ASN1_Class_UNIVERSAL.BIT_STRING + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + _s, # type: Any + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> None + s, nbits = _uper_bit_string_parts(_s) + minimum = uper_min + maximum = uper_max + if size_len: + minimum = maximum = size_len + if minimum is not None and maximum is not None and minimum == maximum: + if nbits >= minimum: + value = int.from_bytes(s, "big") >> (8 * len(s) - minimum) + elif isinstance(_s, str) and _s and all(c in "01" for c in _s): + value = int(_s, 2) + elif nbits > 0: + value = int.from_bytes(s, "big") >> max(0, 8 * len(s) - nbits) + else: + value = 0 + enc.append_non_negative_binary_integer(value, minimum) + elif minimum is not None and maximum is not None: + enc.append_non_negative_binary_integer( + nbits - minimum, UPER_bits_for_range(maximum - minimum) + ) + enc.append_bits(s, nbits) + else: + enc.append_length_determinant((nbits + 7) // 8) + enc.append_bytes(s) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> ASN1_Object[str] + minimum = uper_min + maximum = uper_max + if size_len: + minimum = maximum = size_len + if minimum is not None and maximum is not None and minimum == maximum: + nbits = minimum + elif minimum is not None and maximum is not None: + nbits = minimum + dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + else: + nbytes = dec.read_length_determinant() + raw = dec.read_bytes(nbytes) + nbits = 8 * nbytes + return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)) + raw = dec.read_bits(nbits) + return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)) + + @classmethod + def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False, **_kwargs): + # type: (Any, Optional[int], Optional[int], Optional[int], bool) -> bytes + return _uper_enc_via_encode_into( + cls, _s, size_len, uper_min, uper_max, oer_unsigned, + ) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[str], bytes] + dec = UPER_Decoder(s) + minimum = uper_min + maximum = uper_max + if minimum is not None and maximum is not None and minimum == maximum: + nbits = minimum + elif minimum is not None and maximum is not None: + nbits = minimum + dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + else: + nbytes = dec.read_length_determinant() + raw = dec.read_bytes(nbytes) + nbits = 8 * nbytes + return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)), dec.remaining() + raw = dec.read_bits(nbits) + return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)), dec.remaining() + + +def _uper_octet_string_bounds(size_len, uper_min, uper_max): + # type: (Optional[int], Optional[int], Optional[int]) -> Tuple[Optional[int], Optional[int]] # noqa: E501 + if size_len: + return size_len, size_len + return uper_min, uper_max + + +class UPERcodec_STRING(UPERcodec_Object[str]): + tag = ASN1_Class_UNIVERSAL.STRING + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + _s, # type: Union[str, bytes] + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> None + s = bytes_encode(_s) + minimum, maximum = _uper_octet_string_bounds( + size_len, uper_min, uper_max, + ) + UPER_octet_string_enc(s, minimum, maximum, enc=enc) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> ASN1_Object[Any] + minimum, maximum = _uper_octet_string_bounds( + size_len, uper_min, uper_max, + ) + raw, _ = UPER_octet_string_dec(b"", minimum, maximum, dec=dec) + return cls.asn1_object(raw) + + @classmethod + def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False, **_kwargs): + # type: (Union[str, bytes], Optional[int], Optional[int], Optional[int], bool) -> bytes # noqa: E501 + return _uper_enc_via_encode_into( + cls, _s, size_len, uper_min, uper_max, oer_unsigned, + ) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[Any], bytes] + minimum, maximum = _uper_octet_string_bounds( + size_len, uper_min, uper_max, + ) + raw, remain = UPER_octet_string_dec(s, minimum, maximum) + return cls.asn1_object(raw), remain + + +class UPERcodec_NULL(UPERcodec_Object[None]): + tag = ASN1_Class_UNIVERSAL.NULL + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + _s, # type: Any + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> None + return + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> ASN1_Object[None] + return cls.asn1_object(None) + + @classmethod + def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False, **_kwargs): + # type: (Any, Optional[int], Optional[int], Optional[int], bool) -> bytes + return b"" + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[None], bytes] + return cls.asn1_object(None), s + + +class UPERcodec_OID(UPERcodec_Object[bytes]): + tag = ASN1_Class_UNIVERSAL.OID + + @classmethod + def enc(cls, _oid, size_len=0, uper_min=None, uper_max=None, **_kwargs): + # type: (AnyStr, Optional[int], Optional[int], Optional[int]) -> bytes + oid = bytes_encode(_oid) + if oid: + lst = [int(x) for x in oid.split(b".")] + lst = [40 * lst[0] + lst[1]] + lst[2:] + else: + lst = [] + body = b"".join(BER_num_enc(k) for k in lst) + enc = UPER_Encoder() + enc.append_length_determinant(len(body)) + enc.append_bytes(body) + return enc.as_bytes() + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[bytes], bytes] + dec = UPER_Decoder(s) + length = dec.read_length_determinant() + content = dec.read_bytes(length) + lst = [] + while content: + val, content = BER_num_dec(content) + lst.append(val) + if len(lst) > 0: + lst.insert(0, lst[0] // 40) + lst[1] %= 40 + return ( + cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)), + dec.remaining(), + ) + + +def UPER_enumerated_enc(value, + enum_values, # type: List[int] + enc=None # type: Optional[UPER_Encoder] + ): + # type: (int, List[int], Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + if not enum_values: + raise UPER_Encoding_Error("UPER_enumerated_enc: empty enumeration") + try: + index = enum_values.index(value) + except ValueError: + raise UPER_Encoding_Error( + "UPER_enumerated_enc: unknown enumeration value %r" % value + ) + UPER_choice_index_enc(index, len(enum_values), enc=enc) + return enc.as_bytes() if standalone else b"" + + +def UPER_enumerated_dec(s, + enum_values, # type: List[int] + dec=None # type: Optional[UPER_Decoder] + ): + # type: (bytes, List[int], Optional[UPER_Decoder]) -> Tuple[int, bytes] + standalone = dec is None + if dec is None: + dec = UPER_Decoder(s) + if not enum_values: + raise UPER_Decoding_Error("UPER_enumerated_dec: empty enumeration") + index, _ = UPER_choice_index_dec(b"", len(enum_values), dec=dec) + if index >= len(enum_values): + raise UPER_Decoding_Error( + "UPER_enumerated_dec: index %i out of range" % index + ) + if standalone: + dec.consume_input() + return enum_values[index], b"" + return enum_values[index], b"" + + +class UPERcodec_ENUMERATED(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.ENUMERATED + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + i, # type: int + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_enum_values=None, # type: Optional[List[int]] + ): + # type: (...) -> None + if uper_enum_values is not None: + UPER_enumerated_enc(i, uper_enum_values, enc=enc) + return + minimum = uper_min if uper_min is not None else 0 + maximum = uper_max if uper_max is not None else size_len + if maximum is None: + maximum = max(i, 0) + UPER_constrained_int_enc(i, minimum, maximum, enc=enc) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_enum_values=None, # type: Optional[List[int]] + ): + # type: (...) -> ASN1_Object[int] + if uper_enum_values is not None: + value, _ = UPER_enumerated_dec(b"", uper_enum_values, dec=dec) + return cls.asn1_object(value) + minimum = uper_min if uper_min is not None else 0 + maximum = uper_max if uper_max is not None else size_len + if maximum is None: + raise UPER_Decoding_Error("UPERcodec_ENUMERATED: missing range") + value = dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + minimum + return cls.asn1_object(value) + + @classmethod + def enc(cls, + i, + size_len=0, + uper_min=None, + uper_max=None, + oer_unsigned=False, + uper_enum_values=None, + **_kwargs + ): + # type: (int, Optional[int], Optional[int], Optional[int], bool, Optional[List[int]], **Any) -> bytes # noqa: E501 + return _uper_enc_via_encode_into( + cls, i, size_len, uper_min, uper_max, oer_unsigned, + uper_enum_values=uper_enum_values, + ) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_enum_values=None, # type: Optional[List[int]] + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + if uper_enum_values is not None: + x, t = UPER_enumerated_dec(s, uper_enum_values) + return cls.asn1_object(x), t + minimum = uper_min if uper_min is not None else 0 + maximum = uper_max if uper_max is not None else size_len + if maximum is None: + raise UPER_Decoding_Error("UPERcodec_ENUMERATED: missing range") + x, t = UPER_constrained_int_dec(s, minimum, maximum) + return cls.asn1_object(x), t + + +class UPERcodec_SEQUENCE(UPERcodec_Object[Union[bytes, List[Any]]]): + tag = ASN1_Class_UNIVERSAL.SEQUENCE + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + _ll, # type: Union[bytes, List[UPERcodec_Object[Any]]] + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> None + if isinstance(_ll, bytes): + UPER_append_encoded(enc, _ll) + + @classmethod + def enc(cls, _ll, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False, **_kwargs): + # type: (Union[bytes, List[UPERcodec_Object[Any]]], Optional[int], Optional[int], Optional[int], bool) -> bytes # noqa: E501 + if isinstance(_ll, bytes): + return _ll + raise UPER_Encoding_Error( + "UPERcodec_SEQUENCE: schema-defined field order required" + ) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[Union[bytes, List[Any]]], bytes] + raise UPER_Decoding_Error( + "UPERcodec_SEQUENCE: decoding requires schema-defined field order", + remaining=s + ) + + +class UPERcodec_SET(UPERcodec_SEQUENCE): + tag = ASN1_Class_UNIVERSAL.SET + + +class UPERcodec_IPADDRESS(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.IPADDRESS + + @classmethod + def enc(cls, ipaddr_ascii, size_len=0, uper_min=None, uper_max=None, **_kwargs): + # type: (str, Optional[int], Optional[int], Optional[int]) -> bytes + try: + s = inet_aton(ipaddr_ascii) + except Exception: + raise UPER_Encoding_Error("IPv4 address could not be encoded") + return UPER_octet_string_enc(s, 4, 4) + + @classmethod + def do_dec(cls, s, context=None, safe=False, + size_len=0, uper_min=None, uper_max=None, + oer_unsigned=False): + # type: (bytes, Optional[Any], bool, Optional[int], Optional[int], Optional[int], bool) -> Tuple[ASN1_Object[str], bytes] # noqa: E501 + raw, remain = UPER_octet_string_dec(s, 4, 4) + try: + ipaddr_ascii = inet_ntoa(raw) + except Exception: + raise UPER_Decoding_Error( + "IP address could not be decoded", + remaining=s, + ) + return cls.asn1_object(ipaddr_ascii), remain + + +class UPERcodec_COUNTER32(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER32 + + +class UPERcodec_COUNTER64(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER64 + + +class UPERcodec_GAUGE32(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.GAUGE32 + + +class UPERcodec_TIME_TICKS(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.TIME_TICKS + + +# string aliases +class UPERcodec_UTF8_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTF8_STRING + + +class UPERcodec_NUMERIC_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.NUMERIC_STRING + + +class UPERcodec_PRINTABLE_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING + + +class UPERcodec_T61_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.T61_STRING + + +class UPERcodec_VIDEOTEX_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.VIDEOTEX_STRING + + +class UPERcodec_IA5_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.IA5_STRING + + +class UPERcodec_GENERAL_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERAL_STRING + + +class UPERcodec_UTC_TIME(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTC_TIME + + +class UPERcodec_GENERALIZED_TIME(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME + + +class UPERcodec_ISO646_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.ISO646_STRING + + +class UPERcodec_UNIVERSAL_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UNIVERSAL_STRING + + +class UPERcodec_BMP_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.BMP_STRING diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 9fa0bad0f44..c83f8388d8b 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -101,3 +101,358 @@ ASN1_UTC_TIME(datetime(2020, 12, 31)).val == "201231000000" ASN1_UTC_TIME(datetime(2020, 12, 31, tzinfo=timezone.utc)).val == "201231000000Z" = UTC datetime construction (offset) ASN1_UTC_TIME(datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-23, minutes=-59)))).val == "201231000000-2359" + ++ ASN.1 OER/UPER contrib load += import contrib codecs +import scapy.contrib.oer +import scapy.contrib.uper +from scapy.contrib.oer import * +from scapy.contrib.uper import * + ++ ASN.1 OER codec += OER length determinant short form +OER_len_enc(3) == b"\x03" += OER length determinant long form +OER_len_enc(200) == b"\x81\xc8" += OER boolean false +OERcodec_BOOLEAN.enc(0) == b"\x00" += OER boolean true +OERcodec_BOOLEAN.enc(1) == b"\xff" += OER null +OERcodec_NULL.enc(None) == b"" += OER unconstrained integer +OERcodec_INTEGER.enc(4) == b"\x01\x04" += OER constrained unsigned integer +OERcodec_INTEGER.enc(4, size_len=1) == b"\x04" += OER constrained signed integer +OERcodec_INTEGER.enc(4, size_len=2) == b"\x00\x04" += OER enumerated short form +OERcodec_ENUMERATED.enc(6) == b"\x06" += OER octet string +OERcodec_STRING.enc(b"ABC") == b"\x03ABC" += OER OID +OERcodec_OID.enc("1.2.3") == b"\x02\x2a\x03" += OER integer roundtrip +x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(12345)) +x.val == 12345 and r == b"" += OER boolean roundtrip +x, r = OERcodec_BOOLEAN.do_dec(OERcodec_BOOLEAN.enc(1)) +x.val == 1 and r == b"" += OER ASN1 object encoding +ASN1_INTEGER(42).enc(ASN1_Codecs.OER) == b"\x01*" += OER codec registration +ASN1_Class_UNIVERSAL.INTEGER.get_codec(ASN1_Codecs.OER) is OERcodec_INTEGER + ++ ASN.1 OER codec (extended) += OER length zero +OER_len_enc(0) == b"\x00" += OER length boundary short form +OER_len_enc(127) == b"\x7f" += OER length boundary long form +OER_len_enc(128) == b"\x81\x80" += OER length roundtrip +l, r = OER_len_dec(OER_len_enc(999)) +l == 999 and r == b"" += OER signed integer zero +OER_signed_integer_enc(0) == b"\x01\x00" += OER signed integer negative +OER_signed_integer_enc(-255) == b"\x02\xff\x01" += OER signed integer large +OER_signed_integer_enc(100000) == b"\x03\x01\x86\xa0" += OER signed integer roundtrip +v, r = OER_signed_integer_dec(OER_signed_integer_enc(-1234567)) +v == -1234567 and r == b"" += OER unsigned integer zero +OER_unsigned_integer_enc(0) == b"\x01\x00" += OER unsigned integer roundtrip +v, r = OER_unsigned_integer_dec(OER_unsigned_integer_enc(65535)) +v == 65535 and r == b"" += OER fixed unsigned 1 byte +OERcodec_INTEGER.enc(255, size_len=1) == b"\xff" += OER fixed signed 2 bytes negative +OERcodec_INTEGER.enc(-2, size_len=2) == b"\xff\xfe" += OER fixed signed 4 bytes +OERcodec_INTEGER.enc(-2, size_len=4) == b"\xff\xff\xff\xfe" += OER enumerated long form +OERcodec_ENUMERATED.enc(128) == b"\x82\x00\x80" += OER enumerated negative +OERcodec_ENUMERATED.enc(-1) == b"\x81\xff" += OER enumerated roundtrip +x, r = OERcodec_ENUMERATED.do_dec(OERcodec_ENUMERATED.enc(128)) +x.val == 128 and r == b"" += OER null roundtrip +x, r = OERcodec_NULL.do_dec(OERcodec_NULL.enc(None)) +x.val is None and r == b"" += OER octet string empty +OERcodec_STRING.enc(b"") == b"\x00" += OER octet string fixed size +OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3) == b"\x12\x34\x56" += OER octet string roundtrip +x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34")) +x.val == b"\x12\x34" and r == b"" += OER OID 1.2 +OERcodec_OID.enc("1.2") == b"\x01\x2a" += OER OID roundtrip +x, r = OERcodec_OID.do_dec(OERcodec_OID.enc("1.2.3321")) +x.val == "1.2.3321" and r == b"" += OER bit string variable size +OERcodec_BIT_STRING.enc("0100") == b"\x02\x04\x40" += OER bit string roundtrip +x, r = OERcodec_BIT_STRING.do_dec(OERcodec_BIT_STRING.enc("01000001")) +x.val == "01000001" and r == b"" += OER IA5 string +OERcodec_IA5_STRING.enc(b"ABC") == b"\x03ABC" += OER tag short form +OER_tag_enc(1, OER_CLASS_CONTEXT) == b"\x81" += OER tag roundtrip +cls, num, r = OER_tag_dec(OER_tag_enc(1, OER_CLASS_CONTEXT)) +cls == OER_CLASS_CONTEXT and num == 1 and r == b"" += OER sequence concat +OERcodec_SEQUENCE.enc([ASN1_INTEGER(4), ASN1_INTEGER(5)]) == b"\x01\x04\x01\x05" += OER ASN1 boolean object +ASN1_BOOLEAN(1).enc(ASN1_Codecs.OER) == b"\xff" += OER ASN1 null object +ASN1_NULL(None).enc(ASN1_Codecs.OER) == b"" + ++ ASN.1 OER interoperability (reference vectors) += primitive encode interop +__import__('test.scapy.layers.oer_iop', fromlist=['check_primitive_interop']).check_primitive_interop() += scapy encode reference decode +__import__('test.scapy.layers.oer_iop', fromlist=['check_scapy_encode_reference_decode']).check_scapy_encode_reference_decode() + ++ ASN.1 OER review fixes += OER fixed integer decode roundtrip +x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(128, size_len=1), size_len=1, oer_unsigned=True) +x.val == 128 and r == b"" += OER fixed integer signed decode +x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(-2, size_len=2), size_len=2) +x.val == -2 and r == b"" += OER fixed octet string decode +x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3), size_len=3) +x.val == b"\x12\x34\x56" and r == b"" += OER explicit null tagging +OER_tagging_enc(OERcodec_NULL.enc(None), explicit_tag=0x81) == b"\x81" += OER choice id decode +tag, r = OER_id_dec(b"\x81\x01") +tag == 0x81 and r == b"\x01" + ++ ASN.1 OER fuzzing += OER fuzz encode +__import__('test.scapy.layers.oer_fuzz', fromlist=['check_oer_fuzz_encode']).check_oer_fuzz_encode() += OER fuzz encode roundtrip +__import__('test.scapy.layers.oer_fuzz', fromlist=['check_oer_fuzz_roundtrip']).check_oer_fuzz_roundtrip() += OER fuzz codec decode +__import__('test.scapy.layers.oer_fuzz', fromlist=['check_oer_fuzz_codec_decode']).check_oer_fuzz_codec_decode() += OER fuzz packet decode +__import__('test.scapy.layers.oer_fuzz', fromlist=['check_oer_fuzz_packet_decode']).check_oer_fuzz_packet_decode() + ++ ASN.1 OER packets and fields += OER field explicit tag +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_explicit_tag']).check_oer_field_explicit_tag() += OER field fixed size +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_fixed_size']).check_oer_field_fixed_size() += OER field optional +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_optional']).check_oer_field_optional() += OER field sequence of +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_sequence_of']).check_oer_field_sequence_of() += OER field choice +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_choice']).check_oer_field_choice() += OER packet record +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_packet_record']).check_oer_packet_record() += OER nested sequence +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_nested_sequence']).check_oer_nested_sequence() += OER nested sequence trailing field +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_nested_sequence_trailing']).check_oer_nested_sequence_trailing() += OER sequence of with trailing field +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_sequence_of_with_trailing']).check_oer_sequence_of_with_trailing() + + ++ ASN.1 packet build tests (BER, OER, PER) += BER record build roundtrip +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_ber_record_build_roundtrip']).check_ber_record_build_roundtrip() += OER record build roundtrip +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_oer_record_build_roundtrip']).check_oer_record_build_roundtrip() += PER record build roundtrip +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_per_record_build_roundtrip']).check_per_record_build_roundtrip() += PER default field build +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_per_default_field_build']).check_per_default_field_build() += PER extensible integer build +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_per_extensible_integer_build']).check_per_extensible_integer_build() += PER constrained sequence of build +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_per_constrained_sequence_of_build']).check_per_constrained_sequence_of_build() += BER OER PER choice build +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_ber_oer_per_choice_build']).check_ber_oer_per_choice_build() + ++ ASN.1 packet dissection tests (BER, OER, PER) += BER field dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_ber_field_dissect']).check_ber_field_dissect() += BER record dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_ber_record_dissect']).check_ber_record_dissect() += OER field dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_oer_field_dissect']).check_oer_field_dissect() += OER record dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_oer_record_dissect']).check_oer_record_dissect() += PER field dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_field_dissect']).check_per_field_dissect() += PER record dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_record_dissect']).check_per_record_dissect() += PER default field dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_default_field_dissect']).check_per_default_field_dissect() += PER extensible integer dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_extensible_integer_dissect']).check_per_extensible_integer_dissect() += PER constrained sequence of dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_constrained_sequence_of_dissect']).check_per_constrained_sequence_of_dissect() += BER OER PER record dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_ber_oer_per_record_dissect']).check_ber_oer_per_record_dissect() + ++ ASN.1 UPER codec += UPER boolean true +UPERcodec_BOOLEAN.enc(1) == b"\x80" += UPER boolean false +UPERcodec_BOOLEAN.enc(0) == b"\x00" += UPER unconstrained integer +UPERcodec_INTEGER.enc(42) == b"\x01*" += UPER constrained integer +UPERcodec_INTEGER.enc(200, uper_min=0, uper_max=255) == b"\xc8" += UPER signed constrained integer +UPERcodec_INTEGER.enc(-1, uper_min=-128, uper_max=127) == b"\x7f" += UPER octet string +UPERcodec_STRING.enc(b"AB") == b"\x02AB" += UPER fixed octet string +UPERcodec_STRING.enc(b"\x12\x34\x56", size_len=3) == b"\x12\x34\x56" += UPER null +UPERcodec_NULL.enc(None) == b"" += UPER enumerated index +UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]) == b"\x80" += UPER bit string variable size +UPERcodec_BIT_STRING.enc((b"\xab\xcd", 16), uper_min=1, uper_max=20) == bytes.fromhex("7d5e68") += UPER enumerated roundtrip +x, r = UPERcodec_ENUMERATED.do_dec(UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), uper_enum_values=[1, 200]) +x.val == 200 and r == b"" += UPER integer roundtrip +x, r = UPERcodec_INTEGER.do_dec(UPERcodec_INTEGER.enc(-1)) +x.val == -1 and r == b"" += UPER boolean roundtrip +x, r = UPERcodec_BOOLEAN.do_dec(UPERcodec_BOOLEAN.enc(1)) +x.val == 1 and r == b"" += UPER ASN1 object encoding +ASN1_INTEGER(42).enc(ASN1_Codecs.PER) == b"\x01*" += UPER codec registration +ASN1_Class_UNIVERSAL.INTEGER.get_codec(ASN1_Codecs.PER) is UPERcodec_INTEGER + ++ ASN.1 UPER codec roundtrips += UPER codec primitive roundtrips +__import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_roundtrips']).check_uper_codec_roundtrips() += UPER codec reference decode interop +__import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_reference_decode']).check_uper_codec_reference_decode() += UPER codec scapy encode reference interop +__import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_encode_reference']).check_uper_codec_encode_reference() += UPER codec OID encode interop +__import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_oid_encode_interop']).check_uper_codec_oid_encode_interop() += UPER codec OID roundtrip +__import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_oid_roundtrip']).check_uper_codec_oid_roundtrip() + ++ ASN.1 UPER helpers += UPER length determinant +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_length_determinant']).check_uper_length_determinant() += UPER count roundtrip +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_count_roundtrip']).check_uper_count_roundtrip() += UPER choice index roundtrip +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_choice_index_roundtrip']).check_uper_choice_index_roundtrip() += UPER optional presence +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_optional_presence']).check_uper_optional_presence() += UPER constrained integer helper +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_constrained_integer']).check_uper_constrained_integer() += UPER constrained signed integer helper +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_constrained_signed_integer']).check_uper_constrained_signed_integer() += UPER octet string helper roundtrip +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_octet_string_roundtrip']).check_uper_octet_string_roundtrip() += UPER unexpected remainder detection +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_has_unexpected_remainder']).check_uper_has_unexpected_remainder() += UPER join encodings +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_join_encodings']).check_uper_join_encodings() += UPER chained encode into +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_chained_encode_into']).check_uper_chained_encode_into() + ++ ASN.1 UPER interoperability (reference vectors) += UPER primitive encode interop +__import__('test.scapy.layers.uper_iop', fromlist=['check_primitive_interop']).check_primitive_interop() += UPER composite encode interop +__import__('test.scapy.layers.uper_iop', fromlist=['check_composite_interop']).check_composite_interop() += UPER packet reference interop +__import__('test.scapy.layers.uper_iop', fromlist=['check_packet_reference_interop']).check_packet_reference_interop() += UPER packet decode vectors +__import__('test.scapy.layers.uper_iop', fromlist=['check_packet_decode_vectors']).check_packet_decode_vectors() + ++ ASN.1 UPER asn1scc interoperability += asn1scc vector encode interop +__import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_vectors']).check_asn1scc_vectors() += asn1scc README Message uPER reference +__import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_readme_message_reference']).check_asn1scc_readme_message_reference() += asn1scc README MessagePrefix Scapy interop +__import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_readme_message_prefix']).check_asn1scc_readme_message_prefix() + ++ ASN.1 UPER packets and fields += UPER field fixed size +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_fixed_size']).check_uper_field_fixed_size() += UPER field integer +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_integer']).check_uper_field_integer() += UPER field boolean +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_boolean']).check_uper_field_boolean() += UPER field string +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_string']).check_uper_field_string() += UPER field constrained integer +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_constrained_integer']).check_uper_field_constrained_integer() += UPER field optional +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_optional']).check_uper_field_optional() += UPER field sequence of +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_sequence_of']).check_uper_field_sequence_of() += UPER field choice +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_choice']).check_uper_field_choice() += UPER field choice definition order +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_choice_definition_order']).check_uper_field_choice_definition_order() += UPER packet record +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_packet_record']).check_uper_packet_record() += UPER field enumerated +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_enumerated']).check_uper_field_enumerated() += UPER field bit string +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_bit_string']).check_uper_field_bit_string() += UPER message prefix +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_message_prefix']).check_uper_message_prefix() += UPER sequence with choice +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_with_choice']).check_uper_sequence_with_choice() += UPER null packet +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_null_packet']).check_uper_null_packet() += UPER variable octet string +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_variable_octet_string']).check_uper_variable_octet_string() += UPER constrained range integer +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_constrained_range_integer']).check_uper_constrained_range_integer() += UPER sequence with enumerated +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_with_enumerated']).check_uper_sequence_with_enumerated() += UPER sequence of strings +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_of_strings']).check_uper_sequence_of_strings() += UPER sequence choice hex +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_choice_hex']).check_uper_sequence_choice_hex() += UPER nested sequence +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_nested_sequence']).check_uper_nested_sequence() += UPER sequence with null +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_with_null']).check_uper_sequence_with_null() += UPER fixed bit string packet +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_fixed_bit_string']).check_uper_fixed_bit_string() += UPER multi optional +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_multi_optional']).check_uper_multi_optional() += UPER sequence of constrained integers +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_of_constrained_ints']).check_uper_sequence_of_constrained_ints() += UPER signed integer field +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_signed_integer']).check_uper_signed_integer() + ++ ASN.1 UPER fuzzing += UPER fuzz encode +__import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_encode']).check_uper_fuzz_encode() += UPER fuzz encode roundtrip +__import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_roundtrip']).check_uper_fuzz_roundtrip() += UPER fuzz codec decode +__import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_codec_decode']).check_uper_fuzz_codec_decode() += UPER fuzz packet decode +__import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_packet_decode']).check_uper_fuzz_packet_decode() + diff --git a/test/scapy/layers/asn1_build_tests.py b/test/scapy/layers/asn1_build_tests.py new file mode 100644 index 00000000000..9fc9122a9e7 --- /dev/null +++ b/test/scapy/layers/asn1_build_tests.py @@ -0,0 +1,185 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Cross-codec ASN.1 packet build and round-trip tests (BER, OER, PER). +""" +import scapy.contrib.oer # noqa: F401 # register OER stem +import scapy.contrib.uper # noqa: F401 # register UPER stem + +from scapy.asn1.asn1 import ASN1_Codecs, ASN1_INTEGER, ASN1_STRING +from scapy.asn1fields import ( + ASN1F_BOOLEAN, + ASN1F_CHOICE, + ASN1F_DEFAULT, + ASN1F_INTEGER, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import raw + +from typing import Any + +from test.scapy.layers.ber_packets import BERRecord +from test.scapy.layers.oer_packets import OERRecord +from test.scapy.layers.uper_packets import UPERRecord + + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + + +def _record_kwargs(): + # type: () -> dict + return dict( + id=42, + flag=True, + label=b"hi", + extra=7, + values=[1, 2, 3], + ) + + +def check_ber_record_build_roundtrip(): + # type: () -> None + pkt = BERRecord(**_record_kwargs()) + assert len(raw(pkt)) > 0 + decoded = _roundtrip(BERRecord, pkt) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + +def check_oer_record_build_roundtrip(): + # type: () -> None + pkt = OERRecord(**_record_kwargs()) + assert len(raw(pkt)) > 0 + decoded = _roundtrip(OERRecord, pkt) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + +def check_per_record_build_roundtrip(): + # type: () -> None + pkt = UPERRecord(**_record_kwargs()) + assert len(raw(pkt)) > 0 + decoded = _roundtrip(UPERRecord, pkt) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + + +def check_per_default_field_build(): + # type: () -> None + class UPERDefaultRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_DEFAULT( + ASN1F_INTEGER( + "count", 600, + uper_min=0, uper_max=86401, oer_unsigned=True, + ), + 600, + ), + ) + + absent = UPERDefaultRecord(id=1) + assert raw(absent) == b"\x00\x80" + decoded = _roundtrip(UPERDefaultRecord, absent) + assert decoded.id.val == 1 + assert _asn1_int(decoded.count) == 600 + + present = UPERDefaultRecord(id=1, count=86400) + assert raw(present) == bytes.fromhex("80d46000") + decoded = _roundtrip(UPERDefaultRecord, present) + assert decoded.id.val == 1 + assert _asn1_int(decoded.count) == 86400 + + +def check_per_extensible_integer_build(): + # type: () -> None + class UPERExtInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER( + "n", 0, + uper_min=1, uper_max=65535, + uper_extensible=True, oer_unsigned=True, + ), + ) + + in_range = UPERExtInt(n=42) + assert raw(in_range) == bytes.fromhex("001480") + decoded = _roundtrip(UPERExtInt, in_range) + assert decoded.n.val == 42 + + out_of_range = UPERExtInt(n=1706733817) + assert raw(out_of_range) == bytes.fromhex("8232dd587c80") + decoded = _roundtrip(UPERExtInt, out_of_range) + assert decoded.n.val == 1706733817 + + +def check_per_constrained_sequence_of_build(): + # type: () -> None + class UPERConstrainedSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], + ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7), + uper_min=1, uper_max=3, + ) + + pkt = UPERConstrainedSeqOf(items=[1, 2]) + assert raw(pkt) == bytes.fromhex("4a") + decoded = _roundtrip(UPERConstrainedSeqOf, pkt) + assert [x.val for x in decoded.items] == [1, 2] + + +def check_ber_oer_per_choice_build(): + # type: () -> None + class BERChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + class OERChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + class PERChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + for cls in (BERChoice, OERChoice, PERChoice): + as_int = cls(c=ASN1_INTEGER(99)) + assert len(raw(as_int)) > 0 + decoded = _roundtrip(cls, as_int) + assert decoded.c.val == 99 + + as_str = cls(c=ASN1_STRING(b"AB")) + assert len(raw(as_str)) > 0 + decoded = _roundtrip(cls, as_str) + assert decoded.c.val == b"AB" diff --git a/test/scapy/layers/asn1_coverage.py b/test/scapy/layers/asn1_coverage.py new file mode 100644 index 00000000000..dea2d651e3f --- /dev/null +++ b/test/scapy/layers/asn1_coverage.py @@ -0,0 +1,890 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Additional coverage for UPER, OER, and asn1fields helpers. +""" + + +def _raises(exc, func): + # type: (type, Any) -> None + try: + func() + except exc: + return + raise AssertionError("Expected %s" % exc.__name__) + + +from typing import Any +from unittest import mock + +from scapy.asn1.asn1 import ( + ASN1_BIT_STRING, + ASN1_Class_UNIVERSAL, + ASN1_Codecs, + ASN1_Error, + ASN1_INTEGER, + ASN1_STRING, + ASN1_TIME_TICKS, +) +from scapy.asn1.ber import BER_Decoding_Error +from scapy.contrib.oer import ( + OER_Decoding_Error, + OER_Encoding_Error, + OERcodec_BIT_STRING, + OERcodec_IPADDRESS, + OERcodec_SEQUENCE, + OERcodec_SET, +) +from scapy.contrib.uper import ( + UPER_Decoding_Error, + UPER_Encoding_Error, + UPER_Decoder, + UPER_Encoder, + UPERcodec_BIT_STRING, + UPERcodec_ENUMERATED, + UPERcodec_IPADDRESS, + UPERcodec_SEQUENCE, + UPERcodec_SET, +) +from scapy.asn1fields import ( + ASN1F_BIT_STRING, + ASN1F_BIT_STRING_ENCAPS, + ASN1F_BOOLEAN, + ASN1F_CHOICE, + ASN1F_DEFAULT, + ASN1F_FLAGS, + ASN1F_IPADDRESS, + ASN1F_INTEGER, + ASN1F_OID, + ASN1F_PACKET, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_SET_OF, + ASN1F_STRING, + ASN1F_STRING_ENCAPS, + ASN1F_STRING_PacketField, + ASN1F_TIME_TICKS, + ASN1F_UTC_TIME, + ASN1F_badsequence, + ASN1F_enum_INTEGER, + ASN1F_omit, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import Raw, raw + + +class _InnerRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("mode", ASN1_INTEGER(0), ["off", "on"]), + ) + + +class _EncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_ENCAPS("payload", None, _InnerRecord), + ) + + +class _FlagsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_FLAGS("f", "000", ["read", "write", "exec"]), + ) + + +class _SetOfRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SET_OF("items", [], ASN1F_INTEGER) + + +class _PacketFieldRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_PacketField("data", b""), + ) + + +class _ExplicitPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2) + + +class _BitEncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord), + ) + + +def check_uper_error_str(): + # type: () -> None + obj = ASN1_INTEGER(2) + err = UPER_Encoding_Error("enc", encoded=obj, remaining=b"x") + assert "Already encoded" in str(err) + err2 = UPER_Decoding_Error("dec", decoded=obj, remaining=b"y") + assert "Already decoded" in str(err2) + + +def check_uper_length_determinant_extended(): + # type: () -> None + enc = UPER_Encoder() + assert enc.append_length_determinant(32768) == 32768 + assert enc.as_bytes() == b"\xc2" + + enc = UPER_Encoder() + assert enc.append_length_determinant(49152) == 49152 + assert enc.as_bytes() == b"\xc3" + + enc = UPER_Encoder() + assert enc.append_length_determinant(65535) == 49152 + assert enc.as_bytes() == b"\xc3" + + +def check_uper_unconstrained_whole_number(): + # type: () -> None + enc = UPER_Encoder() + enc.append_unconstrained_whole_number(-256) + dec = UPER_Decoder(enc.as_bytes()) + assert dec.read_unconstrained_whole_number() == -256 + + enc = UPER_Encoder() + enc.append_unconstrained_whole_number(0) + dec = UPER_Decoder(enc.as_bytes()) + assert dec.read_unconstrained_whole_number() == 0 + + +def check_uper_bit_string_paths(): + # type: () -> None + encoded = UPERcodec_BIT_STRING.enc("1010", uper_min=1, uper_max=20) + obj, remain = UPERcodec_BIT_STRING.do_dec( + encoded, uper_min=1, uper_max=20, + ) + assert obj.val == "1010" + + encoded2 = UPERcodec_BIT_STRING.enc(b"\xab", uper_min=4, uper_max=8) + obj2, _ = UPERcodec_BIT_STRING.do_dec(encoded2, uper_min=4, uper_max=8) + assert len(obj2.val) == 8 + + fixed = UPERcodec_BIT_STRING.enc("1010101111001101", uper_min=16, uper_max=16) + obj3, _ = UPERcodec_BIT_STRING.do_dec(fixed, uper_min=16, uper_max=16) + assert obj3.val == "1010101111001101" + + +def check_uper_enumerated_range(): + # type: () -> None + encoded = UPERcodec_ENUMERATED.enc(3, uper_min=0, uper_max=7) + obj, remain = UPERcodec_ENUMERATED.do_dec(encoded, uper_min=0, uper_max=7) + assert obj.val == 3 + assert remain == b"" + + enc = UPER_Encoder() + UPERcodec_ENUMERATED.encode_into(enc, 2, uper_min=0, uper_max=3) + obj2 = UPERcodec_ENUMERATED.dec_from_decoder( + UPER_Decoder(enc.as_bytes()), + uper_min=0, + uper_max=3, + ) + assert obj2.val == 2 + + +def check_uper_sequence_errors(): + # type: () -> None + _raises(UPER_Encoding_Error, lambda: UPERcodec_SEQUENCE.enc([ASN1_INTEGER(1)])) + + _raises(UPER_Decoding_Error, lambda: UPERcodec_SEQUENCE.do_dec(b"\x00")) + + assert UPERcodec_SET.enc(b"raw") == b"raw" + + +def check_uper_ipaddress(): + # type: () -> None + encoded = UPERcodec_IPADDRESS.enc("10.0.0.1") + obj, remain = UPERcodec_IPADDRESS.do_dec(encoded) + assert obj.val == "10.0.0.1" + assert remain == b"" + + _raises(UPER_Encoding_Error, lambda: UPERcodec_IPADDRESS.enc("bad-ip")) + + +def check_oer_error_str(): + # type: () -> None + obj = ASN1_INTEGER(1) + err = OER_Encoding_Error("enc", encoded=obj, remaining=b"z") + assert "Already encoded" in str(err) + err2 = OER_Decoding_Error("dec", decoded=obj, remaining=b"w") + assert "Already decoded" in str(err2) + + +def check_oer_ipaddress_and_sequence(): + # type: () -> None + encoded = OERcodec_IPADDRESS.enc("127.0.0.1") + obj, remain = OERcodec_IPADDRESS.do_dec(encoded) + assert obj.val == "127.0.0.1" + assert remain == b"" + + fixed = OERcodec_IPADDRESS.enc("127.0.0.1", size_len=4) + obj2, remain2 = OERcodec_IPADDRESS.do_dec(fixed, size_len=4) + assert obj2.val == "127.0.0.1" + assert remain2 == b"" + + _raises(OER_Encoding_Error, lambda: OERcodec_IPADDRESS.enc("bad-ip")) + + _raises(OER_Decoding_Error, lambda: OERcodec_IPADDRESS.do_dec(b"\x01")) + + assert OERcodec_SEQUENCE.enc(b"payload") == b"payload" + assert OERcodec_SET.enc(b"payload") == b"payload" + + _raises(OER_Decoding_Error, lambda: OERcodec_SEQUENCE.do_dec(b"\x00")) + + empty, remain = OERcodec_BIT_STRING.do_dec(OERcodec_BIT_STRING.enc("")) + assert empty.val == "" + assert remain == b"" + + +def check_asn1fields_enum_and_flags(): + # type: () -> None + pkt = _InnerRecord(mode="on") + built = raw(pkt) + decoded = _InnerRecord(built) + assert decoded.mode.val == 1 + + flags = _FlagsRecord(f="read+exec") + assert flags.f.val == "101" + assert "read, exec" in _FlagsRecord.ASN1_root.seq[0].i2repr(flags, flags.f) + + set_pkt = _SetOfRecord(items=[ASN1_INTEGER(0), ASN1_INTEGER(1)]) + set_raw = raw(set_pkt) + set_dec = _SetOfRecord(set_raw) + assert [x.val for x in set_dec.items] == [0, 1] + + +def check_asn1fields_encaps_and_packet(): + # type: () -> None + inner = _InnerRecord(mode=1) + enc = _EncapsRecord() + enc.payload = inner + enc_raw = raw(enc) + enc_dec = _EncapsRecord(enc_raw) + assert enc_dec.payload.mode.val == 1 + + pkt_field = _PacketFieldRecord() + pkt_field.data = _InnerRecord(mode=0) + pf_raw = raw(pkt_field) + pf_dec = _PacketFieldRecord(pf_raw) + assert isinstance(pf_dec.data.val, bytes) + + explicit = _ExplicitPacket() + explicit.inner = _InnerRecord(mode=1) + ex_raw = raw(explicit) + ex_dec = _ExplicitPacket(ex_raw) + assert ex_dec.inner.mode.val == 1 + + +def check_asn1fields_choice_and_special(): + # type: () -> None + class _OerChoiceRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + class _BerChoiceRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + oer = _OerChoiceRecord(c=ASN1_INTEGER(1)) + oer_dec = _OerChoiceRecord(raw(oer)) + assert oer_dec.c.val == 1 + + ber = _BerChoiceRecord(c=ASN1_INTEGER(0)) + ber_dec = _BerChoiceRecord(raw(ber)) + assert ber_dec.c.val == 0 + + inner_bytes = raw(_InnerRecord(mode=0)) + bit_payload = ASN1_BIT_STRING( + inner_bytes, + readable=True, + ) + bit_pkt = _BitEncapsRecord(b=bit_payload) + bit_dec = _BitEncapsRecord(raw(bit_pkt)) + assert bit_dec.b.mode.val == 0 + + class _TicksRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_TIME_TICKS("t", ASN1_TIME_TICKS(0)) + + class _IpRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_IPADDRESS("addr", ASN1_STRING(b"")) + + ticks = _TicksRecord(t=ASN1_TIME_TICKS(1234)) + assert raw(ticks).endswith(b"\x04\xd2") + + ip = _IpRecord() + ip.addr = "192.168.1.1" + assert raw(ip) == b"\x40\x04\xc0\xa8\x01\x01" + + +def check_asn1fields_optional_dissect(): + # type: () -> None + class _OptRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ) + + class _BerChoiceRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + pkt = _OptRecord(id=0, extra=None) + assert raw(pkt) + decoded = _OptRecord(raw(pkt)) + assert decoded.extra is None + + choice_rand = _BerChoiceRecord.ASN1_root.randval() + assert choice_rand is not None + + +def check_asn1fields_default_and_omit(): + # type: () -> None + class _DefaultRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_DEFAULT( + ASN1F_INTEGER( + "count", 600, + uper_min=0, uper_max=86401, oer_unsigned=True, + ), + 600, + ), + ) + + absent = _DefaultRecord(id=1) + assert raw(absent) == b"\x00\x80" + decoded = _DefaultRecord(raw(absent)) + assert decoded.id.val == 1 + assert decoded.count == 600 or decoded.count.val == 600 + + present = _DefaultRecord(id=1, count=86400) + decoded = _DefaultRecord(raw(present)) + assert decoded.count.val == 86400 + + class _OmitRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_omit("ignored", None), + ) + + omit_pkt = _OmitRecord(id=7) + assert raw(omit_pkt) == bytes.fromhex("3003020107") + + +def check_asn1fields_extensible_per(): + # type: () -> None + class _ExtSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_optional(ASN1F_INTEGER("extra", 0, uper_min=0, uper_max=7)), + uper_extensible=True, + ) + + pkt = _ExtSeq(id=2, extra=3) + data = raw(pkt) + decoded = _ExtSeq(data) + assert decoded.id.val == 2 + assert decoded.extra.val == 3 + + dec = UPER_Decoder(b"\x80") + _raises( + UPER_Decoding_Error, + lambda: _ExtSeq.ASN1_root.dissect_from_decoder(_ExtSeq(), dec), + ) + + class _ExtChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + uper_extensible=True, + ) + + choice = _ExtChoice(c=ASN1_INTEGER(4)) + assert raw(choice) + dec = UPER_Decoder(b"\x80") + _raises( + UPER_Decoding_Error, + lambda: _ExtChoice.ASN1_root.m2i_from_decoder(_ExtChoice(), dec), + ) + + class _InnerItem(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7) + + class _ExtSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], _InnerItem, + uper_min=1, uper_max=2, uper_extensible=True, + ) + + in_range = _ExtSeqOf(items=[_InnerItem(n=1)]) + assert raw(in_range) + decoded = _ExtSeqOf(raw(in_range)) + assert decoded.items[0].n.val == 1 + + out_of_range = _ExtSeqOf( + items=[_InnerItem(n=i) for i in range(4)], + ) + assert raw(out_of_range) + decoded = _ExtSeqOf(raw(out_of_range)) + assert len(decoded.items) == 4 + + +def check_asn1fields_sequence_of_advanced(): + # type: () -> None + class _Inner(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7) + + class _SeqOfPackets(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], _Inner, uper_min=1, uper_max=3, + ) + + pkt = _SeqOfPackets(items=[_Inner(n=1), _Inner(n=2)]) + decoded = _SeqOfPackets(raw(pkt)) + assert [x.n.val for x in decoded.items] == [1, 2] + + class _OerSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + + oer_pkt = _OerSeqOf(values=[1, 2]) + oer_dec = _OerSeqOf(raw(oer_pkt)) + assert [x.val for x in oer_dec.values] == [1, 2] + + class _EmptySeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + + empty = _EmptySeqOf(values=None) + assert raw(empty) == b"\x00" + assert _EmptySeqOf.ASN1_root.i2repr(empty, None) == "[]" + assert _EmptySeqOf.ASN1_root.i2repr( + _EmptySeqOf(values=[ASN1_INTEGER(1)]), + [ASN1_INTEGER(1)], + ).startswith("[") + + _raises(ValueError, lambda: ASN1F_SEQUENCE_OF("bad", [], object())) + + +def check_asn1fields_choice_advanced(): + # type: () -> None + class _InnerChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + class _NestedChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), _InnerChoice, ASN1F_INTEGER, + ) + + nested = _NestedChoice(c=_InnerChoice(c=ASN1_STRING(b"xy"))) + assert len(raw(nested)) > 0 + nested_dec = _NestedChoice(raw(nested)) + assert isinstance(nested_dec.c, (_InnerChoice, ASN1_STRING)) + + class _OerTaggedChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + explicit_tag=0xA1, + ) + + oer_choice = _OerTaggedChoice(c=ASN1_INTEGER(9)) + assert raw(oer_choice) + + class _PacketChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", + ASN1_INTEGER(0), + ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2), + ASN1F_INTEGER, + ) + + packet_choice = _PacketChoice( + c=_InnerRecord(mode=ASN1_INTEGER(1)), + ) + packet_dec = _PacketChoice(raw(packet_choice)) + assert packet_dec.c.mode.val == 1 + + class _PerChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + _raises( + ASN1_Error, + lambda: ASN1F_CHOICE( + "c", 0, ASN1F_INTEGER, implicit_tag=0xA0, + ), + ) + _raises( + ASN1_Error, + lambda: _PerChoice.ASN1_root.m2i(_PerChoice(), b""), + ) + _raises( + ASN1_Error, + lambda: _PerChoice.ASN1_root._uper_encode_into( + UPER_Encoder(), _PerChoice(), 42, + ), + ) + + +def check_asn1fields_enum_bitstring_and_flags(): + # type: () -> None + class _NamedEnum(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_enum_INTEGER( + "state", 0, ["off", "on", "auto"], + ) + + named = _NamedEnum(state="on") + built = raw(named) + decoded = _NamedEnum(built) + assert decoded.state.val == 1 + assert "'on'" in _NamedEnum.ASN1_root.i2repr(decoded, decoded.state) + + class _BitRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_BIT_STRING("bits", b"\xaa") + + assert raw(_BitRecord()) + + flags = _FlagsRecord() + flags.f = ASN1_BIT_STRING("101") + assert "read, exec" in _FlagsRecord.ASN1_root.seq[0].i2repr(flags, flags.f) + + class _BadBitEncaps(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord) + + _raises( + BER_Decoding_Error, + lambda: _BadBitEncaps.ASN1_root.m2i( + _BadBitEncaps(), + b"\x03\x02\x01\x00", + ), + ) + + +def check_asn1fields_packet_and_sequence_errors(): + # type: () -> None + class _PerInner(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("mode", 0, uper_min=0, uper_max=1) + + class _PacketWrap(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_PACKET("inner", None, _PerInner) + + inner = _PerInner(mode=1) + wrap = _PacketWrap(inner=inner) + decoded = _PacketWrap(raw(wrap)) + assert decoded.inner.mode.val == 1 + + class _DynamicPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_PACKET( + "inner", None, _PerInner, + next_cls_cb=lambda pkt: _PerInner, + ) + + dyn = _DynamicPacket(inner=_PerInner(mode=0)) + assert _DynamicPacket.ASN1_root._resolve_cls(dyn) is _PerInner + + empty_packet = _PacketWrap(inner=None) + assert raw(empty_packet) == b"" + + class _BerSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_INTEGER("extra", 0), + ) + + _raises( + BER_Decoding_Error, + lambda: _BerSeq.ASN1_root.m2i( + _BerSeq(), + bytes.fromhex("300702010102010200ff"), + ), + ) + + class _OerSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, size_len=1, oer_unsigned=True), + ) + + _, remain = _OerSeq.ASN1_root.m2i(_OerSeq(), b"\x01\xff") + assert remain == b"\xff" + + class _PerSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ) + + _raises( + UPER_Decoding_Error, + lambda: _PerSeq.ASN1_root.m2i(_PerSeq(), b"\x80\xff"), + ) + + empty_seq = _BerSeq() + _BerSeq.ASN1_root._dissect_sequence_children(empty_seq, b"") + assert empty_seq.id is None + assert empty_seq.extra is None + + class _OptListRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_optional( + ASN1F_SEQUENCE_OF("items", [], ASN1F_INTEGER), + ), + ) + + opt_list = _OptListRecord(id=1, items=None) + assert raw(opt_list) + + field = ASN1F_INTEGER("n", 0) + with mock.patch.object( + _InnerRecord, "__init__", side_effect=ASN1F_badsequence, + ): + pkt_obj, remain = field.extract_packet( + _InnerRecord, b"\xab\xcd", _underlayer=None, + ) + assert isinstance(pkt_obj, Raw) + assert pkt_obj.load == b"\xab\xcd" + assert remain == b"\xab\xcd" + + +def check_asn1fields_more_coverage(): + # type: () -> None + _raises( + ASN1_Error, + lambda: ASN1F_INTEGER("x", 0, implicit_tag=1, explicit_tag=2), + ) + + class _IntRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER("n", 0) + + field = _IntRecord.ASN1_root + _raises( + ASN1_Error, + lambda: field.i2m(_IntRecord(), ASN1_STRING(b"bad")), + ) + + flex_field = ASN1F_INTEGER("n", 0, flexible_tag=True, explicit_tag=0xA0) + obj, remain = flex_field.m2i(_IntRecord(), bytes.fromhex("a1020101")) + assert obj.tag != ASN1_Class_UNIVERSAL.INTEGER or remain == b"" + + class _FlexSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + explicit_tag=0xA1, + flexible_tag=True, + ) + + flex_seq = _FlexSeq(id=1) + assert raw(flex_seq) + decoded = _FlexSeq(raw(flex_seq)) + assert decoded.id.val == 1 + + assert ASN1F_BOOLEAN("b", False).randval() is not None + assert ASN1F_BIT_STRING("b", b"").randval() is not None + assert ASN1F_OID("o", None).randval() is not None + assert ASN1F_UTC_TIME("t", "").randval() is not None + assert " 0 + + empty_inner, remain = packet_field.m2i(_FlexPacket(), b"") + assert empty_inner is None and remain == b"" + + obj_val = packet_field.i2m(_FlexPacket(), _InnerRecord(mode=0)) + assert len(obj_val) > 0 + + flags_field = _FlagsRecord.ASN1_root.seq[0] + assert flags_field.i2repr(_FlagsRecord(), None) == "None" + + class _OerFlexSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", [], ASN1F_INTEGER, + explicit_tag=0xA1, + ) + + _OerFlexSeqOf.ASN1_root.flexible_tag = True + + oer_seq = _OerFlexSeqOf(values=[1]) + data = raw(oer_seq) + decoded = _OerFlexSeqOf(data) + assert decoded.values[0].val == 1 + + class _BerFlexSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", [], ASN1F_INTEGER, + explicit_tag=0xA1, + ) + + _BerFlexSeqOf.ASN1_root.flexible_tag = True + + ber_seq = _BerFlexSeqOf(values=[2]) + assert raw(ber_seq) + + class _ExtChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + uper_extensible=True, + ) + + dec = UPER_Decoder(b"\x80") + _raises( + UPER_Decoding_Error, + lambda: _ExtChoice.ASN1_root.m2i_from_decoder(_ExtChoice(), dec), + ) + + class _SingleChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE("c", ASN1_INTEGER(0), ASN1F_INTEGER) + + single = _SingleChoice(c=ASN1_INTEGER(3)) + assert raw(single) + + class _FlexChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + flexible_tag=True, + ) + + flex_choice = _FlexChoice(c=ASN1_INTEGER(4)) + assert raw(flex_choice) + + class _OerPktChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + oer_pkt_choice = _OerPktChoice(c=ASN1_STRING(b"hi")) + assert raw(oer_pkt_choice) + diff --git a/test/scapy/layers/asn1_dissect_tests.py b/test/scapy/layers/asn1_dissect_tests.py new file mode 100644 index 00000000000..2383560c3cf --- /dev/null +++ b/test/scapy/layers/asn1_dissect_tests.py @@ -0,0 +1,280 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +ASN.1 packet dissection tests from fixed byte vectors (BER, OER, PER). +""" +import scapy.contrib.oer # noqa: F401 # register OER stem +import scapy.contrib.uper # noqa: F401 # register UPER stem + +from typing import Any, Type + +from scapy.asn1.asn1 import ASN1_Codecs +from scapy.asn1fields import ( + ASN1F_DEFAULT, + ASN1F_INTEGER, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, +) +from scapy.asn1packet import ASN1_Packet + +from test.scapy.layers.ber_packets import ( + BERChoiceField, + BERFixedFields, + BEROptionalField, + BERRecord, + BERSequenceOfIntegers, + BERTaggedInteger, +) +from test.scapy.layers.oer_packets import ( + OERChoiceField, + OERFixedFields, + OEROptionalField, + OERRecord, + OERSequenceOfIntegers, + OERTaggedInteger, +) +from test.scapy.layers.uper_packets import ( + UPERChoiceField, + UPERFixedFields, + UPEROptionalField, + UPERRecord, + UPERSequenceOfIntegers, +) + + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + + +def _assert_record(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + +def _assert_record_empty(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] + + +def _dissect(cls, data_hex): + # type: (Type[ASN1_Packet], str) -> ASN1_Packet + return cls(bytes.fromhex(data_hex)) + + +def check_ber_field_dissect(): + # type: () -> None + tagged = _dissect(BERTaggedInteger, "a103020105") + assert tagged.n.val == 5 + + fixed = _dissect(BERFixedFields, "300d02810200c80483000003414243") + assert fixed.n.val == 200 + assert fixed.s.val == b"ABC" + + present = _dissect(BEROptionalField, "3008020101a003020107") + assert present.id.val == 1 + assert present.extra.val == 7 + + absent = _dissect(BEROptionalField, "3003020101") + assert absent.id.val == 1 + assert absent.extra is None + + seqof = _dissect(BERSequenceOfIntegers, "3009020101020102020103") + assert [x.val for x in seqof.values] == [1, 2, 3] + + as_int = _dissect(BERChoiceField, "020163") + assert as_int.c.val == 99 + + as_str = _dissect(BERChoiceField, "040178") + assert as_str.c.val == b"x" + + +def check_ber_record_dissect(): + # type: () -> None + decoded = _dissect( + BERRecord, + "301a02012a01010104026869" + "a003020107" + "3009020101020102020103", + ) + _assert_record(decoded) + + empty = _dissect(BERRecord, "300a02010101010004003000") + _assert_record_empty(empty) + + +def check_oer_field_dissect(): + # type: () -> None + tagged = _dissect(OERTaggedInteger, "a10105") + assert tagged.n.val == 5 + + fixed = _dissect(OERFixedFields, "c8414243") + assert fixed.n.val == 200 + assert fixed.s.val == b"ABC" + + present = _dissect(OEROptionalField, "0101a00107") + assert present.id.val == 1 + assert present.extra.val == 7 + + absent = _dissect(OEROptionalField, "0101") + assert absent.id.val == 1 + assert absent.extra is None + + seqof = _dissect(OERSequenceOfIntegers, "0103010101020103") + assert [x.val for x in seqof.values] == [1, 2, 3] + + as_int = _dissect(OERChoiceField, "020163") + assert as_int.c.val == 99 + + as_str = _dissect(OERChoiceField, "040178") + assert as_str.c.val == b"x" + + +def check_oer_record_dissect(): + # type: () -> None + decoded = _dissect( + OERRecord, + "012aff026869a00107" + "0103010101020103", + ) + _assert_record(decoded) + + empty = _dissect(OERRecord, "010100000100") + _assert_record_empty(empty) + + +def check_per_field_dissect(): + # type: () -> None + fixed = _dissect(UPERFixedFields, "c8414243") + assert fixed.n.val == 200 + assert fixed.s.val == b"ABC" + + present = _dissect(UPEROptionalField, "80954041c0") + assert present.id.val == 42 + assert present.flag.val == 1 + assert present.extra.val == 7 + + absent = _dissect(UPEROptionalField, "009540") + assert absent.id.val == 42 + assert absent.flag.val == 1 + assert absent.extra is None + + seqof = _dissect(UPERSequenceOfIntegers, "03010101020103") + assert [x.val for x in seqof.values] == [1, 2, 3] + + empty_seqof = _dissect(UPERSequenceOfIntegers, "00") + assert [x.val for x in empty_seqof.values] == [] + + as_int = _dissect(UPERChoiceField, "00b180") + assert as_int.c.val == 99 + + as_str = _dissect(UPERChoiceField, "8120a100") + assert as_str.c.val == b"AB" + + +def check_per_record_dissect(): + # type: () -> None + decoded = _dissect( + UPERRecord, + "8095409a1a4041c0c04040408040c0", + ) + _assert_record(decoded) + + partial = _dissect(UPERRecord, "0095409050808040404080") + assert partial.id.val == 42 + assert partial.flag.val == 1 + assert partial.label.val == b"AB" + assert partial.extra is None + assert [x.val for x in partial.values] == [1, 2] + + empty = _dissect(UPERRecord, "0080800000") + _assert_record_empty(empty) + + +def check_per_default_field_dissect(): + # type: () -> None + class UPERDefaultRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_DEFAULT( + ASN1F_INTEGER( + "count", 600, + uper_min=0, uper_max=86401, oer_unsigned=True, + ), + 600, + ), + ) + + absent = _dissect(UPERDefaultRecord, "0080") + assert absent.id.val == 1 + assert _asn1_int(absent.count) == 600 + + present = _dissect(UPERDefaultRecord, "80d46000") + assert present.id.val == 1 + assert _asn1_int(present.count) == 86400 + + +def check_per_extensible_integer_dissect(): + # type: () -> None + class UPERExtInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER( + "n", 0, + uper_min=1, uper_max=65535, + uper_extensible=True, oer_unsigned=True, + ), + ) + + in_range = _dissect(UPERExtInt, "001480") + assert in_range.n.val == 42 + + out_of_range = _dissect(UPERExtInt, "8232dd587c80") + assert out_of_range.n.val == 1706733817 + + +def check_per_constrained_sequence_of_dissect(): + # type: () -> None + class UPERConstrainedSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], + ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7), + uper_min=1, uper_max=3, + ) + + decoded = _dissect(UPERConstrainedSeqOf, "4a") + assert [x.val for x in decoded.items] == [1, 2] + + +def check_ber_oer_per_record_dissect(): + # type: () -> None + for cls, data_hex in [ + ( + BERRecord, + "301a02012a01010104026869" + "a003020107" + "3009020101020102020103", + ), + ( + OERRecord, + "012aff026869a00107" + "0103010101020103", + ), + ( + UPERRecord, + "8095409a1a4041c0c04040408040c0", + ), + ]: + _assert_record(_dissect(cls, data_hex)) diff --git a/test/scapy/layers/ber_codec.py b/test/scapy/layers/ber_codec.py new file mode 100644 index 00000000000..e6939f7a27e --- /dev/null +++ b/test/scapy/layers/ber_codec.py @@ -0,0 +1,275 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +BER codec and helper coverage tests. +""" + +from typing import Any + + +def _raises(exc, func): + # type: (type, Any) -> None + try: + func() + except exc: + return + raise AssertionError("Expected %s" % exc.__name__) + + +from scapy.asn1.asn1 import ( + ASN1_Class_UNIVERSAL, + ASN1_DECODING_ERROR, + ASN1_INTEGER, + ASN1_Object, +) +from scapy.asn1.ber import ( + BER_BadTag_Decoding_Error, + BER_Decoding_Error, + BER_Encoding_Error, + BER_Exception, + BER_id_dec, + BER_id_enc, + BER_len_dec, + BER_len_enc, + BER_num_dec, + BER_num_enc, + BER_tagging_dec, + BER_tagging_enc, + BERcodec_BIT_STRING, + BERcodec_INTEGER, + BERcodec_IPADDRESS, + BERcodec_NULL, + BERcodec_Object, + BERcodec_OID, + BERcodec_SEQUENCE, + BERcodec_SET, + BERcodec_STRING, +) +from scapy.config import conf + + +def check_ber_error_str(): + # type: () -> None + obj = ASN1_INTEGER(1) + enc_err = BER_Encoding_Error("enc", encoded=obj, remaining=b"rest") + assert "Already encoded" in str(enc_err) + enc_err2 = BER_Encoding_Error("enc", encoded="raw", remaining=b"") + assert "raw" in str(enc_err2) + + dec_err = BER_Decoding_Error("dec", decoded=obj, remaining=b"tail") + assert "Already decoded" in str(dec_err) + dec_err2 = BER_Decoding_Error("dec", decoded=[1], remaining=b"") + assert "[1]" in str(dec_err2) + + +def check_ber_len_enc_dec(): + # type: () -> None + for value in [0, 1, 127, 128, 999]: + encoded = BER_len_enc(value) + length, remain = BER_len_dec(encoded) + assert length == value + assert remain == b"" + + assert BER_len_enc(45, size=None) == BER_len_enc(45, size=0) + assert BER_len_enc(45, size=4) == b"\x84\x00\x00\x00-" + + _raises(BER_Exception, lambda: BER_len_enc(0, size=128)) + + _raises(BER_Decoding_Error, lambda: BER_len_dec(b"\x82")) + + +def check_ber_num_enc_dec(): + # type: () -> None + for value in [0, 1, 127, 256, 16384]: + encoded = BER_num_enc(value) + decoded, remain = BER_num_dec(encoded) + assert decoded == value + assert remain == b"" + + _raises(BER_Decoding_Error, lambda: BER_num_dec(b"")) + + _raises(BER_Decoding_Error, lambda: BER_num_dec(b"\x80\x80")) + + +def check_ber_id_enc_dec(): + # type: () -> None + for tag in [0x02, 0x30, 0x81, 0xA0]: + encoded = BER_id_enc(tag) + decoded, remain = BER_id_dec(encoded) + assert decoded == tag + assert remain == b"" + + high_tag = (0x03 << 5) + 0x22 + encoded = BER_id_enc(high_tag) + decoded, remain = BER_id_dec(encoded) + assert decoded == high_tag + assert remain == b"" + + +def check_ber_tagging(): + # type: () -> None + inner = BERcodec_INTEGER.enc(7) + implicit = BER_tagging_enc(inner, implicit_tag=0xA0) + assert implicit.startswith(b"\xa0") + real_tag, payload = BER_tagging_dec( + implicit, + hidden_tag=ASN1_Class_UNIVERSAL.INTEGER, + implicit_tag=0xA0, + ) + assert real_tag is None + assert payload[0] == int(ASN1_Class_UNIVERSAL.INTEGER) + + conf.ASN1_default_long_size = 4 + try: + explicit = BER_tagging_enc(inner, explicit_tag=0xA1) + assert explicit.startswith(b"\xa1\x84") + real_tag, payload = BER_tagging_dec( + explicit, + explicit_tag=0xA1, + ) + assert real_tag is None + assert payload == inner + finally: + conf.ASN1_default_long_size = 0 + + _raises(BER_Decoding_Error, lambda: BER_tagging_dec( + implicit, + hidden_tag=ASN1_Class_UNIVERSAL.INTEGER, + implicit_tag=0xA1, + )) + + safe_tag, _ = BER_tagging_dec( + implicit, + hidden_tag=ASN1_Class_UNIVERSAL.INTEGER, + implicit_tag=0xA1, + safe=True, + ) + assert safe_tag == 0xA0 + + +def check_ber_integer(): + # type: () -> None + for value in [0, 1, 127, 128, 255, -1, -128, -129]: + encoded = BERcodec_INTEGER.enc(value) + obj, remain = BERcodec_INTEGER.do_dec(encoded) + assert obj.val == value + assert remain == b"" + + _raises(BER_BadTag_Decoding_Error, lambda: BERcodec_INTEGER.do_dec(BERcodec_STRING.enc(b"x"))) + + _raises(BER_Decoding_Error, lambda: BERcodec_INTEGER.check_type_get_len(b"\x02")) + + +def check_ber_bit_string(): + # type: () -> None + encoded = BERcodec_BIT_STRING.enc("1011") + obj, remain = BERcodec_BIT_STRING.do_dec(encoded) + assert obj.val == "1011" + assert remain == b"" + + padded = BERcodec_BIT_STRING.enc("10110000") + obj2, _ = BERcodec_BIT_STRING.do_dec(padded) + assert obj2.val == "10110000" + + _raises(BER_Decoding_Error, lambda: BERcodec_BIT_STRING.do_dec(b"\x03\x01\x08", safe=True)) + + _raises(BER_Decoding_Error, lambda: BERcodec_BIT_STRING.do_dec(b"\x03\x00")) + + +def check_ber_string_and_null(): + # type: () -> None + encoded = BERcodec_STRING.enc(b"hello") + obj, remain = BERcodec_STRING.do_dec(encoded) + assert obj.val == b"hello" + assert remain == b"" + + null = BERcodec_NULL.enc(0) + assert null == b"\x05\x00" + obj, remain = BERcodec_NULL.do_dec(null) + assert obj.val == 0 + + non_null = BERcodec_NULL.enc(42) + obj, remain = BERcodec_NULL.do_dec(non_null) + assert obj.val == 42 + + +def check_ber_oid(): + # type: () -> None + encoded = BERcodec_OID.enc("1.2.840.113556.1.4.529") + obj, remain = BERcodec_OID.do_dec(encoded) + assert obj.val == "1.2.840.113556.1.4.529" + assert remain == b"" + + empty, remain = BERcodec_OID.do_dec(BERcodec_OID.enc("")) + assert empty.val == "" + assert remain == b"" + + +def check_ber_sequence_and_set(): + # type: () -> None + payload = BERcodec_INTEGER.enc(1) + BERcodec_INTEGER.enc(2) + seq = BERcodec_SEQUENCE.enc(payload) + obj, remain = BERcodec_SEQUENCE.do_dec(seq) + assert len(obj.val) == 2 + assert obj.val[0].val == 1 + assert obj.val[1].val == 2 + assert remain == b"" + + as_list = BERcodec_SEQUENCE.enc([ASN1_INTEGER(3), ASN1_INTEGER(4)]) + obj2, remain2 = BERcodec_SEQUENCE.do_dec(as_list) + assert [x.val for x in obj2.val] == [3, 4] + assert remain2 == b"" + + st = BERcodec_SET.enc(payload) + obj3, remain3 = BERcodec_SET.do_dec(st) + assert len(obj3.val) == 2 + assert remain3 == b"" + + conf.ASN1_default_long_size = 4 + try: + long_seq = BERcodec_SEQUENCE.enc(payload) + assert long_seq.startswith(b"0\x84") + finally: + conf.ASN1_default_long_size = 0 + + _raises(BER_Decoding_Error, lambda: BERcodec_SEQUENCE.do_dec(b"\x30\x05" + BERcodec_INTEGER.enc(1))) + + +def check_ber_ipaddress(): + # type: () -> None + encoded = BERcodec_IPADDRESS.enc("192.168.0.1") + obj, remain = BERcodec_IPADDRESS.do_dec(encoded) + assert obj.val == "192.168.0.1" + assert remain == b"" + + _raises(BER_Encoding_Error, lambda: BERcodec_IPADDRESS.enc("not-an-ip")) + + _raises(BER_Decoding_Error, lambda: BERcodec_IPADDRESS.do_dec(BERcodec_STRING.enc(b"bad"))) + + +def check_ber_object_dispatch(): + # type: () -> None + encoded = BERcodec_INTEGER.enc(99) + obj, remain = BERcodec_Object.do_dec(encoded) + assert obj.val == 99 + assert remain == b"" + + _raises(BER_Decoding_Error, lambda: BERcodec_Object.check_string(b"")) + + _raises(BER_Decoding_Error, lambda: BERcodec_Object.do_dec(b"\xff\x00")) + + bad, remain = BERcodec_Object.safedec(b"\x02\x01\x01") + assert isinstance(bad, ASN1_INTEGER) + assert bad.val == 1 + + unknown, remain = BERcodec_Object.safedec(b"\xff\x00") + assert isinstance(unknown, ASN1_DECODING_ERROR) + + truncated, remain = BERcodec_Object.dec(b"\x02\x05\x01", safe=True) + assert isinstance(truncated, ASN1_DECODING_ERROR) + assert remain == b"" + + _raises(TypeError, lambda: BERcodec_Object.enc(object())) + assert BERcodec_Object.enc("42") == BERcodec_STRING.enc("42") diff --git a/test/scapy/layers/ber_packets.py b/test/scapy/layers/ber_packets.py new file mode 100644 index 00000000000..09cb02fe6f1 --- /dev/null +++ b/test/scapy/layers/ber_packets.py @@ -0,0 +1,184 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +BER ASN1_Packet and ASN1F_field build tests. +""" + +from scapy.asn1.asn1 import ASN1_Codecs, ASN1_INTEGER, ASN1_STRING +from scapy.asn1fields import ( + ASN1F_BOOLEAN, + ASN1F_CHOICE, + ASN1F_INTEGER, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import raw + + +class BERTaggedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER("n", 0, explicit_tag=0xA1) + + +class BERFixedFields(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, size_len=1), + ASN1F_STRING("s", "", size_len=3), + ) + + +class BEROptionalField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ) + + +class BERSequenceOfIntegers(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + + +class BERChoiceField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + +class BERRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + + +class BEROptionalSequence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("hdr", 0), + ASN1F_optional(ASN1F_SEQUENCE( + ASN1F_INTEGER("id", None), + ASN1F_STRING("label", None), + explicit_tag=0xA0, + )), + ) + + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + + +def check_ber_field_explicit_tag(): + # type: () -> None + pkt = BERTaggedInteger(n=5) + assert raw(pkt) == b"\xa1\x03\x02\x01\x05" + decoded = _roundtrip(BERTaggedInteger, pkt) + assert decoded.n.val == 5 + + +def check_ber_field_fixed_size(): + # type: () -> None + pkt = BERFixedFields(n=200, s=b"ABC") + assert raw(pkt) == bytes.fromhex("300d02810200c80483000003414243") + decoded = _roundtrip(BERFixedFields, pkt) + assert decoded.n.val == 200 + assert decoded.s.val == b"ABC" + + +def check_ber_field_optional(): + # type: () -> None + present = BEROptionalField(id=1, extra=7) + assert raw(present) == bytes.fromhex("3008020101a003020107") + decoded = _roundtrip(BEROptionalField, present) + assert decoded.id.val == 1 + assert decoded.extra.val == 7 + + absent = BEROptionalField(id=1, extra=None) + assert raw(absent) == bytes.fromhex("3003020101") + decoded = _roundtrip(BEROptionalField, absent) + assert decoded.id.val == 1 + assert decoded.extra is None + + +def check_ber_optional_sequence_is_empty(): + # type: () -> None + """Optional ASN1F_SEQUENCE must use the wrapped field's is_empty(). + + SEQUENCE stores children under their own names (not dummy_seq_name), so + inspecting pkt.dummy_seq_name incorrectly reports present children as empty + and makes the parent SEQUENCE look empty. + """ + opt = BEROptionalSequence.ASN1_root.seq[1] + + present = BEROptionalSequence(hdr=1, id=42, label=b"abc") + assert opt._field.is_empty(present) is False + assert opt.is_empty(present) is False + assert BEROptionalSequence.ASN1_root.is_empty(present) is False + assert raw(present) == bytes.fromhex("300f020101a00a300802012a0403616263") + + absent = BEROptionalSequence(hdr=1, id=None, label=None) + assert opt._field.is_empty(absent) is True + assert opt.is_empty(absent) is True + assert raw(absent) == bytes.fromhex("3003020101") + + +def check_ber_field_sequence_of(): + # type: () -> None + pkt = BERSequenceOfIntegers(values=[1, 2, 3]) + assert raw(pkt) == b"\x30\x09\x02\x01\x01\x02\x01\x02\x02\x01\x03" + decoded = _roundtrip(BERSequenceOfIntegers, pkt) + assert [x.val for x in decoded.values] == [1, 2, 3] + + +def check_ber_field_choice(): + # type: () -> None + as_int = BERChoiceField(c=ASN1_INTEGER(99)) + assert raw(as_int) == b"\x02\x01c" + decoded = _roundtrip(BERChoiceField, as_int) + assert decoded.c.val == 99 + + as_str = BERChoiceField(c=ASN1_STRING("x")) + assert raw(as_str) == b"\x04\x01x" + decoded = _roundtrip(BERChoiceField, as_str) + assert decoded.c.val == b"x" + + +def check_ber_packet_record(): + # type: () -> None + pkt = BERRecord( + id=42, flag=True, label="hi", extra=7, values=[1, 2, 3], + ) + expected = bytes.fromhex( + "301a02012a01010104026869" + "a003020107" + "3009020101020102020103" + ) + assert raw(pkt) == expected + decoded = _roundtrip(BERRecord, pkt) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + empty = BERRecord(id=1, flag=False, label="", extra=None, values=[]) + assert raw(empty) == bytes.fromhex("300a02010101010004003000") + decoded = _roundtrip(BERRecord, empty) + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] diff --git a/test/scapy/layers/oer_fuzz.py b/test/scapy/layers/oer_fuzz.py new file mode 100644 index 00000000000..920e51e8abd --- /dev/null +++ b/test/scapy/layers/oer_fuzz.py @@ -0,0 +1,106 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +OER fuzzing helpers. + +Exercise OER encode/decode paths with packet.fuzz() and random payloads. +""" + +import os +import random +from typing import Iterable, Type + +from scapy.asn1.asn1 import ASN1_Codecs, ASN1_Decoding_Error, ASN1_Error +from scapy.contrib.oer import ( + OER_Decoding_Error, + OERcodec_BIT_STRING, + OERcodec_BOOLEAN, + OERcodec_ENUMERATED, + OERcodec_INTEGER, + OERcodec_NULL, + OERcodec_OID, + OERcodec_STRING, +) +from scapy.asn1fields import ( + ASN1F_BOOLEAN, + ASN1F_INTEGER, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import fuzz, raw + +_OER_CODEC_CLASSES = ( + OERcodec_INTEGER, + OERcodec_BOOLEAN, + OERcodec_NULL, + OERcodec_STRING, + OERcodec_OID, + OERcodec_ENUMERATED, + OERcodec_BIT_STRING, +) + +_DECODE_ERRORS = ( + OER_Decoding_Error, + ASN1_Decoding_Error, + ASN1_Error, + ValueError, + IndexError, +) + + +class OERFuzzRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + + +def _fuzz_packets(): + # type: () -> Iterable[Type[ASN1_Packet]] + return (OERFuzzRecord,) + + +def check_oer_fuzz_encode(iterations=25): + # type: (int) -> None + for cls in _fuzz_packets(): + for _ in range(iterations): + data = raw(fuzz(cls())) + assert isinstance(data, bytes) + + +def check_oer_fuzz_roundtrip(iterations=25): + # type: (int) -> None + for cls in _fuzz_packets(): + for _ in range(iterations): + cls(raw(fuzz(cls()))) + + +def check_oer_fuzz_codec_decode(iterations=100): + # type: (int) -> None + for codec in _OER_CODEC_CLASSES: + for _ in range(iterations): + data = os.urandom(random.randint(0, 64)) + try: + codec.safedec(data) + except _DECODE_ERRORS: + pass + + +def check_oer_fuzz_packet_decode(iterations=100): + # type: (int) -> None + for cls in _fuzz_packets(): + for _ in range(iterations): + data = os.urandom(random.randint(0, 128)) + try: + cls(data) + except _DECODE_ERRORS: + pass diff --git a/test/scapy/layers/oer_iop.py b/test/scapy/layers/oer_iop.py new file mode 100644 index 00000000000..baa68a5b534 --- /dev/null +++ b/test/scapy/layers/oer_iop.py @@ -0,0 +1,160 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +OER interoperability helpers. + +Cross-check Scapy's OER codec against reference encodings (from asn1tools). +Reference vectors are taken from asn1tools/tests/test_oer.py. +""" + +from scapy.contrib.oer import ( + OERcodec_BIT_STRING, + OERcodec_BOOLEAN, + OERcodec_ENUMERATED, + OERcodec_INTEGER, + OERcodec_NULL, + OERcodec_OID, + OERcodec_STRING, + OER_signed_integer_enc, + OER_unsigned_integer_enc, +) + +# (type name, value, scapy encoder callable, reference encoding) +INTEGER_VECTORS = [ + ("A", 0, lambda v: OERcodec_INTEGER.enc(v), b"\x01\x00"), + ("A", 128, lambda v: OERcodec_INTEGER.enc(v), b"\x02\x00\x80"), + ("A", 100000, lambda v: OERcodec_INTEGER.enc(v), b"\x03\x01\x86\xa0"), + ("A", -255, lambda v: OERcodec_INTEGER.enc(v), b"\x02\xff\x01"), + ("A", -1234567, lambda v: OERcodec_INTEGER.enc(v), b"\x03\xed)y"), + ("B", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=1), b"\xfe"), + ("C", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=2), b"\xff\xfe"), + ("D", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=4), b"\xff\xff\xff\xfe"), + ( + "E", + -2, + lambda v: OERcodec_INTEGER.enc(v, size_len=8), + b"\xff\xff\xff\xff\xff\xff\xff\xfe", + ), + ("F", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=1), b"\x80"), + ("G", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=2), b"\x00\x80"), + ("G", 1000, lambda v: OERcodec_INTEGER.enc(v, size_len=2), b"\x03\xe8"), + ("H", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=4), b"\x00\x00\x00\x80"), + ( + "I", + 128, + lambda v: OERcodec_INTEGER.enc(v, size_len=8), + b"\x00\x00\x00\x00\x00\x00\x00\x80", + ), + ("B", 1, lambda v: OERcodec_INTEGER.enc(v, size_len=1), b"\x01"), + ("K", 1, lambda v: OER_unsigned_integer_enc(v), b"\x01\x01"), + ("K", 128, lambda v: OER_unsigned_integer_enc(v), b"\x01\x80"), + ("L", -128, lambda v: OER_signed_integer_enc(v), b"\x01\x80"), +] + +BOOLEAN_VECTORS = [ + (True, lambda v: OERcodec_BOOLEAN.enc(1 if v else 0), b"\xff"), + (False, lambda v: OERcodec_BOOLEAN.enc(1 if v else 0), b"\x00"), +] + +ENUMERATED_VECTORS = [ + ("A", "a", 1, b"\x01"), + ("B", "a", 128, b"\x82\x00\x80"), + ("C", "a", 0, b"\x00"), + ("C", "b", 127, b"\x7f"), + ("E", "a", -1, b"\x81\xff"), +] + +OID_VECTORS = [ + ("1.2", lambda v: OERcodec_OID.enc(v), b"\x01*"), + ("1.2.3321", lambda v: OERcodec_OID.enc(v), b"\x03*\x99y"), +] + +OCTET_STRING_VECTORS = [ + (b"\x12\x34", 0, b"\x02\x124"), + (b"\x12\x34\x56", 3, b"\x124V"), +] + +BIT_STRING_VECTORS = [ + ("0100", b"\x02\x04@"), + ("01000001", b"\x02\x00A"), +] + +# (type name, value, reference encoding) +SCAPY_DECODE_VECTORS = [ + ("A", 42, b"\x01*"), + ("F", 200, b"\xc8"), + ("B", -99, b"\x9d"), +] + + +def check_primitive_interop(): + # type: () -> bool + """Compare Scapy OER primitives against reference encodings.""" + for type_name, value, enc, expected in INTEGER_VECTORS: + got = enc(value) + assert got == expected, ( + "integer %s=%r: reference=%r scapy=%r" % + (type_name, value, expected, got) + ) + if type_name == "A": + dec, remain = OERcodec_INTEGER.do_dec(got) + assert remain == b"" and dec.val == value + + for value, enc, expected in BOOLEAN_VECTORS: + got = enc(value) + assert got == expected + dec, remain = OERcodec_BOOLEAN.do_dec(got) + assert remain == b"" and dec.val == (1 if value else 0) + + got = OERcodec_NULL.enc(None) + assert got == b"" + + for type_name, _enum_name, enum_val, expected in ENUMERATED_VECTORS: + got = OERcodec_ENUMERATED.enc(enum_val) + assert got == expected + dec, remain = OERcodec_ENUMERATED.do_dec(got) + assert remain == b"" and dec.val == enum_val + + for oid, enc, expected in OID_VECTORS: + got = enc(oid) + assert got == expected + dec, remain = OERcodec_OID.do_dec(got) + assert remain == b"" and dec.val == oid + + for data, fixed_size, expected in OCTET_STRING_VECTORS: + got = OERcodec_STRING.enc(data, size_len=fixed_size or 0) + assert got == expected + dec, remain = OERcodec_STRING.do_dec(got, size_len=fixed_size or 0) + assert remain == b"" and dec.val == data + + for bitstr, expected in BIT_STRING_VECTORS: + got = OERcodec_BIT_STRING.enc(bitstr) + assert got == expected + dec, remain = OERcodec_BIT_STRING.do_dec(got) + assert remain == b"" and dec.val == bitstr + + return True + + +def check_scapy_encode_reference_decode(): + # type: () -> bool + """Decode reference encodings with Scapy.""" + for type_name, value, encoded in SCAPY_DECODE_VECTORS: + if type_name == "A": + dec, remain = OERcodec_INTEGER.do_dec(encoded) + elif type_name == "F": + dec, remain = OERcodec_INTEGER.do_dec( + encoded, size_len=1, oer_unsigned=True, + ) + else: + dec, remain = OERcodec_INTEGER.do_dec(encoded, size_len=1) + assert remain == b"" and dec.val == value + + for val in [0, 1]: + encoded = OERcodec_BOOLEAN.enc(val) + dec, remain = OERcodec_BOOLEAN.do_dec(encoded) + assert remain == b"" and dec.val == val + + return True diff --git a/test/scapy/layers/oer_packets.py b/test/scapy/layers/oer_packets.py new file mode 100644 index 00000000000..7260609a42a --- /dev/null +++ b/test/scapy/layers/oer_packets.py @@ -0,0 +1,209 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +OER ASN1_Packet and ASN1F_field tests. +""" +import scapy.contrib.oer # noqa: F401 # register OER stem + +from scapy.asn1.asn1 import ASN1_Codecs, ASN1_INTEGER, ASN1_STRING +from scapy.asn1fields import ( + ASN1F_BOOLEAN, + ASN1F_CHOICE, + ASN1F_INTEGER, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import raw + + +class OERTaggedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0, explicit_tag=0xA1) + + +class OERFixedFields(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_STRING("s", "", size_len=3), + ) + + +class OEROptionalField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ) + + +class OERSequenceOfIntegers(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + + +class OERChoiceField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + +class OERRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + + +class OERNestedSequence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ) + + +class OERNestedSequenceTrailing(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ASN1F_INTEGER("id", 0), + ) + + +class OERSequenceOfWithTrailing(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ASN1F_INTEGER("id", 0), + ) + + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + + +def check_oer_field_explicit_tag(): + # type: () -> None + pkt = OERTaggedInteger(n=5) + assert raw(pkt) == b"\xa1\x01\x05" + decoded = _roundtrip(OERTaggedInteger, pkt) + assert decoded.n.val == 5 + + +def check_oer_field_fixed_size(): + # type: () -> None + pkt = OERFixedFields(n=200, s=b"ABC") + assert raw(pkt) == b"\xc8ABC" + decoded = _roundtrip(OERFixedFields, pkt) + assert decoded.n.val == 200 + assert decoded.s.val == b"ABC" + + +def check_oer_field_optional(): + # type: () -> None + present = OEROptionalField(id=1, extra=7) + assert raw(present) == b"\x01\x01\xa0\x01\x07" + decoded = _roundtrip(OEROptionalField, present) + assert decoded.id.val == 1 + assert decoded.extra.val == 7 + + absent = OEROptionalField(id=1, extra=None) + assert raw(absent) == b"\x01\x01" + decoded = _roundtrip(OEROptionalField, absent) + assert decoded.id.val == 1 + assert decoded.extra is None + + +def check_oer_field_sequence_of(): + # type: () -> None + pkt = OERSequenceOfIntegers(values=[1, 2, 3]) + assert raw(pkt) == b"\x01\x03\x01\x01\x01\x02\x01\x03" + decoded = _roundtrip(OERSequenceOfIntegers, pkt) + assert [x.val for x in decoded.values] == [1, 2, 3] + + +def check_oer_field_choice(): + # type: () -> None + as_int = OERChoiceField(c=ASN1_INTEGER(99)) + assert raw(as_int) == b"\x02\x01c" + decoded = _roundtrip(OERChoiceField, as_int) + assert decoded.c.val == 99 + + as_str = OERChoiceField(c=ASN1_STRING("x")) + assert raw(as_str) == b"\x04\x01x" + decoded = _roundtrip(OERChoiceField, as_str) + assert decoded.c.val == b"x" + + +def check_oer_packet_record(): + # type: () -> None + pkt = OERRecord( + id=42, flag=True, label="hi", extra=7, values=[1, 2, 3], + ) + expected = ( + b"\x01*\xff\x02hi\xa0\x01\x07" + b"\x01\x03\x01\x01\x01\x02\x01\x03" + ) + assert raw(pkt) == expected + decoded = _roundtrip(OERRecord, pkt) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + empty = OERRecord(id=1, flag=False, label="", extra=None, values=[]) + assert raw(empty) == b"\x01\x01\x00\x00\x01\x00" + decoded = _roundtrip(OERRecord, empty) + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] + + +def check_oer_nested_sequence(): + # type: () -> None + pkt = OERNestedSequence(id=5, x=3, y=True) + assert raw(pkt) == b"\x01\x05\x01\x03\xff" + decoded = _roundtrip(OERNestedSequence, pkt) + assert decoded.id.val == 5 + assert decoded.x.val == 3 + assert decoded.y.val == 1 + + +def check_oer_nested_sequence_trailing(): + # type: () -> None + pkt = OERNestedSequenceTrailing(x=3, y=True, id=5) + assert raw(pkt) == b"\x01\x03\xff\x01\x05" + decoded = _roundtrip(OERNestedSequenceTrailing, pkt) + assert decoded.x.val == 3 + assert decoded.y.val == 1 + assert decoded.id.val == 5 + + +def check_oer_sequence_of_with_trailing(): + # type: () -> None + pkt = OERSequenceOfWithTrailing(values=[1, 2], id=7) + assert raw(pkt) == b"\x01\x02\x01\x01\x01\x02\x01\x07" + decoded = _roundtrip(OERSequenceOfWithTrailing, pkt) + assert [x.val for x in decoded.values] == [1, 2] + assert decoded.id.val == 7 diff --git a/test/scapy/layers/uper_asn1scc_iop.py b/test/scapy/layers/uper_asn1scc_iop.py new file mode 100644 index 00000000000..411d23d9460 --- /dev/null +++ b/test/scapy/layers/uper_asn1scc_iop.py @@ -0,0 +1,190 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +UPER interoperability vectors from ESA asn1scc test cases. + +asn1scc (https://github.com/esa/asn1scc) primarily validates C/Ada code generation +with ACN custom encodings. Portable uPER vectors are taken from v4Tests where +``--TCLS MyPDU[]`` selects standard uPER (empty ACN = default PER). + +Cases that need REAL, explicit APPLICATION tags, or ACN overrides are not +compared against Scapy encoders here (or are reference-only). +""" + +from scapy.contrib.uper import ( + UPER_Encoder, + UPER_choice_index_enc, + UPERcodec_BIT_STRING, + UPERcodec_BOOLEAN, + UPERcodec_ENUMERATED, + UPERcodec_INTEGER, + UPERcodec_NULL, + UPERcodec_STRING, +) + +# asn1scc v4Tests/test-cases/acn/05-BOOLEAN/001.asn1 +BOOLEAN_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= BOOLEAN " + "END" +) + +# asn1scc v4Tests/test-cases/acn/18-NULL/001.asn1 +NULL_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= NULL " + "END" +) + +# asn1scc v4Tests/test-cases/acn/06-OCTET-STRING/001.asn1 +OCTET_STRING_VAR_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= OCTET STRING (SIZE(1..20)) " + "END" +) + +# asn1scc v4Tests/test-cases/acn/09-CHOICE/001.asn1 (pdu1 = int1 : 10) +CHOICE_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= CHOICE { " + "int1 INTEGER(0..15), " + "int2 INTEGER(0..65535), " + "enm ENUMERATED { one(1), two(2), three(3), four(4), thousand(1000) }, " + "buf OCTET STRING (SIZE(10)), " + "gg SEQUENCE { " + "int1 INTEGER(0..15), " + "int2 INTEGER(0..65535), " + "enm ENUMERATED { pone(1), ptwo(2), pthree(3), pfour(4), pthousand(1000) }, " + "buf [APPLICATION 104] OCTET STRING (SIZE(10)) " + "} " + "} " + "END" +) + +# asn1scc v4Tests/test-cases/acn/04-ENUMERATED/001.asn1 (pdu1 = beta) +ENUMERATED_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= ENUMERATED { alpha(1), beta(200) } " + "END" +) + +# asn1scc v4Tests/test-cases/acn/08-BIT-STRING/001.asn1 (pdu1 = 'ABCD'H) +BIT_STRING_VAR_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= BIT STRING (SIZE(1..20)) " + "END" +) + +# asn1scc README.md sample.asn (REAL field; reference only) +README_MESSAGE_HEX = ( + "010101020980cd191eb851eb851f48656c6c6f576f726c6480" +) + +README_MESSAGE_PREFIX_HEX = ( + "0101010248656c6c6f576f726c6480" +) + +# (name, pdu value, encoder callable, reference encoding) +ASN1SCC_VECTORS = [ + ( + "05-BOOLEAN/001 pdu1", + True, + lambda _v: UPERcodec_BOOLEAN.enc(1), + b"\x80", + ), + ( + "18-NULL/001 pdu1", + None, + lambda _v: UPERcodec_NULL.enc(None), + b"", + ), + ( + "06-OCTET-STRING/001 pdu1", + bytes.fromhex("afbc4583"), + lambda v: UPERcodec_STRING.enc(v, uper_min=1, uper_max=20), + bytes.fromhex("1d7de22c18"), + ), + ( + "05-BOOLEAN/001 pdu1 false", + False, + lambda _v: UPERcodec_BOOLEAN.enc(0), + b"\x00", + ), + ( + "04-ENUMERATED/001 pdu1 alpha", + "alpha", + lambda _v: UPERcodec_ENUMERATED.enc(1, uper_enum_values=[1, 200]), + b"\x00", + ), + ( + "04-ENUMERATED/001 pdu1 beta", + "beta", + lambda _v: UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), + b"\x80", + ), + ( + "09-CHOICE/001 pdu1 int1:10", + ("int1", 10), + lambda _v: _encode_choice_int1_10(), + b"\x14", + ), + ( + "08-BIT-STRING/001 pdu1 ABCD", + (bytes.fromhex("abcd"), 16), + lambda _v: UPERcodec_BIT_STRING.enc( + (bytes.fromhex("abcd"), 16), uper_min=1, uper_max=20, + ), + bytes.fromhex("7d5e68"), + ), +] + + +def _encode_choice_int1_10(): + # type: () -> bytes + enc = UPER_Encoder() + UPER_choice_index_enc(0, 5, enc=enc) + UPERcodec_INTEGER.encode_into(enc, 10, uper_min=0, uper_max=15) + return enc.as_bytes() + + +def check_asn1scc_vectors(): + # type: () -> None + for name, _value, encoder, expected in ASN1SCC_VECTORS: + got = encoder(_value) + assert got == expected, ( + "%s: expected %s, got %s" % + (name, expected.hex(), got.hex()) + ) + + +def check_asn1scc_readme_message_prefix(): + # type: () -> None + """README sample without REAL; Scapy packet roundtrip vs reference.""" + from test.scapy.layers.uper_packets import UPERMessagePrefix + from scapy.packet import raw + + expected = bytes.fromhex(README_MESSAGE_PREFIX_HEX) + + pkt = UPERMessagePrefix( + msgId=1, + myflag=2, + szDescription=b"HelloWorld", + isReady=True, + ) + got = raw(pkt) + assert got == expected + decoded = UPERMessagePrefix(got) + assert decoded.msgId.val == 1 + assert decoded.myflag.val == 2 + assert decoded.szDescription.val == b"HelloWorld" + assert decoded.isReady.val == 1 + + +def check_asn1scc_readme_message_reference(): + # type: () -> None + """README C sample output; Scapy does not encode REAL in UPER yet.""" + assert README_MESSAGE_HEX == ( + "010101020980cd191eb851eb851f48656c6c6f576f726c6480" + ) diff --git a/test/scapy/layers/uper_codec.py b/test/scapy/layers/uper_codec.py new file mode 100644 index 00000000000..180503fb06e --- /dev/null +++ b/test/scapy/layers/uper_codec.py @@ -0,0 +1,174 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +UPER primitive codec roundtrip and decode interoperability tests. +""" + +from typing import Any, Dict, Tuple, Type + +from scapy.contrib.uper import ( + UPERcodec_BIT_STRING, + UPERcodec_BOOLEAN, + UPERcodec_ENUMERATED, + UPERcodec_INTEGER, + UPERcodec_NULL, + UPERcodec_OID, + UPERcodec_STRING, +) + +CodecRoundtrip = Tuple[ + Type[Any], + Any, + Dict[str, Any], + Any, +] + +CODEC_ROUNDTRIPS = [ + (UPERcodec_NULL, None, {}, None), + (UPERcodec_BOOLEAN, 1, {}, 1), + (UPERcodec_BOOLEAN, 0, {}, 0), + (UPERcodec_INTEGER, 42, {}, 42), + (UPERcodec_INTEGER, -1, {}, -1), + (UPERcodec_INTEGER, 68719476736, {}, 68719476736), + (UPERcodec_INTEGER, 200, {"uper_min": 0, "uper_max": 255}, 200), + (UPERcodec_INTEGER, -1, {"uper_min": -128, "uper_max": 127}, -1), + (UPERcodec_INTEGER, 127, {"uper_min": -128, "uper_max": 127}, 127), + (UPERcodec_INTEGER, -128, {"uper_min": -128, "uper_max": 127}, -128), + (UPERcodec_STRING, b"AB", {}, b"AB"), + (UPERcodec_STRING, b"\x12\x34\x56", {"size_len": 3}, b"\x12\x34\x56"), + ( + UPERcodec_STRING, + bytes.fromhex("afbc4583"), + {"uper_min": 1, "uper_max": 20}, + bytes.fromhex("afbc4583"), + ), + (UPERcodec_ENUMERATED, 1, {"uper_enum_values": [1, 200]}, 1), + (UPERcodec_ENUMERATED, 200, {"uper_enum_values": [1, 200]}, 200), + ( + UPERcodec_BIT_STRING, + (bytes.fromhex("abcd"), 16), + {"uper_min": 1, "uper_max": 20}, + "1010101111001101", + ), + ( + UPERcodec_BIT_STRING, + (bytes.fromhex("abcd"), 16), + {"uper_min": 16, "uper_max": 16}, + "1010101111001101", + ), + (UPERcodec_ENUMERATED, 1, {"uper_enum_values": [1]}, 1), +] + +DecodeVector = Tuple[ + str, + Any, + Type[Any], + Dict[str, Any], + Any, + bytes, +] + +DECODE_VECTORS = [ + ("A", True, UPERcodec_BOOLEAN, {}, 1, b"\x80"), + ("A", False, UPERcodec_BOOLEAN, {}, 0, b"\x00"), + ("B", 42, UPERcodec_INTEGER, {}, 42, b"\x01*"), + ("B", -1, UPERcodec_INTEGER, {}, -1, b"\x01\xff"), + ( + "C", + 200, + UPERcodec_INTEGER, + {"uper_min": 0, "uper_max": 255}, + 200, + b"\xc8", + ), + ( + "Signed", + -1, + UPERcodec_INTEGER, + {"uper_min": -128, "uper_max": 127}, + -1, + b"\x7f", + ), + ( + "Signed", + 127, + UPERcodec_INTEGER, + {"uper_min": -128, "uper_max": 127}, + 127, + b"\xff", + ), + ("D", b"AB", UPERcodec_STRING, {}, b"AB", b"\x02AB"), + ( + "E", + b"\x12\x34\x56", + UPERcodec_STRING, + {"size_len": 3}, + b"\x12\x34\x56", + b"\x12\x34\x56", + ), + ("G", None, UPERcodec_NULL, {}, None, b""), + ("H", "alpha", UPERcodec_ENUMERATED, {"uper_enum_values": [1, 200]}, 1, b"\x00"), + ("H", "beta", UPERcodec_ENUMERATED, {"uper_enum_values": [1, 200]}, 200, b"\x80"), +] + +OID_ENCODE_VECTORS = [ + ("1.2.3", b"\x02*\x03"), + ("2.999.3", b"\x03\x887\x03"), +] + + +def _assert_codec_roundtrip(codec, value, kwargs, expected): + # type: (Type[Any], Any, Dict[str, Any], Any) -> None + data = codec.enc(value, **kwargs) + decoded, _remain = codec.do_dec(data, **kwargs) + assert decoded.val == expected + + +def check_uper_codec_roundtrips(): + # type: () -> None + for codec, value, kwargs, expected in CODEC_ROUNDTRIPS: + _assert_codec_roundtrip(codec, value, kwargs, expected) + + +def check_uper_codec_oid_roundtrip(): + # type: () -> None + import scapy.all # noqa: F401 # loads conf.mib for ASN1_OID + for oid in ("1.2.3", "1.2.840.113549"): + data = UPERcodec_OID.enc(oid) + decoded, remain = UPERcodec_OID.do_dec(data) + assert remain == b"" + assert decoded.val == oid + + +def check_uper_codec_oid_encode_interop(): + # type: () -> None + for oid, expected in OID_ENCODE_VECTORS: + got = UPERcodec_OID.enc(oid) + assert got == expected, ( + "OID %r: expected %s, got %s" % + (oid, expected.hex(), got.hex()) + ) + + +def check_uper_codec_reference_decode(): + # type: () -> None + for _typename, _value, codec, kwargs, expected, encoded in DECODE_VECTORS: + decoded, _remain = codec.do_dec(encoded, **kwargs) + assert decoded.val == expected, ( + "%s %r: expected %r, got %r" % + (_typename, _value, expected, decoded.val) + ) + + +def check_uper_codec_encode_reference(): + # type: () -> None + from test.scapy.layers.uper_iop import PRIMITIVE_VECTORS + + for typename, value, encoder, expected in PRIMITIVE_VECTORS: + encoded = encoder(value) + assert encoded == expected, ( + "%s %r: expected %s, got %s" % + (typename, value, expected.hex(), encoded.hex()) + ) diff --git a/test/scapy/layers/uper_fuzz.py b/test/scapy/layers/uper_fuzz.py new file mode 100644 index 00000000000..d0aa8571192 --- /dev/null +++ b/test/scapy/layers/uper_fuzz.py @@ -0,0 +1,133 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +UPER fuzzing helpers. + +Exercise UPER encode/decode paths with packet.fuzz() and random payloads. +""" + +import os +import random +from typing import Iterable, Type + +from scapy.asn1.asn1 import ASN1_Codecs, ASN1_Decoding_Error, ASN1_Error +from scapy.contrib.uper import ( + UPER_Decoding_Error, + UPER_Encoding_Error, + UPERcodec_BIT_STRING, + UPERcodec_BOOLEAN, + UPERcodec_ENUMERATED, + UPERcodec_INTEGER, + UPERcodec_NULL, + UPERcodec_OID, + UPERcodec_STRING, +) +from scapy.asn1fields import ( + ASN1F_BOOLEAN, + ASN1F_ENUMERATED, + ASN1F_INTEGER, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import fuzz, raw + +_UPER_CODEC_CLASSES = ( + UPERcodec_INTEGER, + UPERcodec_BOOLEAN, + UPERcodec_NULL, + UPERcodec_STRING, + UPERcodec_OID, + UPERcodec_ENUMERATED, + UPERcodec_BIT_STRING, +) + +_DECODE_ERRORS = ( + UPER_Decoding_Error, + UPER_Encoding_Error, + ASN1_Decoding_Error, + ASN1_Error, + ValueError, + IndexError, +) + + +class UPERFuzzRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + + +class UPERFuzzNested(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ) + + +class UPERFuzzEnumerated(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_ENUMERATED( + "state", 1, {1: "alpha", 200: "beta"}, + ) + + +def _fuzz_packets(): + # type: () -> Iterable[Type[ASN1_Packet]] + return (UPERFuzzRecord, UPERFuzzNested, UPERFuzzEnumerated) + + +def check_uper_fuzz_encode(iterations=25): + # type: (int) -> None + for cls in _fuzz_packets(): + for _ in range(iterations): + try: + data = raw(fuzz(cls())) + except _DECODE_ERRORS: + continue + assert isinstance(data, bytes) + + +def check_uper_fuzz_roundtrip(iterations=25): + # type: (int) -> None + for cls in _fuzz_packets(): + for _ in range(iterations): + try: + cls(raw(fuzz(cls()))) + except _DECODE_ERRORS: + pass + + +def check_uper_fuzz_codec_decode(iterations=100): + # type: (int) -> None + for codec in _UPER_CODEC_CLASSES: + for _ in range(iterations): + data = os.urandom(random.randint(0, 64)) + try: + codec.safedec(data) + except _DECODE_ERRORS: + pass + + +def check_uper_fuzz_packet_decode(iterations=100): + # type: (int) -> None + for cls in _fuzz_packets(): + for _ in range(iterations): + data = os.urandom(random.randint(0, 128)) + try: + cls(data) + except _DECODE_ERRORS: + pass diff --git a/test/scapy/layers/uper_helpers.py b/test/scapy/layers/uper_helpers.py new file mode 100644 index 00000000000..dc92ef18e68 --- /dev/null +++ b/test/scapy/layers/uper_helpers.py @@ -0,0 +1,122 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +UPER low-level helper and bitstream tests. +""" + +from scapy.contrib.uper import ( + UPER_Decoder, + UPER_Encoder, + UPER_choice_index_dec, + UPER_choice_index_enc, + UPER_constrained_int_dec, + UPER_constrained_int_enc, + UPER_count_dec, + UPER_count_enc, + UPER_has_unexpected_remainder, + UPER_join_encodings, + UPER_octet_string_dec, + UPER_octet_string_enc, + UPER_optional_presence_enc, + UPERcodec_INTEGER, +) + + +def check_uper_length_determinant(): + # type: () -> None + for length, expected in [ + (0, b"\x00"), + (1, b"\x01"), + (127, b"\x7f"), + (128, b"\x80\x80"), + (16383, b"\xbf\xff"), + (16384, b"\xc1"), + ]: + enc = UPER_Encoder() + enc.append_length_determinant(length) + assert enc.as_bytes() == expected + + +def check_uper_count_roundtrip(): + # type: () -> None + for count in [0, 1, 3, 127]: + enc = UPER_Encoder() + UPER_count_enc(count, enc=enc) + got, _ = UPER_count_dec(enc.as_bytes()) + assert got == count + + +def check_uper_choice_index_roundtrip(): + # type: () -> None + for index, choices in [(0, 2), (1, 5), (3, 5)]: + enc = UPER_Encoder() + UPER_choice_index_enc(index, choices, enc=enc) + got, _ = UPER_choice_index_dec(enc.as_bytes(), choices) + assert got == index + + +def check_uper_optional_presence(): + # type: () -> None + enc = UPER_Encoder() + UPER_optional_presence_enc([0, 1, 0], enc=enc) + assert enc.as_bytes() == b"\x40" + + +def check_uper_constrained_integer(): + # type: () -> None + data = UPER_constrained_int_enc(10, 0, 15) + value, remain = UPER_constrained_int_dec(data, 0, 15) + assert value == 10 + assert remain == b"" + + +def check_uper_constrained_signed_integer(): + # type: () -> None + for value, expected in [(0, b"\x80"), (-1, b"\x7f"), (127, b"\xff"), (-128, b"\x00")]: + data = UPER_constrained_int_enc(value, -128, 127) + assert data == expected + decoded, remain = UPER_constrained_int_dec(data, -128, 127) + assert decoded == value + assert remain == b"" + + +def check_uper_octet_string_roundtrip(): + # type: () -> None + for data, minimum, maximum in [ + (b"AB", None, None), + (b"\x12\x34\x56", 3, 3), + (bytes.fromhex("afbc4583"), 1, 20), + ]: + encoded = UPER_octet_string_enc(data, minimum, maximum) + dec = UPER_Decoder(encoded) + decoded, _ = UPER_octet_string_dec(encoded, minimum, maximum, dec=dec) + assert decoded == data + assert not UPER_has_unexpected_remainder(dec) + + +def check_uper_has_unexpected_remainder(): + # type: () -> None + assert UPER_has_unexpected_remainder(UPER_Decoder(b"\x00")) is False + assert UPER_has_unexpected_remainder(UPER_Decoder(b"\x80")) is True + + +def check_uper_join_encodings(): + # type: () -> None + a = UPERcodec_INTEGER.enc(1) + b = UPERcodec_INTEGER.enc(2) + joined = UPER_join_encodings(a, b) + dec = UPER_Decoder(joined) + assert dec.read_unconstrained_whole_number() == 1 + assert dec.read_unconstrained_whole_number() == 2 + + +def check_uper_chained_encode_into(): + # type: () -> None + enc = UPER_Encoder() + UPERcodec_INTEGER.encode_into(enc, 42) + UPERcodec_INTEGER.encode_into(enc, -7) + dec = UPER_Decoder(enc.as_bytes()) + assert dec.read_unconstrained_whole_number() == 42 + assert dec.read_unconstrained_whole_number() == -7 diff --git a/test/scapy/layers/uper_iop.py b/test/scapy/layers/uper_iop.py new file mode 100644 index 00000000000..af36a6dda7e --- /dev/null +++ b/test/scapy/layers/uper_iop.py @@ -0,0 +1,189 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +UPER interoperability helpers. + +Cross-check Scapy's UPER codec against reference encodings (from asn1tools). +""" + +from typing import Any + +from scapy.contrib.uper import ( + UPERcodec_BOOLEAN, + UPERcodec_ENUMERATED, + UPERcodec_INTEGER, + UPERcodec_NULL, + UPERcodec_STRING, + UPER_Encoder, + UPER_choice_index_enc, +) +from scapy.packet import raw + +from test.scapy.layers.uper_packets import ( + UPERMultiOptional, + UPERNestedSequence, +) + +# (type name, value, scapy encoder callable, reference encoding) +PRIMITIVE_VECTORS = [ + ("A", True, lambda v: UPERcodec_BOOLEAN.enc(1 if v else 0), b"\x80"), + ("A", False, lambda v: UPERcodec_BOOLEAN.enc(1 if v else 0), b"\x00"), + ("B", 42, lambda v: UPERcodec_INTEGER.enc(v), b"\x01*"), + ("B", -1, lambda v: UPERcodec_INTEGER.enc(v), b"\x01\xff"), + ( + "C", + 200, + lambda v: UPERcodec_INTEGER.enc(v, uper_min=0, uper_max=255), + b"\xc8", + ), + ("D", b"AB", lambda v: UPERcodec_STRING.enc(v), b"\x02AB"), + ( + "E", + b"\x12\x34\x56", + lambda v: UPERcodec_STRING.enc(v, size_len=3), + b"\x12\x34\x56", + ), + ("G", None, lambda v: UPERcodec_NULL.enc(None), b""), + ( + "H", + "beta", + lambda v: UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), + b"\x80", + ), +] + +# (type name, value, reference encoding) +COMPOSITE_VECTORS = [ + ("Seq", {"id": 42, "flag": True}, b"\x00\x95@"), + ("Seq", {"id": 42, "flag": True, "extra": 7}, b"\x80\x95@A\xc0"), + ("SeqOf", [1, 2, 3], b"\x03\x01\x01\x01\x02\x01\x03"), + ("SeqOfC", [1, 200, 0], b"\x03\x01\xc8\x00"), + ("Choice", ("a", 99), b"\x00\xb1\x80"), + ("Choice", ("b", b"AB"), b"\x81 \xa1\x00"), + ("ChoiceC", ("a", 10), b"P"), + ("ChoiceC", ("b", b"AB"), b"\x81 \xa1\x00"), +] + +DECODE_PACKET_VECTORS = [ + ( + UPERNestedSequence, + {"id": 5, "x": 3, "y": True}, + bytes.fromhex("0105010380"), + ), + ( + UPERMultiOptional, + {"id": 1, "a": 2, "b": b"hi"}, + bytes.fromhex("c0404040809a1a40"), + ), +] + +PACKET_REFERENCE_VECTORS = [ + ( + UPERNestedSequence, + {"id": 5, "x": 3, "y": True}, + bytes.fromhex("0105010380"), + ), + ( + UPERMultiOptional, + {"id": 1, "a": 2, "b": b"hi"}, + bytes.fromhex("c0404040809a1a40"), + ), +] + + +def check_primitive_interop(): + # type: () -> None + for typename, value, encoder, expected in PRIMITIVE_VECTORS: + got = encoder(value) + assert got == expected, ( + "%s %r: expected %s, got %s" % + (typename, value, expected.hex(), got.hex()) + ) + + +def check_composite_interop(): + # type: () -> None + for typename, value, expected in COMPOSITE_VECTORS: + got = _encode_composite(typename, value) + assert got == expected, ( + "%s %r: expected %s, got %s" % + (typename, value, expected.hex(), got.hex()) + ) + + +def check_packet_reference_interop(): + # type: () -> None + for cls, pkt_kwargs, expected in PACKET_REFERENCE_VECTORS: + got = raw(cls(**pkt_kwargs)) + assert got == expected, ( + "%s: expected %s, got %s" % + (cls.__name__, expected.hex(), got.hex()) + ) + decoded = cls(got) + for key, value in pkt_kwargs.items(): + field = getattr(decoded, key) + if value is None: + assert field is None + elif isinstance(value, bool): + assert field.val == (1 if value else 0) + else: + assert field.val == value + + +def check_packet_decode_vectors(): + # type: () -> None + for cls, pkt_kwargs, data in DECODE_PACKET_VECTORS: + decoded = cls(data) + for key, value in pkt_kwargs.items(): + field = getattr(decoded, key) + if isinstance(value, bool): + assert field.val == (1 if value else 0) + else: + assert field.val == value + + +def _encode_composite(typename, value): + # type: (str, Any) -> bytes + enc = UPER_Encoder() + if typename == "Seq": + enc.append_bit(1 if value.get("extra") is not None else 0) + UPERcodec_INTEGER.encode_into(enc, value["id"]) + UPERcodec_BOOLEAN.encode_into(enc, 1 if value["flag"] else 0) + if value.get("extra") is not None: + UPERcodec_INTEGER.encode_into(enc, value["extra"]) + return enc.as_bytes() + if typename == "SeqOf": + enc.append_length_determinant(len(value)) + for item in value: + UPERcodec_INTEGER.encode_into(enc, item) + return enc.as_bytes() + if typename == "SeqOfC": + enc.append_length_determinant(len(value)) + for item in value: + UPERcodec_INTEGER.encode_into( + enc, item, uper_min=0, uper_max=255, + ) + return enc.as_bytes() + if typename == "Choice": + alt, payload = value + index = 0 if alt == "a" else 1 + UPER_choice_index_enc(index, 2, enc=enc) + if alt == "a": + UPERcodec_INTEGER.encode_into(enc, payload) + else: + UPERcodec_STRING.encode_into(enc, payload) + return enc.as_bytes() + if typename == "ChoiceC": + alt, payload = value + index = 0 if alt == "a" else 1 + UPER_choice_index_enc(index, 2, enc=enc) + if alt == "a": + UPERcodec_INTEGER.encode_into( + enc, payload, uper_min=0, uper_max=15, + ) + else: + UPERcodec_STRING.encode_into(enc, payload) + return enc.as_bytes() + raise ValueError("unknown composite type %s" % typename) diff --git a/test/scapy/layers/uper_packets.py b/test/scapy/layers/uper_packets.py new file mode 100644 index 00000000000..0dec76a9002 --- /dev/null +++ b/test/scapy/layers/uper_packets.py @@ -0,0 +1,543 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +UPER ASN1_Packet and ASN1F_field tests. +""" +import scapy.contrib.uper # noqa: F401 # register UPER stem + +from scapy.asn1.asn1 import ASN1_Codecs, ASN1_INTEGER, ASN1_STRING +from scapy.asn1fields import ( + ASN1F_BIT_STRING, + ASN1F_BOOLEAN, + ASN1F_CHOICE, + ASN1F_ENUMERATED, + ASN1F_INTEGER, + ASN1F_NULL, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import raw + + +class UPERFixedFields(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_STRING("s", "", size_len=3), + ) + + +class UPERIntegerField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0) + + +class UPERBooleanField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BOOLEAN("b", False) + + +class UPERStringField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_STRING("s", "") + + +class UPERConstrainedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER( + "n", 0, size_len=1, oer_unsigned=True, + ) + + +class UPEROptionalField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ) + + +class UPERSequenceOfIntegers(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + + +class UPERChoiceField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + +class UPERChoiceStringFirst(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_STRING(b""), ASN1F_STRING, ASN1F_INTEGER, + ) + + +class UPERRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + + +class UPEREnumeratedField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_ENUMERATED( + "state", 1, {1: "alpha", 200: "beta"}, + ) + + +class UPERBitStringField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BIT_STRING( + "bits", "0", uper_min=1, uper_max=20, + ) + + +class UPERMessagePrefix(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("msgId", 0), + ASN1F_INTEGER("myflag", 0), + ASN1F_STRING("szDescription", "", size_len=10), + ASN1F_BOOLEAN("isReady", False), + ) + + +class UPERSequenceWithChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_CHOICE("c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING), + ) + + +class UPERNullPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_NULL("n", None) + + +class UPERVariableOctetString(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_STRING("data", "", uper_min=1, uper_max=20) + + +class UPERConstrainedRangeInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=15) + + +class UPERSequenceWithEnumerated(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_ENUMERATED("state", 1, {1: "alpha", 200: "beta"}), + ) + + +class UPERSequenceOfStrings(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF("items", [], ASN1F_STRING) + + +class UPERNestedSequence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ) + + +class UPERSequenceWithNull(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_NULL("n", None), + ) + + +class UPERFixedBitString(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BIT_STRING("b", "0", uper_min=16, uper_max=16) + + +class UPERSequenceOfConstrainedInts(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", [], ASN1F_INTEGER("item", 0, uper_min=0, uper_max=255), + ) + + +class UPERSignedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=-128, uper_max=127) + + +class UPERMultiOptional(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("a", 0)), + ASN1F_optional(ASN1F_STRING("b", "")), + ) + + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + + +def check_uper_field_fixed_size(): + # type: () -> None + pkt = UPERFixedFields(n=200, s=b"ABC") + assert raw(pkt) == b"\xc8ABC" + decoded = _roundtrip(UPERFixedFields, pkt) + assert decoded.n.val == 200 + assert decoded.s.val == b"ABC" + + +def check_uper_field_integer(): + # type: () -> None + pkt = UPERIntegerField(n=12345) + assert raw(pkt) == bytes.fromhex("023039") + decoded = _roundtrip(UPERIntegerField, pkt) + assert decoded.n.val == 12345 + + +def check_uper_field_boolean(): + # type: () -> None + true_pkt = UPERBooleanField(b=True) + assert raw(true_pkt) == b"\x80" + decoded = _roundtrip(UPERBooleanField, true_pkt) + assert decoded.b.val == 1 + + false_pkt = UPERBooleanField(b=False) + assert raw(false_pkt) == b"\x00" + decoded = _roundtrip(UPERBooleanField, false_pkt) + assert decoded.b.val == 0 + + +def check_uper_field_string(): + # type: () -> None + pkt = UPERStringField(s=b"hi") + assert raw(pkt) == bytes.fromhex("026869") + decoded = _roundtrip(UPERStringField, pkt) + assert decoded.s.val == b"hi" + + +def check_uper_field_constrained_integer(): + # type: () -> None + pkt = UPERConstrainedInteger(n=200) + assert raw(pkt) == b"\xc8" + decoded = _roundtrip(UPERConstrainedInteger, pkt) + assert decoded.n.val == 200 + + +def check_uper_field_optional(): + # type: () -> None + present = UPEROptionalField(id=42, flag=True, extra=7) + assert raw(present) == bytes.fromhex("80954041c0") + decoded = _roundtrip(UPEROptionalField, present) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.extra.val == 7 + + absent = UPEROptionalField(id=42, flag=True, extra=None) + assert raw(absent) == bytes.fromhex("009540") + decoded = _roundtrip(UPEROptionalField, absent) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.extra is None + + +def check_uper_field_sequence_of(): + # type: () -> None + pkt = UPERSequenceOfIntegers(values=[1, 2, 3]) + assert raw(pkt) == bytes.fromhex("03010101020103") + decoded = _roundtrip(UPERSequenceOfIntegers, pkt) + assert [x.val for x in decoded.values] == [1, 2, 3] + + empty = UPERSequenceOfIntegers(values=[]) + assert raw(empty) == b"\x00" + decoded = _roundtrip(UPERSequenceOfIntegers, empty) + assert [x.val for x in decoded.values] == [] + + +def check_uper_field_choice(): + # type: () -> None + as_int = UPERChoiceField(c=ASN1_INTEGER(99)) + assert raw(as_int) == bytes.fromhex("00b180") + decoded = _roundtrip(UPERChoiceField, as_int) + assert decoded.c.val == 99 + + as_str = UPERChoiceField(c=ASN1_STRING(b"AB")) + assert raw(as_str) == bytes.fromhex("8120a100") + decoded = _roundtrip(UPERChoiceField, as_str) + assert decoded.c.val == b"AB" + + +def check_uper_field_choice_definition_order(): + # type: () -> None + as_str = UPERChoiceStringFirst(c=ASN1_STRING(b"AB")) + assert raw(as_str) == bytes.fromhex("0120a100") + decoded = _roundtrip(UPERChoiceStringFirst, as_str) + assert decoded.c.val == b"AB" + + as_int = UPERChoiceStringFirst(c=ASN1_INTEGER(99)) + assert raw(as_int) == bytes.fromhex("80b180") + decoded = _roundtrip(UPERChoiceStringFirst, as_int) + assert decoded.c.val == 99 + + +def check_uper_packet_record(): + # type: () -> None + full = UPERRecord( + id=42, + flag=True, + label=b"hi", + extra=7, + values=[1, 2, 3], + ) + assert raw(full) == bytes.fromhex("8095409a1a4041c0c04040408040c0") + decoded = _roundtrip(UPERRecord, full) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + pkt = UPERRecord( + id=42, + flag=True, + label=b"AB", + extra=None, + values=[1, 2], + ) + body = bytes.fromhex("0095409050808040404080") + assert raw(pkt) == body + decoded = _roundtrip(UPERRecord, pkt) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"AB" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [1, 2] + + empty = UPERRecord( + id=1, + flag=False, + label=b"", + extra=None, + values=[], + ) + assert raw(empty) == bytes.fromhex("0080800000") + decoded = _roundtrip(UPERRecord, empty) + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] + + +def check_uper_field_enumerated(): + # type: () -> None + alpha = UPEREnumeratedField(state=1) + assert raw(alpha) == b"\x00" + decoded = _roundtrip(UPEREnumeratedField, alpha) + assert decoded.state.val == 1 + + beta = UPEREnumeratedField(state=200) + assert raw(beta) == b"\x80" + decoded = _roundtrip(UPEREnumeratedField, beta) + assert decoded.state.val == 200 + + +def check_uper_field_bit_string(): + # type: () -> None + from scapy.asn1.asn1 import ASN1_BIT_STRING + + pkt = UPERBitStringField(bits=ASN1_BIT_STRING("1010101111001101")) + assert raw(pkt) == bytes.fromhex("7d5e68") + decoded = _roundtrip(UPERBitStringField, pkt) + assert decoded.bits.val == "1010101111001101" + + +def check_uper_message_prefix(): + # type: () -> None + pkt = UPERMessagePrefix( + msgId=1, + myflag=2, + szDescription=b"HelloWorld", + isReady=True, + ) + assert raw(pkt) == bytes.fromhex("0101010248656c6c6f576f726c6480") + decoded = _roundtrip(UPERMessagePrefix, pkt) + assert decoded.msgId.val == 1 + assert decoded.myflag.val == 2 + assert decoded.szDescription.val == b"HelloWorld" + assert decoded.isReady.val == 1 + + +def check_uper_sequence_with_choice(): + # type: () -> None + pkt = UPERSequenceWithChoice(id=42, c=ASN1_INTEGER(99)) + body = raw(pkt) + decoded = UPERSequenceWithChoice(body) + assert decoded.id.val == 42 + assert decoded.c.val == 99 + + as_str = UPERSequenceWithChoice(id=1, c=ASN1_STRING(b"AB")) + decoded = UPERSequenceWithChoice(raw(as_str)) + assert decoded.id.val == 1 + assert decoded.c.val == b"AB" + + +def check_uper_null_packet(): + # type: () -> None + pkt = UPERNullPacket() + assert raw(pkt) == b"" + decoded = _roundtrip(UPERNullPacket, pkt) + assert decoded.n is None + + +def check_uper_variable_octet_string(): + # type: () -> None + pkt = UPERVariableOctetString(data=bytes.fromhex("afbc4583")) + assert raw(pkt) == bytes.fromhex("1d7de22c18") + decoded = _roundtrip(UPERVariableOctetString, pkt) + assert decoded.data.val == bytes.fromhex("afbc4583") + + +def check_uper_constrained_range_integer(): + # type: () -> None + pkt = UPERConstrainedRangeInt(n=10) + assert raw(pkt) == b"\xa0" + decoded = _roundtrip(UPERConstrainedRangeInt, pkt) + assert decoded.n.val == 10 + + +def check_uper_sequence_with_enumerated(): + # type: () -> None + pkt = UPERSequenceWithEnumerated(id=1, state=200) + assert raw(pkt) == bytes.fromhex("010180") + decoded = _roundtrip(UPERSequenceWithEnumerated, pkt) + assert decoded.id.val == 1 + assert decoded.state.val == 200 + + alpha = UPERSequenceWithEnumerated(id=7, state=1) + assert raw(alpha) == bytes.fromhex("010700") + decoded = _roundtrip(UPERSequenceWithEnumerated, alpha) + assert decoded.state.val == 1 + + +def check_uper_sequence_of_strings(): + # type: () -> None + pkt = UPERSequenceOfStrings(items=[b"A", b"BC"]) + assert raw(pkt) == bytes.fromhex("020141024243") + decoded = _roundtrip(UPERSequenceOfStrings, pkt) + assert [x.val for x in decoded.items] == [b"A", b"BC"] + + empty = UPERSequenceOfStrings(items=[]) + assert raw(empty) == b"\x00" + decoded = _roundtrip(UPERSequenceOfStrings, empty) + assert [x.val for x in decoded.items] == [] + + +def check_uper_sequence_choice_hex(): + # type: () -> None + """Cross-check against reference composite encoding.""" + pkt = UPERSequenceWithChoice(id=1, c=ASN1_INTEGER(99)) + assert raw(pkt) == bytes.fromhex("010100b180") + decoded = UPERSequenceWithChoice(raw(pkt)) + assert decoded.id.val == 1 + assert decoded.c.val == 99 + + +def check_uper_nested_sequence(): + # type: () -> None + pkt = UPERNestedSequence(id=5, x=3, y=True) + assert raw(pkt) == bytes.fromhex("0105010380") + decoded = _roundtrip(UPERNestedSequence, pkt) + assert decoded.id.val == 5 + assert decoded.x.val == 3 + assert decoded.y.val == 1 + + +def check_uper_sequence_with_null(): + # type: () -> None + pkt = UPERSequenceWithNull(id=1) + assert raw(pkt) == bytes.fromhex("0101") + decoded = _roundtrip(UPERSequenceWithNull, pkt) + assert decoded.id.val == 1 + assert getattr(decoded.n, "val", decoded.n) is None + + +def check_uper_fixed_bit_string(): + # type: () -> None + from scapy.asn1.asn1 import ASN1_BIT_STRING + + pkt = UPERFixedBitString(b=ASN1_BIT_STRING("1010101111001101")) + assert raw(pkt) == bytes.fromhex("abcd") + decoded = _roundtrip(UPERFixedBitString, pkt) + assert decoded.b.val == "1010101111001101" + + +def check_uper_sequence_of_constrained_ints(): + # type: () -> None + pkt = UPERSequenceOfConstrainedInts(values=[1, 200, 0]) + assert raw(pkt) == bytes.fromhex("0301c800") + decoded = _roundtrip(UPERSequenceOfConstrainedInts, pkt) + assert [x.val for x in decoded.values] == [1, 200, 0] + + +def check_uper_signed_integer(): + # type: () -> None + for value, expected in [ + (0, b"\x80"), + (-1, b"\x7f"), + (127, b"\xff"), + (-128, b"\x00"), + ]: + pkt = UPERSignedInteger(n=value) + assert raw(pkt) == expected + decoded = _roundtrip(UPERSignedInteger, pkt) + assert decoded.n.val == value + + +def check_uper_multi_optional(): + # type: () -> None + both = UPERMultiOptional(id=1, a=2, b=b"hi") + assert raw(both) == bytes.fromhex("c0404040809a1a40") + decoded = _roundtrip(UPERMultiOptional, both) + assert decoded.id.val == 1 + assert decoded.a.val == 2 + assert decoded.b.val == b"hi" + + none = UPERMultiOptional(id=1, a=None, b=None) + assert raw(none) == bytes.fromhex("004040") + decoded = _roundtrip(UPERMultiOptional, none) + assert decoded.id.val == 1 + assert decoded.a is None + assert decoded.b is None + + only_a = UPERMultiOptional(id=3, a=9, b=None) + assert raw(only_a) == bytes.fromhex("8040c04240") + decoded = _roundtrip(UPERMultiOptional, only_a) + assert decoded.id.val == 3 + assert decoded.a.val == 9 + assert decoded.b is None From c9ae98d54d09fc012587875b0e844faf4bae7127 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 7 Aug 2026 20:44:45 +0200 Subject: [PATCH 02/46] Remove python based unit tests AI-Assisted: yes (Cursor) --- test/scapy/layers/asn1.uts | 729 +++--- test/scapy/layers/asn1_build_tests.py | 185 -- test/scapy/layers/asn1_coverage.py | 890 ------- test/scapy/layers/asn1_dissect_tests.py | 280 --- test/scapy/layers/ber.uts | 126 + test/scapy/layers/ber_codec.py | 275 --- test/scapy/layers/ber_packets.py | 184 -- test/scapy/layers/oer.uts | 862 +++++++ test/scapy/layers/oer_fuzz.py | 106 - test/scapy/layers/oer_iop.py | 160 -- test/scapy/layers/oer_packets.py | 209 -- test/scapy/layers/uper.uts | 2829 +++++++++++++++++++++++ test/scapy/layers/uper_asn1scc_iop.py | 190 -- test/scapy/layers/uper_codec.py | 174 -- test/scapy/layers/uper_fuzz.py | 133 -- test/scapy/layers/uper_helpers.py | 122 - test/scapy/layers/uper_iop.py | 189 -- test/scapy/layers/uper_packets.py | 543 ----- 18 files changed, 4199 insertions(+), 3987 deletions(-) delete mode 100644 test/scapy/layers/asn1_build_tests.py delete mode 100644 test/scapy/layers/asn1_coverage.py delete mode 100644 test/scapy/layers/asn1_dissect_tests.py delete mode 100644 test/scapy/layers/ber_codec.py delete mode 100644 test/scapy/layers/ber_packets.py create mode 100644 test/scapy/layers/oer.uts delete mode 100644 test/scapy/layers/oer_fuzz.py delete mode 100644 test/scapy/layers/oer_iop.py delete mode 100644 test/scapy/layers/oer_packets.py create mode 100644 test/scapy/layers/uper.uts delete mode 100644 test/scapy/layers/uper_asn1scc_iop.py delete mode 100644 test/scapy/layers/uper_codec.py delete mode 100644 test/scapy/layers/uper_fuzz.py delete mode 100644 test/scapy/layers/uper_helpers.py delete mode 100644 test/scapy/layers/uper_iop.py delete mode 100644 test/scapy/layers/uper_packets.py diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index c83f8388d8b..7f00cf6e17f 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -23,9 +23,12 @@ repr(ASN1_GENERALIZED_TIME("19991231235959")).startswith("1999-12-31 23:59:59 <" repr(ASN1_GENERALIZED_TIME("19991231235959.999")).startswith("1999-12-31 23:59:59.999 <") = with microseconds (invalid) assert "invalid" in repr(ASN1_GENERALIZED_TIME("1999123125959.99")) + assert "invalid" in repr(ASN1_GENERALIZED_TIME("1999123125959.99x")) + assert "invalid" in repr(ASN1_GENERALIZED_TIME("1999123125959.9999")) +True + ASN.1 Generalized Time (Zulu) = Z short HH @@ -52,8 +55,10 @@ repr(ASN1_GENERALIZED_TIME("19991231235959.999+0100")).startswith("1999-12-31 23 repr(ASN1_GENERALIZED_TIME("19991231235959-2359")).startswith("1999-12-31 23:59:59 -2359 <") = offset invalid (offset >= 24h) assert "invalid" in repr(ASN1_GENERALIZED_TIME("19991231235959-2400")) + assert "invalid" in repr(ASN1_GENERALIZED_TIME("19991231235959+2400")) +True + ASN.1 UTC Time = UTC short HHMM @@ -83,10 +88,17 @@ ASN1_GENERALIZED_TIME("199912312359").datetime == datetime(1999, 12, 31, 23, 59) ASN1_GENERALIZED_TIME("19991231235959").datetime == datetime(1999, 12, 31, 23, 59, 59) = datetime assignment x = ASN1_GENERALIZED_TIME("19991231235959.999") + x.datetime = datetime(2020, 12, 31) + assert x.val == "20201231000000" + x.datetime = x.datetime.replace(tzinfo=timezone.utc) + x.val == "20201231000000Z" + +True + = datetime construction ASN1_GENERALIZED_TIME(datetime(2020, 12, 31)).val == "20201231000000" = datetime construction (UTC) @@ -102,357 +114,380 @@ ASN1_UTC_TIME(datetime(2020, 12, 31, tzinfo=timezone.utc)).val == "201231000000Z = UTC datetime construction (offset) ASN1_UTC_TIME(datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-23, minutes=-59)))).val == "201231000000-2359" -+ ASN.1 OER/UPER contrib load ++ ASN.1 cross-codec build and dissect = import contrib codecs import scapy.contrib.oer import scapy.contrib.uper from scapy.contrib.oer import * from scapy.contrib.uper import * +from scapy.packet import raw += prepare helpers and packet classes +class BERTaggedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER("n", 0, explicit_tag=0xA1) + +class BERFixedFields(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, size_len=1), + ASN1F_STRING("s", "", size_len=3), + ) + +class BEROptionalField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ) + +class BERSequenceOfIntegers(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + +class BERChoiceField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +class BERRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + +class BEROptionalSequence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("hdr", 0), + ASN1F_optional(ASN1F_SEQUENCE( + ASN1F_INTEGER("id", None), + ASN1F_STRING("label", None), + explicit_tag=0xA0, + )), + ) + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + +class OERTaggedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0, explicit_tag=0xA1) + +class OERFixedFields(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_STRING("s", "", size_len=3), + ) + +class OEROptionalField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ) + +class OERSequenceOfIntegers(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + +class OERChoiceField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +class OERRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + +class OERNestedSequence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ) + +class OERNestedSequenceTrailing(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ASN1F_INTEGER("id", 0), + ) + +class OERSequenceOfWithTrailing(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ASN1F_INTEGER("id", 0), + ) + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + +class UPERFixedFields(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_STRING("s", "", size_len=3), + ) + +class UPERIntegerField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0) + +class UPERBooleanField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BOOLEAN("b", False) + +class UPERStringField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_STRING("s", "") + +class UPERConstrainedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER( + "n", 0, size_len=1, oer_unsigned=True, + ) + +class UPEROptionalField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ) + +class UPERSequenceOfIntegers(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + +class UPERChoiceField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +class UPERChoiceStringFirst(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_STRING(b""), ASN1F_STRING, ASN1F_INTEGER, + ) + +class UPERRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + +class UPEREnumeratedField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_ENUMERATED( + "state", 1, {1: "alpha", 200: "beta"}, + ) + +class UPERBitStringField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BIT_STRING( + "bits", "0", uper_min=1, uper_max=20, + ) + +class UPERMessagePrefix(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("msgId", 0), + ASN1F_INTEGER("myflag", 0), + ASN1F_STRING("szDescription", "", size_len=10), + ASN1F_BOOLEAN("isReady", False), + ) + +class UPERSequenceWithChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_CHOICE("c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING), + ) + +class UPERNullPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_NULL("n", None) + +class UPERVariableOctetString(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_STRING("data", "", uper_min=1, uper_max=20) + +class UPERConstrainedRangeInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=15) + +class UPERSequenceWithEnumerated(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_ENUMERATED("state", 1, {1: "alpha", 200: "beta"}), + ) + +class UPERSequenceOfStrings(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF("items", [], ASN1F_STRING) + +class UPERNestedSequence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ) + +class UPERSequenceWithNull(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_NULL("n", None), + ) + +class UPERFixedBitString(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BIT_STRING("b", "0", uper_min=16, uper_max=16) + +class UPERSequenceOfConstrainedInts(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", [], ASN1F_INTEGER("item", 0, uper_min=0, uper_max=255), + ) + +class UPERSignedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=-128, uper_max=127) + +class UPERMultiOptional(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("a", 0)), + ASN1F_optional(ASN1F_STRING("b", "")), + ) + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + +def _record_kwargs(): + # type: () -> dict + return dict( + id=42, + flag=True, + label=b"hi", + extra=7, + values=[1, 2, 3], + ) + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + +def _assert_record(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + +def _assert_record_empty(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] + +def _dissect(cls, data_hex): + # type: (Type[ASN1_Packet], str) -> ASN1_Packet + return cls(bytes.fromhex(data_hex)) += ber oer per choice build +class BERChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +class OERChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +class PERChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +for cls in (BERChoice, OERChoice, PERChoice): + as_int = cls(c=ASN1_INTEGER(99)) + assert len(raw(as_int)) > 0 + decoded = _roundtrip(cls, as_int) + assert decoded.c.val == 99 + as_str = cls(c=ASN1_STRING(b"AB")) + assert len(raw(as_str)) > 0 + decoded = _roundtrip(cls, as_str) + assert decoded.c.val == b"AB" + +True + += ber oer per record dissect +for cls, data_hex in [ + ( + BERRecord, + "301a02012a01010104026869" + "a003020107" + "3009020101020102020103", + ), + ( + OERRecord, + "012aff026869a00107" + "0103010101020103", + ), + ( + UPERRecord, + "8095409a1a4041c0c04040408040c0", + ), +]: + _assert_record(_dissect(cls, data_hex)) -+ ASN.1 OER codec -= OER length determinant short form -OER_len_enc(3) == b"\x03" -= OER length determinant long form -OER_len_enc(200) == b"\x81\xc8" -= OER boolean false -OERcodec_BOOLEAN.enc(0) == b"\x00" -= OER boolean true -OERcodec_BOOLEAN.enc(1) == b"\xff" -= OER null -OERcodec_NULL.enc(None) == b"" -= OER unconstrained integer -OERcodec_INTEGER.enc(4) == b"\x01\x04" -= OER constrained unsigned integer -OERcodec_INTEGER.enc(4, size_len=1) == b"\x04" -= OER constrained signed integer -OERcodec_INTEGER.enc(4, size_len=2) == b"\x00\x04" -= OER enumerated short form -OERcodec_ENUMERATED.enc(6) == b"\x06" -= OER octet string -OERcodec_STRING.enc(b"ABC") == b"\x03ABC" -= OER OID -OERcodec_OID.enc("1.2.3") == b"\x02\x2a\x03" -= OER integer roundtrip -x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(12345)) -x.val == 12345 and r == b"" -= OER boolean roundtrip -x, r = OERcodec_BOOLEAN.do_dec(OERcodec_BOOLEAN.enc(1)) -x.val == 1 and r == b"" -= OER ASN1 object encoding -ASN1_INTEGER(42).enc(ASN1_Codecs.OER) == b"\x01*" -= OER codec registration -ASN1_Class_UNIVERSAL.INTEGER.get_codec(ASN1_Codecs.OER) is OERcodec_INTEGER - -+ ASN.1 OER codec (extended) -= OER length zero -OER_len_enc(0) == b"\x00" -= OER length boundary short form -OER_len_enc(127) == b"\x7f" -= OER length boundary long form -OER_len_enc(128) == b"\x81\x80" -= OER length roundtrip -l, r = OER_len_dec(OER_len_enc(999)) -l == 999 and r == b"" -= OER signed integer zero -OER_signed_integer_enc(0) == b"\x01\x00" -= OER signed integer negative -OER_signed_integer_enc(-255) == b"\x02\xff\x01" -= OER signed integer large -OER_signed_integer_enc(100000) == b"\x03\x01\x86\xa0" -= OER signed integer roundtrip -v, r = OER_signed_integer_dec(OER_signed_integer_enc(-1234567)) -v == -1234567 and r == b"" -= OER unsigned integer zero -OER_unsigned_integer_enc(0) == b"\x01\x00" -= OER unsigned integer roundtrip -v, r = OER_unsigned_integer_dec(OER_unsigned_integer_enc(65535)) -v == 65535 and r == b"" -= OER fixed unsigned 1 byte -OERcodec_INTEGER.enc(255, size_len=1) == b"\xff" -= OER fixed signed 2 bytes negative -OERcodec_INTEGER.enc(-2, size_len=2) == b"\xff\xfe" -= OER fixed signed 4 bytes -OERcodec_INTEGER.enc(-2, size_len=4) == b"\xff\xff\xff\xfe" -= OER enumerated long form -OERcodec_ENUMERATED.enc(128) == b"\x82\x00\x80" -= OER enumerated negative -OERcodec_ENUMERATED.enc(-1) == b"\x81\xff" -= OER enumerated roundtrip -x, r = OERcodec_ENUMERATED.do_dec(OERcodec_ENUMERATED.enc(128)) -x.val == 128 and r == b"" -= OER null roundtrip -x, r = OERcodec_NULL.do_dec(OERcodec_NULL.enc(None)) -x.val is None and r == b"" -= OER octet string empty -OERcodec_STRING.enc(b"") == b"\x00" -= OER octet string fixed size -OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3) == b"\x12\x34\x56" -= OER octet string roundtrip -x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34")) -x.val == b"\x12\x34" and r == b"" -= OER OID 1.2 -OERcodec_OID.enc("1.2") == b"\x01\x2a" -= OER OID roundtrip -x, r = OERcodec_OID.do_dec(OERcodec_OID.enc("1.2.3321")) -x.val == "1.2.3321" and r == b"" -= OER bit string variable size -OERcodec_BIT_STRING.enc("0100") == b"\x02\x04\x40" -= OER bit string roundtrip -x, r = OERcodec_BIT_STRING.do_dec(OERcodec_BIT_STRING.enc("01000001")) -x.val == "01000001" and r == b"" -= OER IA5 string -OERcodec_IA5_STRING.enc(b"ABC") == b"\x03ABC" -= OER tag short form -OER_tag_enc(1, OER_CLASS_CONTEXT) == b"\x81" -= OER tag roundtrip -cls, num, r = OER_tag_dec(OER_tag_enc(1, OER_CLASS_CONTEXT)) -cls == OER_CLASS_CONTEXT and num == 1 and r == b"" -= OER sequence concat -OERcodec_SEQUENCE.enc([ASN1_INTEGER(4), ASN1_INTEGER(5)]) == b"\x01\x04\x01\x05" -= OER ASN1 boolean object -ASN1_BOOLEAN(1).enc(ASN1_Codecs.OER) == b"\xff" -= OER ASN1 null object -ASN1_NULL(None).enc(ASN1_Codecs.OER) == b"" - -+ ASN.1 OER interoperability (reference vectors) -= primitive encode interop -__import__('test.scapy.layers.oer_iop', fromlist=['check_primitive_interop']).check_primitive_interop() -= scapy encode reference decode -__import__('test.scapy.layers.oer_iop', fromlist=['check_scapy_encode_reference_decode']).check_scapy_encode_reference_decode() - -+ ASN.1 OER review fixes -= OER fixed integer decode roundtrip -x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(128, size_len=1), size_len=1, oer_unsigned=True) -x.val == 128 and r == b"" -= OER fixed integer signed decode -x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(-2, size_len=2), size_len=2) -x.val == -2 and r == b"" -= OER fixed octet string decode -x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3), size_len=3) -x.val == b"\x12\x34\x56" and r == b"" -= OER explicit null tagging -OER_tagging_enc(OERcodec_NULL.enc(None), explicit_tag=0x81) == b"\x81" -= OER choice id decode -tag, r = OER_id_dec(b"\x81\x01") -tag == 0x81 and r == b"\x01" - -+ ASN.1 OER fuzzing -= OER fuzz encode -__import__('test.scapy.layers.oer_fuzz', fromlist=['check_oer_fuzz_encode']).check_oer_fuzz_encode() -= OER fuzz encode roundtrip -__import__('test.scapy.layers.oer_fuzz', fromlist=['check_oer_fuzz_roundtrip']).check_oer_fuzz_roundtrip() -= OER fuzz codec decode -__import__('test.scapy.layers.oer_fuzz', fromlist=['check_oer_fuzz_codec_decode']).check_oer_fuzz_codec_decode() -= OER fuzz packet decode -__import__('test.scapy.layers.oer_fuzz', fromlist=['check_oer_fuzz_packet_decode']).check_oer_fuzz_packet_decode() - -+ ASN.1 OER packets and fields -= OER field explicit tag -__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_explicit_tag']).check_oer_field_explicit_tag() -= OER field fixed size -__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_fixed_size']).check_oer_field_fixed_size() -= OER field optional -__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_optional']).check_oer_field_optional() -= OER field sequence of -__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_sequence_of']).check_oer_field_sequence_of() -= OER field choice -__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_choice']).check_oer_field_choice() -= OER packet record -__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_packet_record']).check_oer_packet_record() -= OER nested sequence -__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_nested_sequence']).check_oer_nested_sequence() -= OER nested sequence trailing field -__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_nested_sequence_trailing']).check_oer_nested_sequence_trailing() -= OER sequence of with trailing field -__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_sequence_of_with_trailing']).check_oer_sequence_of_with_trailing() - - -+ ASN.1 packet build tests (BER, OER, PER) -= BER record build roundtrip -__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_ber_record_build_roundtrip']).check_ber_record_build_roundtrip() -= OER record build roundtrip -__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_oer_record_build_roundtrip']).check_oer_record_build_roundtrip() -= PER record build roundtrip -__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_per_record_build_roundtrip']).check_per_record_build_roundtrip() -= PER default field build -__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_per_default_field_build']).check_per_default_field_build() -= PER extensible integer build -__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_per_extensible_integer_build']).check_per_extensible_integer_build() -= PER constrained sequence of build -__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_per_constrained_sequence_of_build']).check_per_constrained_sequence_of_build() -= BER OER PER choice build -__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_ber_oer_per_choice_build']).check_ber_oer_per_choice_build() - -+ ASN.1 packet dissection tests (BER, OER, PER) -= BER field dissect -__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_ber_field_dissect']).check_ber_field_dissect() -= BER record dissect -__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_ber_record_dissect']).check_ber_record_dissect() -= OER field dissect -__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_oer_field_dissect']).check_oer_field_dissect() -= OER record dissect -__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_oer_record_dissect']).check_oer_record_dissect() -= PER field dissect -__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_field_dissect']).check_per_field_dissect() -= PER record dissect -__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_record_dissect']).check_per_record_dissect() -= PER default field dissect -__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_default_field_dissect']).check_per_default_field_dissect() -= PER extensible integer dissect -__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_extensible_integer_dissect']).check_per_extensible_integer_dissect() -= PER constrained sequence of dissect -__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_constrained_sequence_of_dissect']).check_per_constrained_sequence_of_dissect() -= BER OER PER record dissect -__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_ber_oer_per_record_dissect']).check_ber_oer_per_record_dissect() - -+ ASN.1 UPER codec -= UPER boolean true -UPERcodec_BOOLEAN.enc(1) == b"\x80" -= UPER boolean false -UPERcodec_BOOLEAN.enc(0) == b"\x00" -= UPER unconstrained integer -UPERcodec_INTEGER.enc(42) == b"\x01*" -= UPER constrained integer -UPERcodec_INTEGER.enc(200, uper_min=0, uper_max=255) == b"\xc8" -= UPER signed constrained integer -UPERcodec_INTEGER.enc(-1, uper_min=-128, uper_max=127) == b"\x7f" -= UPER octet string -UPERcodec_STRING.enc(b"AB") == b"\x02AB" -= UPER fixed octet string -UPERcodec_STRING.enc(b"\x12\x34\x56", size_len=3) == b"\x12\x34\x56" -= UPER null -UPERcodec_NULL.enc(None) == b"" -= UPER enumerated index -UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]) == b"\x80" -= UPER bit string variable size -UPERcodec_BIT_STRING.enc((b"\xab\xcd", 16), uper_min=1, uper_max=20) == bytes.fromhex("7d5e68") -= UPER enumerated roundtrip -x, r = UPERcodec_ENUMERATED.do_dec(UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), uper_enum_values=[1, 200]) -x.val == 200 and r == b"" -= UPER integer roundtrip -x, r = UPERcodec_INTEGER.do_dec(UPERcodec_INTEGER.enc(-1)) -x.val == -1 and r == b"" -= UPER boolean roundtrip -x, r = UPERcodec_BOOLEAN.do_dec(UPERcodec_BOOLEAN.enc(1)) -x.val == 1 and r == b"" -= UPER ASN1 object encoding -ASN1_INTEGER(42).enc(ASN1_Codecs.PER) == b"\x01*" -= UPER codec registration -ASN1_Class_UNIVERSAL.INTEGER.get_codec(ASN1_Codecs.PER) is UPERcodec_INTEGER - -+ ASN.1 UPER codec roundtrips -= UPER codec primitive roundtrips -__import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_roundtrips']).check_uper_codec_roundtrips() -= UPER codec reference decode interop -__import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_reference_decode']).check_uper_codec_reference_decode() -= UPER codec scapy encode reference interop -__import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_encode_reference']).check_uper_codec_encode_reference() -= UPER codec OID encode interop -__import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_oid_encode_interop']).check_uper_codec_oid_encode_interop() -= UPER codec OID roundtrip -__import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_oid_roundtrip']).check_uper_codec_oid_roundtrip() - -+ ASN.1 UPER helpers -= UPER length determinant -__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_length_determinant']).check_uper_length_determinant() -= UPER count roundtrip -__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_count_roundtrip']).check_uper_count_roundtrip() -= UPER choice index roundtrip -__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_choice_index_roundtrip']).check_uper_choice_index_roundtrip() -= UPER optional presence -__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_optional_presence']).check_uper_optional_presence() -= UPER constrained integer helper -__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_constrained_integer']).check_uper_constrained_integer() -= UPER constrained signed integer helper -__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_constrained_signed_integer']).check_uper_constrained_signed_integer() -= UPER octet string helper roundtrip -__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_octet_string_roundtrip']).check_uper_octet_string_roundtrip() -= UPER unexpected remainder detection -__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_has_unexpected_remainder']).check_uper_has_unexpected_remainder() -= UPER join encodings -__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_join_encodings']).check_uper_join_encodings() -= UPER chained encode into -__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_chained_encode_into']).check_uper_chained_encode_into() - -+ ASN.1 UPER interoperability (reference vectors) -= UPER primitive encode interop -__import__('test.scapy.layers.uper_iop', fromlist=['check_primitive_interop']).check_primitive_interop() -= UPER composite encode interop -__import__('test.scapy.layers.uper_iop', fromlist=['check_composite_interop']).check_composite_interop() -= UPER packet reference interop -__import__('test.scapy.layers.uper_iop', fromlist=['check_packet_reference_interop']).check_packet_reference_interop() -= UPER packet decode vectors -__import__('test.scapy.layers.uper_iop', fromlist=['check_packet_decode_vectors']).check_packet_decode_vectors() - -+ ASN.1 UPER asn1scc interoperability -= asn1scc vector encode interop -__import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_vectors']).check_asn1scc_vectors() -= asn1scc README Message uPER reference -__import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_readme_message_reference']).check_asn1scc_readme_message_reference() -= asn1scc README MessagePrefix Scapy interop -__import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_readme_message_prefix']).check_asn1scc_readme_message_prefix() - -+ ASN.1 UPER packets and fields -= UPER field fixed size -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_fixed_size']).check_uper_field_fixed_size() -= UPER field integer -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_integer']).check_uper_field_integer() -= UPER field boolean -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_boolean']).check_uper_field_boolean() -= UPER field string -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_string']).check_uper_field_string() -= UPER field constrained integer -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_constrained_integer']).check_uper_field_constrained_integer() -= UPER field optional -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_optional']).check_uper_field_optional() -= UPER field sequence of -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_sequence_of']).check_uper_field_sequence_of() -= UPER field choice -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_choice']).check_uper_field_choice() -= UPER field choice definition order -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_choice_definition_order']).check_uper_field_choice_definition_order() -= UPER packet record -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_packet_record']).check_uper_packet_record() -= UPER field enumerated -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_enumerated']).check_uper_field_enumerated() -= UPER field bit string -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_bit_string']).check_uper_field_bit_string() -= UPER message prefix -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_message_prefix']).check_uper_message_prefix() -= UPER sequence with choice -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_with_choice']).check_uper_sequence_with_choice() -= UPER null packet -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_null_packet']).check_uper_null_packet() -= UPER variable octet string -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_variable_octet_string']).check_uper_variable_octet_string() -= UPER constrained range integer -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_constrained_range_integer']).check_uper_constrained_range_integer() -= UPER sequence with enumerated -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_with_enumerated']).check_uper_sequence_with_enumerated() -= UPER sequence of strings -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_of_strings']).check_uper_sequence_of_strings() -= UPER sequence choice hex -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_choice_hex']).check_uper_sequence_choice_hex() -= UPER nested sequence -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_nested_sequence']).check_uper_nested_sequence() -= UPER sequence with null -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_with_null']).check_uper_sequence_with_null() -= UPER fixed bit string packet -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_fixed_bit_string']).check_uper_fixed_bit_string() -= UPER multi optional -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_multi_optional']).check_uper_multi_optional() -= UPER sequence of constrained integers -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_of_constrained_ints']).check_uper_sequence_of_constrained_ints() -= UPER signed integer field -__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_signed_integer']).check_uper_signed_integer() - -+ ASN.1 UPER fuzzing -= UPER fuzz encode -__import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_encode']).check_uper_fuzz_encode() -= UPER fuzz encode roundtrip -__import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_roundtrip']).check_uper_fuzz_roundtrip() -= UPER fuzz codec decode -__import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_codec_decode']).check_uper_fuzz_codec_decode() -= UPER fuzz packet decode -__import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_packet_decode']).check_uper_fuzz_packet_decode() +True diff --git a/test/scapy/layers/asn1_build_tests.py b/test/scapy/layers/asn1_build_tests.py deleted file mode 100644 index 9fc9122a9e7..00000000000 --- a/test/scapy/layers/asn1_build_tests.py +++ /dev/null @@ -1,185 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -Cross-codec ASN.1 packet build and round-trip tests (BER, OER, PER). -""" -import scapy.contrib.oer # noqa: F401 # register OER stem -import scapy.contrib.uper # noqa: F401 # register UPER stem - -from scapy.asn1.asn1 import ASN1_Codecs, ASN1_INTEGER, ASN1_STRING -from scapy.asn1fields import ( - ASN1F_BOOLEAN, - ASN1F_CHOICE, - ASN1F_DEFAULT, - ASN1F_INTEGER, - ASN1F_SEQUENCE, - ASN1F_SEQUENCE_OF, - ASN1F_STRING, - ASN1F_optional, -) -from scapy.asn1packet import ASN1_Packet -from scapy.packet import raw - -from typing import Any - -from test.scapy.layers.ber_packets import BERRecord -from test.scapy.layers.oer_packets import OERRecord -from test.scapy.layers.uper_packets import UPERRecord - - -def _roundtrip(cls, pkt): - # type: (type, ASN1_Packet) -> ASN1_Packet - return cls(raw(pkt)) - - -def _record_kwargs(): - # type: () -> dict - return dict( - id=42, - flag=True, - label=b"hi", - extra=7, - values=[1, 2, 3], - ) - - -def check_ber_record_build_roundtrip(): - # type: () -> None - pkt = BERRecord(**_record_kwargs()) - assert len(raw(pkt)) > 0 - decoded = _roundtrip(BERRecord, pkt) - assert decoded.id.val == 42 - assert decoded.flag.val == 1 - assert decoded.label.val == b"hi" - assert decoded.extra.val == 7 - assert [x.val for x in decoded.values] == [1, 2, 3] - - -def check_oer_record_build_roundtrip(): - # type: () -> None - pkt = OERRecord(**_record_kwargs()) - assert len(raw(pkt)) > 0 - decoded = _roundtrip(OERRecord, pkt) - assert decoded.id.val == 42 - assert decoded.flag.val == 1 - assert decoded.label.val == b"hi" - assert decoded.extra.val == 7 - assert [x.val for x in decoded.values] == [1, 2, 3] - - -def check_per_record_build_roundtrip(): - # type: () -> None - pkt = UPERRecord(**_record_kwargs()) - assert len(raw(pkt)) > 0 - decoded = _roundtrip(UPERRecord, pkt) - assert decoded.id.val == 42 - assert decoded.flag.val == 1 - assert decoded.label.val == b"hi" - assert decoded.extra.val == 7 - assert [x.val for x in decoded.values] == [1, 2, 3] - - -def _asn1_int(val): - # type: (Any) -> int - return val.val if hasattr(val, "val") else val - - -def check_per_default_field_build(): - # type: () -> None - class UPERDefaultRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), - ASN1F_DEFAULT( - ASN1F_INTEGER( - "count", 600, - uper_min=0, uper_max=86401, oer_unsigned=True, - ), - 600, - ), - ) - - absent = UPERDefaultRecord(id=1) - assert raw(absent) == b"\x00\x80" - decoded = _roundtrip(UPERDefaultRecord, absent) - assert decoded.id.val == 1 - assert _asn1_int(decoded.count) == 600 - - present = UPERDefaultRecord(id=1, count=86400) - assert raw(present) == bytes.fromhex("80d46000") - decoded = _roundtrip(UPERDefaultRecord, present) - assert decoded.id.val == 1 - assert _asn1_int(decoded.count) == 86400 - - -def check_per_extensible_integer_build(): - # type: () -> None - class UPERExtInt(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER( - "n", 0, - uper_min=1, uper_max=65535, - uper_extensible=True, oer_unsigned=True, - ), - ) - - in_range = UPERExtInt(n=42) - assert raw(in_range) == bytes.fromhex("001480") - decoded = _roundtrip(UPERExtInt, in_range) - assert decoded.n.val == 42 - - out_of_range = UPERExtInt(n=1706733817) - assert raw(out_of_range) == bytes.fromhex("8232dd587c80") - decoded = _roundtrip(UPERExtInt, out_of_range) - assert decoded.n.val == 1706733817 - - -def check_per_constrained_sequence_of_build(): - # type: () -> None - class UPERConstrainedSeqOf(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE_OF( - "items", [], - ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7), - uper_min=1, uper_max=3, - ) - - pkt = UPERConstrainedSeqOf(items=[1, 2]) - assert raw(pkt) == bytes.fromhex("4a") - decoded = _roundtrip(UPERConstrainedSeqOf, pkt) - assert [x.val for x in decoded.items] == [1, 2] - - -def check_ber_oer_per_choice_build(): - # type: () -> None - class BERChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - - class OERChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - - class PERChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - - for cls in (BERChoice, OERChoice, PERChoice): - as_int = cls(c=ASN1_INTEGER(99)) - assert len(raw(as_int)) > 0 - decoded = _roundtrip(cls, as_int) - assert decoded.c.val == 99 - - as_str = cls(c=ASN1_STRING(b"AB")) - assert len(raw(as_str)) > 0 - decoded = _roundtrip(cls, as_str) - assert decoded.c.val == b"AB" diff --git a/test/scapy/layers/asn1_coverage.py b/test/scapy/layers/asn1_coverage.py deleted file mode 100644 index dea2d651e3f..00000000000 --- a/test/scapy/layers/asn1_coverage.py +++ /dev/null @@ -1,890 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -Additional coverage for UPER, OER, and asn1fields helpers. -""" - - -def _raises(exc, func): - # type: (type, Any) -> None - try: - func() - except exc: - return - raise AssertionError("Expected %s" % exc.__name__) - - -from typing import Any -from unittest import mock - -from scapy.asn1.asn1 import ( - ASN1_BIT_STRING, - ASN1_Class_UNIVERSAL, - ASN1_Codecs, - ASN1_Error, - ASN1_INTEGER, - ASN1_STRING, - ASN1_TIME_TICKS, -) -from scapy.asn1.ber import BER_Decoding_Error -from scapy.contrib.oer import ( - OER_Decoding_Error, - OER_Encoding_Error, - OERcodec_BIT_STRING, - OERcodec_IPADDRESS, - OERcodec_SEQUENCE, - OERcodec_SET, -) -from scapy.contrib.uper import ( - UPER_Decoding_Error, - UPER_Encoding_Error, - UPER_Decoder, - UPER_Encoder, - UPERcodec_BIT_STRING, - UPERcodec_ENUMERATED, - UPERcodec_IPADDRESS, - UPERcodec_SEQUENCE, - UPERcodec_SET, -) -from scapy.asn1fields import ( - ASN1F_BIT_STRING, - ASN1F_BIT_STRING_ENCAPS, - ASN1F_BOOLEAN, - ASN1F_CHOICE, - ASN1F_DEFAULT, - ASN1F_FLAGS, - ASN1F_IPADDRESS, - ASN1F_INTEGER, - ASN1F_OID, - ASN1F_PACKET, - ASN1F_SEQUENCE, - ASN1F_SEQUENCE_OF, - ASN1F_SET_OF, - ASN1F_STRING, - ASN1F_STRING_ENCAPS, - ASN1F_STRING_PacketField, - ASN1F_TIME_TICKS, - ASN1F_UTC_TIME, - ASN1F_badsequence, - ASN1F_enum_INTEGER, - ASN1F_omit, - ASN1F_optional, -) -from scapy.asn1packet import ASN1_Packet -from scapy.packet import Raw, raw - - -class _InnerRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_enum_INTEGER("mode", ASN1_INTEGER(0), ["off", "on"]), - ) - - -class _EncapsRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_STRING_ENCAPS("payload", None, _InnerRecord), - ) - - -class _FlagsRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_FLAGS("f", "000", ["read", "write", "exec"]), - ) - - -class _SetOfRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SET_OF("items", [], ASN1F_INTEGER) - - -class _PacketFieldRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_STRING_PacketField("data", b""), - ) - - -class _ExplicitPacket(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2) - - -class _BitEncapsRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord), - ) - - -def check_uper_error_str(): - # type: () -> None - obj = ASN1_INTEGER(2) - err = UPER_Encoding_Error("enc", encoded=obj, remaining=b"x") - assert "Already encoded" in str(err) - err2 = UPER_Decoding_Error("dec", decoded=obj, remaining=b"y") - assert "Already decoded" in str(err2) - - -def check_uper_length_determinant_extended(): - # type: () -> None - enc = UPER_Encoder() - assert enc.append_length_determinant(32768) == 32768 - assert enc.as_bytes() == b"\xc2" - - enc = UPER_Encoder() - assert enc.append_length_determinant(49152) == 49152 - assert enc.as_bytes() == b"\xc3" - - enc = UPER_Encoder() - assert enc.append_length_determinant(65535) == 49152 - assert enc.as_bytes() == b"\xc3" - - -def check_uper_unconstrained_whole_number(): - # type: () -> None - enc = UPER_Encoder() - enc.append_unconstrained_whole_number(-256) - dec = UPER_Decoder(enc.as_bytes()) - assert dec.read_unconstrained_whole_number() == -256 - - enc = UPER_Encoder() - enc.append_unconstrained_whole_number(0) - dec = UPER_Decoder(enc.as_bytes()) - assert dec.read_unconstrained_whole_number() == 0 - - -def check_uper_bit_string_paths(): - # type: () -> None - encoded = UPERcodec_BIT_STRING.enc("1010", uper_min=1, uper_max=20) - obj, remain = UPERcodec_BIT_STRING.do_dec( - encoded, uper_min=1, uper_max=20, - ) - assert obj.val == "1010" - - encoded2 = UPERcodec_BIT_STRING.enc(b"\xab", uper_min=4, uper_max=8) - obj2, _ = UPERcodec_BIT_STRING.do_dec(encoded2, uper_min=4, uper_max=8) - assert len(obj2.val) == 8 - - fixed = UPERcodec_BIT_STRING.enc("1010101111001101", uper_min=16, uper_max=16) - obj3, _ = UPERcodec_BIT_STRING.do_dec(fixed, uper_min=16, uper_max=16) - assert obj3.val == "1010101111001101" - - -def check_uper_enumerated_range(): - # type: () -> None - encoded = UPERcodec_ENUMERATED.enc(3, uper_min=0, uper_max=7) - obj, remain = UPERcodec_ENUMERATED.do_dec(encoded, uper_min=0, uper_max=7) - assert obj.val == 3 - assert remain == b"" - - enc = UPER_Encoder() - UPERcodec_ENUMERATED.encode_into(enc, 2, uper_min=0, uper_max=3) - obj2 = UPERcodec_ENUMERATED.dec_from_decoder( - UPER_Decoder(enc.as_bytes()), - uper_min=0, - uper_max=3, - ) - assert obj2.val == 2 - - -def check_uper_sequence_errors(): - # type: () -> None - _raises(UPER_Encoding_Error, lambda: UPERcodec_SEQUENCE.enc([ASN1_INTEGER(1)])) - - _raises(UPER_Decoding_Error, lambda: UPERcodec_SEQUENCE.do_dec(b"\x00")) - - assert UPERcodec_SET.enc(b"raw") == b"raw" - - -def check_uper_ipaddress(): - # type: () -> None - encoded = UPERcodec_IPADDRESS.enc("10.0.0.1") - obj, remain = UPERcodec_IPADDRESS.do_dec(encoded) - assert obj.val == "10.0.0.1" - assert remain == b"" - - _raises(UPER_Encoding_Error, lambda: UPERcodec_IPADDRESS.enc("bad-ip")) - - -def check_oer_error_str(): - # type: () -> None - obj = ASN1_INTEGER(1) - err = OER_Encoding_Error("enc", encoded=obj, remaining=b"z") - assert "Already encoded" in str(err) - err2 = OER_Decoding_Error("dec", decoded=obj, remaining=b"w") - assert "Already decoded" in str(err2) - - -def check_oer_ipaddress_and_sequence(): - # type: () -> None - encoded = OERcodec_IPADDRESS.enc("127.0.0.1") - obj, remain = OERcodec_IPADDRESS.do_dec(encoded) - assert obj.val == "127.0.0.1" - assert remain == b"" - - fixed = OERcodec_IPADDRESS.enc("127.0.0.1", size_len=4) - obj2, remain2 = OERcodec_IPADDRESS.do_dec(fixed, size_len=4) - assert obj2.val == "127.0.0.1" - assert remain2 == b"" - - _raises(OER_Encoding_Error, lambda: OERcodec_IPADDRESS.enc("bad-ip")) - - _raises(OER_Decoding_Error, lambda: OERcodec_IPADDRESS.do_dec(b"\x01")) - - assert OERcodec_SEQUENCE.enc(b"payload") == b"payload" - assert OERcodec_SET.enc(b"payload") == b"payload" - - _raises(OER_Decoding_Error, lambda: OERcodec_SEQUENCE.do_dec(b"\x00")) - - empty, remain = OERcodec_BIT_STRING.do_dec(OERcodec_BIT_STRING.enc("")) - assert empty.val == "" - assert remain == b"" - - -def check_asn1fields_enum_and_flags(): - # type: () -> None - pkt = _InnerRecord(mode="on") - built = raw(pkt) - decoded = _InnerRecord(built) - assert decoded.mode.val == 1 - - flags = _FlagsRecord(f="read+exec") - assert flags.f.val == "101" - assert "read, exec" in _FlagsRecord.ASN1_root.seq[0].i2repr(flags, flags.f) - - set_pkt = _SetOfRecord(items=[ASN1_INTEGER(0), ASN1_INTEGER(1)]) - set_raw = raw(set_pkt) - set_dec = _SetOfRecord(set_raw) - assert [x.val for x in set_dec.items] == [0, 1] - - -def check_asn1fields_encaps_and_packet(): - # type: () -> None - inner = _InnerRecord(mode=1) - enc = _EncapsRecord() - enc.payload = inner - enc_raw = raw(enc) - enc_dec = _EncapsRecord(enc_raw) - assert enc_dec.payload.mode.val == 1 - - pkt_field = _PacketFieldRecord() - pkt_field.data = _InnerRecord(mode=0) - pf_raw = raw(pkt_field) - pf_dec = _PacketFieldRecord(pf_raw) - assert isinstance(pf_dec.data.val, bytes) - - explicit = _ExplicitPacket() - explicit.inner = _InnerRecord(mode=1) - ex_raw = raw(explicit) - ex_dec = _ExplicitPacket(ex_raw) - assert ex_dec.inner.mode.val == 1 - - -def check_asn1fields_choice_and_special(): - # type: () -> None - class _OerChoiceRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - - class _BerChoiceRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - - oer = _OerChoiceRecord(c=ASN1_INTEGER(1)) - oer_dec = _OerChoiceRecord(raw(oer)) - assert oer_dec.c.val == 1 - - ber = _BerChoiceRecord(c=ASN1_INTEGER(0)) - ber_dec = _BerChoiceRecord(raw(ber)) - assert ber_dec.c.val == 0 - - inner_bytes = raw(_InnerRecord(mode=0)) - bit_payload = ASN1_BIT_STRING( - inner_bytes, - readable=True, - ) - bit_pkt = _BitEncapsRecord(b=bit_payload) - bit_dec = _BitEncapsRecord(raw(bit_pkt)) - assert bit_dec.b.mode.val == 0 - - class _TicksRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_TIME_TICKS("t", ASN1_TIME_TICKS(0)) - - class _IpRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_IPADDRESS("addr", ASN1_STRING(b"")) - - ticks = _TicksRecord(t=ASN1_TIME_TICKS(1234)) - assert raw(ticks).endswith(b"\x04\xd2") - - ip = _IpRecord() - ip.addr = "192.168.1.1" - assert raw(ip) == b"\x40\x04\xc0\xa8\x01\x01" - - -def check_asn1fields_optional_dissect(): - # type: () -> None - class _OptRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_optional(ASN1F_INTEGER("extra", 0)), - ) - - class _BerChoiceRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - - pkt = _OptRecord(id=0, extra=None) - assert raw(pkt) - decoded = _OptRecord(raw(pkt)) - assert decoded.extra is None - - choice_rand = _BerChoiceRecord.ASN1_root.randval() - assert choice_rand is not None - - -def check_asn1fields_default_and_omit(): - # type: () -> None - class _DefaultRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), - ASN1F_DEFAULT( - ASN1F_INTEGER( - "count", 600, - uper_min=0, uper_max=86401, oer_unsigned=True, - ), - 600, - ), - ) - - absent = _DefaultRecord(id=1) - assert raw(absent) == b"\x00\x80" - decoded = _DefaultRecord(raw(absent)) - assert decoded.id.val == 1 - assert decoded.count == 600 or decoded.count.val == 600 - - present = _DefaultRecord(id=1, count=86400) - decoded = _DefaultRecord(raw(present)) - assert decoded.count.val == 86400 - - class _OmitRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_omit("ignored", None), - ) - - omit_pkt = _OmitRecord(id=7) - assert raw(omit_pkt) == bytes.fromhex("3003020107") - - -def check_asn1fields_extensible_per(): - # type: () -> None - class _ExtSeq(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), - ASN1F_optional(ASN1F_INTEGER("extra", 0, uper_min=0, uper_max=7)), - uper_extensible=True, - ) - - pkt = _ExtSeq(id=2, extra=3) - data = raw(pkt) - decoded = _ExtSeq(data) - assert decoded.id.val == 2 - assert decoded.extra.val == 3 - - dec = UPER_Decoder(b"\x80") - _raises( - UPER_Decoding_Error, - lambda: _ExtSeq.ASN1_root.dissect_from_decoder(_ExtSeq(), dec), - ) - - class _ExtChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - uper_extensible=True, - ) - - choice = _ExtChoice(c=ASN1_INTEGER(4)) - assert raw(choice) - dec = UPER_Decoder(b"\x80") - _raises( - UPER_Decoding_Error, - lambda: _ExtChoice.ASN1_root.m2i_from_decoder(_ExtChoice(), dec), - ) - - class _InnerItem(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7) - - class _ExtSeqOf(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE_OF( - "items", [], _InnerItem, - uper_min=1, uper_max=2, uper_extensible=True, - ) - - in_range = _ExtSeqOf(items=[_InnerItem(n=1)]) - assert raw(in_range) - decoded = _ExtSeqOf(raw(in_range)) - assert decoded.items[0].n.val == 1 - - out_of_range = _ExtSeqOf( - items=[_InnerItem(n=i) for i in range(4)], - ) - assert raw(out_of_range) - decoded = _ExtSeqOf(raw(out_of_range)) - assert len(decoded.items) == 4 - - -def check_asn1fields_sequence_of_advanced(): - # type: () -> None - class _Inner(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7) - - class _SeqOfPackets(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE_OF( - "items", [], _Inner, uper_min=1, uper_max=3, - ) - - pkt = _SeqOfPackets(items=[_Inner(n=1), _Inner(n=2)]) - decoded = _SeqOfPackets(raw(pkt)) - assert [x.n.val for x in decoded.items] == [1, 2] - - class _OerSeqOf(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) - - oer_pkt = _OerSeqOf(values=[1, 2]) - oer_dec = _OerSeqOf(raw(oer_pkt)) - assert [x.val for x in oer_dec.values] == [1, 2] - - class _EmptySeqOf(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) - - empty = _EmptySeqOf(values=None) - assert raw(empty) == b"\x00" - assert _EmptySeqOf.ASN1_root.i2repr(empty, None) == "[]" - assert _EmptySeqOf.ASN1_root.i2repr( - _EmptySeqOf(values=[ASN1_INTEGER(1)]), - [ASN1_INTEGER(1)], - ).startswith("[") - - _raises(ValueError, lambda: ASN1F_SEQUENCE_OF("bad", [], object())) - - -def check_asn1fields_choice_advanced(): - # type: () -> None - class _InnerChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - - class _NestedChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), _InnerChoice, ASN1F_INTEGER, - ) - - nested = _NestedChoice(c=_InnerChoice(c=ASN1_STRING(b"xy"))) - assert len(raw(nested)) > 0 - nested_dec = _NestedChoice(raw(nested)) - assert isinstance(nested_dec.c, (_InnerChoice, ASN1_STRING)) - - class _OerTaggedChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - explicit_tag=0xA1, - ) - - oer_choice = _OerTaggedChoice(c=ASN1_INTEGER(9)) - assert raw(oer_choice) - - class _PacketChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_CHOICE( - "c", - ASN1_INTEGER(0), - ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2), - ASN1F_INTEGER, - ) - - packet_choice = _PacketChoice( - c=_InnerRecord(mode=ASN1_INTEGER(1)), - ) - packet_dec = _PacketChoice(raw(packet_choice)) - assert packet_dec.c.mode.val == 1 - - class _PerChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - - _raises( - ASN1_Error, - lambda: ASN1F_CHOICE( - "c", 0, ASN1F_INTEGER, implicit_tag=0xA0, - ), - ) - _raises( - ASN1_Error, - lambda: _PerChoice.ASN1_root.m2i(_PerChoice(), b""), - ) - _raises( - ASN1_Error, - lambda: _PerChoice.ASN1_root._uper_encode_into( - UPER_Encoder(), _PerChoice(), 42, - ), - ) - - -def check_asn1fields_enum_bitstring_and_flags(): - # type: () -> None - class _NamedEnum(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_enum_INTEGER( - "state", 0, ["off", "on", "auto"], - ) - - named = _NamedEnum(state="on") - built = raw(named) - decoded = _NamedEnum(built) - assert decoded.state.val == 1 - assert "'on'" in _NamedEnum.ASN1_root.i2repr(decoded, decoded.state) - - class _BitRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_BIT_STRING("bits", b"\xaa") - - assert raw(_BitRecord()) - - flags = _FlagsRecord() - flags.f = ASN1_BIT_STRING("101") - assert "read, exec" in _FlagsRecord.ASN1_root.seq[0].i2repr(flags, flags.f) - - class _BadBitEncaps(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord) - - _raises( - BER_Decoding_Error, - lambda: _BadBitEncaps.ASN1_root.m2i( - _BadBitEncaps(), - b"\x03\x02\x01\x00", - ), - ) - - -def check_asn1fields_packet_and_sequence_errors(): - # type: () -> None - class _PerInner(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("mode", 0, uper_min=0, uper_max=1) - - class _PacketWrap(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_PACKET("inner", None, _PerInner) - - inner = _PerInner(mode=1) - wrap = _PacketWrap(inner=inner) - decoded = _PacketWrap(raw(wrap)) - assert decoded.inner.mode.val == 1 - - class _DynamicPacket(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_PACKET( - "inner", None, _PerInner, - next_cls_cb=lambda pkt: _PerInner, - ) - - dyn = _DynamicPacket(inner=_PerInner(mode=0)) - assert _DynamicPacket.ASN1_root._resolve_cls(dyn) is _PerInner - - empty_packet = _PacketWrap(inner=None) - assert raw(empty_packet) == b"" - - class _BerSeq(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_INTEGER("extra", 0), - ) - - _raises( - BER_Decoding_Error, - lambda: _BerSeq.ASN1_root.m2i( - _BerSeq(), - bytes.fromhex("300702010102010200ff"), - ), - ) - - class _OerSeq(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, size_len=1, oer_unsigned=True), - ) - - _, remain = _OerSeq.ASN1_root.m2i(_OerSeq(), b"\x01\xff") - assert remain == b"\xff" - - class _PerSeq(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), - ) - - _raises( - UPER_Decoding_Error, - lambda: _PerSeq.ASN1_root.m2i(_PerSeq(), b"\x80\xff"), - ) - - empty_seq = _BerSeq() - _BerSeq.ASN1_root._dissect_sequence_children(empty_seq, b"") - assert empty_seq.id is None - assert empty_seq.extra is None - - class _OptListRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), - ASN1F_optional( - ASN1F_SEQUENCE_OF("items", [], ASN1F_INTEGER), - ), - ) - - opt_list = _OptListRecord(id=1, items=None) - assert raw(opt_list) - - field = ASN1F_INTEGER("n", 0) - with mock.patch.object( - _InnerRecord, "__init__", side_effect=ASN1F_badsequence, - ): - pkt_obj, remain = field.extract_packet( - _InnerRecord, b"\xab\xcd", _underlayer=None, - ) - assert isinstance(pkt_obj, Raw) - assert pkt_obj.load == b"\xab\xcd" - assert remain == b"\xab\xcd" - - -def check_asn1fields_more_coverage(): - # type: () -> None - _raises( - ASN1_Error, - lambda: ASN1F_INTEGER("x", 0, implicit_tag=1, explicit_tag=2), - ) - - class _IntRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_INTEGER("n", 0) - - field = _IntRecord.ASN1_root - _raises( - ASN1_Error, - lambda: field.i2m(_IntRecord(), ASN1_STRING(b"bad")), - ) - - flex_field = ASN1F_INTEGER("n", 0, flexible_tag=True, explicit_tag=0xA0) - obj, remain = flex_field.m2i(_IntRecord(), bytes.fromhex("a1020101")) - assert obj.tag != ASN1_Class_UNIVERSAL.INTEGER or remain == b"" - - class _FlexSeq(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - explicit_tag=0xA1, - flexible_tag=True, - ) - - flex_seq = _FlexSeq(id=1) - assert raw(flex_seq) - decoded = _FlexSeq(raw(flex_seq)) - assert decoded.id.val == 1 - - assert ASN1F_BOOLEAN("b", False).randval() is not None - assert ASN1F_BIT_STRING("b", b"").randval() is not None - assert ASN1F_OID("o", None).randval() is not None - assert ASN1F_UTC_TIME("t", "").randval() is not None - assert " 0 - - empty_inner, remain = packet_field.m2i(_FlexPacket(), b"") - assert empty_inner is None and remain == b"" - - obj_val = packet_field.i2m(_FlexPacket(), _InnerRecord(mode=0)) - assert len(obj_val) > 0 - - flags_field = _FlagsRecord.ASN1_root.seq[0] - assert flags_field.i2repr(_FlagsRecord(), None) == "None" - - class _OerFlexSeqOf(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE_OF( - "values", [], ASN1F_INTEGER, - explicit_tag=0xA1, - ) - - _OerFlexSeqOf.ASN1_root.flexible_tag = True - - oer_seq = _OerFlexSeqOf(values=[1]) - data = raw(oer_seq) - decoded = _OerFlexSeqOf(data) - assert decoded.values[0].val == 1 - - class _BerFlexSeqOf(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE_OF( - "values", [], ASN1F_INTEGER, - explicit_tag=0xA1, - ) - - _BerFlexSeqOf.ASN1_root.flexible_tag = True - - ber_seq = _BerFlexSeqOf(values=[2]) - assert raw(ber_seq) - - class _ExtChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - uper_extensible=True, - ) - - dec = UPER_Decoder(b"\x80") - _raises( - UPER_Decoding_Error, - lambda: _ExtChoice.ASN1_root.m2i_from_decoder(_ExtChoice(), dec), - ) - - class _SingleChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_CHOICE("c", ASN1_INTEGER(0), ASN1F_INTEGER) - - single = _SingleChoice(c=ASN1_INTEGER(3)) - assert raw(single) - - class _FlexChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - flexible_tag=True, - ) - - flex_choice = _FlexChoice(c=ASN1_INTEGER(4)) - assert raw(flex_choice) - - class _OerPktChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - - oer_pkt_choice = _OerPktChoice(c=ASN1_STRING(b"hi")) - assert raw(oer_pkt_choice) - diff --git a/test/scapy/layers/asn1_dissect_tests.py b/test/scapy/layers/asn1_dissect_tests.py deleted file mode 100644 index 2383560c3cf..00000000000 --- a/test/scapy/layers/asn1_dissect_tests.py +++ /dev/null @@ -1,280 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -ASN.1 packet dissection tests from fixed byte vectors (BER, OER, PER). -""" -import scapy.contrib.oer # noqa: F401 # register OER stem -import scapy.contrib.uper # noqa: F401 # register UPER stem - -from typing import Any, Type - -from scapy.asn1.asn1 import ASN1_Codecs -from scapy.asn1fields import ( - ASN1F_DEFAULT, - ASN1F_INTEGER, - ASN1F_SEQUENCE, - ASN1F_SEQUENCE_OF, -) -from scapy.asn1packet import ASN1_Packet - -from test.scapy.layers.ber_packets import ( - BERChoiceField, - BERFixedFields, - BEROptionalField, - BERRecord, - BERSequenceOfIntegers, - BERTaggedInteger, -) -from test.scapy.layers.oer_packets import ( - OERChoiceField, - OERFixedFields, - OEROptionalField, - OERRecord, - OERSequenceOfIntegers, - OERTaggedInteger, -) -from test.scapy.layers.uper_packets import ( - UPERChoiceField, - UPERFixedFields, - UPEROptionalField, - UPERRecord, - UPERSequenceOfIntegers, -) - - -def _asn1_int(val): - # type: (Any) -> int - return val.val if hasattr(val, "val") else val - - -def _assert_record(decoded): - # type: (ASN1_Packet) -> None - assert decoded.id.val == 42 - assert decoded.flag.val == 1 - assert decoded.label.val == b"hi" - assert decoded.extra.val == 7 - assert [x.val for x in decoded.values] == [1, 2, 3] - - -def _assert_record_empty(decoded): - # type: (ASN1_Packet) -> None - assert decoded.id.val == 1 - assert decoded.flag.val == 0 - assert decoded.label.val == b"" - assert decoded.extra is None - assert [x.val for x in decoded.values] == [] - - -def _dissect(cls, data_hex): - # type: (Type[ASN1_Packet], str) -> ASN1_Packet - return cls(bytes.fromhex(data_hex)) - - -def check_ber_field_dissect(): - # type: () -> None - tagged = _dissect(BERTaggedInteger, "a103020105") - assert tagged.n.val == 5 - - fixed = _dissect(BERFixedFields, "300d02810200c80483000003414243") - assert fixed.n.val == 200 - assert fixed.s.val == b"ABC" - - present = _dissect(BEROptionalField, "3008020101a003020107") - assert present.id.val == 1 - assert present.extra.val == 7 - - absent = _dissect(BEROptionalField, "3003020101") - assert absent.id.val == 1 - assert absent.extra is None - - seqof = _dissect(BERSequenceOfIntegers, "3009020101020102020103") - assert [x.val for x in seqof.values] == [1, 2, 3] - - as_int = _dissect(BERChoiceField, "020163") - assert as_int.c.val == 99 - - as_str = _dissect(BERChoiceField, "040178") - assert as_str.c.val == b"x" - - -def check_ber_record_dissect(): - # type: () -> None - decoded = _dissect( - BERRecord, - "301a02012a01010104026869" - "a003020107" - "3009020101020102020103", - ) - _assert_record(decoded) - - empty = _dissect(BERRecord, "300a02010101010004003000") - _assert_record_empty(empty) - - -def check_oer_field_dissect(): - # type: () -> None - tagged = _dissect(OERTaggedInteger, "a10105") - assert tagged.n.val == 5 - - fixed = _dissect(OERFixedFields, "c8414243") - assert fixed.n.val == 200 - assert fixed.s.val == b"ABC" - - present = _dissect(OEROptionalField, "0101a00107") - assert present.id.val == 1 - assert present.extra.val == 7 - - absent = _dissect(OEROptionalField, "0101") - assert absent.id.val == 1 - assert absent.extra is None - - seqof = _dissect(OERSequenceOfIntegers, "0103010101020103") - assert [x.val for x in seqof.values] == [1, 2, 3] - - as_int = _dissect(OERChoiceField, "020163") - assert as_int.c.val == 99 - - as_str = _dissect(OERChoiceField, "040178") - assert as_str.c.val == b"x" - - -def check_oer_record_dissect(): - # type: () -> None - decoded = _dissect( - OERRecord, - "012aff026869a00107" - "0103010101020103", - ) - _assert_record(decoded) - - empty = _dissect(OERRecord, "010100000100") - _assert_record_empty(empty) - - -def check_per_field_dissect(): - # type: () -> None - fixed = _dissect(UPERFixedFields, "c8414243") - assert fixed.n.val == 200 - assert fixed.s.val == b"ABC" - - present = _dissect(UPEROptionalField, "80954041c0") - assert present.id.val == 42 - assert present.flag.val == 1 - assert present.extra.val == 7 - - absent = _dissect(UPEROptionalField, "009540") - assert absent.id.val == 42 - assert absent.flag.val == 1 - assert absent.extra is None - - seqof = _dissect(UPERSequenceOfIntegers, "03010101020103") - assert [x.val for x in seqof.values] == [1, 2, 3] - - empty_seqof = _dissect(UPERSequenceOfIntegers, "00") - assert [x.val for x in empty_seqof.values] == [] - - as_int = _dissect(UPERChoiceField, "00b180") - assert as_int.c.val == 99 - - as_str = _dissect(UPERChoiceField, "8120a100") - assert as_str.c.val == b"AB" - - -def check_per_record_dissect(): - # type: () -> None - decoded = _dissect( - UPERRecord, - "8095409a1a4041c0c04040408040c0", - ) - _assert_record(decoded) - - partial = _dissect(UPERRecord, "0095409050808040404080") - assert partial.id.val == 42 - assert partial.flag.val == 1 - assert partial.label.val == b"AB" - assert partial.extra is None - assert [x.val for x in partial.values] == [1, 2] - - empty = _dissect(UPERRecord, "0080800000") - _assert_record_empty(empty) - - -def check_per_default_field_dissect(): - # type: () -> None - class UPERDefaultRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), - ASN1F_DEFAULT( - ASN1F_INTEGER( - "count", 600, - uper_min=0, uper_max=86401, oer_unsigned=True, - ), - 600, - ), - ) - - absent = _dissect(UPERDefaultRecord, "0080") - assert absent.id.val == 1 - assert _asn1_int(absent.count) == 600 - - present = _dissect(UPERDefaultRecord, "80d46000") - assert present.id.val == 1 - assert _asn1_int(present.count) == 86400 - - -def check_per_extensible_integer_dissect(): - # type: () -> None - class UPERExtInt(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER( - "n", 0, - uper_min=1, uper_max=65535, - uper_extensible=True, oer_unsigned=True, - ), - ) - - in_range = _dissect(UPERExtInt, "001480") - assert in_range.n.val == 42 - - out_of_range = _dissect(UPERExtInt, "8232dd587c80") - assert out_of_range.n.val == 1706733817 - - -def check_per_constrained_sequence_of_dissect(): - # type: () -> None - class UPERConstrainedSeqOf(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE_OF( - "items", [], - ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7), - uper_min=1, uper_max=3, - ) - - decoded = _dissect(UPERConstrainedSeqOf, "4a") - assert [x.val for x in decoded.items] == [1, 2] - - -def check_ber_oer_per_record_dissect(): - # type: () -> None - for cls, data_hex in [ - ( - BERRecord, - "301a02012a01010104026869" - "a003020107" - "3009020101020102020103", - ), - ( - OERRecord, - "012aff026869a00107" - "0103010101020103", - ), - ( - UPERRecord, - "8095409a1a4041c0c04040408040c0", - ), - ]: - _assert_record(_dissect(cls, data_hex)) diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index 896f2ec746a..8087b383ec0 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -513,3 +513,129 @@ class ExtraPkt(ASN1_Packet): # BER enc swallows unknown kwargs; round-trip still works. assert raw(ExtraPkt(n=7)) == b"\x02\x01\x07" ExtraPkt(raw(ExtraPkt(n=7))).n.val == 7 + ++ ASN.1 BER build and dissect extras + += import helpers +from scapy.packet import raw + += prepare helpers +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + +def _record_kwargs(): + # type: () -> dict + return dict( + id=42, + flag=True, + label=b"hi", + extra=7, + values=[1, 2, 3], + ) + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + +def _assert_record(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + +def _assert_record_empty(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] + +def _dissect(cls, data_hex): + # type: (Type[ASN1_Packet], str) -> ASN1_Packet + return cls(bytes.fromhex(data_hex)) + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + += ber record build roundtrip +pkt = BERRecord(**_record_kwargs()) + +assert len(raw(pkt)) > 0 + +decoded = _roundtrip(BERRecord, pkt) + +assert decoded.id.val == 42 + +assert decoded.flag.val == 1 + +assert decoded.label.val == b"hi" + +assert decoded.extra.val == 7 + +assert [x.val for x in decoded.values] == [1, 2, 3] + +True + += ber field dissect +tagged = _dissect(BERTaggedInteger, "a103020105") + +assert tagged.n.val == 5 + +fixed = _dissect(BERFixedFields, "300d02810200c80483000003414243") + +assert fixed.n.val == 200 + +assert fixed.s.val == b"ABC" + +present = _dissect(BEROptionalField, "3008020101a003020107") + +assert present.id.val == 1 + +assert present.extra.val == 7 + +absent = _dissect(BEROptionalField, "3003020101") + +assert absent.id.val == 1 + +assert absent.extra is None + +seqof = _dissect(BERSequenceOfIntegers, "3009020101020102020103") + +assert [x.val for x in seqof.values] == [1, 2, 3] + +as_int = _dissect(BERChoiceField, "020163") + +assert as_int.c.val == 99 + +as_str = _dissect(BERChoiceField, "040178") + +assert as_str.c.val == b"x" + +True + += ber record dissect +decoded = _dissect( + BERRecord, + "301a02012a01010104026869" + "a003020107" + "3009020101020102020103", + +) + +_assert_record(decoded) + +empty = _dissect(BERRecord, "300a02010101010004003000") + +_assert_record_empty(empty) + +True + diff --git a/test/scapy/layers/ber_codec.py b/test/scapy/layers/ber_codec.py deleted file mode 100644 index e6939f7a27e..00000000000 --- a/test/scapy/layers/ber_codec.py +++ /dev/null @@ -1,275 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -BER codec and helper coverage tests. -""" - -from typing import Any - - -def _raises(exc, func): - # type: (type, Any) -> None - try: - func() - except exc: - return - raise AssertionError("Expected %s" % exc.__name__) - - -from scapy.asn1.asn1 import ( - ASN1_Class_UNIVERSAL, - ASN1_DECODING_ERROR, - ASN1_INTEGER, - ASN1_Object, -) -from scapy.asn1.ber import ( - BER_BadTag_Decoding_Error, - BER_Decoding_Error, - BER_Encoding_Error, - BER_Exception, - BER_id_dec, - BER_id_enc, - BER_len_dec, - BER_len_enc, - BER_num_dec, - BER_num_enc, - BER_tagging_dec, - BER_tagging_enc, - BERcodec_BIT_STRING, - BERcodec_INTEGER, - BERcodec_IPADDRESS, - BERcodec_NULL, - BERcodec_Object, - BERcodec_OID, - BERcodec_SEQUENCE, - BERcodec_SET, - BERcodec_STRING, -) -from scapy.config import conf - - -def check_ber_error_str(): - # type: () -> None - obj = ASN1_INTEGER(1) - enc_err = BER_Encoding_Error("enc", encoded=obj, remaining=b"rest") - assert "Already encoded" in str(enc_err) - enc_err2 = BER_Encoding_Error("enc", encoded="raw", remaining=b"") - assert "raw" in str(enc_err2) - - dec_err = BER_Decoding_Error("dec", decoded=obj, remaining=b"tail") - assert "Already decoded" in str(dec_err) - dec_err2 = BER_Decoding_Error("dec", decoded=[1], remaining=b"") - assert "[1]" in str(dec_err2) - - -def check_ber_len_enc_dec(): - # type: () -> None - for value in [0, 1, 127, 128, 999]: - encoded = BER_len_enc(value) - length, remain = BER_len_dec(encoded) - assert length == value - assert remain == b"" - - assert BER_len_enc(45, size=None) == BER_len_enc(45, size=0) - assert BER_len_enc(45, size=4) == b"\x84\x00\x00\x00-" - - _raises(BER_Exception, lambda: BER_len_enc(0, size=128)) - - _raises(BER_Decoding_Error, lambda: BER_len_dec(b"\x82")) - - -def check_ber_num_enc_dec(): - # type: () -> None - for value in [0, 1, 127, 256, 16384]: - encoded = BER_num_enc(value) - decoded, remain = BER_num_dec(encoded) - assert decoded == value - assert remain == b"" - - _raises(BER_Decoding_Error, lambda: BER_num_dec(b"")) - - _raises(BER_Decoding_Error, lambda: BER_num_dec(b"\x80\x80")) - - -def check_ber_id_enc_dec(): - # type: () -> None - for tag in [0x02, 0x30, 0x81, 0xA0]: - encoded = BER_id_enc(tag) - decoded, remain = BER_id_dec(encoded) - assert decoded == tag - assert remain == b"" - - high_tag = (0x03 << 5) + 0x22 - encoded = BER_id_enc(high_tag) - decoded, remain = BER_id_dec(encoded) - assert decoded == high_tag - assert remain == b"" - - -def check_ber_tagging(): - # type: () -> None - inner = BERcodec_INTEGER.enc(7) - implicit = BER_tagging_enc(inner, implicit_tag=0xA0) - assert implicit.startswith(b"\xa0") - real_tag, payload = BER_tagging_dec( - implicit, - hidden_tag=ASN1_Class_UNIVERSAL.INTEGER, - implicit_tag=0xA0, - ) - assert real_tag is None - assert payload[0] == int(ASN1_Class_UNIVERSAL.INTEGER) - - conf.ASN1_default_long_size = 4 - try: - explicit = BER_tagging_enc(inner, explicit_tag=0xA1) - assert explicit.startswith(b"\xa1\x84") - real_tag, payload = BER_tagging_dec( - explicit, - explicit_tag=0xA1, - ) - assert real_tag is None - assert payload == inner - finally: - conf.ASN1_default_long_size = 0 - - _raises(BER_Decoding_Error, lambda: BER_tagging_dec( - implicit, - hidden_tag=ASN1_Class_UNIVERSAL.INTEGER, - implicit_tag=0xA1, - )) - - safe_tag, _ = BER_tagging_dec( - implicit, - hidden_tag=ASN1_Class_UNIVERSAL.INTEGER, - implicit_tag=0xA1, - safe=True, - ) - assert safe_tag == 0xA0 - - -def check_ber_integer(): - # type: () -> None - for value in [0, 1, 127, 128, 255, -1, -128, -129]: - encoded = BERcodec_INTEGER.enc(value) - obj, remain = BERcodec_INTEGER.do_dec(encoded) - assert obj.val == value - assert remain == b"" - - _raises(BER_BadTag_Decoding_Error, lambda: BERcodec_INTEGER.do_dec(BERcodec_STRING.enc(b"x"))) - - _raises(BER_Decoding_Error, lambda: BERcodec_INTEGER.check_type_get_len(b"\x02")) - - -def check_ber_bit_string(): - # type: () -> None - encoded = BERcodec_BIT_STRING.enc("1011") - obj, remain = BERcodec_BIT_STRING.do_dec(encoded) - assert obj.val == "1011" - assert remain == b"" - - padded = BERcodec_BIT_STRING.enc("10110000") - obj2, _ = BERcodec_BIT_STRING.do_dec(padded) - assert obj2.val == "10110000" - - _raises(BER_Decoding_Error, lambda: BERcodec_BIT_STRING.do_dec(b"\x03\x01\x08", safe=True)) - - _raises(BER_Decoding_Error, lambda: BERcodec_BIT_STRING.do_dec(b"\x03\x00")) - - -def check_ber_string_and_null(): - # type: () -> None - encoded = BERcodec_STRING.enc(b"hello") - obj, remain = BERcodec_STRING.do_dec(encoded) - assert obj.val == b"hello" - assert remain == b"" - - null = BERcodec_NULL.enc(0) - assert null == b"\x05\x00" - obj, remain = BERcodec_NULL.do_dec(null) - assert obj.val == 0 - - non_null = BERcodec_NULL.enc(42) - obj, remain = BERcodec_NULL.do_dec(non_null) - assert obj.val == 42 - - -def check_ber_oid(): - # type: () -> None - encoded = BERcodec_OID.enc("1.2.840.113556.1.4.529") - obj, remain = BERcodec_OID.do_dec(encoded) - assert obj.val == "1.2.840.113556.1.4.529" - assert remain == b"" - - empty, remain = BERcodec_OID.do_dec(BERcodec_OID.enc("")) - assert empty.val == "" - assert remain == b"" - - -def check_ber_sequence_and_set(): - # type: () -> None - payload = BERcodec_INTEGER.enc(1) + BERcodec_INTEGER.enc(2) - seq = BERcodec_SEQUENCE.enc(payload) - obj, remain = BERcodec_SEQUENCE.do_dec(seq) - assert len(obj.val) == 2 - assert obj.val[0].val == 1 - assert obj.val[1].val == 2 - assert remain == b"" - - as_list = BERcodec_SEQUENCE.enc([ASN1_INTEGER(3), ASN1_INTEGER(4)]) - obj2, remain2 = BERcodec_SEQUENCE.do_dec(as_list) - assert [x.val for x in obj2.val] == [3, 4] - assert remain2 == b"" - - st = BERcodec_SET.enc(payload) - obj3, remain3 = BERcodec_SET.do_dec(st) - assert len(obj3.val) == 2 - assert remain3 == b"" - - conf.ASN1_default_long_size = 4 - try: - long_seq = BERcodec_SEQUENCE.enc(payload) - assert long_seq.startswith(b"0\x84") - finally: - conf.ASN1_default_long_size = 0 - - _raises(BER_Decoding_Error, lambda: BERcodec_SEQUENCE.do_dec(b"\x30\x05" + BERcodec_INTEGER.enc(1))) - - -def check_ber_ipaddress(): - # type: () -> None - encoded = BERcodec_IPADDRESS.enc("192.168.0.1") - obj, remain = BERcodec_IPADDRESS.do_dec(encoded) - assert obj.val == "192.168.0.1" - assert remain == b"" - - _raises(BER_Encoding_Error, lambda: BERcodec_IPADDRESS.enc("not-an-ip")) - - _raises(BER_Decoding_Error, lambda: BERcodec_IPADDRESS.do_dec(BERcodec_STRING.enc(b"bad"))) - - -def check_ber_object_dispatch(): - # type: () -> None - encoded = BERcodec_INTEGER.enc(99) - obj, remain = BERcodec_Object.do_dec(encoded) - assert obj.val == 99 - assert remain == b"" - - _raises(BER_Decoding_Error, lambda: BERcodec_Object.check_string(b"")) - - _raises(BER_Decoding_Error, lambda: BERcodec_Object.do_dec(b"\xff\x00")) - - bad, remain = BERcodec_Object.safedec(b"\x02\x01\x01") - assert isinstance(bad, ASN1_INTEGER) - assert bad.val == 1 - - unknown, remain = BERcodec_Object.safedec(b"\xff\x00") - assert isinstance(unknown, ASN1_DECODING_ERROR) - - truncated, remain = BERcodec_Object.dec(b"\x02\x05\x01", safe=True) - assert isinstance(truncated, ASN1_DECODING_ERROR) - assert remain == b"" - - _raises(TypeError, lambda: BERcodec_Object.enc(object())) - assert BERcodec_Object.enc("42") == BERcodec_STRING.enc("42") diff --git a/test/scapy/layers/ber_packets.py b/test/scapy/layers/ber_packets.py deleted file mode 100644 index 09cb02fe6f1..00000000000 --- a/test/scapy/layers/ber_packets.py +++ /dev/null @@ -1,184 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -BER ASN1_Packet and ASN1F_field build tests. -""" - -from scapy.asn1.asn1 import ASN1_Codecs, ASN1_INTEGER, ASN1_STRING -from scapy.asn1fields import ( - ASN1F_BOOLEAN, - ASN1F_CHOICE, - ASN1F_INTEGER, - ASN1F_SEQUENCE, - ASN1F_SEQUENCE_OF, - ASN1F_STRING, - ASN1F_optional, -) -from scapy.asn1packet import ASN1_Packet -from scapy.packet import raw - - -class BERTaggedInteger(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_INTEGER("n", 0, explicit_tag=0xA1) - - -class BERFixedFields(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("n", 0, size_len=1), - ASN1F_STRING("s", "", size_len=3), - ) - - -class BEROptionalField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), - ) - - -class BERSequenceOfIntegers(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) - - -class BERChoiceField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - - -class BERRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_BOOLEAN("flag", False), - ASN1F_STRING("label", ""), - ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), - ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), - ) - - -class BEROptionalSequence(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("hdr", 0), - ASN1F_optional(ASN1F_SEQUENCE( - ASN1F_INTEGER("id", None), - ASN1F_STRING("label", None), - explicit_tag=0xA0, - )), - ) - - -def _roundtrip(cls, pkt): - # type: (type, ASN1_Packet) -> ASN1_Packet - return cls(raw(pkt)) - - -def check_ber_field_explicit_tag(): - # type: () -> None - pkt = BERTaggedInteger(n=5) - assert raw(pkt) == b"\xa1\x03\x02\x01\x05" - decoded = _roundtrip(BERTaggedInteger, pkt) - assert decoded.n.val == 5 - - -def check_ber_field_fixed_size(): - # type: () -> None - pkt = BERFixedFields(n=200, s=b"ABC") - assert raw(pkt) == bytes.fromhex("300d02810200c80483000003414243") - decoded = _roundtrip(BERFixedFields, pkt) - assert decoded.n.val == 200 - assert decoded.s.val == b"ABC" - - -def check_ber_field_optional(): - # type: () -> None - present = BEROptionalField(id=1, extra=7) - assert raw(present) == bytes.fromhex("3008020101a003020107") - decoded = _roundtrip(BEROptionalField, present) - assert decoded.id.val == 1 - assert decoded.extra.val == 7 - - absent = BEROptionalField(id=1, extra=None) - assert raw(absent) == bytes.fromhex("3003020101") - decoded = _roundtrip(BEROptionalField, absent) - assert decoded.id.val == 1 - assert decoded.extra is None - - -def check_ber_optional_sequence_is_empty(): - # type: () -> None - """Optional ASN1F_SEQUENCE must use the wrapped field's is_empty(). - - SEQUENCE stores children under their own names (not dummy_seq_name), so - inspecting pkt.dummy_seq_name incorrectly reports present children as empty - and makes the parent SEQUENCE look empty. - """ - opt = BEROptionalSequence.ASN1_root.seq[1] - - present = BEROptionalSequence(hdr=1, id=42, label=b"abc") - assert opt._field.is_empty(present) is False - assert opt.is_empty(present) is False - assert BEROptionalSequence.ASN1_root.is_empty(present) is False - assert raw(present) == bytes.fromhex("300f020101a00a300802012a0403616263") - - absent = BEROptionalSequence(hdr=1, id=None, label=None) - assert opt._field.is_empty(absent) is True - assert opt.is_empty(absent) is True - assert raw(absent) == bytes.fromhex("3003020101") - - -def check_ber_field_sequence_of(): - # type: () -> None - pkt = BERSequenceOfIntegers(values=[1, 2, 3]) - assert raw(pkt) == b"\x30\x09\x02\x01\x01\x02\x01\x02\x02\x01\x03" - decoded = _roundtrip(BERSequenceOfIntegers, pkt) - assert [x.val for x in decoded.values] == [1, 2, 3] - - -def check_ber_field_choice(): - # type: () -> None - as_int = BERChoiceField(c=ASN1_INTEGER(99)) - assert raw(as_int) == b"\x02\x01c" - decoded = _roundtrip(BERChoiceField, as_int) - assert decoded.c.val == 99 - - as_str = BERChoiceField(c=ASN1_STRING("x")) - assert raw(as_str) == b"\x04\x01x" - decoded = _roundtrip(BERChoiceField, as_str) - assert decoded.c.val == b"x" - - -def check_ber_packet_record(): - # type: () -> None - pkt = BERRecord( - id=42, flag=True, label="hi", extra=7, values=[1, 2, 3], - ) - expected = bytes.fromhex( - "301a02012a01010104026869" - "a003020107" - "3009020101020102020103" - ) - assert raw(pkt) == expected - decoded = _roundtrip(BERRecord, pkt) - assert decoded.id.val == 42 - assert decoded.flag.val == 1 - assert decoded.label.val == b"hi" - assert decoded.extra.val == 7 - assert [x.val for x in decoded.values] == [1, 2, 3] - - empty = BERRecord(id=1, flag=False, label="", extra=None, values=[]) - assert raw(empty) == bytes.fromhex("300a02010101010004003000") - decoded = _roundtrip(BERRecord, empty) - assert decoded.id.val == 1 - assert decoded.flag.val == 0 - assert decoded.label.val == b"" - assert decoded.extra is None - assert [x.val for x in decoded.values] == [] diff --git a/test/scapy/layers/oer.uts b/test/scapy/layers/oer.uts new file mode 100644 index 00000000000..fe690a05ccd --- /dev/null +++ b/test/scapy/layers/oer.uts @@ -0,0 +1,862 @@ +% Tests for ASN.1 OER encoding + +# +# Try me with: +# bash test/run_tests -t test/scapy/layers/oer.uts -F + ++ ASN.1 OER load += import contrib codecs +import scapy.contrib.oer +from scapy.contrib.oer import * +from scapy.packet import raw + + ++ ASN.1 OER codec += OER length determinant short form +OER_len_enc(3) == b"\x03" += OER length determinant long form +OER_len_enc(200) == b"\x81\xc8" += OER boolean false +OERcodec_BOOLEAN.enc(0) == b"\x00" += OER boolean true +OERcodec_BOOLEAN.enc(1) == b"\xff" += OER null +OERcodec_NULL.enc(None) == b"" += OER unconstrained integer +OERcodec_INTEGER.enc(4) == b"\x01\x04" += OER constrained unsigned integer +OERcodec_INTEGER.enc(4, size_len=1) == b"\x04" += OER constrained signed integer +OERcodec_INTEGER.enc(4, size_len=2) == b"\x00\x04" += OER enumerated short form +OERcodec_ENUMERATED.enc(6) == b"\x06" += OER octet string +OERcodec_STRING.enc(b"ABC") == b"\x03ABC" += OER OID +OERcodec_OID.enc("1.2.3") == b"\x02\x2a\x03" += OER integer roundtrip +x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(12345)) +x.val == 12345 and r == b"" += OER boolean roundtrip +x, r = OERcodec_BOOLEAN.do_dec(OERcodec_BOOLEAN.enc(1)) +x.val == 1 and r == b"" += OER ASN1 object encoding +ASN1_INTEGER(42).enc(ASN1_Codecs.OER) == b"\x01*" += OER codec registration +ASN1_Class_UNIVERSAL.INTEGER.get_codec(ASN1_Codecs.OER) is OERcodec_INTEGER + ++ ASN.1 OER codec (extended) += OER length zero +OER_len_enc(0) == b"\x00" += OER length boundary short form +OER_len_enc(127) == b"\x7f" += OER length boundary long form +OER_len_enc(128) == b"\x81\x80" += OER length roundtrip +l, r = OER_len_dec(OER_len_enc(999)) +l == 999 and r == b"" += OER signed integer zero +OER_signed_integer_enc(0) == b"\x01\x00" += OER signed integer negative +OER_signed_integer_enc(-255) == b"\x02\xff\x01" += OER signed integer large +OER_signed_integer_enc(100000) == b"\x03\x01\x86\xa0" += OER signed integer roundtrip +v, r = OER_signed_integer_dec(OER_signed_integer_enc(-1234567)) +v == -1234567 and r == b"" += OER unsigned integer zero +OER_unsigned_integer_enc(0) == b"\x01\x00" += OER unsigned integer roundtrip +v, r = OER_unsigned_integer_dec(OER_unsigned_integer_enc(65535)) +v == 65535 and r == b"" += OER fixed unsigned 1 byte +OERcodec_INTEGER.enc(255, size_len=1) == b"\xff" += OER fixed signed 2 bytes negative +OERcodec_INTEGER.enc(-2, size_len=2) == b"\xff\xfe" += OER fixed signed 4 bytes +OERcodec_INTEGER.enc(-2, size_len=4) == b"\xff\xff\xff\xfe" += OER enumerated long form +OERcodec_ENUMERATED.enc(128) == b"\x82\x00\x80" += OER enumerated negative +OERcodec_ENUMERATED.enc(-1) == b"\x81\xff" += OER enumerated roundtrip +x, r = OERcodec_ENUMERATED.do_dec(OERcodec_ENUMERATED.enc(128)) +x.val == 128 and r == b"" += OER null roundtrip +x, r = OERcodec_NULL.do_dec(OERcodec_NULL.enc(None)) +x.val is None and r == b"" += OER octet string empty +OERcodec_STRING.enc(b"") == b"\x00" += OER octet string fixed size +OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3) == b"\x12\x34\x56" += OER octet string roundtrip +x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34")) +x.val == b"\x12\x34" and r == b"" += OER OID 1.2 +OERcodec_OID.enc("1.2") == b"\x01\x2a" += OER OID roundtrip +x, r = OERcodec_OID.do_dec(OERcodec_OID.enc("1.2.3321")) +x.val == "1.2.3321" and r == b"" += OER bit string variable size +OERcodec_BIT_STRING.enc("0100") == b"\x02\x04\x40" += OER bit string roundtrip +x, r = OERcodec_BIT_STRING.do_dec(OERcodec_BIT_STRING.enc("01000001")) +x.val == "01000001" and r == b"" += OER IA5 string +OERcodec_IA5_STRING.enc(b"ABC") == b"\x03ABC" += OER tag short form +OER_tag_enc(1, OER_CLASS_CONTEXT) == b"\x81" += OER tag roundtrip +cls, num, r = OER_tag_dec(OER_tag_enc(1, OER_CLASS_CONTEXT)) +cls == OER_CLASS_CONTEXT and num == 1 and r == b"" += OER sequence concat +OERcodec_SEQUENCE.enc([ASN1_INTEGER(4), ASN1_INTEGER(5)]) == b"\x01\x04\x01\x05" += OER ASN1 boolean object +ASN1_BOOLEAN(1).enc(ASN1_Codecs.OER) == b"\xff" += OER ASN1 null object +ASN1_NULL(None).enc(ASN1_Codecs.OER) == b"" + ++ ASN.1 OER review fixes += OER fixed integer decode roundtrip +x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(128, size_len=1), size_len=1, oer_unsigned=True) +x.val == 128 and r == b"" += OER fixed integer signed decode +x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(-2, size_len=2), size_len=2) +x.val == -2 and r == b"" += OER fixed octet string decode +x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3), size_len=3) +x.val == b"\x12\x34\x56" and r == b"" += OER explicit null tagging +OER_tagging_enc(OERcodec_NULL.enc(None), explicit_tag=0x81) == b"\x81" += OER choice id decode +tag, r = OER_id_dec(b"\x81\x01") +tag == 0x81 and r == b"\x01" + ++ ASN.1 OER packets, interop and fuzz += import contrib codecs +import scapy.contrib.oer +from scapy.contrib.oer import * +from scapy.packet import raw += prepare helpers and packet classes +class OERTaggedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0, explicit_tag=0xA1) + +class OERFixedFields(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_STRING("s", "", size_len=3), + ) + +class OEROptionalField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ) + +class OERSequenceOfIntegers(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + +class OERChoiceField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +class OERRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + +class OERNestedSequence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ) + +class OERNestedSequenceTrailing(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ASN1F_INTEGER("id", 0), + ) + +class OERSequenceOfWithTrailing(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ASN1F_INTEGER("id", 0), + ) + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + +INTEGER_VECTORS = [ + ("A", 0, lambda v: OERcodec_INTEGER.enc(v), b"\x01\x00"), + ("A", 128, lambda v: OERcodec_INTEGER.enc(v), b"\x02\x00\x80"), + ("A", 100000, lambda v: OERcodec_INTEGER.enc(v), b"\x03\x01\x86\xa0"), + ("A", -255, lambda v: OERcodec_INTEGER.enc(v), b"\x02\xff\x01"), + ("A", -1234567, lambda v: OERcodec_INTEGER.enc(v), b"\x03\xed)y"), + ("B", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=1), b"\xfe"), + ("C", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=2), b"\xff\xfe"), + ("D", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=4), b"\xff\xff\xff\xfe"), + ( + "E", + -2, + lambda v: OERcodec_INTEGER.enc(v, size_len=8), + b"\xff\xff\xff\xff\xff\xff\xff\xfe", + ), + ("F", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=1), b"\x80"), + ("G", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=2), b"\x00\x80"), + ("G", 1000, lambda v: OERcodec_INTEGER.enc(v, size_len=2), b"\x03\xe8"), + ("H", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=4), b"\x00\x00\x00\x80"), + ( + "I", + 128, + lambda v: OERcodec_INTEGER.enc(v, size_len=8), + b"\x00\x00\x00\x00\x00\x00\x00\x80", + ), + ("B", 1, lambda v: OERcodec_INTEGER.enc(v, size_len=1), b"\x01"), + ("K", 1, lambda v: OER_unsigned_integer_enc(v), b"\x01\x01"), + ("K", 128, lambda v: OER_unsigned_integer_enc(v), b"\x01\x80"), + ("L", -128, lambda v: OER_signed_integer_enc(v), b"\x01\x80"), +] + +BOOLEAN_VECTORS = [ + (True, lambda v: OERcodec_BOOLEAN.enc(1 if v else 0), b"\xff"), + (False, lambda v: OERcodec_BOOLEAN.enc(1 if v else 0), b"\x00"), +] + +ENUMERATED_VECTORS = [ + ("A", "a", 1, b"\x01"), + ("B", "a", 128, b"\x82\x00\x80"), + ("C", "a", 0, b"\x00"), + ("C", "b", 127, b"\x7f"), + ("E", "a", -1, b"\x81\xff"), +] + +OID_VECTORS = [ + ("1.2", lambda v: OERcodec_OID.enc(v), b"\x01*"), + ("1.2.3321", lambda v: OERcodec_OID.enc(v), b"\x03*\x99y"), +] + +OCTET_STRING_VECTORS = [ + (b"\x12\x34", 0, b"\x02\x124"), + (b"\x12\x34\x56", 3, b"\x124V"), +] + +BIT_STRING_VECTORS = [ + ("0100", b"\x02\x04@"), + ("01000001", b"\x02\x00A"), +] + +SCAPY_DECODE_VECTORS = [ + ("A", 42, b"\x01*"), + ("F", 200, b"\xc8"), + ("B", -99, b"\x9d"), +] + +_OER_CODEC_CLASSES = ( + OERcodec_INTEGER, + OERcodec_BOOLEAN, + OERcodec_NULL, + OERcodec_STRING, + OERcodec_OID, + OERcodec_ENUMERATED, + OERcodec_BIT_STRING, +) + +_DECODE_ERRORS = ( + OER_Decoding_Error, + ASN1_Decoding_Error, + ASN1_Error, + ValueError, + IndexError, +) + +class OERFuzzRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + +def _fuzz_packets(): + # type: () -> Iterable[Type[ASN1_Packet]] + return (OERFuzzRecord,) += oer field explicit tag +pkt = OERTaggedInteger(n=5) + +assert raw(pkt) == b"\xa1\x01\x05" + +decoded = _roundtrip(OERTaggedInteger, pkt) + +assert decoded.n.val == 5 + +True + += oer field fixed size +pkt = OERFixedFields(n=200, s=b"ABC") + +assert raw(pkt) == b"\xc8ABC" + +decoded = _roundtrip(OERFixedFields, pkt) + +assert decoded.n.val == 200 + +assert decoded.s.val == b"ABC" + +True + += oer field optional +present = OEROptionalField(id=1, extra=7) + +assert raw(present) == b"\x01\x01\xa0\x01\x07" + +decoded = _roundtrip(OEROptionalField, present) + +assert decoded.id.val == 1 + +assert decoded.extra.val == 7 + +absent = OEROptionalField(id=1, extra=None) + +assert raw(absent) == b"\x01\x01" + +decoded = _roundtrip(OEROptionalField, absent) + +assert decoded.id.val == 1 + +assert decoded.extra is None + +True + += oer field sequence of +pkt = OERSequenceOfIntegers(values=[1, 2, 3]) + +assert raw(pkt) == b"\x01\x03\x01\x01\x01\x02\x01\x03" + +decoded = _roundtrip(OERSequenceOfIntegers, pkt) + +assert [x.val for x in decoded.values] == [1, 2, 3] + +True + += oer field choice +as_int = OERChoiceField(c=ASN1_INTEGER(99)) + +assert raw(as_int) == b"\x02\x01c" + +decoded = _roundtrip(OERChoiceField, as_int) + +assert decoded.c.val == 99 + +as_str = OERChoiceField(c=ASN1_STRING("x")) + +assert raw(as_str) == b"\x04\x01x" + +decoded = _roundtrip(OERChoiceField, as_str) + +assert decoded.c.val == b"x" + +True + += oer packet record +pkt = OERRecord( + id=42, flag=True, label="hi", extra=7, values=[1, 2, 3], +) + +expected = ( + b"\x01*\xff\x02hi\xa0\x01\x07" + b"\x01\x03\x01\x01\x01\x02\x01\x03" +) + +assert raw(pkt) == expected + +decoded = _roundtrip(OERRecord, pkt) + +assert decoded.id.val == 42 + +assert decoded.flag.val == 1 + +assert decoded.label.val == b"hi" + +assert decoded.extra.val == 7 + +assert [x.val for x in decoded.values] == [1, 2, 3] + +empty = OERRecord(id=1, flag=False, label="", extra=None, values=[]) + +assert raw(empty) == b"\x01\x01\x00\x00\x01\x00" + +decoded = _roundtrip(OERRecord, empty) + +assert decoded.id.val == 1 + +assert decoded.flag.val == 0 + +assert decoded.label.val == b"" + +assert decoded.extra is None + +assert [x.val for x in decoded.values] == [] + +True + += oer nested sequence +pkt = OERNestedSequence(id=5, x=3, y=True) + +assert raw(pkt) == b"\x01\x05\x01\x03\xff" + +decoded = _roundtrip(OERNestedSequence, pkt) + +assert decoded.id.val == 5 + +assert decoded.x.val == 3 + +assert decoded.y.val == 1 + +True + += oer nested sequence trailing +pkt = OERNestedSequenceTrailing(x=3, y=True, id=5) + +assert raw(pkt) == b"\x01\x03\xff\x01\x05" + +decoded = _roundtrip(OERNestedSequenceTrailing, pkt) + +assert decoded.x.val == 3 + +assert decoded.y.val == 1 + +assert decoded.id.val == 5 + +True + += oer sequence of with trailing +pkt = OERSequenceOfWithTrailing(values=[1, 2], id=7) + +assert raw(pkt) == b"\x01\x02\x01\x01\x01\x02\x01\x07" + +decoded = _roundtrip(OERSequenceOfWithTrailing, pkt) + +assert [x.val for x in decoded.values] == [1, 2] + +assert decoded.id.val == 7 + +True + += primitive interop +for type_name, value, enc, expected in INTEGER_VECTORS: + got = enc(value) + assert got == expected, ( + "integer %s=%r: reference=%r scapy=%r" % + (type_name, value, expected, got) + ) + if type_name == "A": + dec, remain = OERcodec_INTEGER.do_dec(got) + assert remain == b"" and dec.val == value + +for value, enc, expected in BOOLEAN_VECTORS: + got = enc(value) + assert got == expected + dec, remain = OERcodec_BOOLEAN.do_dec(got) + assert remain == b"" and dec.val == (1 if value else 0) + +got = OERcodec_NULL.enc(None) + +assert got == b"" + +for type_name, _enum_name, enum_val, expected in ENUMERATED_VECTORS: + got = OERcodec_ENUMERATED.enc(enum_val) + assert got == expected + dec, remain = OERcodec_ENUMERATED.do_dec(got) + assert remain == b"" and dec.val == enum_val + +for oid, enc, expected in OID_VECTORS: + got = enc(oid) + assert got == expected + dec, remain = OERcodec_OID.do_dec(got) + assert remain == b"" and dec.val == oid + +for data, fixed_size, expected in OCTET_STRING_VECTORS: + got = OERcodec_STRING.enc(data, size_len=fixed_size or 0) + assert got == expected + dec, remain = OERcodec_STRING.do_dec(got, size_len=fixed_size or 0) + assert remain == b"" and dec.val == data + +for bitstr, expected in BIT_STRING_VECTORS: + got = OERcodec_BIT_STRING.enc(bitstr) + assert got == expected + dec, remain = OERcodec_BIT_STRING.do_dec(got) + assert remain == b"" and dec.val == bitstr + +True + += scapy encode reference decode +for type_name, value, encoded in SCAPY_DECODE_VECTORS: + if type_name == "A": + dec, remain = OERcodec_INTEGER.do_dec(encoded) + elif type_name == "F": + dec, remain = OERcodec_INTEGER.do_dec( + encoded, size_len=1, oer_unsigned=True, + ) + else: + dec, remain = OERcodec_INTEGER.do_dec(encoded, size_len=1) + assert remain == b"" and dec.val == value + +for val in [0, 1]: + encoded = OERcodec_BOOLEAN.enc(val) + dec, remain = OERcodec_BOOLEAN.do_dec(encoded) + assert remain == b"" and dec.val == val + +True + += oer fuzz encode +iterations = 25 + +for cls in _fuzz_packets(): + for _ in range(iterations): + data = raw(fuzz(cls())) + assert isinstance(data, bytes) + +True + += oer fuzz roundtrip +iterations = 25 + +for cls in _fuzz_packets(): + for _ in range(iterations): + cls(raw(fuzz(cls()))) + +True + += oer fuzz codec decode +iterations = 100 + +for codec in _OER_CODEC_CLASSES: + for _ in range(iterations): + data = os.urandom(random.randint(0, 64)) + try: + codec.safedec(data) + except _DECODE_ERRORS: + pass + +True + += oer fuzz packet decode +iterations = 100 + +for cls in _fuzz_packets(): + for _ in range(iterations): + data = os.urandom(random.randint(0, 128)) + try: + cls(data) + except _DECODE_ERRORS: + pass + +True + ++ ASN.1 OER build and dissect += import contrib codecs +import scapy.contrib.oer +from scapy.contrib.oer import * +from scapy.packet import raw += prepare helpers and packet classes +class OERTaggedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0, explicit_tag=0xA1) + +class OERFixedFields(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_STRING("s", "", size_len=3), + ) + +class OEROptionalField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ) + +class OERSequenceOfIntegers(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + +class OERChoiceField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +class OERRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + +class OERNestedSequence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ) + +class OERNestedSequenceTrailing(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ASN1F_INTEGER("id", 0), + ) + +class OERSequenceOfWithTrailing(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ASN1F_INTEGER("id", 0), + ) + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + +def _record_kwargs(): + # type: () -> dict + return dict( + id=42, + flag=True, + label=b"hi", + extra=7, + values=[1, 2, 3], + ) + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + +def _assert_record(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + +def _assert_record_empty(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] + +def _dissect(cls, data_hex): + # type: (Type[ASN1_Packet], str) -> ASN1_Packet + return cls(bytes.fromhex(data_hex)) += oer record build roundtrip +pkt = OERRecord(**_record_kwargs()) + +assert len(raw(pkt)) > 0 + +decoded = _roundtrip(OERRecord, pkt) + +assert decoded.id.val == 42 + +assert decoded.flag.val == 1 + +assert decoded.label.val == b"hi" + +assert decoded.extra.val == 7 + +assert [x.val for x in decoded.values] == [1, 2, 3] + +True + += oer field dissect +tagged = _dissect(OERTaggedInteger, "a10105") + +assert tagged.n.val == 5 + +fixed = _dissect(OERFixedFields, "c8414243") + +assert fixed.n.val == 200 + +assert fixed.s.val == b"ABC" + +present = _dissect(OEROptionalField, "0101a00107") + +assert present.id.val == 1 + +assert present.extra.val == 7 + +absent = _dissect(OEROptionalField, "0101") + +assert absent.id.val == 1 + +assert absent.extra is None + +seqof = _dissect(OERSequenceOfIntegers, "0103010101020103") + +assert [x.val for x in seqof.values] == [1, 2, 3] + +as_int = _dissect(OERChoiceField, "020163") + +assert as_int.c.val == 99 + +as_str = _dissect(OERChoiceField, "040178") + +assert as_str.c.val == b"x" + +True + += oer record dissect +decoded = _dissect( + OERRecord, + "012aff026869a00107" + "0103010101020103", +) +_assert_record(decoded) +empty = _dissect(OERRecord, "010100000100") +_assert_record_empty(empty) + +True + + ++ ASN.1 OER coverage += import contrib codecs +import scapy.contrib.oer +from scapy.contrib.oer import * +from scapy.packet import raw += prepare helpers and packet classes +class _InnerRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("mode", ASN1_INTEGER(0), ["off", "on"]), + ) + +class _EncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_ENCAPS("payload", None, _InnerRecord), + ) + +class _FlagsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_FLAGS("f", "000", ["read", "write", "exec"]), + ) + +class _SetOfRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SET_OF("items", [], ASN1F_INTEGER) + +class _PacketFieldRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_PacketField("data", b""), + ) + +class _ExplicitPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2) + +class _BitEncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord), + ) + +def _raises(exc, func): + # type: (type, Any) -> None + try: + func() + except exc: + return + raise AssertionError("Expected %s" % exc.__name__) += oer error str +obj = ASN1_INTEGER(1) + +err = OER_Encoding_Error("enc", encoded=obj, remaining=b"z") + +assert "Already encoded" in str(err) + +err2 = OER_Decoding_Error("dec", decoded=obj, remaining=b"w") + +assert "Already decoded" in str(err2) + +True + += oer ipaddress and sequence +encoded = OERcodec_IPADDRESS.enc("127.0.0.1") + +obj, remain = OERcodec_IPADDRESS.do_dec(encoded) + +assert obj.val == "127.0.0.1" + +assert remain == b"" + +fixed = OERcodec_IPADDRESS.enc("127.0.0.1", size_len=4) + +obj2, remain2 = OERcodec_IPADDRESS.do_dec(fixed, size_len=4) + +assert obj2.val == "127.0.0.1" + +assert remain2 == b"" + +_raises(OER_Encoding_Error, lambda: OERcodec_IPADDRESS.enc("bad-ip")) + +_raises(OER_Decoding_Error, lambda: OERcodec_IPADDRESS.do_dec(b"\x01")) + +assert OERcodec_SEQUENCE.enc(b"payload") == b"payload" + +assert OERcodec_SET.enc(b"payload") == b"payload" + +_raises(OER_Decoding_Error, lambda: OERcodec_SEQUENCE.do_dec(b"\x00")) + +empty, remain = OERcodec_BIT_STRING.do_dec(OERcodec_BIT_STRING.enc("")) + +assert empty.val == "" + +assert remain == b"" + +True + diff --git a/test/scapy/layers/oer_fuzz.py b/test/scapy/layers/oer_fuzz.py deleted file mode 100644 index 920e51e8abd..00000000000 --- a/test/scapy/layers/oer_fuzz.py +++ /dev/null @@ -1,106 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -OER fuzzing helpers. - -Exercise OER encode/decode paths with packet.fuzz() and random payloads. -""" - -import os -import random -from typing import Iterable, Type - -from scapy.asn1.asn1 import ASN1_Codecs, ASN1_Decoding_Error, ASN1_Error -from scapy.contrib.oer import ( - OER_Decoding_Error, - OERcodec_BIT_STRING, - OERcodec_BOOLEAN, - OERcodec_ENUMERATED, - OERcodec_INTEGER, - OERcodec_NULL, - OERcodec_OID, - OERcodec_STRING, -) -from scapy.asn1fields import ( - ASN1F_BOOLEAN, - ASN1F_INTEGER, - ASN1F_SEQUENCE, - ASN1F_SEQUENCE_OF, - ASN1F_STRING, - ASN1F_optional, -) -from scapy.asn1packet import ASN1_Packet -from scapy.packet import fuzz, raw - -_OER_CODEC_CLASSES = ( - OERcodec_INTEGER, - OERcodec_BOOLEAN, - OERcodec_NULL, - OERcodec_STRING, - OERcodec_OID, - OERcodec_ENUMERATED, - OERcodec_BIT_STRING, -) - -_DECODE_ERRORS = ( - OER_Decoding_Error, - ASN1_Decoding_Error, - ASN1_Error, - ValueError, - IndexError, -) - - -class OERFuzzRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_BOOLEAN("flag", False), - ASN1F_STRING("label", ""), - ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), - ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), - ) - - -def _fuzz_packets(): - # type: () -> Iterable[Type[ASN1_Packet]] - return (OERFuzzRecord,) - - -def check_oer_fuzz_encode(iterations=25): - # type: (int) -> None - for cls in _fuzz_packets(): - for _ in range(iterations): - data = raw(fuzz(cls())) - assert isinstance(data, bytes) - - -def check_oer_fuzz_roundtrip(iterations=25): - # type: (int) -> None - for cls in _fuzz_packets(): - for _ in range(iterations): - cls(raw(fuzz(cls()))) - - -def check_oer_fuzz_codec_decode(iterations=100): - # type: (int) -> None - for codec in _OER_CODEC_CLASSES: - for _ in range(iterations): - data = os.urandom(random.randint(0, 64)) - try: - codec.safedec(data) - except _DECODE_ERRORS: - pass - - -def check_oer_fuzz_packet_decode(iterations=100): - # type: (int) -> None - for cls in _fuzz_packets(): - for _ in range(iterations): - data = os.urandom(random.randint(0, 128)) - try: - cls(data) - except _DECODE_ERRORS: - pass diff --git a/test/scapy/layers/oer_iop.py b/test/scapy/layers/oer_iop.py deleted file mode 100644 index baa68a5b534..00000000000 --- a/test/scapy/layers/oer_iop.py +++ /dev/null @@ -1,160 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -OER interoperability helpers. - -Cross-check Scapy's OER codec against reference encodings (from asn1tools). -Reference vectors are taken from asn1tools/tests/test_oer.py. -""" - -from scapy.contrib.oer import ( - OERcodec_BIT_STRING, - OERcodec_BOOLEAN, - OERcodec_ENUMERATED, - OERcodec_INTEGER, - OERcodec_NULL, - OERcodec_OID, - OERcodec_STRING, - OER_signed_integer_enc, - OER_unsigned_integer_enc, -) - -# (type name, value, scapy encoder callable, reference encoding) -INTEGER_VECTORS = [ - ("A", 0, lambda v: OERcodec_INTEGER.enc(v), b"\x01\x00"), - ("A", 128, lambda v: OERcodec_INTEGER.enc(v), b"\x02\x00\x80"), - ("A", 100000, lambda v: OERcodec_INTEGER.enc(v), b"\x03\x01\x86\xa0"), - ("A", -255, lambda v: OERcodec_INTEGER.enc(v), b"\x02\xff\x01"), - ("A", -1234567, lambda v: OERcodec_INTEGER.enc(v), b"\x03\xed)y"), - ("B", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=1), b"\xfe"), - ("C", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=2), b"\xff\xfe"), - ("D", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=4), b"\xff\xff\xff\xfe"), - ( - "E", - -2, - lambda v: OERcodec_INTEGER.enc(v, size_len=8), - b"\xff\xff\xff\xff\xff\xff\xff\xfe", - ), - ("F", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=1), b"\x80"), - ("G", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=2), b"\x00\x80"), - ("G", 1000, lambda v: OERcodec_INTEGER.enc(v, size_len=2), b"\x03\xe8"), - ("H", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=4), b"\x00\x00\x00\x80"), - ( - "I", - 128, - lambda v: OERcodec_INTEGER.enc(v, size_len=8), - b"\x00\x00\x00\x00\x00\x00\x00\x80", - ), - ("B", 1, lambda v: OERcodec_INTEGER.enc(v, size_len=1), b"\x01"), - ("K", 1, lambda v: OER_unsigned_integer_enc(v), b"\x01\x01"), - ("K", 128, lambda v: OER_unsigned_integer_enc(v), b"\x01\x80"), - ("L", -128, lambda v: OER_signed_integer_enc(v), b"\x01\x80"), -] - -BOOLEAN_VECTORS = [ - (True, lambda v: OERcodec_BOOLEAN.enc(1 if v else 0), b"\xff"), - (False, lambda v: OERcodec_BOOLEAN.enc(1 if v else 0), b"\x00"), -] - -ENUMERATED_VECTORS = [ - ("A", "a", 1, b"\x01"), - ("B", "a", 128, b"\x82\x00\x80"), - ("C", "a", 0, b"\x00"), - ("C", "b", 127, b"\x7f"), - ("E", "a", -1, b"\x81\xff"), -] - -OID_VECTORS = [ - ("1.2", lambda v: OERcodec_OID.enc(v), b"\x01*"), - ("1.2.3321", lambda v: OERcodec_OID.enc(v), b"\x03*\x99y"), -] - -OCTET_STRING_VECTORS = [ - (b"\x12\x34", 0, b"\x02\x124"), - (b"\x12\x34\x56", 3, b"\x124V"), -] - -BIT_STRING_VECTORS = [ - ("0100", b"\x02\x04@"), - ("01000001", b"\x02\x00A"), -] - -# (type name, value, reference encoding) -SCAPY_DECODE_VECTORS = [ - ("A", 42, b"\x01*"), - ("F", 200, b"\xc8"), - ("B", -99, b"\x9d"), -] - - -def check_primitive_interop(): - # type: () -> bool - """Compare Scapy OER primitives against reference encodings.""" - for type_name, value, enc, expected in INTEGER_VECTORS: - got = enc(value) - assert got == expected, ( - "integer %s=%r: reference=%r scapy=%r" % - (type_name, value, expected, got) - ) - if type_name == "A": - dec, remain = OERcodec_INTEGER.do_dec(got) - assert remain == b"" and dec.val == value - - for value, enc, expected in BOOLEAN_VECTORS: - got = enc(value) - assert got == expected - dec, remain = OERcodec_BOOLEAN.do_dec(got) - assert remain == b"" and dec.val == (1 if value else 0) - - got = OERcodec_NULL.enc(None) - assert got == b"" - - for type_name, _enum_name, enum_val, expected in ENUMERATED_VECTORS: - got = OERcodec_ENUMERATED.enc(enum_val) - assert got == expected - dec, remain = OERcodec_ENUMERATED.do_dec(got) - assert remain == b"" and dec.val == enum_val - - for oid, enc, expected in OID_VECTORS: - got = enc(oid) - assert got == expected - dec, remain = OERcodec_OID.do_dec(got) - assert remain == b"" and dec.val == oid - - for data, fixed_size, expected in OCTET_STRING_VECTORS: - got = OERcodec_STRING.enc(data, size_len=fixed_size or 0) - assert got == expected - dec, remain = OERcodec_STRING.do_dec(got, size_len=fixed_size or 0) - assert remain == b"" and dec.val == data - - for bitstr, expected in BIT_STRING_VECTORS: - got = OERcodec_BIT_STRING.enc(bitstr) - assert got == expected - dec, remain = OERcodec_BIT_STRING.do_dec(got) - assert remain == b"" and dec.val == bitstr - - return True - - -def check_scapy_encode_reference_decode(): - # type: () -> bool - """Decode reference encodings with Scapy.""" - for type_name, value, encoded in SCAPY_DECODE_VECTORS: - if type_name == "A": - dec, remain = OERcodec_INTEGER.do_dec(encoded) - elif type_name == "F": - dec, remain = OERcodec_INTEGER.do_dec( - encoded, size_len=1, oer_unsigned=True, - ) - else: - dec, remain = OERcodec_INTEGER.do_dec(encoded, size_len=1) - assert remain == b"" and dec.val == value - - for val in [0, 1]: - encoded = OERcodec_BOOLEAN.enc(val) - dec, remain = OERcodec_BOOLEAN.do_dec(encoded) - assert remain == b"" and dec.val == val - - return True diff --git a/test/scapy/layers/oer_packets.py b/test/scapy/layers/oer_packets.py deleted file mode 100644 index 7260609a42a..00000000000 --- a/test/scapy/layers/oer_packets.py +++ /dev/null @@ -1,209 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -OER ASN1_Packet and ASN1F_field tests. -""" -import scapy.contrib.oer # noqa: F401 # register OER stem - -from scapy.asn1.asn1 import ASN1_Codecs, ASN1_INTEGER, ASN1_STRING -from scapy.asn1fields import ( - ASN1F_BOOLEAN, - ASN1F_CHOICE, - ASN1F_INTEGER, - ASN1F_SEQUENCE, - ASN1F_SEQUENCE_OF, - ASN1F_STRING, - ASN1F_optional, -) -from scapy.asn1packet import ASN1_Packet -from scapy.packet import raw - - -class OERTaggedInteger(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_INTEGER("n", 0, explicit_tag=0xA1) - - -class OERFixedFields(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), - ASN1F_STRING("s", "", size_len=3), - ) - - -class OEROptionalField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), - ) - - -class OERSequenceOfIntegers(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) - - -class OERChoiceField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - - -class OERRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_BOOLEAN("flag", False), - ASN1F_STRING("label", ""), - ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), - ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), - ) - - -class OERNestedSequence(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0), - ASN1F_BOOLEAN("y", False), - ), - ) - - -class OERNestedSequenceTrailing(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0), - ASN1F_BOOLEAN("y", False), - ), - ASN1F_INTEGER("id", 0), - ) - - -class OERSequenceOfWithTrailing(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), - ASN1F_INTEGER("id", 0), - ) - - -def _roundtrip(cls, pkt): - # type: (type, ASN1_Packet) -> ASN1_Packet - return cls(raw(pkt)) - - -def check_oer_field_explicit_tag(): - # type: () -> None - pkt = OERTaggedInteger(n=5) - assert raw(pkt) == b"\xa1\x01\x05" - decoded = _roundtrip(OERTaggedInteger, pkt) - assert decoded.n.val == 5 - - -def check_oer_field_fixed_size(): - # type: () -> None - pkt = OERFixedFields(n=200, s=b"ABC") - assert raw(pkt) == b"\xc8ABC" - decoded = _roundtrip(OERFixedFields, pkt) - assert decoded.n.val == 200 - assert decoded.s.val == b"ABC" - - -def check_oer_field_optional(): - # type: () -> None - present = OEROptionalField(id=1, extra=7) - assert raw(present) == b"\x01\x01\xa0\x01\x07" - decoded = _roundtrip(OEROptionalField, present) - assert decoded.id.val == 1 - assert decoded.extra.val == 7 - - absent = OEROptionalField(id=1, extra=None) - assert raw(absent) == b"\x01\x01" - decoded = _roundtrip(OEROptionalField, absent) - assert decoded.id.val == 1 - assert decoded.extra is None - - -def check_oer_field_sequence_of(): - # type: () -> None - pkt = OERSequenceOfIntegers(values=[1, 2, 3]) - assert raw(pkt) == b"\x01\x03\x01\x01\x01\x02\x01\x03" - decoded = _roundtrip(OERSequenceOfIntegers, pkt) - assert [x.val for x in decoded.values] == [1, 2, 3] - - -def check_oer_field_choice(): - # type: () -> None - as_int = OERChoiceField(c=ASN1_INTEGER(99)) - assert raw(as_int) == b"\x02\x01c" - decoded = _roundtrip(OERChoiceField, as_int) - assert decoded.c.val == 99 - - as_str = OERChoiceField(c=ASN1_STRING("x")) - assert raw(as_str) == b"\x04\x01x" - decoded = _roundtrip(OERChoiceField, as_str) - assert decoded.c.val == b"x" - - -def check_oer_packet_record(): - # type: () -> None - pkt = OERRecord( - id=42, flag=True, label="hi", extra=7, values=[1, 2, 3], - ) - expected = ( - b"\x01*\xff\x02hi\xa0\x01\x07" - b"\x01\x03\x01\x01\x01\x02\x01\x03" - ) - assert raw(pkt) == expected - decoded = _roundtrip(OERRecord, pkt) - assert decoded.id.val == 42 - assert decoded.flag.val == 1 - assert decoded.label.val == b"hi" - assert decoded.extra.val == 7 - assert [x.val for x in decoded.values] == [1, 2, 3] - - empty = OERRecord(id=1, flag=False, label="", extra=None, values=[]) - assert raw(empty) == b"\x01\x01\x00\x00\x01\x00" - decoded = _roundtrip(OERRecord, empty) - assert decoded.id.val == 1 - assert decoded.flag.val == 0 - assert decoded.label.val == b"" - assert decoded.extra is None - assert [x.val for x in decoded.values] == [] - - -def check_oer_nested_sequence(): - # type: () -> None - pkt = OERNestedSequence(id=5, x=3, y=True) - assert raw(pkt) == b"\x01\x05\x01\x03\xff" - decoded = _roundtrip(OERNestedSequence, pkt) - assert decoded.id.val == 5 - assert decoded.x.val == 3 - assert decoded.y.val == 1 - - -def check_oer_nested_sequence_trailing(): - # type: () -> None - pkt = OERNestedSequenceTrailing(x=3, y=True, id=5) - assert raw(pkt) == b"\x01\x03\xff\x01\x05" - decoded = _roundtrip(OERNestedSequenceTrailing, pkt) - assert decoded.x.val == 3 - assert decoded.y.val == 1 - assert decoded.id.val == 5 - - -def check_oer_sequence_of_with_trailing(): - # type: () -> None - pkt = OERSequenceOfWithTrailing(values=[1, 2], id=7) - assert raw(pkt) == b"\x01\x02\x01\x01\x01\x02\x01\x07" - decoded = _roundtrip(OERSequenceOfWithTrailing, pkt) - assert [x.val for x in decoded.values] == [1, 2] - assert decoded.id.val == 7 diff --git a/test/scapy/layers/uper.uts b/test/scapy/layers/uper.uts new file mode 100644 index 00000000000..6b962e4dacb --- /dev/null +++ b/test/scapy/layers/uper.uts @@ -0,0 +1,2829 @@ +% Tests for ASN.1 UPER encoding + +# +# Try me with: +# bash test/run_tests -t test/scapy/layers/uper.uts -F + ++ ASN.1 UPER load += import contrib codecs +import scapy.contrib.uper +from scapy.contrib.uper import * +from scapy.packet import raw + + ++ ASN.1 UPER codec += UPER boolean true +UPERcodec_BOOLEAN.enc(1) == b"\x80" += UPER boolean false +UPERcodec_BOOLEAN.enc(0) == b"\x00" += UPER unconstrained integer +UPERcodec_INTEGER.enc(42) == b"\x01*" += UPER constrained integer +UPERcodec_INTEGER.enc(200, uper_min=0, uper_max=255) == b"\xc8" += UPER signed constrained integer +UPERcodec_INTEGER.enc(-1, uper_min=-128, uper_max=127) == b"\x7f" += UPER octet string +UPERcodec_STRING.enc(b"AB") == b"\x02AB" += UPER fixed octet string +UPERcodec_STRING.enc(b"\x12\x34\x56", size_len=3) == b"\x12\x34\x56" += UPER null +UPERcodec_NULL.enc(None) == b"" += UPER enumerated index +UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]) == b"\x80" += UPER bit string variable size +UPERcodec_BIT_STRING.enc((b"\xab\xcd", 16), uper_min=1, uper_max=20) == bytes.fromhex("7d5e68") += UPER enumerated roundtrip +x, r = UPERcodec_ENUMERATED.do_dec(UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), uper_enum_values=[1, 200]) +x.val == 200 and r == b"" += UPER integer roundtrip +x, r = UPERcodec_INTEGER.do_dec(UPERcodec_INTEGER.enc(-1)) +x.val == -1 and r == b"" += UPER boolean roundtrip +x, r = UPERcodec_BOOLEAN.do_dec(UPERcodec_BOOLEAN.enc(1)) +x.val == 1 and r == b"" += UPER ASN1 object encoding +ASN1_INTEGER(42).enc(ASN1_Codecs.PER) == b"\x01*" += UPER codec registration +ASN1_Class_UNIVERSAL.INTEGER.get_codec(ASN1_Codecs.PER) is UPERcodec_INTEGER + ++ ASN.1 UPER packets, helpers, interop and fuzz += import contrib codecs +import scapy.contrib.uper +from scapy.contrib.uper import * +from scapy.packet import raw += prepare helpers and packet classes +class UPERFixedFields(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_STRING("s", "", size_len=3), + ) + +class UPERIntegerField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0) + +class UPERBooleanField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BOOLEAN("b", False) + +class UPERStringField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_STRING("s", "") + +class UPERConstrainedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER( + "n", 0, size_len=1, oer_unsigned=True, + ) + +class UPEROptionalField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ) + +class UPERSequenceOfIntegers(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + +class UPERChoiceField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +class UPERChoiceStringFirst(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_STRING(b""), ASN1F_STRING, ASN1F_INTEGER, + ) + +class UPERRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + +class UPEREnumeratedField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_ENUMERATED( + "state", 1, {1: "alpha", 200: "beta"}, + ) + +class UPERBitStringField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BIT_STRING( + "bits", "0", uper_min=1, uper_max=20, + ) + +class UPERMessagePrefix(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("msgId", 0), + ASN1F_INTEGER("myflag", 0), + ASN1F_STRING("szDescription", "", size_len=10), + ASN1F_BOOLEAN("isReady", False), + ) + +class UPERSequenceWithChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_CHOICE("c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING), + ) + +class UPERNullPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_NULL("n", None) + +class UPERVariableOctetString(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_STRING("data", "", uper_min=1, uper_max=20) + +class UPERConstrainedRangeInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=15) + +class UPERSequenceWithEnumerated(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_ENUMERATED("state", 1, {1: "alpha", 200: "beta"}), + ) + +class UPERSequenceOfStrings(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF("items", [], ASN1F_STRING) + +class UPERNestedSequence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ) + +class UPERSequenceWithNull(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_NULL("n", None), + ) + +class UPERFixedBitString(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BIT_STRING("b", "0", uper_min=16, uper_max=16) + +class UPERSequenceOfConstrainedInts(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", [], ASN1F_INTEGER("item", 0, uper_min=0, uper_max=255), + ) + +class UPERSignedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=-128, uper_max=127) + +class UPERMultiOptional(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("a", 0)), + ASN1F_optional(ASN1F_STRING("b", "")), + ) + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + +CodecRoundtrip = Tuple[ + Type[Any], + Any, + Dict[str, Any], + Any, +] + +CODEC_ROUNDTRIPS = [ + (UPERcodec_NULL, None, {}, None), + (UPERcodec_BOOLEAN, 1, {}, 1), + (UPERcodec_BOOLEAN, 0, {}, 0), + (UPERcodec_INTEGER, 42, {}, 42), + (UPERcodec_INTEGER, -1, {}, -1), + (UPERcodec_INTEGER, 68719476736, {}, 68719476736), + (UPERcodec_INTEGER, 200, {"uper_min": 0, "uper_max": 255}, 200), + (UPERcodec_INTEGER, -1, {"uper_min": -128, "uper_max": 127}, -1), + (UPERcodec_INTEGER, 127, {"uper_min": -128, "uper_max": 127}, 127), + (UPERcodec_INTEGER, -128, {"uper_min": -128, "uper_max": 127}, -128), + (UPERcodec_STRING, b"AB", {}, b"AB"), + (UPERcodec_STRING, b"\x12\x34\x56", {"size_len": 3}, b"\x12\x34\x56"), + ( + UPERcodec_STRING, + bytes.fromhex("afbc4583"), + {"uper_min": 1, "uper_max": 20}, + bytes.fromhex("afbc4583"), + ), + (UPERcodec_ENUMERATED, 1, {"uper_enum_values": [1, 200]}, 1), + (UPERcodec_ENUMERATED, 200, {"uper_enum_values": [1, 200]}, 200), + ( + UPERcodec_BIT_STRING, + (bytes.fromhex("abcd"), 16), + {"uper_min": 1, "uper_max": 20}, + "1010101111001101", + ), + ( + UPERcodec_BIT_STRING, + (bytes.fromhex("abcd"), 16), + {"uper_min": 16, "uper_max": 16}, + "1010101111001101", + ), + (UPERcodec_ENUMERATED, 1, {"uper_enum_values": [1]}, 1), +] + +DecodeVector = Tuple[ + str, + Any, + Type[Any], + Dict[str, Any], + Any, + bytes, +] + +DECODE_VECTORS = [ + ("A", True, UPERcodec_BOOLEAN, {}, 1, b"\x80"), + ("A", False, UPERcodec_BOOLEAN, {}, 0, b"\x00"), + ("B", 42, UPERcodec_INTEGER, {}, 42, b"\x01*"), + ("B", -1, UPERcodec_INTEGER, {}, -1, b"\x01\xff"), + ( + "C", + 200, + UPERcodec_INTEGER, + {"uper_min": 0, "uper_max": 255}, + 200, + b"\xc8", + ), + ( + "Signed", + -1, + UPERcodec_INTEGER, + {"uper_min": -128, "uper_max": 127}, + -1, + b"\x7f", + ), + ( + "Signed", + 127, + UPERcodec_INTEGER, + {"uper_min": -128, "uper_max": 127}, + 127, + b"\xff", + ), + ("D", b"AB", UPERcodec_STRING, {}, b"AB", b"\x02AB"), + ( + "E", + b"\x12\x34\x56", + UPERcodec_STRING, + {"size_len": 3}, + b"\x12\x34\x56", + b"\x12\x34\x56", + ), + ("G", None, UPERcodec_NULL, {}, None, b""), + ("H", "alpha", UPERcodec_ENUMERATED, {"uper_enum_values": [1, 200]}, 1, b"\x00"), + ("H", "beta", UPERcodec_ENUMERATED, {"uper_enum_values": [1, 200]}, 200, b"\x80"), +] + +OID_ENCODE_VECTORS = [ + ("1.2.3", b"\x02*\x03"), + ("2.999.3", b"\x03\x887\x03"), +] + +def _assert_codec_roundtrip(codec, value, kwargs, expected): + # type: (Type[Any], Any, Dict[str, Any], Any) -> None + data = codec.enc(value, **kwargs) + decoded, _remain = codec.do_dec(data, **kwargs) + assert decoded.val == expected + +PRIMITIVE_VECTORS = [ + ("A", True, lambda v: UPERcodec_BOOLEAN.enc(1 if v else 0), b"\x80"), + ("A", False, lambda v: UPERcodec_BOOLEAN.enc(1 if v else 0), b"\x00"), + ("B", 42, lambda v: UPERcodec_INTEGER.enc(v), b"\x01*"), + ("B", -1, lambda v: UPERcodec_INTEGER.enc(v), b"\x01\xff"), + ( + "C", + 200, + lambda v: UPERcodec_INTEGER.enc(v, uper_min=0, uper_max=255), + b"\xc8", + ), + ("D", b"AB", lambda v: UPERcodec_STRING.enc(v), b"\x02AB"), + ( + "E", + b"\x12\x34\x56", + lambda v: UPERcodec_STRING.enc(v, size_len=3), + b"\x12\x34\x56", + ), + ("G", None, lambda v: UPERcodec_NULL.enc(None), b""), + ( + "H", + "beta", + lambda v: UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), + b"\x80", + ), +] + +COMPOSITE_VECTORS = [ + ("Seq", {"id": 42, "flag": True}, b"\x00\x95@"), + ("Seq", {"id": 42, "flag": True, "extra": 7}, b"\x80\x95@A\xc0"), + ("SeqOf", [1, 2, 3], b"\x03\x01\x01\x01\x02\x01\x03"), + ("SeqOfC", [1, 200, 0], b"\x03\x01\xc8\x00"), + ("Choice", ("a", 99), b"\x00\xb1\x80"), + ("Choice", ("b", b"AB"), b"\x81 \xa1\x00"), + ("ChoiceC", ("a", 10), b"P"), + ("ChoiceC", ("b", b"AB"), b"\x81 \xa1\x00"), +] + +DECODE_PACKET_VECTORS = [ + ( + UPERNestedSequence, + {"id": 5, "x": 3, "y": True}, + bytes.fromhex("0105010380"), + ), + ( + UPERMultiOptional, + {"id": 1, "a": 2, "b": b"hi"}, + bytes.fromhex("c0404040809a1a40"), + ), +] + +PACKET_REFERENCE_VECTORS = [ + ( + UPERNestedSequence, + {"id": 5, "x": 3, "y": True}, + bytes.fromhex("0105010380"), + ), + ( + UPERMultiOptional, + {"id": 1, "a": 2, "b": b"hi"}, + bytes.fromhex("c0404040809a1a40"), + ), +] + +def _encode_composite(typename, value): + # type: (str, Any) -> bytes + enc = UPER_Encoder() + if typename == "Seq": + enc.append_bit(1 if value.get("extra") is not None else 0) + UPERcodec_INTEGER.encode_into(enc, value["id"]) + UPERcodec_BOOLEAN.encode_into(enc, 1 if value["flag"] else 0) + if value.get("extra") is not None: + UPERcodec_INTEGER.encode_into(enc, value["extra"]) + return enc.as_bytes() + if typename == "SeqOf": + enc.append_length_determinant(len(value)) + for item in value: + UPERcodec_INTEGER.encode_into(enc, item) + return enc.as_bytes() + if typename == "SeqOfC": + enc.append_length_determinant(len(value)) + for item in value: + UPERcodec_INTEGER.encode_into( + enc, item, uper_min=0, uper_max=255, + ) + return enc.as_bytes() + if typename == "Choice": + alt, payload = value + index = 0 if alt == "a" else 1 + UPER_choice_index_enc(index, 2, enc=enc) + if alt == "a": + UPERcodec_INTEGER.encode_into(enc, payload) + else: + UPERcodec_STRING.encode_into(enc, payload) + return enc.as_bytes() + if typename == "ChoiceC": + alt, payload = value + index = 0 if alt == "a" else 1 + UPER_choice_index_enc(index, 2, enc=enc) + if alt == "a": + UPERcodec_INTEGER.encode_into( + enc, payload, uper_min=0, uper_max=15, + ) + else: + UPERcodec_STRING.encode_into(enc, payload) + return enc.as_bytes() + raise ValueError("unknown composite type %s" % typename) + +BOOLEAN_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= BOOLEAN " + "END" +) + +NULL_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= NULL " + "END" +) + +OCTET_STRING_VAR_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= OCTET STRING (SIZE(1..20)) " + "END" +) + +CHOICE_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= CHOICE { " + "int1 INTEGER(0..15), " + "int2 INTEGER(0..65535), " + "enm ENUMERATED { one(1), two(2), three(3), four(4), thousand(1000) }, " + "buf OCTET STRING (SIZE(10)), " + "gg SEQUENCE { " + "int1 INTEGER(0..15), " + "int2 INTEGER(0..65535), " + "enm ENUMERATED { pone(1), ptwo(2), pthree(3), pfour(4), pthousand(1000) }, " + "buf [APPLICATION 104] OCTET STRING (SIZE(10)) " + "} " + "} " + "END" +) + +ENUMERATED_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= ENUMERATED { alpha(1), beta(200) } " + "END" +) + +BIT_STRING_VAR_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= BIT STRING (SIZE(1..20)) " + "END" +) + +README_MESSAGE_HEX = ( + "010101020980cd191eb851eb851f48656c6c6f576f726c6480" +) + +README_MESSAGE_PREFIX_HEX = ( + "0101010248656c6c6f576f726c6480" +) + +ASN1SCC_VECTORS = [ + ( + "05-BOOLEAN/001 pdu1", + True, + lambda _v: UPERcodec_BOOLEAN.enc(1), + b"\x80", + ), + ( + "18-NULL/001 pdu1", + None, + lambda _v: UPERcodec_NULL.enc(None), + b"", + ), + ( + "06-OCTET-STRING/001 pdu1", + bytes.fromhex("afbc4583"), + lambda v: UPERcodec_STRING.enc(v, uper_min=1, uper_max=20), + bytes.fromhex("1d7de22c18"), + ), + ( + "05-BOOLEAN/001 pdu1 false", + False, + lambda _v: UPERcodec_BOOLEAN.enc(0), + b"\x00", + ), + ( + "04-ENUMERATED/001 pdu1 alpha", + "alpha", + lambda _v: UPERcodec_ENUMERATED.enc(1, uper_enum_values=[1, 200]), + b"\x00", + ), + ( + "04-ENUMERATED/001 pdu1 beta", + "beta", + lambda _v: UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), + b"\x80", + ), + ( + "09-CHOICE/001 pdu1 int1:10", + ("int1", 10), + lambda _v: _encode_choice_int1_10(), + b"\x14", + ), + ( + "08-BIT-STRING/001 pdu1 ABCD", + (bytes.fromhex("abcd"), 16), + lambda _v: UPERcodec_BIT_STRING.enc( + (bytes.fromhex("abcd"), 16), uper_min=1, uper_max=20, + ), + bytes.fromhex("7d5e68"), + ), +] + +def _encode_choice_int1_10(): + # type: () -> bytes + enc = UPER_Encoder() + UPER_choice_index_enc(0, 5, enc=enc) + UPERcodec_INTEGER.encode_into(enc, 10, uper_min=0, uper_max=15) + return enc.as_bytes() + +_UPER_CODEC_CLASSES = ( + UPERcodec_INTEGER, + UPERcodec_BOOLEAN, + UPERcodec_NULL, + UPERcodec_STRING, + UPERcodec_OID, + UPERcodec_ENUMERATED, + UPERcodec_BIT_STRING, +) + +_DECODE_ERRORS = ( + UPER_Decoding_Error, + UPER_Encoding_Error, + ASN1_Decoding_Error, + ASN1_Error, + ValueError, + IndexError, +) + +class UPERFuzzRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + +class UPERFuzzNested(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ) + +class UPERFuzzEnumerated(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_ENUMERATED( + "state", 1, {1: "alpha", 200: "beta"}, + ) + +def _fuzz_packets(): + # type: () -> Iterable[Type[ASN1_Packet]] + return (UPERFuzzRecord, UPERFuzzNested, UPERFuzzEnumerated) += uper field fixed size +pkt = UPERFixedFields(n=200, s=b"ABC") + +assert raw(pkt) == b"\xc8ABC" + +decoded = _roundtrip(UPERFixedFields, pkt) + +assert decoded.n.val == 200 + +assert decoded.s.val == b"ABC" + +True + += uper field integer +pkt = UPERIntegerField(n=12345) + +assert raw(pkt) == bytes.fromhex("023039") + +decoded = _roundtrip(UPERIntegerField, pkt) + +assert decoded.n.val == 12345 + +True + += uper field boolean +true_pkt = UPERBooleanField(b=True) + +assert raw(true_pkt) == b"\x80" + +decoded = _roundtrip(UPERBooleanField, true_pkt) + +assert decoded.b.val == 1 + +false_pkt = UPERBooleanField(b=False) + +assert raw(false_pkt) == b"\x00" + +decoded = _roundtrip(UPERBooleanField, false_pkt) + +assert decoded.b.val == 0 + +True + += uper field string +pkt = UPERStringField(s=b"hi") + +assert raw(pkt) == bytes.fromhex("026869") + +decoded = _roundtrip(UPERStringField, pkt) + +assert decoded.s.val == b"hi" + +True + += uper field constrained integer +pkt = UPERConstrainedInteger(n=200) + +assert raw(pkt) == b"\xc8" + +decoded = _roundtrip(UPERConstrainedInteger, pkt) + +assert decoded.n.val == 200 + +True + += uper field optional +present = UPEROptionalField(id=42, flag=True, extra=7) + +assert raw(present) == bytes.fromhex("80954041c0") + +decoded = _roundtrip(UPEROptionalField, present) + +assert decoded.id.val == 42 + +assert decoded.flag.val == 1 + +assert decoded.extra.val == 7 + +absent = UPEROptionalField(id=42, flag=True, extra=None) + +assert raw(absent) == bytes.fromhex("009540") + +decoded = _roundtrip(UPEROptionalField, absent) + +assert decoded.id.val == 42 + +assert decoded.flag.val == 1 + +assert decoded.extra is None + +True + += uper field sequence of +pkt = UPERSequenceOfIntegers(values=[1, 2, 3]) + +assert raw(pkt) == bytes.fromhex("03010101020103") + +decoded = _roundtrip(UPERSequenceOfIntegers, pkt) + +assert [x.val for x in decoded.values] == [1, 2, 3] + +empty = UPERSequenceOfIntegers(values=[]) + +assert raw(empty) == b"\x00" + +decoded = _roundtrip(UPERSequenceOfIntegers, empty) + +assert [x.val for x in decoded.values] == [] + +True + += uper field choice +as_int = UPERChoiceField(c=ASN1_INTEGER(99)) + +assert raw(as_int) == bytes.fromhex("00b180") + +decoded = _roundtrip(UPERChoiceField, as_int) + +assert decoded.c.val == 99 + +as_str = UPERChoiceField(c=ASN1_STRING(b"AB")) + +assert raw(as_str) == bytes.fromhex("8120a100") + +decoded = _roundtrip(UPERChoiceField, as_str) + +assert decoded.c.val == b"AB" + +True + += uper field choice definition order +as_str = UPERChoiceStringFirst(c=ASN1_STRING(b"AB")) + +assert raw(as_str) == bytes.fromhex("0120a100") + +decoded = _roundtrip(UPERChoiceStringFirst, as_str) + +assert decoded.c.val == b"AB" + +as_int = UPERChoiceStringFirst(c=ASN1_INTEGER(99)) + +assert raw(as_int) == bytes.fromhex("80b180") + +decoded = _roundtrip(UPERChoiceStringFirst, as_int) + +assert decoded.c.val == 99 + +True + += uper packet record +full = UPERRecord( + id=42, + flag=True, + label=b"hi", + extra=7, + values=[1, 2, 3], +) + +assert raw(full) == bytes.fromhex("8095409a1a4041c0c04040408040c0") + +decoded = _roundtrip(UPERRecord, full) + +assert decoded.id.val == 42 + +assert decoded.flag.val == 1 + +assert decoded.label.val == b"hi" + +assert decoded.extra.val == 7 + +assert [x.val for x in decoded.values] == [1, 2, 3] + +pkt = UPERRecord( + id=42, + flag=True, + label=b"AB", + extra=None, + values=[1, 2], +) + +body = bytes.fromhex("0095409050808040404080") + +assert raw(pkt) == body + +decoded = _roundtrip(UPERRecord, pkt) + +assert decoded.id.val == 42 + +assert decoded.flag.val == 1 + +assert decoded.label.val == b"AB" + +assert decoded.extra is None + +assert [x.val for x in decoded.values] == [1, 2] + +empty = UPERRecord( + id=1, + flag=False, + label=b"", + extra=None, + values=[], +) + +assert raw(empty) == bytes.fromhex("0080800000") + +decoded = _roundtrip(UPERRecord, empty) + +assert decoded.id.val == 1 + +assert decoded.flag.val == 0 + +assert decoded.label.val == b"" + +assert decoded.extra is None + +assert [x.val for x in decoded.values] == [] + +True + += uper field enumerated +alpha = UPEREnumeratedField(state=1) + +assert raw(alpha) == b"\x00" + +decoded = _roundtrip(UPEREnumeratedField, alpha) + +assert decoded.state.val == 1 + +beta = UPEREnumeratedField(state=200) + +assert raw(beta) == b"\x80" + +decoded = _roundtrip(UPEREnumeratedField, beta) + +assert decoded.state.val == 200 + +True + += uper field bit string +from scapy.asn1.asn1 import ASN1_BIT_STRING + +pkt = UPERBitStringField(bits=ASN1_BIT_STRING("1010101111001101")) + +assert raw(pkt) == bytes.fromhex("7d5e68") + +decoded = _roundtrip(UPERBitStringField, pkt) + +assert decoded.bits.val == "1010101111001101" + +True + += uper message prefix +pkt = UPERMessagePrefix( + msgId=1, + myflag=2, + szDescription=b"HelloWorld", + isReady=True, +) + +assert raw(pkt) == bytes.fromhex("0101010248656c6c6f576f726c6480") + +decoded = _roundtrip(UPERMessagePrefix, pkt) + +assert decoded.msgId.val == 1 + +assert decoded.myflag.val == 2 + +assert decoded.szDescription.val == b"HelloWorld" + +assert decoded.isReady.val == 1 + +True + += uper sequence with choice +pkt = UPERSequenceWithChoice(id=42, c=ASN1_INTEGER(99)) + +body = raw(pkt) + +decoded = UPERSequenceWithChoice(body) + +assert decoded.id.val == 42 + +assert decoded.c.val == 99 + +as_str = UPERSequenceWithChoice(id=1, c=ASN1_STRING(b"AB")) + +decoded = UPERSequenceWithChoice(raw(as_str)) + +assert decoded.id.val == 1 + +assert decoded.c.val == b"AB" + +True + += uper null packet +pkt = UPERNullPacket() + +assert raw(pkt) == b"" + +decoded = _roundtrip(UPERNullPacket, pkt) + +assert decoded.n is None + +True + += uper variable octet string +pkt = UPERVariableOctetString(data=bytes.fromhex("afbc4583")) + +assert raw(pkt) == bytes.fromhex("1d7de22c18") + +decoded = _roundtrip(UPERVariableOctetString, pkt) + +assert decoded.data.val == bytes.fromhex("afbc4583") + +True + += uper constrained range integer +pkt = UPERConstrainedRangeInt(n=10) + +assert raw(pkt) == b"\xa0" + +decoded = _roundtrip(UPERConstrainedRangeInt, pkt) + +assert decoded.n.val == 10 + +True + += uper sequence with enumerated +pkt = UPERSequenceWithEnumerated(id=1, state=200) + +assert raw(pkt) == bytes.fromhex("010180") + +decoded = _roundtrip(UPERSequenceWithEnumerated, pkt) + +assert decoded.id.val == 1 + +assert decoded.state.val == 200 + +alpha = UPERSequenceWithEnumerated(id=7, state=1) + +assert raw(alpha) == bytes.fromhex("010700") + +decoded = _roundtrip(UPERSequenceWithEnumerated, alpha) + +assert decoded.state.val == 1 + +True + += uper sequence of strings +pkt = UPERSequenceOfStrings(items=[b"A", b"BC"]) + +assert raw(pkt) == bytes.fromhex("020141024243") + +decoded = _roundtrip(UPERSequenceOfStrings, pkt) + +assert [x.val for x in decoded.items] == [b"A", b"BC"] + +empty = UPERSequenceOfStrings(items=[]) + +assert raw(empty) == b"\x00" + +decoded = _roundtrip(UPERSequenceOfStrings, empty) + +assert [x.val for x in decoded.items] == [] + +True + += uper sequence choice hex +pkt = UPERSequenceWithChoice(id=1, c=ASN1_INTEGER(99)) + +assert raw(pkt) == bytes.fromhex("010100b180") + +decoded = UPERSequenceWithChoice(raw(pkt)) + +assert decoded.id.val == 1 + +assert decoded.c.val == 99 + +True + += uper nested sequence +pkt = UPERNestedSequence(id=5, x=3, y=True) + +assert raw(pkt) == bytes.fromhex("0105010380") + +decoded = _roundtrip(UPERNestedSequence, pkt) + +assert decoded.id.val == 5 + +assert decoded.x.val == 3 + +assert decoded.y.val == 1 + +True + += uper sequence with null +pkt = UPERSequenceWithNull(id=1) + +assert raw(pkt) == bytes.fromhex("0101") + +decoded = _roundtrip(UPERSequenceWithNull, pkt) + +assert decoded.id.val == 1 + +assert getattr(decoded.n, "val", decoded.n) is None + +True + += uper fixed bit string +from scapy.asn1.asn1 import ASN1_BIT_STRING + +pkt = UPERFixedBitString(b=ASN1_BIT_STRING("1010101111001101")) + +assert raw(pkt) == bytes.fromhex("abcd") + +decoded = _roundtrip(UPERFixedBitString, pkt) + +assert decoded.b.val == "1010101111001101" + +True + += uper sequence of constrained ints +pkt = UPERSequenceOfConstrainedInts(values=[1, 200, 0]) + +assert raw(pkt) == bytes.fromhex("0301c800") + +decoded = _roundtrip(UPERSequenceOfConstrainedInts, pkt) + +assert [x.val for x in decoded.values] == [1, 200, 0] + +True + += uper signed integer +for value, expected in [ + (0, b"\x80"), + (-1, b"\x7f"), + (127, b"\xff"), + (-128, b"\x00"), +]: + pkt = UPERSignedInteger(n=value) + assert raw(pkt) == expected + decoded = _roundtrip(UPERSignedInteger, pkt) + assert decoded.n.val == value + +True + += uper multi optional +both = UPERMultiOptional(id=1, a=2, b=b"hi") + +assert raw(both) == bytes.fromhex("c0404040809a1a40") + +decoded = _roundtrip(UPERMultiOptional, both) + +assert decoded.id.val == 1 + +assert decoded.a.val == 2 + +assert decoded.b.val == b"hi" + +none = UPERMultiOptional(id=1, a=None, b=None) + +assert raw(none) == bytes.fromhex("004040") + +decoded = _roundtrip(UPERMultiOptional, none) + +assert decoded.id.val == 1 + +assert decoded.a is None + +assert decoded.b is None + +only_a = UPERMultiOptional(id=3, a=9, b=None) + +assert raw(only_a) == bytes.fromhex("8040c04240") + +decoded = _roundtrip(UPERMultiOptional, only_a) + +assert decoded.id.val == 3 + +assert decoded.a.val == 9 + +assert decoded.b is None + +True + += uper length determinant +for length, expected in [ + (0, b"\x00"), + (1, b"\x01"), + (127, b"\x7f"), + (128, b"\x80\x80"), + (16383, b"\xbf\xff"), + (16384, b"\xc1"), +]: + enc = UPER_Encoder() + enc.append_length_determinant(length) + assert enc.as_bytes() == expected + +True + += uper count roundtrip +for count in [0, 1, 3, 127]: + enc = UPER_Encoder() + UPER_count_enc(count, enc=enc) + got, _ = UPER_count_dec(enc.as_bytes()) + assert got == count + +True + += uper choice index roundtrip +for index, choices in [(0, 2), (1, 5), (3, 5)]: + enc = UPER_Encoder() + UPER_choice_index_enc(index, choices, enc=enc) + got, _ = UPER_choice_index_dec(enc.as_bytes(), choices) + assert got == index + +True + += uper optional presence +enc = UPER_Encoder() + +UPER_optional_presence_enc([0, 1, 0], enc=enc) + +assert enc.as_bytes() == b"\x40" + +True + += uper constrained integer +data = UPER_constrained_int_enc(10, 0, 15) + +value, remain = UPER_constrained_int_dec(data, 0, 15) + +assert value == 10 + +assert remain == b"" + +True + += uper constrained signed integer +for value, expected in [(0, b"\x80"), (-1, b"\x7f"), (127, b"\xff"), (-128, b"\x00")]: + data = UPER_constrained_int_enc(value, -128, 127) + assert data == expected + decoded, remain = UPER_constrained_int_dec(data, -128, 127) + assert decoded == value + assert remain == b"" + +True + += uper octet string roundtrip +for data, minimum, maximum in [ + (b"AB", None, None), + (b"\x12\x34\x56", 3, 3), + (bytes.fromhex("afbc4583"), 1, 20), +]: + encoded = UPER_octet_string_enc(data, minimum, maximum) + dec = UPER_Decoder(encoded) + decoded, _ = UPER_octet_string_dec(encoded, minimum, maximum, dec=dec) + assert decoded == data + assert not UPER_has_unexpected_remainder(dec) + +True + += uper has unexpected remainder +assert UPER_has_unexpected_remainder(UPER_Decoder(b"\x00")) is False + +assert UPER_has_unexpected_remainder(UPER_Decoder(b"\x80")) is True + +True + += uper join encodings +a = UPERcodec_INTEGER.enc(1) + +b = UPERcodec_INTEGER.enc(2) + +joined = UPER_join_encodings(a, b) + +dec = UPER_Decoder(joined) + +assert dec.read_unconstrained_whole_number() == 1 + +assert dec.read_unconstrained_whole_number() == 2 + +True + += uper chained encode into +enc = UPER_Encoder() + +UPERcodec_INTEGER.encode_into(enc, 42) + +UPERcodec_INTEGER.encode_into(enc, -7) + +dec = UPER_Decoder(enc.as_bytes()) + +assert dec.read_unconstrained_whole_number() == 42 + +assert dec.read_unconstrained_whole_number() == -7 + +True + += uper codec roundtrips +for codec, value, kwargs, expected in CODEC_ROUNDTRIPS: + _assert_codec_roundtrip(codec, value, kwargs, expected) + +True + += uper codec oid roundtrip +import scapy.all # noqa: F401 # loads conf.mib for ASN1_OID + +for oid in ("1.2.3", "1.2.840.113549"): + data = UPERcodec_OID.enc(oid) + decoded, remain = UPERcodec_OID.do_dec(data) + assert remain == b"" + assert decoded.val == oid + +True + += uper codec oid encode interop +for oid, expected in OID_ENCODE_VECTORS: + got = UPERcodec_OID.enc(oid) + assert got == expected, ( + "OID %r: expected %s, got %s" % + (oid, expected.hex(), got.hex()) + ) + +True + += uper codec reference decode +for _typename, _value, codec, kwargs, expected, encoded in DECODE_VECTORS: + decoded, _remain = codec.do_dec(encoded, **kwargs) + assert decoded.val == expected, ( + "%s %r: expected %r, got %r" % + (_typename, _value, expected, decoded.val) + ) + +True + += uper codec encode reference +for typename, value, encoder, expected in PRIMITIVE_VECTORS: + encoded = encoder(value) + assert encoded == expected, ( + "%s %r: expected %s, got %s" % + (typename, value, expected.hex(), encoded.hex()) + ) + +True + += primitive interop +for typename, value, encoder, expected in PRIMITIVE_VECTORS: + got = encoder(value) + assert got == expected, ( + "%s %r: expected %s, got %s" % + (typename, value, expected.hex(), got.hex()) + ) + +True + += composite interop +for typename, value, expected in COMPOSITE_VECTORS: + got = _encode_composite(typename, value) + assert got == expected, ( + "%s %r: expected %s, got %s" % + (typename, value, expected.hex(), got.hex()) + ) + +True + += packet reference interop +for cls, pkt_kwargs, expected in PACKET_REFERENCE_VECTORS: + got = raw(cls(**pkt_kwargs)) + assert got == expected, ( + "%s: expected %s, got %s" % + (cls.__name__, expected.hex(), got.hex()) + ) + decoded = cls(got) + for key, value in pkt_kwargs.items(): + field = getattr(decoded, key) + if value is None: + assert field is None + elif isinstance(value, bool): + assert field.val == (1 if value else 0) + else: + assert field.val == value + +True + += packet decode vectors +for cls, pkt_kwargs, data in DECODE_PACKET_VECTORS: + decoded = cls(data) + for key, value in pkt_kwargs.items(): + field = getattr(decoded, key) + if isinstance(value, bool): + assert field.val == (1 if value else 0) + else: + assert field.val == value + +True + += asn1scc vectors +for name, _value, encoder, expected in ASN1SCC_VECTORS: + got = encoder(_value) + assert got == expected, ( + "%s: expected %s, got %s" % + (name, expected.hex(), got.hex()) + ) + +True + += asn1scc readme message prefix +from scapy.packet import raw + +expected = bytes.fromhex(README_MESSAGE_PREFIX_HEX) + +pkt = UPERMessagePrefix( + msgId=1, + myflag=2, + szDescription=b"HelloWorld", + isReady=True, +) + +got = raw(pkt) + +assert got == expected + +decoded = UPERMessagePrefix(got) + +assert decoded.msgId.val == 1 + +assert decoded.myflag.val == 2 + +assert decoded.szDescription.val == b"HelloWorld" + +assert decoded.isReady.val == 1 + +True + += asn1scc readme message reference +assert README_MESSAGE_HEX == ( + "010101020980cd191eb851eb851f48656c6c6f576f726c6480" +) + +True + += uper fuzz encode +iterations = 25 + +for cls in _fuzz_packets(): + for _ in range(iterations): + try: + data = raw(fuzz(cls())) + except _DECODE_ERRORS: + continue + assert isinstance(data, bytes) + +True + += uper fuzz roundtrip +iterations = 25 + +for cls in _fuzz_packets(): + for _ in range(iterations): + try: + cls(raw(fuzz(cls()))) + except _DECODE_ERRORS: + pass + +True + += uper fuzz codec decode +iterations = 100 + +for codec in _UPER_CODEC_CLASSES: + for _ in range(iterations): + data = os.urandom(random.randint(0, 64)) + try: + codec.safedec(data) + except _DECODE_ERRORS: + pass + +True + += uper fuzz packet decode +iterations = 100 + +for cls in _fuzz_packets(): + for _ in range(iterations): + data = os.urandom(random.randint(0, 128)) + try: + cls(data) + except _DECODE_ERRORS: + pass + +True + ++ ASN.1 UPER build and dissect += import contrib codecs +import scapy.contrib.uper +from scapy.contrib.uper import * +from scapy.packet import raw += prepare helpers and packet classes +class UPERFixedFields(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_STRING("s", "", size_len=3), + ) + +class UPERIntegerField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0) + +class UPERBooleanField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BOOLEAN("b", False) + +class UPERStringField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_STRING("s", "") + +class UPERConstrainedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER( + "n", 0, size_len=1, oer_unsigned=True, + ) + +class UPEROptionalField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ) + +class UPERSequenceOfIntegers(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + +class UPERChoiceField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +class UPERChoiceStringFirst(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_STRING(b""), ASN1F_STRING, ASN1F_INTEGER, + ) + +class UPERRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + +class UPEREnumeratedField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_ENUMERATED( + "state", 1, {1: "alpha", 200: "beta"}, + ) + +class UPERBitStringField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BIT_STRING( + "bits", "0", uper_min=1, uper_max=20, + ) + +class UPERMessagePrefix(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("msgId", 0), + ASN1F_INTEGER("myflag", 0), + ASN1F_STRING("szDescription", "", size_len=10), + ASN1F_BOOLEAN("isReady", False), + ) + +class UPERSequenceWithChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_CHOICE("c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING), + ) + +class UPERNullPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_NULL("n", None) + +class UPERVariableOctetString(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_STRING("data", "", uper_min=1, uper_max=20) + +class UPERConstrainedRangeInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=15) + +class UPERSequenceWithEnumerated(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_ENUMERATED("state", 1, {1: "alpha", 200: "beta"}), + ) + +class UPERSequenceOfStrings(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF("items", [], ASN1F_STRING) + +class UPERNestedSequence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ) + +class UPERSequenceWithNull(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_NULL("n", None), + ) + +class UPERFixedBitString(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BIT_STRING("b", "0", uper_min=16, uper_max=16) + +class UPERSequenceOfConstrainedInts(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", [], ASN1F_INTEGER("item", 0, uper_min=0, uper_max=255), + ) + +class UPERSignedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=-128, uper_max=127) + +class UPERMultiOptional(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("a", 0)), + ASN1F_optional(ASN1F_STRING("b", "")), + ) + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + +def _record_kwargs(): + # type: () -> dict + return dict( + id=42, + flag=True, + label=b"hi", + extra=7, + values=[1, 2, 3], + ) + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + +def _assert_record(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + +def _assert_record_empty(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] + +def _dissect(cls, data_hex): + # type: (Type[ASN1_Packet], str) -> ASN1_Packet + return cls(bytes.fromhex(data_hex)) += per record build roundtrip +pkt = UPERRecord(**_record_kwargs()) + +assert len(raw(pkt)) > 0 + +decoded = _roundtrip(UPERRecord, pkt) + +assert decoded.id.val == 42 + +assert decoded.flag.val == 1 + +assert decoded.label.val == b"hi" + +assert decoded.extra.val == 7 + +assert [x.val for x in decoded.values] == [1, 2, 3] + +True + += per default field build +class UPERDefaultRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_DEFAULT( + ASN1F_INTEGER( + "count", 600, + uper_min=0, uper_max=86401, oer_unsigned=True, + ), + 600, + ), + ) + +absent = UPERDefaultRecord(id=1) + +assert raw(absent) == b"\x00\x80" + +decoded = _roundtrip(UPERDefaultRecord, absent) + +assert decoded.id.val == 1 + +assert _asn1_int(decoded.count) == 600 + +present = UPERDefaultRecord(id=1, count=86400) + +assert raw(present) == bytes.fromhex("80d46000") + +decoded = _roundtrip(UPERDefaultRecord, present) + +assert decoded.id.val == 1 + +assert _asn1_int(decoded.count) == 86400 + +True + += per extensible integer build +class UPERExtInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER( + "n", 0, + uper_min=1, uper_max=65535, + uper_extensible=True, oer_unsigned=True, + ), + ) + +in_range = UPERExtInt(n=42) + +assert raw(in_range) == bytes.fromhex("001480") + +decoded = _roundtrip(UPERExtInt, in_range) + +assert decoded.n.val == 42 + +out_of_range = UPERExtInt(n=1706733817) + +assert raw(out_of_range) == bytes.fromhex("8232dd587c80") + +decoded = _roundtrip(UPERExtInt, out_of_range) + +assert decoded.n.val == 1706733817 + +True + += per constrained sequence of build +class UPERConstrainedSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], + ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7), + uper_min=1, uper_max=3, + ) + +pkt = UPERConstrainedSeqOf(items=[1, 2]) + +assert raw(pkt) == bytes.fromhex("4a") + +decoded = _roundtrip(UPERConstrainedSeqOf, pkt) + +assert [x.val for x in decoded.items] == [1, 2] + +True + += per field dissect +fixed = _dissect(UPERFixedFields, "c8414243") + +assert fixed.n.val == 200 + +assert fixed.s.val == b"ABC" + +present = _dissect(UPEROptionalField, "80954041c0") + +assert present.id.val == 42 + +assert present.flag.val == 1 + +assert present.extra.val == 7 + +absent = _dissect(UPEROptionalField, "009540") + +assert absent.id.val == 42 + +assert absent.flag.val == 1 + +assert absent.extra is None + +seqof = _dissect(UPERSequenceOfIntegers, "03010101020103") + +assert [x.val for x in seqof.values] == [1, 2, 3] + +empty_seqof = _dissect(UPERSequenceOfIntegers, "00") + +assert [x.val for x in empty_seqof.values] == [] + +as_int = _dissect(UPERChoiceField, "00b180") + +assert as_int.c.val == 99 + +as_str = _dissect(UPERChoiceField, "8120a100") + +assert as_str.c.val == b"AB" + +True + += per record dissect +decoded = _dissect( + UPERRecord, + "8095409a1a4041c0c04040408040c0", +) + +_assert_record(decoded) + +partial = _dissect(UPERRecord, "0095409050808040404080") + +assert partial.id.val == 42 + +assert partial.flag.val == 1 + +assert partial.label.val == b"AB" + +assert partial.extra is None + +assert [x.val for x in partial.values] == [1, 2] + +empty = _dissect(UPERRecord, "0080800000") + +_assert_record_empty(empty) + +True + += per default field dissect +class UPERDefaultRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_DEFAULT( + ASN1F_INTEGER( + "count", 600, + uper_min=0, uper_max=86401, oer_unsigned=True, + ), + 600, + ), + ) + +absent = _dissect(UPERDefaultRecord, "0080") + +assert absent.id.val == 1 + +assert _asn1_int(absent.count) == 600 + +present = _dissect(UPERDefaultRecord, "80d46000") + +assert present.id.val == 1 + +assert _asn1_int(present.count) == 86400 + +True + += per extensible integer dissect +class UPERExtInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER( + "n", 0, + uper_min=1, uper_max=65535, + uper_extensible=True, oer_unsigned=True, + ), + ) + +in_range = _dissect(UPERExtInt, "001480") + +assert in_range.n.val == 42 + +out_of_range = _dissect(UPERExtInt, "8232dd587c80") + +assert out_of_range.n.val == 1706733817 + +True + += per constrained sequence of dissect +class UPERConstrainedSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], + ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7), + uper_min=1, uper_max=3, + ) + +decoded = _dissect(UPERConstrainedSeqOf, "4a") + +assert [x.val for x in decoded.items] == [1, 2] + +True + ++ ASN.1 UPER coverage += import contrib codecs +import scapy.contrib.uper +from scapy.contrib.uper import * +from scapy.packet import raw +from unittest import mock +from scapy.asn1.ber import BER_Decoding_Error +from scapy.contrib.oer import OER_Decoding_Error, OER_Encoding_Error +from scapy.contrib.uper import ( + UPER_Decoding_Error, UPER_Encoding_Error, UPER_Decoder, UPER_Encoder, +) +from scapy.packet import Raw, raw += prepare helpers and packet classes +class _InnerRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("mode", ASN1_INTEGER(0), ["off", "on"]), + ) + +class _EncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_ENCAPS("payload", None, _InnerRecord), + ) + +class _FlagsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_FLAGS("f", "000", ["read", "write", "exec"]), + ) + +class _SetOfRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SET_OF("items", [], ASN1F_INTEGER) + +class _PacketFieldRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_PacketField("data", b""), + ) + +class _ExplicitPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2) + +class _BitEncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord), + ) + +def _raises(exc, func): + # type: (type, Any) -> None + try: + func() + except exc: + return + raise AssertionError("Expected %s" % exc.__name__) += uper error str +obj = ASN1_INTEGER(2) + +err = UPER_Encoding_Error("enc", encoded=obj, remaining=b"x") + +assert "Already encoded" in str(err) + +err2 = UPER_Decoding_Error("dec", decoded=obj, remaining=b"y") + +assert "Already decoded" in str(err2) + +True + += uper length determinant extended +enc = UPER_Encoder() + +assert enc.append_length_determinant(32768) == 32768 + +assert enc.as_bytes() == b"\xc2" + +enc = UPER_Encoder() + +assert enc.append_length_determinant(49152) == 49152 + +assert enc.as_bytes() == b"\xc3" + +enc = UPER_Encoder() + +assert enc.append_length_determinant(65535) == 49152 + +assert enc.as_bytes() == b"\xc3" + +True + += uper unconstrained whole number +enc = UPER_Encoder() + +enc.append_unconstrained_whole_number(-256) + +dec = UPER_Decoder(enc.as_bytes()) + +assert dec.read_unconstrained_whole_number() == -256 + +enc = UPER_Encoder() + +enc.append_unconstrained_whole_number(0) + +dec = UPER_Decoder(enc.as_bytes()) + +assert dec.read_unconstrained_whole_number() == 0 + +True + += uper bit string paths +encoded = UPERcodec_BIT_STRING.enc("1010", uper_min=1, uper_max=20) + +obj, remain = UPERcodec_BIT_STRING.do_dec( + encoded, uper_min=1, uper_max=20, +) + +assert obj.val == "1010" + +encoded2 = UPERcodec_BIT_STRING.enc(b"\xab", uper_min=4, uper_max=8) + +obj2, _ = UPERcodec_BIT_STRING.do_dec(encoded2, uper_min=4, uper_max=8) + +assert len(obj2.val) == 8 + +fixed = UPERcodec_BIT_STRING.enc("1010101111001101", uper_min=16, uper_max=16) + +obj3, _ = UPERcodec_BIT_STRING.do_dec(fixed, uper_min=16, uper_max=16) + +assert obj3.val == "1010101111001101" + +True + += uper enumerated range +encoded = UPERcodec_ENUMERATED.enc(3, uper_min=0, uper_max=7) + +obj, remain = UPERcodec_ENUMERATED.do_dec(encoded, uper_min=0, uper_max=7) + +assert obj.val == 3 + +assert remain == b"" + +enc = UPER_Encoder() + +UPERcodec_ENUMERATED.encode_into(enc, 2, uper_min=0, uper_max=3) + +obj2 = UPERcodec_ENUMERATED.dec_from_decoder( + UPER_Decoder(enc.as_bytes()), + uper_min=0, + uper_max=3, +) + +assert obj2.val == 2 + +True + += uper sequence errors +_raises(UPER_Encoding_Error, lambda: UPERcodec_SEQUENCE.enc([ASN1_INTEGER(1)])) + +_raises(UPER_Decoding_Error, lambda: UPERcodec_SEQUENCE.do_dec(b"\x00")) + +assert UPERcodec_SET.enc(b"raw") == b"raw" + +True + += uper ipaddress +encoded = UPERcodec_IPADDRESS.enc("10.0.0.1") + +obj, remain = UPERcodec_IPADDRESS.do_dec(encoded) + +assert obj.val == "10.0.0.1" + +assert remain == b"" + +_raises(UPER_Encoding_Error, lambda: UPERcodec_IPADDRESS.enc("bad-ip")) + +True + ++ ASN.1 fields coverage += import contrib codecs +import scapy.contrib.oer +import scapy.contrib.uper +from scapy.contrib.oer import * +from scapy.contrib.uper import * +from scapy.packet import raw +from unittest import mock +from scapy.asn1.ber import BER_Decoding_Error +from scapy.contrib.oer import OER_Decoding_Error, OER_Encoding_Error +from scapy.contrib.uper import ( + UPER_Decoding_Error, UPER_Encoding_Error, UPER_Decoder, UPER_Encoder, +) +from scapy.packet import Raw, raw += prepare helpers and packet classes +class _InnerRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("mode", ASN1_INTEGER(0), ["off", "on"]), + ) + +class _EncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_ENCAPS("payload", None, _InnerRecord), + ) + +class _FlagsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_FLAGS("f", "000", ["read", "write", "exec"]), + ) + +class _SetOfRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SET_OF("items", [], ASN1F_INTEGER) + +class _PacketFieldRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_PacketField("data", b""), + ) + +class _ExplicitPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2) + +class _BitEncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord), + ) + +def _raises(exc, func): + # type: (type, Any) -> None + try: + func() + except exc: + return + raise AssertionError("Expected %s" % exc.__name__) += asn1fields enum and flags +pkt = _InnerRecord(mode="on") + +built = raw(pkt) + +decoded = _InnerRecord(built) + +assert decoded.mode.val == 1 + +flags = _FlagsRecord(f="read+exec") + +assert flags.f.val == "101" + +assert "read, exec" in _FlagsRecord.ASN1_root.seq[0].i2repr(flags, flags.f) + +set_pkt = _SetOfRecord(items=[ASN1_INTEGER(0), ASN1_INTEGER(1)]) + +set_raw = raw(set_pkt) + +set_dec = _SetOfRecord(set_raw) + +assert [x.val for x in set_dec.items] == [0, 1] + +True + += asn1fields encaps and packet +inner = _InnerRecord(mode=1) + +enc = _EncapsRecord() + +enc.payload = inner + +enc_raw = raw(enc) + +enc_dec = _EncapsRecord(enc_raw) + +assert enc_dec.payload.mode.val == 1 + +pkt_field = _PacketFieldRecord() + +pkt_field.data = _InnerRecord(mode=0) + +pf_raw = raw(pkt_field) + +pf_dec = _PacketFieldRecord(pf_raw) + +assert isinstance(pf_dec.data.val, bytes) + +explicit = _ExplicitPacket() + +explicit.inner = _InnerRecord(mode=1) + +ex_raw = raw(explicit) + +ex_dec = _ExplicitPacket(ex_raw) + +assert ex_dec.inner.mode.val == 1 + +True + += asn1fields choice and special +class _OerChoiceRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +class _BerChoiceRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +oer = _OerChoiceRecord(c=ASN1_INTEGER(1)) + +oer_dec = _OerChoiceRecord(raw(oer)) + +assert oer_dec.c.val == 1 + +ber = _BerChoiceRecord(c=ASN1_INTEGER(0)) + +ber_dec = _BerChoiceRecord(raw(ber)) + +assert ber_dec.c.val == 0 + +inner_bytes = raw(_InnerRecord(mode=0)) + +bit_payload = ASN1_BIT_STRING( + inner_bytes, + readable=True, +) + +bit_pkt = _BitEncapsRecord(b=bit_payload) + +bit_dec = _BitEncapsRecord(raw(bit_pkt)) + +assert bit_dec.b.mode.val == 0 + +class _TicksRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_TIME_TICKS("t", ASN1_TIME_TICKS(0)) + +class _IpRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_IPADDRESS("addr", ASN1_STRING(b"")) + +ticks = _TicksRecord(t=ASN1_TIME_TICKS(1234)) + +assert raw(ticks).endswith(b"\x04\xd2") + +ip = _IpRecord() + +ip.addr = "192.168.1.1" + +assert raw(ip) == b"\x40\x04\xc0\xa8\x01\x01" + +True + += asn1fields optional dissect +class _OptRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ) + +class _BerChoiceRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +pkt = _OptRecord(id=0, extra=None) + +assert raw(pkt) + +decoded = _OptRecord(raw(pkt)) + +assert decoded.extra is None + +choice_rand = _BerChoiceRecord.ASN1_root.randval() + +assert choice_rand is not None + +True + += asn1fields default and omit +class _DefaultRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_DEFAULT( + ASN1F_INTEGER( + "count", 600, + uper_min=0, uper_max=86401, oer_unsigned=True, + ), + 600, + ), + ) + +absent = _DefaultRecord(id=1) + +assert raw(absent) == b"\x00\x80" + +decoded = _DefaultRecord(raw(absent)) + +assert decoded.id.val == 1 + +assert decoded.count == 600 or decoded.count.val == 600 + +present = _DefaultRecord(id=1, count=86400) + +decoded = _DefaultRecord(raw(present)) + +assert decoded.count.val == 86400 + +class _OmitRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_omit("ignored", None), + ) + +omit_pkt = _OmitRecord(id=7) + +assert raw(omit_pkt) == bytes.fromhex("3003020107") + +True + += asn1fields extensible per +class _ExtSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_optional(ASN1F_INTEGER("extra", 0, uper_min=0, uper_max=7)), + uper_extensible=True, + ) + +pkt = _ExtSeq(id=2, extra=3) + +data = raw(pkt) + +decoded = _ExtSeq(data) + +assert decoded.id.val == 2 + +assert decoded.extra.val == 3 + +dec = UPER_Decoder(b"\x80") + +_raises( + UPER_Decoding_Error, + lambda: _ExtSeq.ASN1_root.dissect_from_decoder(_ExtSeq(), dec), +) + +class _ExtChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + uper_extensible=True, + ) + +choice = _ExtChoice(c=ASN1_INTEGER(4)) + +assert raw(choice) + +dec = UPER_Decoder(b"\x80") + +_raises( + UPER_Decoding_Error, + lambda: _ExtChoice.ASN1_root.m2i_from_decoder(_ExtChoice(), dec), +) + +class _InnerItem(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7) + +class _ExtSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], _InnerItem, + uper_min=1, uper_max=2, uper_extensible=True, + ) + +in_range = _ExtSeqOf(items=[_InnerItem(n=1)]) + +assert raw(in_range) + +decoded = _ExtSeqOf(raw(in_range)) + +assert decoded.items[0].n.val == 1 + +out_of_range = _ExtSeqOf( + items=[_InnerItem(n=i) for i in range(4)], +) + +assert raw(out_of_range) + +decoded = _ExtSeqOf(raw(out_of_range)) + +assert len(decoded.items) == 4 + +True + += asn1fields sequence of advanced +class _Inner(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7) + +class _SeqOfPackets(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], _Inner, uper_min=1, uper_max=3, + ) + +pkt = _SeqOfPackets(items=[_Inner(n=1), _Inner(n=2)]) + +decoded = _SeqOfPackets(raw(pkt)) + +assert [x.n.val for x in decoded.items] == [1, 2] + +class _OerSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + +oer_pkt = _OerSeqOf(values=[1, 2]) + +oer_dec = _OerSeqOf(raw(oer_pkt)) + +assert [x.val for x in oer_dec.values] == [1, 2] + +class _EmptySeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + +empty = _EmptySeqOf(values=None) + +assert raw(empty) == b"\x00" + +assert _EmptySeqOf.ASN1_root.i2repr(empty, None) == "[]" + +assert _EmptySeqOf.ASN1_root.i2repr( + _EmptySeqOf(values=[ASN1_INTEGER(1)]), + [ASN1_INTEGER(1)], +).startswith("[") + +_raises(ValueError, lambda: ASN1F_SEQUENCE_OF("bad", [], object())) + +True + += asn1fields choice advanced +class _InnerChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +class _NestedChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), _InnerChoice, ASN1F_INTEGER, + ) + +nested = _NestedChoice(c=_InnerChoice(c=ASN1_STRING(b"xy"))) + +assert len(raw(nested)) > 0 + +nested_dec = _NestedChoice(raw(nested)) + +assert isinstance(nested_dec.c, (_InnerChoice, ASN1_STRING)) + +class _OerTaggedChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + explicit_tag=0xA1, + ) + +oer_choice = _OerTaggedChoice(c=ASN1_INTEGER(9)) + +assert raw(oer_choice) + +class _PacketChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", + ASN1_INTEGER(0), + ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2), + ASN1F_INTEGER, + ) + +packet_choice = _PacketChoice( + c=_InnerRecord(mode=ASN1_INTEGER(1)), +) + +packet_dec = _PacketChoice(raw(packet_choice)) + +assert packet_dec.c.mode.val == 1 + +class _PerChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +_raises( + ASN1_Error, + lambda: ASN1F_CHOICE( + "c", 0, ASN1F_INTEGER, implicit_tag=0xA0, + ), +) + +_raises( + ASN1_Error, + lambda: _PerChoice.ASN1_root.m2i(_PerChoice(), b""), +) + +_raises( + ASN1_Error, + lambda: _PerChoice.ASN1_root._uper_encode_into( + UPER_Encoder(), _PerChoice(), 42, + ), +) + +True + += asn1fields enum bitstring and flags +class _NamedEnum(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_enum_INTEGER( + "state", 0, ["off", "on", "auto"], + ) + +named = _NamedEnum(state="on") + +built = raw(named) + +decoded = _NamedEnum(built) + +assert decoded.state.val == 1 + +assert "'on'" in _NamedEnum.ASN1_root.i2repr(decoded, decoded.state) + +class _BitRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_BIT_STRING("bits", b"\xaa") + +assert raw(_BitRecord()) + +flags = _FlagsRecord() + +flags.f = ASN1_BIT_STRING("101") + +assert "read, exec" in _FlagsRecord.ASN1_root.seq[0].i2repr(flags, flags.f) + +class _BadBitEncaps(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord) + +_raises( + BER_Decoding_Error, + lambda: _BadBitEncaps.ASN1_root.m2i( + _BadBitEncaps(), + b"\x03\x02\x01\x00", + ), +) + +True + += asn1fields packet and sequence errors +class _PerInner(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("mode", 0, uper_min=0, uper_max=1) + +class _PacketWrap(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_PACKET("inner", None, _PerInner) + +inner = _PerInner(mode=1) + +wrap = _PacketWrap(inner=inner) + +decoded = _PacketWrap(raw(wrap)) + +assert decoded.inner.mode.val == 1 + +class _DynamicPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_PACKET( + "inner", None, _PerInner, + next_cls_cb=lambda pkt: _PerInner, + ) + +dyn = _DynamicPacket(inner=_PerInner(mode=0)) + +assert _DynamicPacket.ASN1_root._resolve_cls(dyn) is _PerInner + +empty_packet = _PacketWrap(inner=None) + +assert raw(empty_packet) == b"" + +class _BerSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_INTEGER("extra", 0), + ) + +_raises( + BER_Decoding_Error, + lambda: _BerSeq.ASN1_root.m2i( + _BerSeq(), + bytes.fromhex("300702010102010200ff"), + ), +) + +class _OerSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, size_len=1, oer_unsigned=True), + ) + +_, remain = _OerSeq.ASN1_root.m2i(_OerSeq(), b"\x01\xff") + +assert remain == b"\xff" + +class _PerSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ) + +_raises( + UPER_Decoding_Error, + lambda: _PerSeq.ASN1_root.m2i(_PerSeq(), b"\x80\xff"), +) + +empty_seq = _BerSeq() + +_BerSeq.ASN1_root._dissect_sequence_children(empty_seq, b"") + +assert empty_seq.id is None + +assert empty_seq.extra is None + +class _OptListRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_optional( + ASN1F_SEQUENCE_OF("items", [], ASN1F_INTEGER), + ), + ) + +opt_list = _OptListRecord(id=1, items=None) + +assert raw(opt_list) + +field = ASN1F_INTEGER("n", 0) + +with mock.patch.object( + _InnerRecord, "__init__", side_effect=ASN1F_badsequence, +): + pkt_obj, remain = field.extract_packet( + _InnerRecord, b"\xab\xcd", _underlayer=None, + ) + +assert isinstance(pkt_obj, Raw) + +assert pkt_obj.load == b"\xab\xcd" + +assert remain == b"\xab\xcd" + +True + += asn1fields more coverage +_raises( + ASN1_Error, + lambda: ASN1F_INTEGER("x", 0, implicit_tag=1, explicit_tag=2), +) + +class _IntRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER("n", 0) + +field = _IntRecord.ASN1_root + +_raises( + ASN1_Error, + lambda: field.i2m(_IntRecord(), ASN1_STRING(b"bad")), +) + +flex_field = ASN1F_INTEGER("n", 0, flexible_tag=True, explicit_tag=0xA0) + +obj, remain = flex_field.m2i(_IntRecord(), bytes.fromhex("a1020101")) + +assert obj.tag != ASN1_Class_UNIVERSAL.INTEGER or remain == b"" + +class _FlexSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + explicit_tag=0xA1, + flexible_tag=True, + ) + +flex_seq = _FlexSeq(id=1) + +assert raw(flex_seq) + +decoded = _FlexSeq(raw(flex_seq)) + +assert decoded.id.val == 1 + +assert ASN1F_BOOLEAN("b", False).randval() is not None + +assert ASN1F_BIT_STRING("b", b"").randval() is not None + +assert ASN1F_OID("o", None).randval() is not None + +assert ASN1F_UTC_TIME("t", "").randval() is not None + +assert " 0 + +empty_inner, remain = packet_field.m2i(_FlexPacket(), b"") + +assert empty_inner is None and remain == b"" + +obj_val = packet_field.i2m(_FlexPacket(), _InnerRecord(mode=0)) + +assert len(obj_val) > 0 + +flags_field = _FlagsRecord.ASN1_root.seq[0] + +assert flags_field.i2repr(_FlagsRecord(), None) == "None" + +class _OerFlexSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", [], ASN1F_INTEGER, + explicit_tag=0xA1, + ) + +_OerFlexSeqOf.ASN1_root.flexible_tag = True + +oer_seq = _OerFlexSeqOf(values=[1]) + +data = raw(oer_seq) + +decoded = _OerFlexSeqOf(data) + +assert decoded.values[0].val == 1 + +class _BerFlexSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", [], ASN1F_INTEGER, + explicit_tag=0xA1, + ) + +_BerFlexSeqOf.ASN1_root.flexible_tag = True + +ber_seq = _BerFlexSeqOf(values=[2]) + +assert raw(ber_seq) + +class _ExtChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + uper_extensible=True, + ) + +dec = UPER_Decoder(b"\x80") + +_raises( + UPER_Decoding_Error, + lambda: _ExtChoice.ASN1_root.m2i_from_decoder(_ExtChoice(), dec), +) + +class _SingleChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE("c", ASN1_INTEGER(0), ASN1F_INTEGER) + +single = _SingleChoice(c=ASN1_INTEGER(3)) + +assert raw(single) + +class _FlexChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + flexible_tag=True, + ) + +flex_choice = _FlexChoice(c=ASN1_INTEGER(4)) + +assert raw(flex_choice) + +class _OerPktChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + +oer_pkt_choice = _OerPktChoice(c=ASN1_STRING(b"hi")) + +assert raw(oer_pkt_choice) + +True + diff --git a/test/scapy/layers/uper_asn1scc_iop.py b/test/scapy/layers/uper_asn1scc_iop.py deleted file mode 100644 index 411d23d9460..00000000000 --- a/test/scapy/layers/uper_asn1scc_iop.py +++ /dev/null @@ -1,190 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -UPER interoperability vectors from ESA asn1scc test cases. - -asn1scc (https://github.com/esa/asn1scc) primarily validates C/Ada code generation -with ACN custom encodings. Portable uPER vectors are taken from v4Tests where -``--TCLS MyPDU[]`` selects standard uPER (empty ACN = default PER). - -Cases that need REAL, explicit APPLICATION tags, or ACN overrides are not -compared against Scapy encoders here (or are reference-only). -""" - -from scapy.contrib.uper import ( - UPER_Encoder, - UPER_choice_index_enc, - UPERcodec_BIT_STRING, - UPERcodec_BOOLEAN, - UPERcodec_ENUMERATED, - UPERcodec_INTEGER, - UPERcodec_NULL, - UPERcodec_STRING, -) - -# asn1scc v4Tests/test-cases/acn/05-BOOLEAN/001.asn1 -BOOLEAN_SPEC = ( - "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " - "MyPDU ::= BOOLEAN " - "END" -) - -# asn1scc v4Tests/test-cases/acn/18-NULL/001.asn1 -NULL_SPEC = ( - "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " - "MyPDU ::= NULL " - "END" -) - -# asn1scc v4Tests/test-cases/acn/06-OCTET-STRING/001.asn1 -OCTET_STRING_VAR_SPEC = ( - "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " - "MyPDU ::= OCTET STRING (SIZE(1..20)) " - "END" -) - -# asn1scc v4Tests/test-cases/acn/09-CHOICE/001.asn1 (pdu1 = int1 : 10) -CHOICE_SPEC = ( - "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " - "MyPDU ::= CHOICE { " - "int1 INTEGER(0..15), " - "int2 INTEGER(0..65535), " - "enm ENUMERATED { one(1), two(2), three(3), four(4), thousand(1000) }, " - "buf OCTET STRING (SIZE(10)), " - "gg SEQUENCE { " - "int1 INTEGER(0..15), " - "int2 INTEGER(0..65535), " - "enm ENUMERATED { pone(1), ptwo(2), pthree(3), pfour(4), pthousand(1000) }, " - "buf [APPLICATION 104] OCTET STRING (SIZE(10)) " - "} " - "} " - "END" -) - -# asn1scc v4Tests/test-cases/acn/04-ENUMERATED/001.asn1 (pdu1 = beta) -ENUMERATED_SPEC = ( - "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " - "MyPDU ::= ENUMERATED { alpha(1), beta(200) } " - "END" -) - -# asn1scc v4Tests/test-cases/acn/08-BIT-STRING/001.asn1 (pdu1 = 'ABCD'H) -BIT_STRING_VAR_SPEC = ( - "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " - "MyPDU ::= BIT STRING (SIZE(1..20)) " - "END" -) - -# asn1scc README.md sample.asn (REAL field; reference only) -README_MESSAGE_HEX = ( - "010101020980cd191eb851eb851f48656c6c6f576f726c6480" -) - -README_MESSAGE_PREFIX_HEX = ( - "0101010248656c6c6f576f726c6480" -) - -# (name, pdu value, encoder callable, reference encoding) -ASN1SCC_VECTORS = [ - ( - "05-BOOLEAN/001 pdu1", - True, - lambda _v: UPERcodec_BOOLEAN.enc(1), - b"\x80", - ), - ( - "18-NULL/001 pdu1", - None, - lambda _v: UPERcodec_NULL.enc(None), - b"", - ), - ( - "06-OCTET-STRING/001 pdu1", - bytes.fromhex("afbc4583"), - lambda v: UPERcodec_STRING.enc(v, uper_min=1, uper_max=20), - bytes.fromhex("1d7de22c18"), - ), - ( - "05-BOOLEAN/001 pdu1 false", - False, - lambda _v: UPERcodec_BOOLEAN.enc(0), - b"\x00", - ), - ( - "04-ENUMERATED/001 pdu1 alpha", - "alpha", - lambda _v: UPERcodec_ENUMERATED.enc(1, uper_enum_values=[1, 200]), - b"\x00", - ), - ( - "04-ENUMERATED/001 pdu1 beta", - "beta", - lambda _v: UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), - b"\x80", - ), - ( - "09-CHOICE/001 pdu1 int1:10", - ("int1", 10), - lambda _v: _encode_choice_int1_10(), - b"\x14", - ), - ( - "08-BIT-STRING/001 pdu1 ABCD", - (bytes.fromhex("abcd"), 16), - lambda _v: UPERcodec_BIT_STRING.enc( - (bytes.fromhex("abcd"), 16), uper_min=1, uper_max=20, - ), - bytes.fromhex("7d5e68"), - ), -] - - -def _encode_choice_int1_10(): - # type: () -> bytes - enc = UPER_Encoder() - UPER_choice_index_enc(0, 5, enc=enc) - UPERcodec_INTEGER.encode_into(enc, 10, uper_min=0, uper_max=15) - return enc.as_bytes() - - -def check_asn1scc_vectors(): - # type: () -> None - for name, _value, encoder, expected in ASN1SCC_VECTORS: - got = encoder(_value) - assert got == expected, ( - "%s: expected %s, got %s" % - (name, expected.hex(), got.hex()) - ) - - -def check_asn1scc_readme_message_prefix(): - # type: () -> None - """README sample without REAL; Scapy packet roundtrip vs reference.""" - from test.scapy.layers.uper_packets import UPERMessagePrefix - from scapy.packet import raw - - expected = bytes.fromhex(README_MESSAGE_PREFIX_HEX) - - pkt = UPERMessagePrefix( - msgId=1, - myflag=2, - szDescription=b"HelloWorld", - isReady=True, - ) - got = raw(pkt) - assert got == expected - decoded = UPERMessagePrefix(got) - assert decoded.msgId.val == 1 - assert decoded.myflag.val == 2 - assert decoded.szDescription.val == b"HelloWorld" - assert decoded.isReady.val == 1 - - -def check_asn1scc_readme_message_reference(): - # type: () -> None - """README C sample output; Scapy does not encode REAL in UPER yet.""" - assert README_MESSAGE_HEX == ( - "010101020980cd191eb851eb851f48656c6c6f576f726c6480" - ) diff --git a/test/scapy/layers/uper_codec.py b/test/scapy/layers/uper_codec.py deleted file mode 100644 index 180503fb06e..00000000000 --- a/test/scapy/layers/uper_codec.py +++ /dev/null @@ -1,174 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -UPER primitive codec roundtrip and decode interoperability tests. -""" - -from typing import Any, Dict, Tuple, Type - -from scapy.contrib.uper import ( - UPERcodec_BIT_STRING, - UPERcodec_BOOLEAN, - UPERcodec_ENUMERATED, - UPERcodec_INTEGER, - UPERcodec_NULL, - UPERcodec_OID, - UPERcodec_STRING, -) - -CodecRoundtrip = Tuple[ - Type[Any], - Any, - Dict[str, Any], - Any, -] - -CODEC_ROUNDTRIPS = [ - (UPERcodec_NULL, None, {}, None), - (UPERcodec_BOOLEAN, 1, {}, 1), - (UPERcodec_BOOLEAN, 0, {}, 0), - (UPERcodec_INTEGER, 42, {}, 42), - (UPERcodec_INTEGER, -1, {}, -1), - (UPERcodec_INTEGER, 68719476736, {}, 68719476736), - (UPERcodec_INTEGER, 200, {"uper_min": 0, "uper_max": 255}, 200), - (UPERcodec_INTEGER, -1, {"uper_min": -128, "uper_max": 127}, -1), - (UPERcodec_INTEGER, 127, {"uper_min": -128, "uper_max": 127}, 127), - (UPERcodec_INTEGER, -128, {"uper_min": -128, "uper_max": 127}, -128), - (UPERcodec_STRING, b"AB", {}, b"AB"), - (UPERcodec_STRING, b"\x12\x34\x56", {"size_len": 3}, b"\x12\x34\x56"), - ( - UPERcodec_STRING, - bytes.fromhex("afbc4583"), - {"uper_min": 1, "uper_max": 20}, - bytes.fromhex("afbc4583"), - ), - (UPERcodec_ENUMERATED, 1, {"uper_enum_values": [1, 200]}, 1), - (UPERcodec_ENUMERATED, 200, {"uper_enum_values": [1, 200]}, 200), - ( - UPERcodec_BIT_STRING, - (bytes.fromhex("abcd"), 16), - {"uper_min": 1, "uper_max": 20}, - "1010101111001101", - ), - ( - UPERcodec_BIT_STRING, - (bytes.fromhex("abcd"), 16), - {"uper_min": 16, "uper_max": 16}, - "1010101111001101", - ), - (UPERcodec_ENUMERATED, 1, {"uper_enum_values": [1]}, 1), -] - -DecodeVector = Tuple[ - str, - Any, - Type[Any], - Dict[str, Any], - Any, - bytes, -] - -DECODE_VECTORS = [ - ("A", True, UPERcodec_BOOLEAN, {}, 1, b"\x80"), - ("A", False, UPERcodec_BOOLEAN, {}, 0, b"\x00"), - ("B", 42, UPERcodec_INTEGER, {}, 42, b"\x01*"), - ("B", -1, UPERcodec_INTEGER, {}, -1, b"\x01\xff"), - ( - "C", - 200, - UPERcodec_INTEGER, - {"uper_min": 0, "uper_max": 255}, - 200, - b"\xc8", - ), - ( - "Signed", - -1, - UPERcodec_INTEGER, - {"uper_min": -128, "uper_max": 127}, - -1, - b"\x7f", - ), - ( - "Signed", - 127, - UPERcodec_INTEGER, - {"uper_min": -128, "uper_max": 127}, - 127, - b"\xff", - ), - ("D", b"AB", UPERcodec_STRING, {}, b"AB", b"\x02AB"), - ( - "E", - b"\x12\x34\x56", - UPERcodec_STRING, - {"size_len": 3}, - b"\x12\x34\x56", - b"\x12\x34\x56", - ), - ("G", None, UPERcodec_NULL, {}, None, b""), - ("H", "alpha", UPERcodec_ENUMERATED, {"uper_enum_values": [1, 200]}, 1, b"\x00"), - ("H", "beta", UPERcodec_ENUMERATED, {"uper_enum_values": [1, 200]}, 200, b"\x80"), -] - -OID_ENCODE_VECTORS = [ - ("1.2.3", b"\x02*\x03"), - ("2.999.3", b"\x03\x887\x03"), -] - - -def _assert_codec_roundtrip(codec, value, kwargs, expected): - # type: (Type[Any], Any, Dict[str, Any], Any) -> None - data = codec.enc(value, **kwargs) - decoded, _remain = codec.do_dec(data, **kwargs) - assert decoded.val == expected - - -def check_uper_codec_roundtrips(): - # type: () -> None - for codec, value, kwargs, expected in CODEC_ROUNDTRIPS: - _assert_codec_roundtrip(codec, value, kwargs, expected) - - -def check_uper_codec_oid_roundtrip(): - # type: () -> None - import scapy.all # noqa: F401 # loads conf.mib for ASN1_OID - for oid in ("1.2.3", "1.2.840.113549"): - data = UPERcodec_OID.enc(oid) - decoded, remain = UPERcodec_OID.do_dec(data) - assert remain == b"" - assert decoded.val == oid - - -def check_uper_codec_oid_encode_interop(): - # type: () -> None - for oid, expected in OID_ENCODE_VECTORS: - got = UPERcodec_OID.enc(oid) - assert got == expected, ( - "OID %r: expected %s, got %s" % - (oid, expected.hex(), got.hex()) - ) - - -def check_uper_codec_reference_decode(): - # type: () -> None - for _typename, _value, codec, kwargs, expected, encoded in DECODE_VECTORS: - decoded, _remain = codec.do_dec(encoded, **kwargs) - assert decoded.val == expected, ( - "%s %r: expected %r, got %r" % - (_typename, _value, expected, decoded.val) - ) - - -def check_uper_codec_encode_reference(): - # type: () -> None - from test.scapy.layers.uper_iop import PRIMITIVE_VECTORS - - for typename, value, encoder, expected in PRIMITIVE_VECTORS: - encoded = encoder(value) - assert encoded == expected, ( - "%s %r: expected %s, got %s" % - (typename, value, expected.hex(), encoded.hex()) - ) diff --git a/test/scapy/layers/uper_fuzz.py b/test/scapy/layers/uper_fuzz.py deleted file mode 100644 index d0aa8571192..00000000000 --- a/test/scapy/layers/uper_fuzz.py +++ /dev/null @@ -1,133 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -UPER fuzzing helpers. - -Exercise UPER encode/decode paths with packet.fuzz() and random payloads. -""" - -import os -import random -from typing import Iterable, Type - -from scapy.asn1.asn1 import ASN1_Codecs, ASN1_Decoding_Error, ASN1_Error -from scapy.contrib.uper import ( - UPER_Decoding_Error, - UPER_Encoding_Error, - UPERcodec_BIT_STRING, - UPERcodec_BOOLEAN, - UPERcodec_ENUMERATED, - UPERcodec_INTEGER, - UPERcodec_NULL, - UPERcodec_OID, - UPERcodec_STRING, -) -from scapy.asn1fields import ( - ASN1F_BOOLEAN, - ASN1F_ENUMERATED, - ASN1F_INTEGER, - ASN1F_SEQUENCE, - ASN1F_SEQUENCE_OF, - ASN1F_STRING, - ASN1F_optional, -) -from scapy.asn1packet import ASN1_Packet -from scapy.packet import fuzz, raw - -_UPER_CODEC_CLASSES = ( - UPERcodec_INTEGER, - UPERcodec_BOOLEAN, - UPERcodec_NULL, - UPERcodec_STRING, - UPERcodec_OID, - UPERcodec_ENUMERATED, - UPERcodec_BIT_STRING, -) - -_DECODE_ERRORS = ( - UPER_Decoding_Error, - UPER_Encoding_Error, - ASN1_Decoding_Error, - ASN1_Error, - ValueError, - IndexError, -) - - -class UPERFuzzRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_BOOLEAN("flag", False), - ASN1F_STRING("label", ""), - ASN1F_optional(ASN1F_INTEGER("extra", 0)), - ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), - ) - - -class UPERFuzzNested(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0), - ASN1F_BOOLEAN("y", False), - ), - ) - - -class UPERFuzzEnumerated(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_ENUMERATED( - "state", 1, {1: "alpha", 200: "beta"}, - ) - - -def _fuzz_packets(): - # type: () -> Iterable[Type[ASN1_Packet]] - return (UPERFuzzRecord, UPERFuzzNested, UPERFuzzEnumerated) - - -def check_uper_fuzz_encode(iterations=25): - # type: (int) -> None - for cls in _fuzz_packets(): - for _ in range(iterations): - try: - data = raw(fuzz(cls())) - except _DECODE_ERRORS: - continue - assert isinstance(data, bytes) - - -def check_uper_fuzz_roundtrip(iterations=25): - # type: (int) -> None - for cls in _fuzz_packets(): - for _ in range(iterations): - try: - cls(raw(fuzz(cls()))) - except _DECODE_ERRORS: - pass - - -def check_uper_fuzz_codec_decode(iterations=100): - # type: (int) -> None - for codec in _UPER_CODEC_CLASSES: - for _ in range(iterations): - data = os.urandom(random.randint(0, 64)) - try: - codec.safedec(data) - except _DECODE_ERRORS: - pass - - -def check_uper_fuzz_packet_decode(iterations=100): - # type: (int) -> None - for cls in _fuzz_packets(): - for _ in range(iterations): - data = os.urandom(random.randint(0, 128)) - try: - cls(data) - except _DECODE_ERRORS: - pass diff --git a/test/scapy/layers/uper_helpers.py b/test/scapy/layers/uper_helpers.py deleted file mode 100644 index dc92ef18e68..00000000000 --- a/test/scapy/layers/uper_helpers.py +++ /dev/null @@ -1,122 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -UPER low-level helper and bitstream tests. -""" - -from scapy.contrib.uper import ( - UPER_Decoder, - UPER_Encoder, - UPER_choice_index_dec, - UPER_choice_index_enc, - UPER_constrained_int_dec, - UPER_constrained_int_enc, - UPER_count_dec, - UPER_count_enc, - UPER_has_unexpected_remainder, - UPER_join_encodings, - UPER_octet_string_dec, - UPER_octet_string_enc, - UPER_optional_presence_enc, - UPERcodec_INTEGER, -) - - -def check_uper_length_determinant(): - # type: () -> None - for length, expected in [ - (0, b"\x00"), - (1, b"\x01"), - (127, b"\x7f"), - (128, b"\x80\x80"), - (16383, b"\xbf\xff"), - (16384, b"\xc1"), - ]: - enc = UPER_Encoder() - enc.append_length_determinant(length) - assert enc.as_bytes() == expected - - -def check_uper_count_roundtrip(): - # type: () -> None - for count in [0, 1, 3, 127]: - enc = UPER_Encoder() - UPER_count_enc(count, enc=enc) - got, _ = UPER_count_dec(enc.as_bytes()) - assert got == count - - -def check_uper_choice_index_roundtrip(): - # type: () -> None - for index, choices in [(0, 2), (1, 5), (3, 5)]: - enc = UPER_Encoder() - UPER_choice_index_enc(index, choices, enc=enc) - got, _ = UPER_choice_index_dec(enc.as_bytes(), choices) - assert got == index - - -def check_uper_optional_presence(): - # type: () -> None - enc = UPER_Encoder() - UPER_optional_presence_enc([0, 1, 0], enc=enc) - assert enc.as_bytes() == b"\x40" - - -def check_uper_constrained_integer(): - # type: () -> None - data = UPER_constrained_int_enc(10, 0, 15) - value, remain = UPER_constrained_int_dec(data, 0, 15) - assert value == 10 - assert remain == b"" - - -def check_uper_constrained_signed_integer(): - # type: () -> None - for value, expected in [(0, b"\x80"), (-1, b"\x7f"), (127, b"\xff"), (-128, b"\x00")]: - data = UPER_constrained_int_enc(value, -128, 127) - assert data == expected - decoded, remain = UPER_constrained_int_dec(data, -128, 127) - assert decoded == value - assert remain == b"" - - -def check_uper_octet_string_roundtrip(): - # type: () -> None - for data, minimum, maximum in [ - (b"AB", None, None), - (b"\x12\x34\x56", 3, 3), - (bytes.fromhex("afbc4583"), 1, 20), - ]: - encoded = UPER_octet_string_enc(data, minimum, maximum) - dec = UPER_Decoder(encoded) - decoded, _ = UPER_octet_string_dec(encoded, minimum, maximum, dec=dec) - assert decoded == data - assert not UPER_has_unexpected_remainder(dec) - - -def check_uper_has_unexpected_remainder(): - # type: () -> None - assert UPER_has_unexpected_remainder(UPER_Decoder(b"\x00")) is False - assert UPER_has_unexpected_remainder(UPER_Decoder(b"\x80")) is True - - -def check_uper_join_encodings(): - # type: () -> None - a = UPERcodec_INTEGER.enc(1) - b = UPERcodec_INTEGER.enc(2) - joined = UPER_join_encodings(a, b) - dec = UPER_Decoder(joined) - assert dec.read_unconstrained_whole_number() == 1 - assert dec.read_unconstrained_whole_number() == 2 - - -def check_uper_chained_encode_into(): - # type: () -> None - enc = UPER_Encoder() - UPERcodec_INTEGER.encode_into(enc, 42) - UPERcodec_INTEGER.encode_into(enc, -7) - dec = UPER_Decoder(enc.as_bytes()) - assert dec.read_unconstrained_whole_number() == 42 - assert dec.read_unconstrained_whole_number() == -7 diff --git a/test/scapy/layers/uper_iop.py b/test/scapy/layers/uper_iop.py deleted file mode 100644 index af36a6dda7e..00000000000 --- a/test/scapy/layers/uper_iop.py +++ /dev/null @@ -1,189 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -UPER interoperability helpers. - -Cross-check Scapy's UPER codec against reference encodings (from asn1tools). -""" - -from typing import Any - -from scapy.contrib.uper import ( - UPERcodec_BOOLEAN, - UPERcodec_ENUMERATED, - UPERcodec_INTEGER, - UPERcodec_NULL, - UPERcodec_STRING, - UPER_Encoder, - UPER_choice_index_enc, -) -from scapy.packet import raw - -from test.scapy.layers.uper_packets import ( - UPERMultiOptional, - UPERNestedSequence, -) - -# (type name, value, scapy encoder callable, reference encoding) -PRIMITIVE_VECTORS = [ - ("A", True, lambda v: UPERcodec_BOOLEAN.enc(1 if v else 0), b"\x80"), - ("A", False, lambda v: UPERcodec_BOOLEAN.enc(1 if v else 0), b"\x00"), - ("B", 42, lambda v: UPERcodec_INTEGER.enc(v), b"\x01*"), - ("B", -1, lambda v: UPERcodec_INTEGER.enc(v), b"\x01\xff"), - ( - "C", - 200, - lambda v: UPERcodec_INTEGER.enc(v, uper_min=0, uper_max=255), - b"\xc8", - ), - ("D", b"AB", lambda v: UPERcodec_STRING.enc(v), b"\x02AB"), - ( - "E", - b"\x12\x34\x56", - lambda v: UPERcodec_STRING.enc(v, size_len=3), - b"\x12\x34\x56", - ), - ("G", None, lambda v: UPERcodec_NULL.enc(None), b""), - ( - "H", - "beta", - lambda v: UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), - b"\x80", - ), -] - -# (type name, value, reference encoding) -COMPOSITE_VECTORS = [ - ("Seq", {"id": 42, "flag": True}, b"\x00\x95@"), - ("Seq", {"id": 42, "flag": True, "extra": 7}, b"\x80\x95@A\xc0"), - ("SeqOf", [1, 2, 3], b"\x03\x01\x01\x01\x02\x01\x03"), - ("SeqOfC", [1, 200, 0], b"\x03\x01\xc8\x00"), - ("Choice", ("a", 99), b"\x00\xb1\x80"), - ("Choice", ("b", b"AB"), b"\x81 \xa1\x00"), - ("ChoiceC", ("a", 10), b"P"), - ("ChoiceC", ("b", b"AB"), b"\x81 \xa1\x00"), -] - -DECODE_PACKET_VECTORS = [ - ( - UPERNestedSequence, - {"id": 5, "x": 3, "y": True}, - bytes.fromhex("0105010380"), - ), - ( - UPERMultiOptional, - {"id": 1, "a": 2, "b": b"hi"}, - bytes.fromhex("c0404040809a1a40"), - ), -] - -PACKET_REFERENCE_VECTORS = [ - ( - UPERNestedSequence, - {"id": 5, "x": 3, "y": True}, - bytes.fromhex("0105010380"), - ), - ( - UPERMultiOptional, - {"id": 1, "a": 2, "b": b"hi"}, - bytes.fromhex("c0404040809a1a40"), - ), -] - - -def check_primitive_interop(): - # type: () -> None - for typename, value, encoder, expected in PRIMITIVE_VECTORS: - got = encoder(value) - assert got == expected, ( - "%s %r: expected %s, got %s" % - (typename, value, expected.hex(), got.hex()) - ) - - -def check_composite_interop(): - # type: () -> None - for typename, value, expected in COMPOSITE_VECTORS: - got = _encode_composite(typename, value) - assert got == expected, ( - "%s %r: expected %s, got %s" % - (typename, value, expected.hex(), got.hex()) - ) - - -def check_packet_reference_interop(): - # type: () -> None - for cls, pkt_kwargs, expected in PACKET_REFERENCE_VECTORS: - got = raw(cls(**pkt_kwargs)) - assert got == expected, ( - "%s: expected %s, got %s" % - (cls.__name__, expected.hex(), got.hex()) - ) - decoded = cls(got) - for key, value in pkt_kwargs.items(): - field = getattr(decoded, key) - if value is None: - assert field is None - elif isinstance(value, bool): - assert field.val == (1 if value else 0) - else: - assert field.val == value - - -def check_packet_decode_vectors(): - # type: () -> None - for cls, pkt_kwargs, data in DECODE_PACKET_VECTORS: - decoded = cls(data) - for key, value in pkt_kwargs.items(): - field = getattr(decoded, key) - if isinstance(value, bool): - assert field.val == (1 if value else 0) - else: - assert field.val == value - - -def _encode_composite(typename, value): - # type: (str, Any) -> bytes - enc = UPER_Encoder() - if typename == "Seq": - enc.append_bit(1 if value.get("extra") is not None else 0) - UPERcodec_INTEGER.encode_into(enc, value["id"]) - UPERcodec_BOOLEAN.encode_into(enc, 1 if value["flag"] else 0) - if value.get("extra") is not None: - UPERcodec_INTEGER.encode_into(enc, value["extra"]) - return enc.as_bytes() - if typename == "SeqOf": - enc.append_length_determinant(len(value)) - for item in value: - UPERcodec_INTEGER.encode_into(enc, item) - return enc.as_bytes() - if typename == "SeqOfC": - enc.append_length_determinant(len(value)) - for item in value: - UPERcodec_INTEGER.encode_into( - enc, item, uper_min=0, uper_max=255, - ) - return enc.as_bytes() - if typename == "Choice": - alt, payload = value - index = 0 if alt == "a" else 1 - UPER_choice_index_enc(index, 2, enc=enc) - if alt == "a": - UPERcodec_INTEGER.encode_into(enc, payload) - else: - UPERcodec_STRING.encode_into(enc, payload) - return enc.as_bytes() - if typename == "ChoiceC": - alt, payload = value - index = 0 if alt == "a" else 1 - UPER_choice_index_enc(index, 2, enc=enc) - if alt == "a": - UPERcodec_INTEGER.encode_into( - enc, payload, uper_min=0, uper_max=15, - ) - else: - UPERcodec_STRING.encode_into(enc, payload) - return enc.as_bytes() - raise ValueError("unknown composite type %s" % typename) diff --git a/test/scapy/layers/uper_packets.py b/test/scapy/layers/uper_packets.py deleted file mode 100644 index 0dec76a9002..00000000000 --- a/test/scapy/layers/uper_packets.py +++ /dev/null @@ -1,543 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -""" -UPER ASN1_Packet and ASN1F_field tests. -""" -import scapy.contrib.uper # noqa: F401 # register UPER stem - -from scapy.asn1.asn1 import ASN1_Codecs, ASN1_INTEGER, ASN1_STRING -from scapy.asn1fields import ( - ASN1F_BIT_STRING, - ASN1F_BOOLEAN, - ASN1F_CHOICE, - ASN1F_ENUMERATED, - ASN1F_INTEGER, - ASN1F_NULL, - ASN1F_SEQUENCE, - ASN1F_SEQUENCE_OF, - ASN1F_STRING, - ASN1F_optional, -) -from scapy.asn1packet import ASN1_Packet -from scapy.packet import raw - - -class UPERFixedFields(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), - ASN1F_STRING("s", "", size_len=3), - ) - - -class UPERIntegerField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0) - - -class UPERBooleanField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_BOOLEAN("b", False) - - -class UPERStringField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_STRING("s", "") - - -class UPERConstrainedInteger(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER( - "n", 0, size_len=1, oer_unsigned=True, - ) - - -class UPEROptionalField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_BOOLEAN("flag", False), - ASN1F_optional(ASN1F_INTEGER("extra", 0)), - ) - - -class UPERSequenceOfIntegers(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) - - -class UPERChoiceField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - - -class UPERChoiceStringFirst(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_STRING(b""), ASN1F_STRING, ASN1F_INTEGER, - ) - - -class UPERRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_BOOLEAN("flag", False), - ASN1F_STRING("label", ""), - ASN1F_optional(ASN1F_INTEGER("extra", 0)), - ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), - ) - - -class UPEREnumeratedField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_ENUMERATED( - "state", 1, {1: "alpha", 200: "beta"}, - ) - - -class UPERBitStringField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_BIT_STRING( - "bits", "0", uper_min=1, uper_max=20, - ) - - -class UPERMessagePrefix(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("msgId", 0), - ASN1F_INTEGER("myflag", 0), - ASN1F_STRING("szDescription", "", size_len=10), - ASN1F_BOOLEAN("isReady", False), - ) - - -class UPERSequenceWithChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_CHOICE("c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING), - ) - - -class UPERNullPacket(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_NULL("n", None) - - -class UPERVariableOctetString(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_STRING("data", "", uper_min=1, uper_max=20) - - -class UPERConstrainedRangeInt(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=15) - - -class UPERSequenceWithEnumerated(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_ENUMERATED("state", 1, {1: "alpha", 200: "beta"}), - ) - - -class UPERSequenceOfStrings(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE_OF("items", [], ASN1F_STRING) - - -class UPERNestedSequence(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0), - ASN1F_BOOLEAN("y", False), - ), - ) - - -class UPERSequenceWithNull(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_NULL("n", None), - ) - - -class UPERFixedBitString(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_BIT_STRING("b", "0", uper_min=16, uper_max=16) - - -class UPERSequenceOfConstrainedInts(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE_OF( - "values", [], ASN1F_INTEGER("item", 0, uper_min=0, uper_max=255), - ) - - -class UPERSignedInteger(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=-128, uper_max=127) - - -class UPERMultiOptional(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_optional(ASN1F_INTEGER("a", 0)), - ASN1F_optional(ASN1F_STRING("b", "")), - ) - - -def _roundtrip(cls, pkt): - # type: (type, ASN1_Packet) -> ASN1_Packet - return cls(raw(pkt)) - - -def check_uper_field_fixed_size(): - # type: () -> None - pkt = UPERFixedFields(n=200, s=b"ABC") - assert raw(pkt) == b"\xc8ABC" - decoded = _roundtrip(UPERFixedFields, pkt) - assert decoded.n.val == 200 - assert decoded.s.val == b"ABC" - - -def check_uper_field_integer(): - # type: () -> None - pkt = UPERIntegerField(n=12345) - assert raw(pkt) == bytes.fromhex("023039") - decoded = _roundtrip(UPERIntegerField, pkt) - assert decoded.n.val == 12345 - - -def check_uper_field_boolean(): - # type: () -> None - true_pkt = UPERBooleanField(b=True) - assert raw(true_pkt) == b"\x80" - decoded = _roundtrip(UPERBooleanField, true_pkt) - assert decoded.b.val == 1 - - false_pkt = UPERBooleanField(b=False) - assert raw(false_pkt) == b"\x00" - decoded = _roundtrip(UPERBooleanField, false_pkt) - assert decoded.b.val == 0 - - -def check_uper_field_string(): - # type: () -> None - pkt = UPERStringField(s=b"hi") - assert raw(pkt) == bytes.fromhex("026869") - decoded = _roundtrip(UPERStringField, pkt) - assert decoded.s.val == b"hi" - - -def check_uper_field_constrained_integer(): - # type: () -> None - pkt = UPERConstrainedInteger(n=200) - assert raw(pkt) == b"\xc8" - decoded = _roundtrip(UPERConstrainedInteger, pkt) - assert decoded.n.val == 200 - - -def check_uper_field_optional(): - # type: () -> None - present = UPEROptionalField(id=42, flag=True, extra=7) - assert raw(present) == bytes.fromhex("80954041c0") - decoded = _roundtrip(UPEROptionalField, present) - assert decoded.id.val == 42 - assert decoded.flag.val == 1 - assert decoded.extra.val == 7 - - absent = UPEROptionalField(id=42, flag=True, extra=None) - assert raw(absent) == bytes.fromhex("009540") - decoded = _roundtrip(UPEROptionalField, absent) - assert decoded.id.val == 42 - assert decoded.flag.val == 1 - assert decoded.extra is None - - -def check_uper_field_sequence_of(): - # type: () -> None - pkt = UPERSequenceOfIntegers(values=[1, 2, 3]) - assert raw(pkt) == bytes.fromhex("03010101020103") - decoded = _roundtrip(UPERSequenceOfIntegers, pkt) - assert [x.val for x in decoded.values] == [1, 2, 3] - - empty = UPERSequenceOfIntegers(values=[]) - assert raw(empty) == b"\x00" - decoded = _roundtrip(UPERSequenceOfIntegers, empty) - assert [x.val for x in decoded.values] == [] - - -def check_uper_field_choice(): - # type: () -> None - as_int = UPERChoiceField(c=ASN1_INTEGER(99)) - assert raw(as_int) == bytes.fromhex("00b180") - decoded = _roundtrip(UPERChoiceField, as_int) - assert decoded.c.val == 99 - - as_str = UPERChoiceField(c=ASN1_STRING(b"AB")) - assert raw(as_str) == bytes.fromhex("8120a100") - decoded = _roundtrip(UPERChoiceField, as_str) - assert decoded.c.val == b"AB" - - -def check_uper_field_choice_definition_order(): - # type: () -> None - as_str = UPERChoiceStringFirst(c=ASN1_STRING(b"AB")) - assert raw(as_str) == bytes.fromhex("0120a100") - decoded = _roundtrip(UPERChoiceStringFirst, as_str) - assert decoded.c.val == b"AB" - - as_int = UPERChoiceStringFirst(c=ASN1_INTEGER(99)) - assert raw(as_int) == bytes.fromhex("80b180") - decoded = _roundtrip(UPERChoiceStringFirst, as_int) - assert decoded.c.val == 99 - - -def check_uper_packet_record(): - # type: () -> None - full = UPERRecord( - id=42, - flag=True, - label=b"hi", - extra=7, - values=[1, 2, 3], - ) - assert raw(full) == bytes.fromhex("8095409a1a4041c0c04040408040c0") - decoded = _roundtrip(UPERRecord, full) - assert decoded.id.val == 42 - assert decoded.flag.val == 1 - assert decoded.label.val == b"hi" - assert decoded.extra.val == 7 - assert [x.val for x in decoded.values] == [1, 2, 3] - - pkt = UPERRecord( - id=42, - flag=True, - label=b"AB", - extra=None, - values=[1, 2], - ) - body = bytes.fromhex("0095409050808040404080") - assert raw(pkt) == body - decoded = _roundtrip(UPERRecord, pkt) - assert decoded.id.val == 42 - assert decoded.flag.val == 1 - assert decoded.label.val == b"AB" - assert decoded.extra is None - assert [x.val for x in decoded.values] == [1, 2] - - empty = UPERRecord( - id=1, - flag=False, - label=b"", - extra=None, - values=[], - ) - assert raw(empty) == bytes.fromhex("0080800000") - decoded = _roundtrip(UPERRecord, empty) - assert decoded.id.val == 1 - assert decoded.flag.val == 0 - assert decoded.label.val == b"" - assert decoded.extra is None - assert [x.val for x in decoded.values] == [] - - -def check_uper_field_enumerated(): - # type: () -> None - alpha = UPEREnumeratedField(state=1) - assert raw(alpha) == b"\x00" - decoded = _roundtrip(UPEREnumeratedField, alpha) - assert decoded.state.val == 1 - - beta = UPEREnumeratedField(state=200) - assert raw(beta) == b"\x80" - decoded = _roundtrip(UPEREnumeratedField, beta) - assert decoded.state.val == 200 - - -def check_uper_field_bit_string(): - # type: () -> None - from scapy.asn1.asn1 import ASN1_BIT_STRING - - pkt = UPERBitStringField(bits=ASN1_BIT_STRING("1010101111001101")) - assert raw(pkt) == bytes.fromhex("7d5e68") - decoded = _roundtrip(UPERBitStringField, pkt) - assert decoded.bits.val == "1010101111001101" - - -def check_uper_message_prefix(): - # type: () -> None - pkt = UPERMessagePrefix( - msgId=1, - myflag=2, - szDescription=b"HelloWorld", - isReady=True, - ) - assert raw(pkt) == bytes.fromhex("0101010248656c6c6f576f726c6480") - decoded = _roundtrip(UPERMessagePrefix, pkt) - assert decoded.msgId.val == 1 - assert decoded.myflag.val == 2 - assert decoded.szDescription.val == b"HelloWorld" - assert decoded.isReady.val == 1 - - -def check_uper_sequence_with_choice(): - # type: () -> None - pkt = UPERSequenceWithChoice(id=42, c=ASN1_INTEGER(99)) - body = raw(pkt) - decoded = UPERSequenceWithChoice(body) - assert decoded.id.val == 42 - assert decoded.c.val == 99 - - as_str = UPERSequenceWithChoice(id=1, c=ASN1_STRING(b"AB")) - decoded = UPERSequenceWithChoice(raw(as_str)) - assert decoded.id.val == 1 - assert decoded.c.val == b"AB" - - -def check_uper_null_packet(): - # type: () -> None - pkt = UPERNullPacket() - assert raw(pkt) == b"" - decoded = _roundtrip(UPERNullPacket, pkt) - assert decoded.n is None - - -def check_uper_variable_octet_string(): - # type: () -> None - pkt = UPERVariableOctetString(data=bytes.fromhex("afbc4583")) - assert raw(pkt) == bytes.fromhex("1d7de22c18") - decoded = _roundtrip(UPERVariableOctetString, pkt) - assert decoded.data.val == bytes.fromhex("afbc4583") - - -def check_uper_constrained_range_integer(): - # type: () -> None - pkt = UPERConstrainedRangeInt(n=10) - assert raw(pkt) == b"\xa0" - decoded = _roundtrip(UPERConstrainedRangeInt, pkt) - assert decoded.n.val == 10 - - -def check_uper_sequence_with_enumerated(): - # type: () -> None - pkt = UPERSequenceWithEnumerated(id=1, state=200) - assert raw(pkt) == bytes.fromhex("010180") - decoded = _roundtrip(UPERSequenceWithEnumerated, pkt) - assert decoded.id.val == 1 - assert decoded.state.val == 200 - - alpha = UPERSequenceWithEnumerated(id=7, state=1) - assert raw(alpha) == bytes.fromhex("010700") - decoded = _roundtrip(UPERSequenceWithEnumerated, alpha) - assert decoded.state.val == 1 - - -def check_uper_sequence_of_strings(): - # type: () -> None - pkt = UPERSequenceOfStrings(items=[b"A", b"BC"]) - assert raw(pkt) == bytes.fromhex("020141024243") - decoded = _roundtrip(UPERSequenceOfStrings, pkt) - assert [x.val for x in decoded.items] == [b"A", b"BC"] - - empty = UPERSequenceOfStrings(items=[]) - assert raw(empty) == b"\x00" - decoded = _roundtrip(UPERSequenceOfStrings, empty) - assert [x.val for x in decoded.items] == [] - - -def check_uper_sequence_choice_hex(): - # type: () -> None - """Cross-check against reference composite encoding.""" - pkt = UPERSequenceWithChoice(id=1, c=ASN1_INTEGER(99)) - assert raw(pkt) == bytes.fromhex("010100b180") - decoded = UPERSequenceWithChoice(raw(pkt)) - assert decoded.id.val == 1 - assert decoded.c.val == 99 - - -def check_uper_nested_sequence(): - # type: () -> None - pkt = UPERNestedSequence(id=5, x=3, y=True) - assert raw(pkt) == bytes.fromhex("0105010380") - decoded = _roundtrip(UPERNestedSequence, pkt) - assert decoded.id.val == 5 - assert decoded.x.val == 3 - assert decoded.y.val == 1 - - -def check_uper_sequence_with_null(): - # type: () -> None - pkt = UPERSequenceWithNull(id=1) - assert raw(pkt) == bytes.fromhex("0101") - decoded = _roundtrip(UPERSequenceWithNull, pkt) - assert decoded.id.val == 1 - assert getattr(decoded.n, "val", decoded.n) is None - - -def check_uper_fixed_bit_string(): - # type: () -> None - from scapy.asn1.asn1 import ASN1_BIT_STRING - - pkt = UPERFixedBitString(b=ASN1_BIT_STRING("1010101111001101")) - assert raw(pkt) == bytes.fromhex("abcd") - decoded = _roundtrip(UPERFixedBitString, pkt) - assert decoded.b.val == "1010101111001101" - - -def check_uper_sequence_of_constrained_ints(): - # type: () -> None - pkt = UPERSequenceOfConstrainedInts(values=[1, 200, 0]) - assert raw(pkt) == bytes.fromhex("0301c800") - decoded = _roundtrip(UPERSequenceOfConstrainedInts, pkt) - assert [x.val for x in decoded.values] == [1, 200, 0] - - -def check_uper_signed_integer(): - # type: () -> None - for value, expected in [ - (0, b"\x80"), - (-1, b"\x7f"), - (127, b"\xff"), - (-128, b"\x00"), - ]: - pkt = UPERSignedInteger(n=value) - assert raw(pkt) == expected - decoded = _roundtrip(UPERSignedInteger, pkt) - assert decoded.n.val == value - - -def check_uper_multi_optional(): - # type: () -> None - both = UPERMultiOptional(id=1, a=2, b=b"hi") - assert raw(both) == bytes.fromhex("c0404040809a1a40") - decoded = _roundtrip(UPERMultiOptional, both) - assert decoded.id.val == 1 - assert decoded.a.val == 2 - assert decoded.b.val == b"hi" - - none = UPERMultiOptional(id=1, a=None, b=None) - assert raw(none) == bytes.fromhex("004040") - decoded = _roundtrip(UPERMultiOptional, none) - assert decoded.id.val == 1 - assert decoded.a is None - assert decoded.b is None - - only_a = UPERMultiOptional(id=3, a=9, b=None) - assert raw(only_a) == bytes.fromhex("8040c04240") - decoded = _roundtrip(UPERMultiOptional, only_a) - assert decoded.id.val == 3 - assert decoded.a.val == 9 - assert decoded.b is None From 00d28164bebee52be5590675d2060d75a7edff5c Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Sat, 8 Aug 2026 07:11:14 +0200 Subject: [PATCH 03/46] Cleanup asn1fields AI-Assisted: yes (Cursor) --- scapy/asn1/asn1.py | 5 + scapy/asn1fields.py | 696 ++++++++---------------------------------- scapy/contrib/oer.py | 127 ++++++++ scapy/contrib/uper.py | 491 +++++++++++++++++++++++++++++ 4 files changed, 746 insertions(+), 573 deletions(-) diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index dbf865f3ee6..c2b556c855d 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -132,6 +132,11 @@ def register_tagging(cls, enc, dec): cls._tagging_enc = enc cls._tagging_dec = dec + def register_field_hooks(cls, hooks): + # type: (Any) -> None + # Optional compound-field helpers (SEQUENCE/CHOICE/…) for contrib codecs. + cls._field_hooks = hooks + def tagging_enc(cls, s, **kwargs): # type: (bytes, **Any) -> bytes return cls._tagging_enc(s, **kwargs) # type: ignore diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index d5e4f932fb3..81fd9b007cb 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -9,30 +9,14 @@ """ import copy + from functools import reduce -from typing import ( - Any, - AnyStr, - Callable, - Dict, - Generic, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, - cast, - TYPE_CHECKING, -) -from scapy import packet from scapy.asn1.asn1 import ( ASN1_BIT_STRING, ASN1_BOOLEAN, ASN1_Class, ASN1_Class_UNIVERSAL, - ASN1_Codecs, ASN1_Decoding_Error, ASN1_Error, ASN1_INTEGER, @@ -56,20 +40,26 @@ RandField, ) -if TYPE_CHECKING: - from scapy.asn1packet import ASN1_Packet - - -def _oer(): - # type: () -> Any - from scapy.contrib import oer as _m - return _m +from scapy import packet +from typing import ( + Any, + AnyStr, + Callable, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, + TYPE_CHECKING, +) -def _uper(): - # type: () -> Any - from scapy.contrib import uper as _m - return _m +if TYPE_CHECKING: + from scapy.asn1packet import ASN1_Packet class ASN1F_badsequence(Exception): @@ -102,10 +92,7 @@ def __init__(self, explicit_tag=None, # type: Optional[int] flexible_tag=False, # type: Optional[bool] size_len=None, # type: Optional[int] - oer_unsigned=False, # type: Optional[bool] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - uper_enum_values=None, # type: Optional[List[int]] + **codec_opts # type: Any ): # type: (...) -> None if context is not None: @@ -118,10 +105,11 @@ def __init__(self, else: self.default = self.ASN1_tag.asn1_object(default) # type: ignore self.size_len = size_len - self.oer_unsigned = oer_unsigned - self.uper_min = uper_min - self.uper_max = uper_max - self.uper_enum_values = uper_enum_values + # Contrib codecs (OER/UPER/…) pass constraints here, e.g. + # oer_unsigned=, uper_min=/uper_max=, uper_extensible=. + self.codec_opts = codec_opts # type: Dict[str, Any] + for key, val in codec_opts.items(): + setattr(self, key, val) self.flexible_tag = flexible_tag if (implicit_tag is not None) and (explicit_tag is not None): err_msg = "field cannot be both implicitly and explicitly tagged" @@ -131,7 +119,6 @@ def __init__(self, # network_tag gets useful for ASN1F_CHOICE self.network_tag = int(implicit_tag or explicit_tag or self.ASN1_tag) self.owners = [] # type: List[Type[ASN1_Packet]] - self._uper_kwargs_cache = None # type: Optional[Dict[str, Any]] def register_owner(self, cls): # type: (Type[ASN1_Packet]) -> None @@ -175,25 +162,19 @@ def _apply_tagging_dec(self, s, pkt, hidden_tag=None, **kwargs): def _codec_kwargs(self, pkt): # type: (ASN1_Packet) -> Dict[str, Any] - # OER/UPER need extra constraints on every enc/dec call. - if pkt.ASN1_codec == ASN1_Codecs.PER: - return self._uper_codec_kwargs() + # BER ignores unknown keys via **_kwargs. Contrib codecs read + # constraints from field.codec_opts. kwargs = {"size_len": self.size_len} # type: Dict[str, Any] - if pkt.ASN1_codec == ASN1_Codecs.OER: - kwargs["size_len"] = self.size_len or 0 - if self.oer_unsigned: - kwargs["oer_unsigned"] = self.oer_unsigned + kwargs.update(self.codec_opts) return kwargs def _use_object_enc(self, pkt, item): # type: (ASN1_Packet, ASN1_Object[Any]) -> bool - # BER/LDAP: item.enc() when size_len is unset. PER/OER constraints - # must go through codec.enc(**kwargs). - if pkt.ASN1_codec == ASN1_Codecs.PER: - return False - if pkt.ASN1_codec == ASN1_Codecs.OER: - return self.size_len is None and not self.oer_unsigned - return self.size_len is None + # Contrib codecs may force codec.enc(**kwargs) via field hooks. + hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) + if hooks is not None and hasattr(hooks, "use_object_enc"): + return hooks.use_object_enc(self, pkt, item) + return self.size_len is None and not self.codec_opts def _encode_item(self, pkt, item): # type: (ASN1_Packet, Any) -> bytes @@ -215,7 +196,7 @@ def _encode_item(self, pkt, item): item = item.val elif hasattr(item, "self_build"): # Packet values (e.g. ASN1F_STRING_PacketField) must still go through - # the type codec so the universal tag/length are applied. + # the BER type codec so the universal tag/length are applied. item = item.self_build() codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) return codec.enc(item, **self._codec_kwargs(pkt)) @@ -247,20 +228,6 @@ def m2i(self, pkt, s): dec = codec.safedec if self.flexible_tag else codec.dec return dec(s, context=self.context, **self._codec_kwargs(pkt)) # type: ignore - def m2i_from_decoder(self, pkt, dec): - # type: (ASN1_Packet, Any) -> _A - codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - return cast( - _A, - codec.dec_from_decoder( # type: ignore[attr-defined] - dec, **self._codec_kwargs(pkt), - ), - ) - - def dissect_from_decoder(self, pkt, dec): - # type: (ASN1_Packet, Any) -> None - self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) - def i2m(self, pkt, x): # type: (ASN1_Packet, Union[bytes, _I, _A]) -> bytes if x is None: @@ -340,54 +307,6 @@ def copy(self): # type: () -> ASN1F_field[_I, _A] return copy.copy(self) - def _uper_codec_kwargs(self, size_len=None): - # type: (Optional[int]) -> Dict[str, Any] - # These kwargs only depend on attributes set once at __init__ time, - # so the common (no override) case is cached to avoid rebuilding the - # dict on every field access during build/dissect. - if size_len is None and self._uper_kwargs_cache is not None: - return self._uper_kwargs_cache - kwargs = { - "size_len": (self.size_len if size_len is None else size_len) or 0, - "oer_unsigned": self.oer_unsigned, - "uper_min": self.uper_min, - "uper_max": self.uper_max, - } # type: Dict[str, Any] - if ( - getattr(self, "uper_extensible", False) and - self.ASN1_tag == ASN1_Class_UNIVERSAL.INTEGER - ): - kwargs["uper_extensible"] = True - if self.uper_enum_values is not None: - kwargs["uper_enum_values"] = self.uper_enum_values - if size_len is None: - self._uper_kwargs_cache = kwargs - return kwargs - - def _uper_encode_into(self, enc, pkt, value=None): - # type: (Any, ASN1_Packet, Any) -> None - if value is None: - value = getattr(pkt, self.name) - if value is None: - return - codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - if isinstance(value, ASN1_Object): - if (self.ASN1_tag == ASN1_Class_UNIVERSAL.ANY or - value.tag == ASN1_Class_UNIVERSAL.RAW or - value.tag == ASN1_Class_UNIVERSAL.ERROR or - self.ASN1_tag == value.tag): - raw = value.val - else: - raise ASN1_Error( - "Encoding Error: got %r instead of an %r for field [%s]" % - (value, self.ASN1_tag, self.name) - ) - else: - raw = value - codec.encode_into( # type: ignore[attr-defined] - enc, raw, **self._codec_kwargs(pkt), - ) - ############################ # Simple ASN1 Fields # @@ -404,32 +323,9 @@ def randval(self): class ASN1F_INTEGER(ASN1F_field[int, ASN1_INTEGER]): ASN1_tag = ASN1_Class_UNIVERSAL.INTEGER - def __init__(self, - name, # type: str - default, # type: Optional[Union[int, ASN1_INTEGER]] - context=None, # type: Optional[Type[ASN1_Class]] - implicit_tag=None, # type: Optional[int] - explicit_tag=None, # type: Optional[int] - flexible_tag=False, # type: Optional[bool] - size_len=None, # type: Optional[int] - oer_unsigned=False, # type: Optional[bool] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - uper_extensible=False, # type: bool - ): - # type: (...) -> None - super(ASN1F_INTEGER, self).__init__( - name, cast(Optional[ASN1_INTEGER], default), context=context, - implicit_tag=implicit_tag, explicit_tag=explicit_tag, - flexible_tag=flexible_tag, size_len=size_len, - oer_unsigned=oer_unsigned, uper_min=uper_min, - uper_max=uper_max, - ) - self.uper_extensible = uper_extensible - def randval(self): # type: () -> RandNum - return RandNum(-2 ** 64, 2 ** 64 - 1) + return RandNum(-2**64, 2**64 - 1) class ASN1F_enum_INTEGER(ASN1F_INTEGER): @@ -458,7 +354,6 @@ def __init__(self, for k in keys: i2s[k] = enum[k] s2i[enum[k]] = k - self.uper_enum_values = list(keys) def i2m(self, pkt, # type: ASN1_Packet @@ -493,16 +388,14 @@ def __init__(self, context=None, # type: Optional[Any] implicit_tag=None, # type: Optional[int] explicit_tag=None, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] + **codec_opts # type: Any ): # type: (...) -> None super(ASN1F_BIT_STRING, self).__init__( name, None, context=context, implicit_tag=implicit_tag, explicit_tag=explicit_tag, - uper_min=uper_min, - uper_max=uper_max, + **codec_opts, ) if isinstance(default, (bytes, str)): self.default = ASN1_BIT_STRING(default, @@ -612,18 +505,13 @@ class ASN1F_SEQUENCE(ASN1F_field[List[Any], List[Any]]): def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None - uper_extensible = kwargs.pop("uper_extensible", False) name = "dummy_seq_name" default = [field.default for field in seq] super(ASN1F_SEQUENCE, self).__init__( name, default, **kwargs ) - self.uper_extensible = uper_extensible self.seq = seq self.islist = len(seq) > 1 - self._optionals = tuple( - f for f in seq if isinstance(f, (ASN1F_optional, ASN1F_DEFAULT)) - ) def __repr__(self): # type: () -> str @@ -651,33 +539,6 @@ def _dissect_sequence_children(self, pkt, s): break return s - def _m2i_oer(self, pkt, s): - # type: (Any, bytes) -> Tuple[Any, bytes] - s = self._apply_tagging_dec(s, pkt, _fname=pkt.name) - s = self._dissect_sequence_children(pkt, s) - return [], s - - def _m2i_per(self, pkt, s): - # type: (Any, bytes) -> Tuple[Any, bytes] - dec = _uper().UPER_Decoder(s) - self._uper_dissect_from_decoder(pkt, dec) - if _uper().UPER_has_unexpected_remainder(dec): - raise _uper().UPER_Decoding_Error( - "unexpected remainder", - remaining=dec.remaining(), - ) - return [], b"" - - def _m2i_ber(self, pkt, s): - # type: (Any, bytes) -> Tuple[Any, bytes] - s = self._apply_tagging_dec(s, pkt, _fname=pkt.name) - codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - i, s, remain = codec.check_type_check_len(s) - s = self._dissect_sequence_children(pkt, s) - if len(s) > 0: - raise BER_Decoding_Error("unexpected remainder", remaining=s) - return [], remain - def m2i(self, pkt, s): # type: (Any, bytes) -> Tuple[Any, bytes] """ @@ -688,36 +549,19 @@ def m2i(self, pkt, s): Thus m2i returns an empty list (along with the proper remainder). It is discarded by dissect() and should not be missed elsewhere. """ - if pkt.ASN1_codec == ASN1_Codecs.OER: - return self._m2i_oer(pkt, s) - if pkt.ASN1_codec == ASN1_Codecs.PER: - return self._m2i_per(pkt, s) - return self._m2i_ber(pkt, s) - - def _uper_dissect_from_decoder(self, pkt, dec): - # type: (Any, Any) -> None - if self.uper_extensible: - if dec.read_bit(): - raise _uper().UPER_Decoding_Error( - "ASN1F_SEQUENCE: extension additions are not supported" - ) - presence = [dec.read_bit() for _ in self._optionals] - opt_idx = 0 - for obj in self.seq: - if isinstance(obj, (ASN1F_optional, ASN1F_DEFAULT)): - if not presence[opt_idx]: - obj.set_absent(pkt) - opt_idx += 1 - continue - opt_idx += 1 - try: - obj.dissect_from_decoder(pkt, dec) - except ASN1F_badsequence: - break - - def dissect_from_decoder(self, pkt, dec): - # type: (Any, Any) -> None - self._uper_dissect_from_decoder(pkt, dec) + hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) + if hooks is not None: + return hooks.sequence_m2i(self, pkt, s) + s = self._apply_tagging_dec(s, pkt, _fname=pkt.name) + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + i, s, remain = codec.check_type_check_len(s) + s = self._dissect_sequence_children(pkt, s) + if len(s) > 0: + raise BER_Decoding_Error( + "unexpected remainder in %s" % pkt.name, + remaining=s, + ) + return [], remain def dissect(self, pkt, s): # type: (Any, bytes) -> bytes @@ -726,25 +570,13 @@ def dissect(self, pkt, s): def build(self, pkt): # type: (ASN1_Packet) -> bytes - if pkt.ASN1_codec == ASN1_Codecs.PER: - enc = _uper().UPER_Encoder() - self._uper_encode_into(enc, pkt) - return super(ASN1F_SEQUENCE, self).i2m(pkt, enc.as_bytes()) + hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) + if hooks is not None: + return hooks.sequence_build(self, pkt) s = reduce(lambda x, y: x + y.build(pkt), self.seq, b"") return super(ASN1F_SEQUENCE, self).i2m(pkt, s) - def _uper_encode_into(self, enc, pkt, value=None): - # type: (Any, ASN1_Packet, Optional[Any]) -> None - if self.uper_extensible: - enc.append_bit(0) - for opt in self._optionals: - enc.append_bit(0 if opt.is_empty(pkt) else 1) - for obj in self.seq: - if isinstance(obj, (ASN1F_optional, ASN1F_DEFAULT)) and obj.is_empty(pkt): - continue - obj._uper_encode_into(enc, pkt) - class ASN1F_SET(ASN1F_SEQUENCE): ASN1_tag = ASN1_Class_UNIVERSAL.SET @@ -759,7 +591,7 @@ class ASN1F_SET(ASN1F_SEQUENCE): class ASN1F_SEQUENCE_OF(ASN1F_field[List[_SEQ_T], -List[ASN1_Object[Any]]]): + List[ASN1_Object[Any]]]): """ Two types are allowed as cls: ASN1_Packet, ASN1F_field """ @@ -773,9 +605,7 @@ def __init__(self, context=None, # type: Optional[Any] implicit_tag=None, # type: Optional[Any] explicit_tag=None, # type: Optional[Any] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - uper_extensible=False, # type: bool + **codec_opts # type: Any ): # type: (...) -> None if isinstance(cls, type) and issubclass(cls, ASN1F_field) or \ @@ -795,31 +625,10 @@ def __init__(self, raise ValueError("cls should be an ASN1_Packet or ASN1_field") super(ASN1F_SEQUENCE_OF, self).__init__( name, None, context=context, - implicit_tag=implicit_tag, explicit_tag=explicit_tag + implicit_tag=implicit_tag, explicit_tag=explicit_tag, + **codec_opts, ) self.default = default - self.uper_min = uper_min - self.uper_max = uper_max - self.uper_extensible = uper_extensible - - def _uper_count_enc(self, enc, count): - # type: (Any, int) -> None - if self.uper_min is not None and self.uper_max is not None: - _uper().UPER_constrained_int_enc(count, self.uper_min, self.uper_max, enc=enc) - else: - enc.append_length_determinant(count) - - def _uper_count_dec(self, dec): - # type: (Any) -> int - if self.uper_min is not None and self.uper_max is not None: - size = self.uper_max - self.uper_min - return cast( - int, - dec.read_non_negative_binary_integer( - _uper().UPER_bits_for_range(size), - ) + self.uper_min, - ) - return cast(int, dec.read_length_determinant()) def is_empty(self, pkt, # type: ASN1_Packet @@ -827,90 +636,14 @@ def is_empty(self, # type: (...) -> bool return ASN1F_field.is_empty(self, pkt) - def _extract_packet_from_decoder(self, dec, pkt): - # type: (Any, ASN1_Packet) -> Tuple[Any, bytes] - if self.holds_packets: - p = self.cls() - p.add_underlayer(pkt) - p.ASN1_root.dissect_from_decoder(p, dec) - return p, b"" - return self.fld.m2i_from_decoder(pkt, dec), b"" - - def m2i_from_decoder(self, pkt, dec): - # type: (ASN1_Packet, Any) -> List[Any] - if self.uper_extensible and dec.read_bit(): - count = dec.read_length_determinant() - else: - count = self._uper_count_dec(dec) - lst = [] - for _ in range(count): - item, _ = self._extract_packet_from_decoder(dec, pkt) - lst.append(item) - return lst - - def _uper_encode_into(self, enc, pkt, value=None): - # type: (Any, ASN1_Packet, Any) -> None - if value is None: - value = getattr(pkt, self.name) - if value is None: - self._uper_count_enc(enc, 0) - return - count = len(value) - if self.uper_extensible: - if ( - self.uper_min is not None and self.uper_max is not None and - self.uper_min <= count <= self.uper_max - ): - enc.append_bit(0) - else: - enc.append_bit(1) - enc.append_length_determinant(count) - for item in value: - if self.holds_packets: - cast("ASN1_Packet", item).ASN1_root._uper_encode_into( - enc, item, - ) - else: - self.fld._uper_encode_into(enc, pkt, item) - return - self._uper_count_enc(enc, count) - for item in value: - if self.holds_packets: - cast("ASN1_Packet", item).ASN1_root._uper_encode_into( - enc, item, - ) - else: - self.fld._uper_encode_into(enc, pkt, item) - def m2i(self, pkt, # type: ASN1_Packet s, # type: bytes ): # type: (...) -> Tuple[List[Any], bytes] - if pkt.ASN1_codec == ASN1_Codecs.OER: - s = self._apply_tagging_dec(s, pkt) - count, s = _oer().OER_unsigned_integer_dec(s) - lst = [] - for _ in range(count): - c, s = self._extract_packet(s, pkt) # type: ignore - if c: - lst.append(c) - return lst, s - if pkt.ASN1_codec == ASN1_Codecs.PER: - dec = _uper().UPER_Decoder(s) - if self.uper_extensible and dec.read_bit(): - count = dec.read_length_determinant() - else: - count = self._uper_count_dec(dec) - lst = [] - for _ in range(count): - c, _ = self._extract_packet_from_decoder(dec, pkt) - if c: - lst.append(c) - if _uper().UPER_has_unexpected_remainder(dec): - raise _uper().UPER_Decoding_Error("unexpected remainder", - remaining=dec.remaining()) - return lst, b"" + hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) + if hooks is not None: + return hooks.sequence_of_m2i(self, pkt, s) s = self._apply_tagging_dec(s, pkt) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) i, s, remain = codec.check_type_check_len(s) @@ -928,33 +661,21 @@ def m2i(self, def build(self, pkt): # type: (ASN1_Packet) -> bytes + hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) + if hooks is not None: + return hooks.sequence_of_build(self, pkt) val = getattr(pkt, self.name) if isinstance(val, ASN1_Object) and \ val.tag == ASN1_Class_UNIVERSAL.RAW: s = cast(Union[List[_SEQ_T], bytes], val) elif val is None: s = b"" - if pkt.ASN1_codec == ASN1_Codecs.OER: - s = _oer().OER_unsigned_integer_enc(0) - elif pkt.ASN1_codec == ASN1_Codecs.PER: - enc = _uper().UPER_Encoder() - enc.append_length_determinant(0) - s = enc.as_bytes() + elif self.holds_packets: + s = b"".join(bytes(i) for i in val) else: - if pkt.ASN1_codec == ASN1_Codecs.PER: - enc = _uper().UPER_Encoder() - self._uper_encode_into(enc, pkt, val) - s = enc.as_bytes() - elif self.holds_packets: - s = b"".join(bytes(i) for i in val) - if pkt.ASN1_codec == ASN1_Codecs.OER: - s = _oer().OER_unsigned_integer_enc(len(val)) + s - else: - # BER/OER: element fields may carry implicit/explicit tags; - # i2m matches m2i()/fld.m2i(). - s = b"".join(self.fld.i2m(pkt, i) for i in val) - if pkt.ASN1_codec == ASN1_Codecs.OER: - s = _oer().OER_unsigned_integer_enc(len(val)) + s + # BER: element fields may carry implicit/explicit tags; i2m + # matches m2i()/fld.m2i(). (Packet elements use bytes() above.) + s = b"".join(self.fld.i2m(pkt, i) for i in val) return self.i2m(pkt, s) def i2repr(self, pkt, x): @@ -1000,7 +721,6 @@ class ASN1F_optional(ASN1F_element): """ ASN.1 field that is optional. """ - def __init__(self, field): # type: (ASN1F_field[Any, Any]) -> None field.flexible_tag = False @@ -1026,10 +746,6 @@ def dissect(self, pkt, s): self._field.set_val(pkt, None) return s - def dissect_from_decoder(self, pkt, dec): - # type: (ASN1_Packet, Any) -> None - return self._field.dissect_from_decoder(pkt, dec) - def build(self, pkt): # type: (ASN1_Packet) -> bytes if self._field.is_empty(pkt): @@ -1044,57 +760,12 @@ def i2repr(self, pkt, x): # type: (ASN1_Packet, Any) -> str return self._field.i2repr(pkt, x) - def set_val(self, pkt, val): - # type: (ASN1_Packet, Any) -> None - self._field.set_val(pkt, val) - - def set_absent(self, pkt): - # type: (ASN1_Packet) -> None - self.set_val(pkt, None) - - def is_empty(self, pkt): - # type: (ASN1_Packet) -> bool - # Delegate to the wrapped field (e.g. SEQUENCE checks children). - return self._field.is_empty(pkt) - - def _uper_encode_into(self, enc, pkt, value=None): - # type: (Any, ASN1_Packet, Optional[Any]) -> None - self._field._uper_encode_into(enc, pkt, value) - - -class ASN1F_DEFAULT(ASN1F_optional): - """ - ASN.1 field with a DEFAULT value (PER presence bit). - """ - - def __init__(self, field, default): - # type: (ASN1F_field[Any, Any], Any) -> None - super(ASN1F_DEFAULT, self).__init__(field) - self._default = default - - def is_empty(self, pkt): - # type: (ASN1_Packet) -> bool - val = getattr(pkt, self._field.name, None) - if val is None: - return True - if isinstance(val, ASN1_Object): - val = val.val - default = self._default - if isinstance(default, ASN1_Object): - default = default.val - return bool(val == default) - - def set_absent(self, pkt): - # type: (ASN1_Packet) -> None - self.set_val(pkt, self._default) - class ASN1F_omit(ASN1F_field[None, None]): """ ASN.1 field that is not specified. This is simply omitted on the network. This is different from ASN1F_NULL which has a network representation. """ - def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[None, bytes] return None, s @@ -1121,20 +792,18 @@ def __init__(self, name, default, *args, **kwargs): if "implicit_tag" in kwargs: err_msg = "ASN1F_CHOICE has been called with an implicit_tag" raise ASN1_Error(err_msg) - uper_extensible = kwargs.pop("uper_extensible", False) self.implicit_tag = None - for kwarg in ["context", "explicit_tag"]: - setattr(self, kwarg, kwargs.get(kwarg)) + context = kwargs.pop("context", None) + explicit_tag = kwargs.pop("explicit_tag", None) + # Remaining kwargs are codec constraints (e.g. uper_extensible=). super(ASN1F_CHOICE, self).__init__( - name, None, context=self.context, - explicit_tag=self.explicit_tag + name, None, context=context, + explicit_tag=explicit_tag, + **kwargs ) - self.uper_extensible = uper_extensible self.default = default self.current_choice = None self.choices = {} # type: Dict[int, _CHOICE_T] - self.choice_order = [] # type: List[int] - self.choice_list = [] # type: List[_CHOICE_T] self.pktchoices = {} for p in args: if hasattr(p, "ASN1_root"): @@ -1142,75 +811,31 @@ def __init__(self, name, default, *args, **kwargs): # should be ASN1_Packet if hasattr(p.ASN1_root, "choices"): root = cast(ASN1F_CHOICE, p.ASN1_root) - for k in root.choice_order: - self._register_choice(k, root.choices[k]) + for k, v in root.choices.items(): + # ASN1F_CHOICE recursion + self.choices[k] = v else: - self._register_choice(p.ASN1_root.network_tag, p) + self.choices[p.ASN1_root.network_tag] = p elif hasattr(p, "ASN1_tag"): if isinstance(p, type): # should be ASN1F_field class - self._register_choice(int(p.ASN1_tag), p) + self.choices[int(p.ASN1_tag)] = p else: # should be ASN1F_PACKET instance - self._register_choice(p.network_tag, p) + self.choices[p.network_tag] = p self.pktchoices[hash(p.cls)] = (p.implicit_tag, p.explicit_tag) # noqa: E501 else: raise ASN1_Error("ASN1F_CHOICE: no tag found for one field") - self._tag_to_index = { - tag: idx for idx, tag in enumerate(self.choice_order) - } - - def _register_choice(self, tag, choice): - # type: (int, _CHOICE_T) -> None - self.choices[tag] = choice - self.choice_order.append(tag) - self.choice_list.append(choice) - - def _dissect_choice_payload(self, pkt, choice, payload): - # type: (ASN1_Packet, _CHOICE_T, bytes) -> Tuple[ASN1_Object[Any], bytes] - if hasattr(choice, "ASN1_root"): - return self.extract_packet(choice, payload, _underlayer=pkt) # type: ignore - if isinstance(choice, type): - return choice(self.name, b"").m2i(pkt, payload) - return choice.m2i(pkt, payload) - def _m2i_oer(self, pkt, s): - # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] - s = self._apply_tagging_dec(s, pkt) - tag, payload = _oer().OER_id_dec(s) - return self._m2i_tagged(pkt, tag, payload) - - def _m2i_per(self, pkt, s): - # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] - dec = _uper().UPER_Decoder(s) - val = self.m2i_from_decoder(pkt, dec) - if _uper().UPER_has_unexpected_remainder(dec): - raise _uper().UPER_Decoding_Error( - "unexpected remainder", - remaining=dec.remaining(), - ) - return val, b"" + @property + def choice_order(self): + # type: () -> List[int] + return list(self.choices.keys()) - def _m2i_ber(self, pkt, s): - # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] - s = self._apply_tagging_dec(s, pkt) - tag, _ = BER_id_dec(s) - return self._m2i_tagged(pkt, tag, s) - - def _m2i_tagged(self, pkt, tag, payload): - # type: (ASN1_Packet, int, bytes) -> Tuple[ASN1_Object[Any], bytes] - if tag in self.choices: - choice = self.choices[tag] - elif self.flexible_tag: - choice = ASN1F_field - else: - raise ASN1_Error( - "ASN1F_CHOICE: unexpected field in '%s' " - "(tag %s not in possible tags %s)" % ( - self.name, tag, list(self.choices.keys()) - ) - ) - return self._dissect_choice_payload(pkt, choice, payload) + @property + def choice_list(self): + # type: () -> List[_CHOICE_T] + return list(self.choices.values()) def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] @@ -1220,92 +845,39 @@ def m2i(self, pkt, s): """ if len(s) == 0: raise ASN1_Error("ASN1F_CHOICE: got empty string") - if pkt.ASN1_codec == ASN1_Codecs.OER: - return self._m2i_oer(pkt, s) - if pkt.ASN1_codec == ASN1_Codecs.PER: - return self._m2i_per(pkt, s) - return self._m2i_ber(pkt, s) - - def _choice_tag_for(self, x): - # type: (Any) -> Optional[int] - index = self._choice_index_for(x) - return None if index is None else self.choice_order[index] - - def _choice_index_for(self, x): - # type: (Any) -> Optional[int] - for index, choice in enumerate(self.choice_list): - if isinstance(choice, type) and hasattr(choice, "ASN1_root"): - if isinstance(x, choice): - return index - elif hasattr(choice, "ASN1_tag"): - if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: - return index - return None - - def _choice_for_index(self, index): - # type: (int) -> _CHOICE_T - return self.choice_list[index] - - def m2i_from_decoder(self, pkt, dec): - # type: (ASN1_Packet, Any) -> ASN1_Object[Any] - if self.uper_extensible: - if dec.read_bit(): - raise _uper().UPER_Decoding_Error( - "ASN1F_CHOICE: extension additions are not supported" - ) - if len(self.choice_order) > 1: - index, _ = _uper().UPER_choice_index_dec(b"", len(self.choice_order), dec=dec) + hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) + if hooks is not None: + return hooks.choice_m2i(self, pkt, s) + s = self._apply_tagging_dec(s, pkt) + tag, _ = BER_id_dec(s) + if tag in self.choices: + choice = self.choices[tag] else: - index = 0 - if index >= len(self.choice_order): - raise ASN1_Error( - "ASN1F_CHOICE: unexpected index %s in '%s'" % - (index, self.name) - ) - choice = self._choice_for_index(index) - if isinstance(choice, type) and hasattr(choice, "ASN1_root"): - pkt_cls = cast("Type[ASN1_Packet]", choice) - p = pkt_cls() - p.add_underlayer(pkt) - p.ASN1_root.dissect_from_decoder(p, dec) - return cast(ASN1_Object[Any], p) - if isinstance(choice, type): - return cast( - ASN1_Object[Any], - choice(self.name, b"").m2i_from_decoder(pkt, dec), - ) - return cast(ASN1_Object[Any], choice.m2i_from_decoder(pkt, dec)) - - def _uper_encode_into(self, enc, pkt, value=None): - # type: (Any, ASN1_Packet, Any) -> None - if value is None: - value = getattr(pkt, self.name) - index = self._choice_index_for(value) - if index is None: - raise ASN1_Error( - "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % - self.name - ) - if self.uper_extensible: - enc.append_bit(0) - if len(self.choice_order) > 1: - _uper().UPER_choice_index_enc(index, len(self.choice_order), enc=enc) - choice = self._choice_for_index(index) + if self.flexible_tag: + choice = ASN1F_field + else: + raise ASN1_Error( + "ASN1F_CHOICE: unexpected field in '%s' " + "(tag %s not in possible tags %s)" % ( + self.name, tag, list(self.choices.keys()) + ) + ) if hasattr(choice, "ASN1_root"): - cast("ASN1_Packet", value).ASN1_root._uper_encode_into(enc, value) + # we don't want to import ASN1_Packet in this module... + return self.extract_packet(choice, s, _underlayer=pkt) # type: ignore elif isinstance(choice, type): - choice(self.name, b"")._uper_encode_into(enc, pkt, value) + return choice(self.name, b"").m2i(pkt, s) else: - choice._uper_encode_into(enc, pkt, value) + # XXX check properly if this is an ASN1F_PACKET + return choice.m2i(pkt, s) def i2m(self, pkt, x): # type: (ASN1_Packet, Any) -> bytes + hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) + if hooks is not None: + return hooks.choice_i2m(self, pkt, x) if x is None: s = b"" - elif pkt.ASN1_codec == ASN1_Codecs.PER: - enc = _uper().UPER_Encoder() - self._uper_encode_into(enc, pkt, x) - s = enc.as_bytes() else: # Use the packet codec for ASN1_Object values; bytes(x) would # follow conf.ASN1_default_codec instead. @@ -1313,11 +885,7 @@ def i2m(self, pkt, x): s = x.enc(pkt.ASN1_codec) else: s = bytes(x) - if pkt.ASN1_codec == ASN1_Codecs.OER: - alt_tag = self._choice_tag_for(x) - if alt_tag is not None: - s = _oer().OER_tag_enc(alt_tag & 0x3f, alt_tag & 0xc0) + s - elif hash(type(x)) in self.pktchoices: + if hash(type(x)) in self.pktchoices: imp, exp = self.pktchoices[hash(type(x))] s = self._tagging_enc( pkt, s, @@ -1373,24 +941,6 @@ def _resolve_cls(self, pkt): return self.next_cls_cb(pkt) or self.cls return self.cls - def m2i_from_decoder(self, pkt, dec): - # type: (ASN1_Packet, Any) -> Optional[ASN1_Packet] - cls = self._resolve_cls(pkt) - p = cls() - p.add_underlayer(pkt) - p.ASN1_root.dissect_from_decoder(p, dec) - return p - - def _uper_encode_into(self, enc, pkt, value=None): - # type: (Any, ASN1_Packet, Any) -> None - if value is None: - value = getattr(pkt, self.name) - if value is None: - return - if isinstance(value, ASN1_Object): - value = value.val - cast("ASN1_Packet", value).ASN1_root._uper_encode_into(enc, value) - def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[Any, bytes] cls = self._resolve_cls(pkt) @@ -1399,7 +949,7 @@ def m2i(self, pkt, s): return self.extract_packet(cls, s, _underlayer=pkt) s = self._apply_tagging_dec( s, pkt, - hidden_tag=cls.ASN1_root.ASN1_tag, + hidden_tag=cls.ASN1_root.ASN1_tag, # noqa: E501 _fname=self.name, ) if not s: @@ -1411,12 +961,11 @@ def i2m(self, x # type: Union[bytes, ASN1_Packet, None, ASN1_Object[Optional[ASN1_Packet]]] # noqa: E501 ): # type: (...) -> bytes + hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) + if hooks is not None and hasattr(hooks, "packet_i2m"): + return hooks.packet_i2m(self, pkt, x) if x is None: s = b"" - elif pkt.ASN1_codec == ASN1_Codecs.PER: - enc = _uper().UPER_Encoder() - self._uper_encode_into(enc, pkt, x) - s = enc.as_bytes() elif isinstance(x, bytes): s = x elif isinstance(x, ASN1_Object): @@ -1485,7 +1034,10 @@ def m2i(self, pkt, s): # type: ignore else: return None, bit_string.val_readable if len(s) > 0: - raise BER_Decoding_Error("unexpected remainder", remaining=s) + raise BER_Decoding_Error( + "unexpected remainder in %s" % pkt.name, + remaining=s, + ) return p, remain def i2m(self, pkt, x): # type: ignore @@ -1506,8 +1058,7 @@ def __init__(self, context=None, # type: Optional[Any] implicit_tag=None, # type: Optional[int] explicit_tag=None, # type: Optional[Any] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] + **codec_opts # type: Any ): # type: (...) -> None self.mapping = mapping @@ -1517,8 +1068,7 @@ def __init__(self, context=context, implicit_tag=implicit_tag, explicit_tag=explicit_tag, - uper_min=uper_min, - uper_max=uper_max, + **codec_opts, ) def any2i(self, pkt, x): diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py index ab68b2b0e83..06df49c3ffe 100644 --- a/scapy/contrib/oer.py +++ b/scapy/contrib/oer.py @@ -823,3 +823,130 @@ class OERcodec_GAUGE32(OERcodec_INTEGER): class OERcodec_TIME_TICKS(OERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.TIME_TICKS + + +########################## +# ASN1F field hooks # +########################## + +class _OER_FieldHooks(object): + """Compound ASN1F_* helpers for OER (kept out of asn1fields.py).""" + + @staticmethod + def use_object_enc(field, pkt, item): + # type: (Any, Any, Any) -> bool + # Constraints (e.g. oer_unsigned) must go through codec.enc(**kwargs). + return field.size_len is None and not field.codec_opts + + @staticmethod + def sequence_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) + s = field._dissect_sequence_children(pkt, s) + return [], s + + @staticmethod + def sequence_build(field, pkt): + # type: (Any, Any) -> bytes + from functools import reduce + s = reduce(lambda x, y: x + y.build(pkt), field.seq, b"") + return ASN1F_field_i2m(field, pkt, s) + + @staticmethod + def sequence_of_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[list, bytes] + s = field._apply_tagging_dec(s, pkt) + count, s = OER_unsigned_integer_dec(s) + lst = [] + for _ in range(count): + c, s = field._extract_packet(s, pkt) + if c: + lst.append(c) + return lst, s + + @staticmethod + def sequence_of_build(field, pkt): + # type: (Any, Any) -> bytes + from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Object + val = getattr(pkt, field.name) + if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: + s = val # type: Any + elif val is None: + s = OER_unsigned_integer_enc(0) + elif field.holds_packets: + s = OER_unsigned_integer_enc(len(val)) + b"".join(bytes(i) for i in val) + else: + s = ( + OER_unsigned_integer_enc(len(val)) + + b"".join(field.fld.i2m(pkt, i) for i in val) + ) + return field.i2m(pkt, s) + + @staticmethod + def choice_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + from scapy.asn1fields import ASN1F_field + from scapy.asn1.asn1 import ASN1_Error + s = field._apply_tagging_dec(s, pkt) + tag, payload = OER_id_dec(s) + if tag in field.choices: + choice = field.choices[tag] + elif field.flexible_tag: + choice = ASN1F_field + else: + raise ASN1_Error( + "ASN1F_CHOICE: unexpected field in '%s' " + "(tag %s not in possible tags %s)" % ( + field.name, tag, list(field.choices.keys()) + ) + ) + if hasattr(choice, "ASN1_root"): + return field.extract_packet(choice, payload, _underlayer=pkt) + if isinstance(choice, type): + return choice(field.name, b"").m2i(pkt, payload) + return choice.m2i(pkt, payload) + + @staticmethod + def choice_i2m(field, pkt, x): + # type: (Any, Any, Any) -> bytes + from scapy.asn1.asn1 import ASN1_Object + if x is None: + s = b"" + else: + if isinstance(x, ASN1_Object): + s = x.enc(pkt.ASN1_codec) + else: + s = bytes(x) + alt_tag = _choice_tag_for(field, x) + if alt_tag is not None: + s = OER_tag_enc(alt_tag & 0x3f, alt_tag & 0xc0) + s + return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) + + +def _choice_index_for(field, x): + # type: (Any, Any) -> Optional[int] + from scapy.asn1.asn1 import ASN1_Object + for index, choice in enumerate(field.choice_list): + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + if isinstance(x, choice): + return index + elif hasattr(choice, "ASN1_tag"): + if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + return index + return None + + +def _choice_tag_for(field, x): + # type: (Any, Any) -> Optional[int] + index = _choice_index_for(field, x) + return None if index is None else field.choice_order[index] + + +def ASN1F_field_i2m(field, pkt, s): + # type: (Any, Any, bytes) -> bytes + # Call ASN1F_field.i2m without compound overrides. + from scapy.asn1fields import ASN1F_field + return ASN1F_field.i2m(field, pkt, s) + + +ASN1_Codecs.OER.register_field_hooks(_OER_FieldHooks) diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index 1af22cd4e7a..9d190b4b0ba 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -1367,3 +1367,494 @@ class UPERcodec_UNIVERSAL_STRING(UPERcodec_STRING): class UPERcodec_BMP_STRING(UPERcodec_STRING): tag = ASN1_Class_UNIVERSAL.BMP_STRING + + +########################## +# ASN1F field hooks # +########################## + +def _field_extensible(field): + # type: (Any) -> bool + return bool(getattr(field, "uper_extensible", False)) + + +def _field_range(field): + # type: (Any) -> Tuple[Optional[int], Optional[int]] + return getattr(field, "uper_min", None), getattr(field, "uper_max", None) + + +class _UPER_FieldHooks(object): + """Compound ASN1F_* helpers for UPER/PER (kept out of asn1fields.py).""" + + @staticmethod + def use_object_enc(field, pkt, item): + # type: (Any, Any, Any) -> bool + # Always pass constraints through codec.enc(**kwargs). + return False + + @staticmethod + def sequence_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + dec = UPER_Decoder(s) + _UPER_FieldHooks.sequence_dissect_from_decoder(field, pkt, dec) + if UPER_has_unexpected_remainder(dec): + raise UPER_Decoding_Error( + "unexpected remainder", + remaining=dec.remaining(), + ) + return [], b"" + + @staticmethod + def sequence_build(field, pkt): + # type: (Any, Any) -> bytes + from scapy.asn1fields import ASN1F_field + enc = UPER_Encoder() + _UPER_FieldHooks.sequence_encode_into(field, enc, pkt) + return ASN1F_field.i2m(field, pkt, enc.as_bytes()) + + @staticmethod + def _optionals(field): + # type: (Any) -> Tuple[Any, ...] + from scapy.asn1fields import ASN1F_optional + return tuple(f for f in field.seq if isinstance(f, ASN1F_optional)) + + @staticmethod + def sequence_dissect_from_decoder(field, pkt, dec): + # type: (Any, Any, Any) -> None + from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional + if _field_extensible(field): + if dec.read_bit(): + raise UPER_Decoding_Error( + "ASN1F_SEQUENCE: extension additions are not supported" + ) + optionals = _UPER_FieldHooks._optionals(field) + presence = [dec.read_bit() for _ in optionals] + opt_idx = 0 + for obj in field.seq: + if isinstance(obj, ASN1F_optional): + if not presence[opt_idx]: + obj.set_absent(pkt) + opt_idx += 1 + continue + opt_idx += 1 + try: + obj.dissect_from_decoder(pkt, dec) + except ASN1F_badsequence: + break + + @staticmethod + def sequence_encode_into(field, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + from scapy.asn1fields import ASN1F_optional + if _field_extensible(field): + enc.append_bit(0) + for opt in _UPER_FieldHooks._optionals(field): + enc.append_bit(0 if opt.is_empty(pkt) else 1) + for obj in field.seq: + if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): + continue + obj.encode_into(enc, pkt) + + @staticmethod + def sequence_of_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[list, bytes] + dec = UPER_Decoder(s) + if _field_extensible(field) and dec.read_bit(): + count = dec.read_length_determinant() + else: + count = _uper_count_dec(field, dec) + lst = [] + for _ in range(count): + c, _ = _extract_packet_from_decoder(field, dec, pkt) + if c: + lst.append(c) + if UPER_has_unexpected_remainder(dec): + raise UPER_Decoding_Error( + "unexpected remainder", + remaining=dec.remaining(), + ) + return lst, b"" + + @staticmethod + def sequence_of_build(field, pkt): + # type: (Any, Any) -> bytes + from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Object + val = getattr(pkt, field.name) + if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: + s = val # type: Any + elif val is None: + enc = UPER_Encoder() + enc.append_length_determinant(0) + s = enc.as_bytes() + else: + enc = UPER_Encoder() + _UPER_FieldHooks.sequence_of_encode_into(field, enc, pkt, val) + s = enc.as_bytes() + return field.i2m(pkt, s) + + @staticmethod + def sequence_of_m2i_from_decoder(field, pkt, dec): + # type: (Any, Any, Any) -> list + if _field_extensible(field) and dec.read_bit(): + count = dec.read_length_determinant() + else: + count = _uper_count_dec(field, dec) + lst = [] + for _ in range(count): + item, _ = _extract_packet_from_decoder(field, dec, pkt) + lst.append(item) + return lst + + @staticmethod + def sequence_of_encode_into(field, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + if value is None: + value = getattr(pkt, field.name) + if value is None: + _uper_count_enc(field, enc, 0) + return + count = len(value) + uper_min, uper_max = _field_range(field) + if _field_extensible(field): + if ( + uper_min is not None and uper_max is not None and + uper_min <= count <= uper_max + ): + enc.append_bit(0) + else: + enc.append_bit(1) + enc.append_length_determinant(count) + for item in value: + if field.holds_packets: + item.ASN1_root.encode_into(enc, item) + else: + field.fld.encode_into(enc, pkt, item) + return + _uper_count_enc(field, enc, count) + for item in value: + if field.holds_packets: + item.ASN1_root.encode_into(enc, item) + else: + field.fld.encode_into(enc, pkt, item) + + @staticmethod + def choice_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + dec = UPER_Decoder(s) + val = _UPER_FieldHooks.choice_m2i_from_decoder(field, pkt, dec) + if UPER_has_unexpected_remainder(dec): + raise UPER_Decoding_Error( + "unexpected remainder", + remaining=dec.remaining(), + ) + return val, b"" + + @staticmethod + def choice_i2m(field, pkt, x): + # type: (Any, Any, Any) -> bytes + if x is None: + s = b"" + else: + enc = UPER_Encoder() + _UPER_FieldHooks.choice_encode_into(field, enc, pkt, x) + s = enc.as_bytes() + return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) + + @staticmethod + def choice_m2i_from_decoder(field, pkt, dec): + # type: (Any, Any, Any) -> Any + from scapy.asn1.asn1 import ASN1_Error + if _field_extensible(field): + if dec.read_bit(): + raise UPER_Decoding_Error( + "ASN1F_CHOICE: extension additions are not supported" + ) + order = field.choice_order + if len(order) > 1: + index, _ = UPER_choice_index_dec(b"", len(order), dec=dec) + else: + index = 0 + if index >= len(order): + raise ASN1_Error( + "ASN1F_CHOICE: unexpected index %s in '%s'" % + (index, field.name) + ) + choice = field.choice_list[index] + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + p = choice() + p.add_underlayer(pkt) + p.ASN1_root.dissect_from_decoder(p, dec) + return p + if isinstance(choice, type): + return choice(field.name, b"").m2i_from_decoder(pkt, dec) + return choice.m2i_from_decoder(pkt, dec) + + @staticmethod + def choice_encode_into(field, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + from scapy.asn1.asn1 import ASN1_Error + if value is None: + value = getattr(pkt, field.name) + index = _choice_index_for(field, value) + if index is None: + raise ASN1_Error( + "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % + field.name + ) + if _field_extensible(field): + enc.append_bit(0) + order = field.choice_order + if len(order) > 1: + UPER_choice_index_enc(index, len(order), enc=enc) + choice = field.choice_list[index] + if hasattr(choice, "ASN1_root"): + value.ASN1_root.encode_into(enc, value) + elif isinstance(choice, type): + choice(field.name, b"").encode_into(enc, pkt, value) + else: + choice.encode_into(enc, pkt, value) + + @staticmethod + def packet_m2i_from_decoder(field, pkt, dec): + # type: (Any, Any, Any) -> Any + cls = field._resolve_cls(pkt) + p = cls() + p.add_underlayer(pkt) + p.ASN1_root.dissect_from_decoder(p, dec) + return p + + @staticmethod + def packet_i2m(field, pkt, x): + # type: (Any, Any, Any) -> bytes + if x is None: + s = b"" + else: + enc = UPER_Encoder() + _UPER_FieldHooks.packet_encode_into(field, enc, pkt, x) + s = enc.as_bytes() + return field._tagging_enc( + pkt, s, + implicit_tag=field.implicit_tag, + explicit_tag=field.explicit_tag, + ) + + @staticmethod + def packet_encode_into(field, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + from scapy.asn1.asn1 import ASN1_Object + if value is None: + value = getattr(pkt, field.name) + if value is None: + return + if isinstance(value, ASN1_Object): + value = value.val + value.ASN1_root.encode_into(enc, value) + + +def _choice_index_for(field, x): + # type: (Any, Any) -> Optional[int] + from scapy.asn1.asn1 import ASN1_Object + for index, choice in enumerate(field.choice_list): + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + if isinstance(x, choice): + return index + elif hasattr(choice, "ASN1_tag"): + if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + return index + return None + + +def _uper_count_enc(field, enc, count): + # type: (Any, Any, int) -> None + uper_min, uper_max = _field_range(field) + if uper_min is not None and uper_max is not None: + UPER_constrained_int_enc(count, uper_min, uper_max, enc=enc) + else: + enc.append_length_determinant(count) + + +def _uper_count_dec(field, dec): + # type: (Any, Any) -> int + uper_min, uper_max = _field_range(field) + if uper_min is not None and uper_max is not None: + size = uper_max - uper_min + return ( + dec.read_non_negative_binary_integer(UPER_bits_for_range(size)) + + uper_min + ) + return dec.read_length_determinant() + + +def _extract_packet_from_decoder(field, dec, pkt): + # type: (Any, Any, Any) -> Tuple[Any, bytes] + if field.holds_packets: + p = field.cls() + p.add_underlayer(pkt) + p.ASN1_root.dissect_from_decoder(p, dec) + return p, b"" + return field.fld.m2i_from_decoder(pkt, dec), b"" + + +# Populated by _install_uper_asn1fields() (also published on scapy.asn1fields). +ASN1F_DEFAULT = None # type: Any + + +def _install_uper_asn1fields(): + # type: () -> None + """Attach UPER bitstream helpers and DEFAULT onto asn1fields classes.""" + from scapy import asn1fields as af + from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Error, ASN1_Object + + class _ASN1F_DEFAULT(af.ASN1F_optional): + """ASN.1 field with a DEFAULT value (PER presence bit).""" + + def __init__(self, field, default): + # type: (Any, Any) -> None + super(_ASN1F_DEFAULT, self).__init__(field) + self._default = default + + def is_empty(self, pkt): + # type: (Any) -> bool + val = getattr(pkt, self._field.name, None) + if val is None: + return True + if isinstance(val, ASN1_Object): + val = val.val + default = self._default + if isinstance(default, ASN1_Object): + default = default.val + return bool(val == default) + + def set_absent(self, pkt): + # type: (Any) -> None + self.set_val(pkt, self._default) + + global ASN1F_DEFAULT + ASN1F_DEFAULT = _ASN1F_DEFAULT # type: ignore[misc,assignment] + af.ASN1F_DEFAULT = _ASN1F_DEFAULT + + def m2i_from_decoder(self, pkt, dec): + # type: (Any, Any, Any) -> Any + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + return codec.dec_from_decoder( # type: ignore[attr-defined] + dec, **self._codec_kwargs(pkt), + ) + + def dissect_from_decoder(self, pkt, dec): + # type: (Any, Any, Any) -> None + self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) + + def encode_into(self, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + if value is None: + value = getattr(pkt, self.name) + if value is None: + return + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + if isinstance(value, ASN1_Object): + if (self.ASN1_tag == ASN1_Class_UNIVERSAL.ANY or + value.tag == ASN1_Class_UNIVERSAL.RAW or + value.tag == ASN1_Class_UNIVERSAL.ERROR or + self.ASN1_tag == value.tag): + raw = value.val + else: + raise ASN1_Error( + "Encoding Error: got %r instead of an %r for field [%s]" % + (value, self.ASN1_tag, self.name) + ) + else: + raw = value + codec.encode_into( # type: ignore[attr-defined] + enc, raw, **self._codec_kwargs(pkt), + ) + + af.ASN1F_field.m2i_from_decoder = m2i_from_decoder # type: ignore[attr-defined] + af.ASN1F_field.dissect_from_decoder = dissect_from_decoder # type: ignore[attr-defined] + af.ASN1F_field.encode_into = encode_into # type: ignore[attr-defined] + af.ASN1F_field._uper_encode_into = encode_into # type: ignore[attr-defined] + + def seq_dissect_from_decoder(self, pkt, dec): + # type: (Any, Any, Any) -> None + return _UPER_FieldHooks.sequence_dissect_from_decoder(self, pkt, dec) + + def seq_encode_into(self, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + return _UPER_FieldHooks.sequence_encode_into(self, enc, pkt, value) + + af.ASN1F_SEQUENCE.dissect_from_decoder = seq_dissect_from_decoder # type: ignore[attr-defined] + af.ASN1F_SEQUENCE.encode_into = seq_encode_into # type: ignore[attr-defined] + af.ASN1F_SEQUENCE._uper_encode_into = seq_encode_into # type: ignore[attr-defined] + + def seqof_m2i_from_decoder(self, pkt, dec): + # type: (Any, Any, Any) -> Any + return _UPER_FieldHooks.sequence_of_m2i_from_decoder(self, pkt, dec) + + def seqof_encode_into(self, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + return _UPER_FieldHooks.sequence_of_encode_into(self, enc, pkt, value) + + af.ASN1F_SEQUENCE_OF.m2i_from_decoder = seqof_m2i_from_decoder # type: ignore[attr-defined] + af.ASN1F_SEQUENCE_OF.encode_into = seqof_encode_into # type: ignore[attr-defined] + af.ASN1F_SEQUENCE_OF._uper_encode_into = seqof_encode_into # type: ignore[attr-defined] + + def choice_m2i_from_decoder(self, pkt, dec): + # type: (Any, Any, Any) -> Any + return _UPER_FieldHooks.choice_m2i_from_decoder(self, pkt, dec) + + def choice_encode_into(self, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + return _UPER_FieldHooks.choice_encode_into(self, enc, pkt, value) + + af.ASN1F_CHOICE.m2i_from_decoder = choice_m2i_from_decoder # type: ignore[attr-defined] + af.ASN1F_CHOICE.encode_into = choice_encode_into # type: ignore[attr-defined] + af.ASN1F_CHOICE._uper_encode_into = choice_encode_into # type: ignore[attr-defined] + + def packet_m2i_from_decoder(self, pkt, dec): + # type: (Any, Any, Any) -> Any + return _UPER_FieldHooks.packet_m2i_from_decoder(self, pkt, dec) + + def packet_encode_into(self, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + return _UPER_FieldHooks.packet_encode_into(self, enc, pkt, value) + + af.ASN1F_PACKET.m2i_from_decoder = packet_m2i_from_decoder # type: ignore[attr-defined] + af.ASN1F_PACKET.encode_into = packet_encode_into # type: ignore[attr-defined] + af.ASN1F_PACKET._uper_encode_into = packet_encode_into # type: ignore[attr-defined] + + def opt_set_absent(self, pkt): + # type: (Any, Any) -> None + self.set_val(pkt, None) + + def opt_dissect_from_decoder(self, pkt, dec): + # type: (Any, Any, Any) -> None + return self._field.dissect_from_decoder(pkt, dec) + + def opt_encode_into(self, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + self._field.encode_into(enc, pkt, value) + + af.ASN1F_optional.set_absent = opt_set_absent # type: ignore[attr-defined] + af.ASN1F_optional.dissect_from_decoder = opt_dissect_from_decoder # type: ignore[attr-defined] + af.ASN1F_optional.encode_into = opt_encode_into # type: ignore[attr-defined] + af.ASN1F_optional._uper_encode_into = opt_encode_into # type: ignore[attr-defined] + + _orig_enum_init = af.ASN1F_enum_INTEGER.__init__ + + def enum_init(self, name, default, enum, context=None, + implicit_tag=None, explicit_tag=None): + # type: (Any, str, Any, Any, Any, Any, Any) -> None + _orig_enum_init( + self, name, default, enum, context=context, + implicit_tag=implicit_tag, explicit_tag=explicit_tag, + ) + values = list(self.i2s) + self.uper_enum_values = values + opts = dict(getattr(self, "codec_opts", {})) + opts["uper_enum_values"] = values + self.codec_opts = opts + + af.ASN1F_enum_INTEGER.__init__ = enum_init # type: ignore[assignment] + + +_install_uper_asn1fields() +ASN1_Codecs.PER.register_field_hooks(_UPER_FieldHooks) From c2095fe84625ac0f86ac5c3401303deda4f5a046 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Sat, 8 Aug 2026 07:19:08 +0200 Subject: [PATCH 04/46] More tests AI-Assisted: yes (Cursor) --- scapy/contrib/oer.py | 5 + test/scapy/layers/asn1.uts | 71 +++++++++++++ test/scapy/layers/ber.uts | 63 ++++++++++++ test/scapy/layers/oer.uts | 153 +++++++++++++++++++++++++++ test/scapy/layers/uper.uts | 205 +++++++++++++++++++++++++++++++++++++ 5 files changed, 497 insertions(+) diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py index 06df49c3ffe..4fdac91544b 100644 --- a/scapy/contrib/oer.py +++ b/scapy/contrib/oer.py @@ -404,6 +404,7 @@ def do_dec(cls, safe=False, # type: bool size_len=0, # type: Optional[int] oer_unsigned=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[Any], bytes] raise OER_Decoding_Error( @@ -418,8 +419,11 @@ def dec(cls, safe=False, # type: bool size_len=0, # type: Optional[int] oer_unsigned=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] + # Ignore unknown kwargs so shared field._codec_kwargs() dicts (UPER + # keys) do not TypeError on OER packets. if not safe: return cls.do_dec(s, context, safe, size_len, oer_unsigned) try: @@ -440,6 +444,7 @@ def safedec(cls, context=None, # type: Optional[Type[ASN1_Class]] size_len=0, # type: Optional[int] oer_unsigned=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] return cls.dec( diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 7f00cf6e17f..a00e8a79848 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -491,3 +491,74 @@ for cls, data_hex in [ True += ber oer per constrained integer codec_opts +class BERConstrained(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER( + "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, + ) + +class OERConstrained(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER( + "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, + ) + +class PERConstrained(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER( + "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, + ) + +for cls, expected in ( + (BERConstrained, b"\x02\x81\x02\x00\xc8"), + (OERConstrained, b"\xc8"), + (PERConstrained, b"\xc8"), +): + pkt = cls(n=200) + assert raw(pkt) == expected + assert _roundtrip(cls, pkt).n.val == 200 + assert cls.ASN1_root.codec_opts["oer_unsigned"] is True + assert cls.ASN1_root.codec_opts["uper_min"] == 0 + assert cls.ASN1_root.codec_opts["uper_max"] == 255 + +True + += ber oer per empty sequence of +class BEREmptySeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + +class OEREmptySeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + +class PEREmptySeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", + [], + ASN1F_INTEGER("item", 0, uper_min=0, uper_max=7), + uper_min=0, + uper_max=3, + ) + +for cls in (BEREmptySeqOf, OEREmptySeqOf, PEREmptySeqOf): + pkt = cls(values=[]) + decoded = _roundtrip(cls, pkt) + assert decoded.values == [] + assert len(raw(pkt)) > 0 + +True + += field hooks present after contrib load +assert hasattr(ASN1_Codecs.OER, "_field_hooks") + +assert hasattr(ASN1_Codecs.PER, "_field_hooks") + +assert ASN1_Codecs.OER._field_hooks is not None + +assert ASN1_Codecs.PER._field_hooks is not None + +True + diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index 8087b383ec0..f33e5763a95 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -514,6 +514,69 @@ class ExtraPkt(ASN1_Packet): assert raw(ExtraPkt(n=7)) == b"\x02\x01\x07" ExtraPkt(raw(ExtraPkt(n=7))).n.val == 7 += field codec_opts storage +plain = ASN1F_INTEGER("n", 0) + +assert plain.codec_opts == {} + +assert plain._codec_kwargs(type("P", (), {"ASN1_codec": ASN1_Codecs.BER})()) == { + "size_len": None, +} + +constrained = ASN1F_INTEGER( + "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, +) + +assert constrained.codec_opts == { + "oer_unsigned": True, + "uper_min": 0, + "uper_max": 255, +} + +assert constrained.oer_unsigned is True + +assert constrained.uper_min == 0 + +assert constrained.uper_max == 255 + +kwargs = constrained._codec_kwargs( + type("P", (), {"ASN1_codec": ASN1_Codecs.BER})() +) + +assert kwargs["size_len"] == 1 + +assert kwargs["oer_unsigned"] is True + +assert kwargs["uper_min"] == 0 + +assert kwargs["uper_max"] == 255 + +# BER still encodes with constraints present in kwargs. +class ConstrainedBer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER( + "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, + ) + +assert raw(ConstrainedBer(n=5)) == b"\x02\x81\x01\x05" + +assert ConstrainedBer(raw(ConstrainedBer(n=5))).n.val == 5 + +True + += CHOICE order properties +choice = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, +) + +assert choice.choice_order == [2, 4] + +assert choice.choice_list[0] is ASN1F_INTEGER + +assert choice.choice_list[1] is ASN1F_STRING + +True + + ASN.1 BER build and dissect extras = import helpers diff --git a/test/scapy/layers/oer.uts b/test/scapy/layers/oer.uts index fe690a05ccd..5ef6dfdaf39 100644 --- a/test/scapy/layers/oer.uts +++ b/test/scapy/layers/oer.uts @@ -860,3 +860,156 @@ assert remain == b"" True + ++ ASN.1 OER field hooks and packet extras += import contrib codecs +import scapy.contrib.oer +import scapy.contrib.uper +from scapy.contrib.oer import * +from scapy.packet import raw += prepare helpers and packet classes +class OEREmptySequenceOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + +class OEREnumField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_ENUMERATED("e", 0, {0: "a", 1: "b"}) + +class OERBitStringField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_BIT_STRING("b", "0101") + +class OERNullRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_NULL("z", 0), + ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ) + +class OEROidField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_OID("oid", "1.2.3") + +class OERInnerSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, size_len=1, oer_unsigned=True), + ) + +class OERPacketChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE("c", None, OERInnerSeq, ASN1F_INTEGER) + +class OERUnsignedField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER( + "n", 0, size_len=1, oer_unsigned=True, + ) + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) += oer field hooks registered +assert hasattr(ASN1_Codecs.OER, "_field_hooks") + +assert ASN1_Codecs.OER._field_hooks is not None + +assert hasattr(ASN1_Codecs.OER._field_hooks, "sequence_m2i") + +assert hasattr(ASN1_Codecs.OER._field_hooks, "use_object_enc") + +True + += oer use_object_enc via hooks +fld = OERUnsignedField.ASN1_root + +assert fld.codec_opts["oer_unsigned"] is True + +assert fld._use_object_enc(OERUnsignedField(), ASN1_INTEGER(5)) is False + +assert raw(OERUnsignedField(n=5)) == b"\x05" + +assert _roundtrip(OERUnsignedField, OERUnsignedField(n=5)).n.val == 5 + +True + += oer empty sequence of +pkt = OEREmptySequenceOf(values=[]) + +assert raw(pkt) == b"\x01\x00" + +decoded = _roundtrip(OEREmptySequenceOf, pkt) + +assert decoded.values == [] + +True + += oer enumerated field +pkt = OEREnumField(e=1) + +assert raw(pkt) == b"\x01" + +decoded = _roundtrip(OEREnumField, pkt) + +assert decoded.e.val == 1 + +True + += oer bit string field +pkt = OERBitStringField(b="0101") + +assert raw(pkt) == b"\x02\x04\x50" + +decoded = _roundtrip(OERBitStringField, pkt) + +assert decoded.b.val == "0101" + +True + += oer null and oid fields +null_pkt = OERNullRecord(z=0, n=2) + +assert raw(null_pkt) == b"\x02" + +assert _roundtrip(OERNullRecord, null_pkt).n.val == 2 + +oid_pkt = OEROidField(oid="1.2.3") + +assert raw(oid_pkt) == b"\x02\x2a\x03" + +assert _roundtrip(OEROidField, oid_pkt).oid.val == "1.2.3" + +True + += oer choice with packet alternative +pkt = OERPacketChoice(c=OERInnerSeq(x=3)) + +assert raw(pkt) == b"\x30\x03" + +decoded = _roundtrip(OERPacketChoice, pkt) + +assert isinstance(decoded.c, OERInnerSeq) + +assert decoded.c.x.val == 3 + +as_int = OERPacketChoice(c=ASN1_INTEGER(9)) + +decoded_int = _roundtrip(OERPacketChoice, as_int) + +assert decoded_int.c.val == 9 + +True + += oer dec ignores foreign codec kwargs +# Shared field.codec_opts may include UPER keys after contrib.uper is loaded. +x, remain = OERcodec_ENUMERATED.dec( + b"\x01", uper_enum_values=[0, 1], uper_min=0, +) + +assert x.val == 1 + +assert remain == b"" + +True + diff --git a/test/scapy/layers/uper.uts b/test/scapy/layers/uper.uts index 6b962e4dacb..4e5168c55a2 100644 --- a/test/scapy/layers/uper.uts +++ b/test/scapy/layers/uper.uts @@ -2827,3 +2827,208 @@ assert raw(oer_pkt_choice) True + ++ ASN.1 UPER field hooks and packet extras += import contrib codecs +import scapy.contrib.uper +from scapy.contrib.uper import * +from scapy.contrib.uper import ASN1F_DEFAULT +from scapy.packet import raw +import scapy.asn1fields as asn1fields += prepare helpers and packet classes +def _val(x): + # type: (Any) -> Any + return x.val if hasattr(x, "val") else x + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + +class UPERDefaultRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_DEFAULT( + ASN1F_INTEGER("n", 5, uper_min=0, uper_max=10), + 5, + ), + ) + +class UPEREmptySeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", + [], + ASN1F_INTEGER("item", 0, uper_min=0, uper_max=7), + uper_min=0, + uper_max=3, + ) + +class UPERExtSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", + [], + ASN1F_INTEGER("item", 0, uper_min=0, uper_max=7), + uper_min=1, + uper_max=2, + uper_extensible=True, + ) + +class UPERFlagsField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_FLAGS( + "f", "101", ["a", "b", "c"], uper_min=3, uper_max=3, + ) + +class UPERInnerPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, uper_min=0, uper_max=15), + ) + +class UPERWrappedPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_PACKET("inner", None, UPERInnerPacket), + ) + +class UPERConstrainedInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=255) += uper field hooks registered +assert hasattr(ASN1_Codecs.PER, "_field_hooks") + +assert ASN1_Codecs.PER._field_hooks is not None + +assert hasattr(ASN1_Codecs.PER._field_hooks, "sequence_m2i") + +assert hasattr(ASN1_Codecs.PER._field_hooks, "use_object_enc") + +assert ASN1_Codecs.PER._field_hooks.use_object_enc( + UPERConstrainedInt.ASN1_root, UPERConstrainedInt(), ASN1_INTEGER(1), +) is False + +True + += uper DEFAULT published on asn1fields +assert ASN1F_DEFAULT is asn1fields.ASN1F_DEFAULT + +assert issubclass(ASN1F_DEFAULT, ASN1F_optional) + +enum_fld = ASN1F_ENUMERATED("e", 0, {1: "a", 2: "b"}) + +assert enum_fld.uper_enum_values == [1, 2] + +assert enum_fld.codec_opts["uper_enum_values"] == [1, 2] + +assert hasattr(ASN1F_field, "encode_into") + +assert hasattr(ASN1F_SEQUENCE, "dissect_from_decoder") + +True + += uper use_object_enc and codec_opts +fld = UPERConstrainedInt.ASN1_root + +assert fld.codec_opts == {"uper_min": 0, "uper_max": 255} + +assert fld._use_object_enc(UPERConstrainedInt(), ASN1_INTEGER(5)) is False + +assert raw(UPERConstrainedInt(n=5)) == b"\x05" + +assert _val(_roundtrip(UPERConstrainedInt, UPERConstrainedInt(n=5)).n) == 5 + +True + += uper DEFAULT presence bit +absent = UPERDefaultRecord(id=1, n=5) + +present = UPERDefaultRecord(id=1, n=7) + +assert raw(absent) == bytes.fromhex("0080") + +assert raw(present) == bytes.fromhex("80b8") + +assert raw(absent) != raw(present) + +decoded_absent = _roundtrip(UPERDefaultRecord, absent) + +decoded_present = _roundtrip(UPERDefaultRecord, present) + +assert _val(decoded_absent.n) == 5 + +assert _val(decoded_present.n) == 7 + +True + += uper empty constrained sequence of +pkt = UPEREmptySeqOf(values=[]) + +assert raw(pkt) == b"\x00" + +decoded = _roundtrip(UPEREmptySeqOf, pkt) + +assert decoded.values == [] + +True + += uper extensible sequence of outside range +pkt = UPERExtSeqOf(values=[1, 2, 3]) + +assert raw(pkt) == bytes.fromhex("8194c0") + +decoded = _roundtrip(UPERExtSeqOf, pkt) + +assert [_val(x) for x in decoded.values] == [1, 2, 3] + +True + += uper FLAGS field +pkt = UPERFlagsField(f="101") + +assert raw(pkt) == bytes.fromhex("a0") + +decoded = _roundtrip(UPERFlagsField, pkt) + +assert decoded.f.val == "101" + +assert UPERFlagsField.ASN1_root.get_flags(decoded) == ["a", "c"] + +True + += uper nested ASN1F_PACKET +pkt = UPERWrappedPacket(id=1, inner=UPERInnerPacket(x=7)) + +assert raw(pkt) == bytes.fromhex("0170") + +decoded = _roundtrip(UPERWrappedPacket, pkt) + +assert _val(decoded.id) == 1 + +assert _val(decoded.inner.x) == 7 + +True + += uper field encode_into nesting +built = UPERWrappedPacket(id=1, inner=UPERInnerPacket(x=7)) + +enc = UPER_Encoder() + +UPERWrappedPacket.ASN1_root.encode_into(enc, built) + +assert enc.as_bytes() == raw(built) + +empty = UPERWrappedPacket() + +UPERWrappedPacket.ASN1_root.dissect_from_decoder( + empty, UPER_Decoder(raw(built)), +) + +assert _val(empty.id) == 1 + +assert _val(empty.inner.x) == 7 + +True + From 65c39580ef796d243c437b9ac7cdec7f5f8b9e2e Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Sat, 8 Aug 2026 11:04:34 +0200 Subject: [PATCH 05/46] More tests AI-Assisted: yes (Cursor) --- scapy/contrib/oer.py | 3 +++ scapy/contrib/uper.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py index 4fdac91544b..9942c59709d 100644 --- a/scapy/contrib/oer.py +++ b/scapy/contrib/oer.py @@ -2,6 +2,9 @@ # This file is part of Scapy # See https://scapy.net/ for more information +# scapy.contrib.description = ASN.1 Octet Encoding Rules (OER) +# scapy.contrib.status = loads + """ Octet Encoding Rules (OER) for ASN.1 diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index 9d190b4b0ba..94958e7a686 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -2,6 +2,9 @@ # This file is part of Scapy # See https://scapy.net/ for more information +# scapy.contrib.description = ASN.1 Unaligned Packed Encoding Rules (UPER) +# scapy.contrib.status = loads + """ Unaligned Packed Encoding Rules (UPER) for ASN.1 From a15984b47b912e90865d541fdc9317e68c556af1 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Mon, 10 Aug 2026 08:13:35 +0200 Subject: [PATCH 06/46] Cleanup asn1fields AI-Assisted: yes (Cursor) --- scapy/asn1fields.py | 18 +++++---- scapy/contrib/oer.py | 21 +++++----- scapy/contrib/uper.py | 81 +++++++++++++++++++++------------------ test/scapy/layers/ber.uts | 10 ++++- 4 files changed, 72 insertions(+), 58 deletions(-) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 81fd9b007cb..ba43478f95f 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -173,7 +173,7 @@ def _use_object_enc(self, pkt, item): # Contrib codecs may force codec.enc(**kwargs) via field hooks. hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) if hooks is not None and hasattr(hooks, "use_object_enc"): - return hooks.use_object_enc(self, pkt, item) + return cast(bool, hooks.use_object_enc(self, pkt, item)) return self.size_len is None and not self.codec_opts def _encode_item(self, pkt, item): @@ -551,7 +551,7 @@ def m2i(self, pkt, s): """ hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) if hooks is not None: - return hooks.sequence_m2i(self, pkt, s) + return cast(Tuple[Any, bytes], hooks.sequence_m2i(self, pkt, s)) s = self._apply_tagging_dec(s, pkt, _fname=pkt.name) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) i, s, remain = codec.check_type_check_len(s) @@ -572,7 +572,7 @@ def build(self, pkt): # type: (ASN1_Packet) -> bytes hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) if hooks is not None: - return hooks.sequence_build(self, pkt) + return cast(bytes, hooks.sequence_build(self, pkt)) s = reduce(lambda x, y: x + y.build(pkt), self.seq, b"") return super(ASN1F_SEQUENCE, self).i2m(pkt, s) @@ -643,7 +643,8 @@ def m2i(self, # type: (...) -> Tuple[List[Any], bytes] hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) if hooks is not None: - return hooks.sequence_of_m2i(self, pkt, s) + return cast(Tuple[List[Any], bytes], + hooks.sequence_of_m2i(self, pkt, s)) s = self._apply_tagging_dec(s, pkt) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) i, s, remain = codec.check_type_check_len(s) @@ -663,7 +664,7 @@ def build(self, pkt): # type: (ASN1_Packet) -> bytes hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) if hooks is not None: - return hooks.sequence_of_build(self, pkt) + return cast(bytes, hooks.sequence_of_build(self, pkt)) val = getattr(pkt, self.name) if isinstance(val, ASN1_Object) and \ val.tag == ASN1_Class_UNIVERSAL.RAW: @@ -847,7 +848,8 @@ def m2i(self, pkt, s): raise ASN1_Error("ASN1F_CHOICE: got empty string") hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) if hooks is not None: - return hooks.choice_m2i(self, pkt, s) + return cast(Tuple[ASN1_Object[Any], bytes], + hooks.choice_m2i(self, pkt, s)) s = self._apply_tagging_dec(s, pkt) tag, _ = BER_id_dec(s) if tag in self.choices: @@ -875,7 +877,7 @@ def i2m(self, pkt, x): # type: (ASN1_Packet, Any) -> bytes hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) if hooks is not None: - return hooks.choice_i2m(self, pkt, x) + return cast(bytes, hooks.choice_i2m(self, pkt, x)) if x is None: s = b"" else: @@ -963,7 +965,7 @@ def i2m(self, # type: (...) -> bytes hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) if hooks is not None and hasattr(hooks, "packet_i2m"): - return hooks.packet_i2m(self, pkt, x) + return cast(bytes, hooks.packet_i2m(self, pkt, x)) if x is None: s = b"" elif isinstance(x, bytes): diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py index 9942c59709d..45f5f22fc67 100644 --- a/scapy/contrib/oer.py +++ b/scapy/contrib/oer.py @@ -368,6 +368,7 @@ def __new__(cls, class OERcodec_Object(Generic[_K], metaclass=OERcodec_metaclass): codec = ASN1_Codecs.OER tag = ASN1_Class_UNIVERSAL.ANY + @classmethod def asn1_object(cls, val): # type: (_K) -> ASN1_Object[_K] @@ -457,7 +458,7 @@ def safedec(cls, @classmethod def enc(cls, s, size_len=0, **_kwargs): - # type: (_K, Optional[int]) -> bytes + # type: (_K, Optional[int], **Any) -> bytes if isinstance(s, (str, bytes)): return OERcodec_STRING.enc(s, size_len=size_len) else: @@ -480,7 +481,7 @@ class OERcodec_INTEGER(OERcodec_Object[int]): @classmethod def enc(cls, i, size_len=0, **_kwargs): - # type: (int, Optional[int]) -> bytes + # type: (int, Optional[int], **Any) -> bytes if size_len in (1, 2, 4, 8): if i >= 0: if size_len == 1 and 0 <= i <= 255: @@ -520,7 +521,7 @@ class OERcodec_BOOLEAN(OERcodec_Object[int]): @classmethod def enc(cls, i, size_len=0, **_kwargs): - # type: (int, Optional[int]) -> bytes + # type: (int, Optional[int], **Any) -> bytes return chb(0xff if i else 0x00) @classmethod @@ -569,7 +570,7 @@ def do_dec(cls, @classmethod def enc(cls, _s, size_len=0, **_kwargs): - # type: (AnyStr, Optional[int]) -> bytes + # type: (AnyStr, Optional[int], **Any) -> bytes s = bytes_encode(_s) if len(s) % 8 == 0: unused_bits = 0 @@ -587,7 +588,7 @@ class OERcodec_STRING(OERcodec_Object[str]): @classmethod def enc(cls, _s, size_len=0, **_kwargs): - # type: (Union[str, bytes], Optional[int]) -> bytes + # type: (Union[str, bytes], Optional[int], **Any) -> bytes s = bytes_encode(_s) if size_len and size_len == len(s): return s @@ -624,7 +625,7 @@ class OERcodec_NULL(OERcodec_Object[None]): @classmethod def enc(cls, i, size_len=0, **_kwargs): - # type: (Any, Optional[int]) -> bytes + # type: (Any, Optional[int], **Any) -> bytes return b"" @classmethod @@ -644,7 +645,7 @@ class OERcodec_OID(OERcodec_Object[bytes]): @classmethod def enc(cls, _oid, size_len=0, **_kwargs): - # type: (AnyStr, Optional[int]) -> bytes + # type: (AnyStr, Optional[int], **Any) -> bytes oid = bytes_encode(_oid) if oid: lst = [int(x) for x in oid.strip(b".").split(b".")] @@ -690,7 +691,7 @@ class OERcodec_ENUMERATED(OERcodec_INTEGER): @classmethod def enc(cls, i, size_len=0, **_kwargs): - # type: (int, Optional[int]) -> bytes + # type: (int, Optional[int], **Any) -> bytes return OER_enumerated_enc(i) @classmethod @@ -759,7 +760,7 @@ class OERcodec_SEQUENCE(OERcodec_Object[Union[bytes, List['OERcodec_Object[Any]' @classmethod def enc(cls, _ll, size_len=0, **_kwargs): - # type: (Union[bytes, List[OERcodec_Object[Any]]], Optional[int]) -> bytes + # type: (Union[bytes, List[OERcodec_Object[Any]]], Optional[int], **Any) -> bytes # noqa: E501 if isinstance(_ll, bytes): return _ll return b"".join(x.enc(cls.codec) for x in _ll) @@ -788,7 +789,7 @@ class OERcodec_IPADDRESS(OERcodec_STRING): @classmethod def enc(cls, ipaddr_ascii, size_len=0, **_kwargs): # type: ignore - # type: (str, Optional[int]) -> bytes + # type: (str, Optional[int], **Any) -> bytes try: s = inet_aton(ipaddr_ascii) except Exception: diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index 94958e7a686..d1637c3f529 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -652,7 +652,7 @@ def safedec(cls, @classmethod def enc(cls, s, size_len=0, uper_min=None, uper_max=None, **_kwargs): - # type: (_K, Optional[int], Optional[int], Optional[int]) -> bytes + # type: (_K, Optional[int], Optional[int], Optional[int], **Any) -> bytes if isinstance(s, (str, bytes)): return UPERcodec_STRING.enc(s, size_len=size_len, uper_min=uper_min, uper_max=uper_max) @@ -678,7 +678,6 @@ def _uper_enc_via_encode_into(cls, *args, **kwargs): return enc.as_bytes() - def UPER_tagging_enc(s, **kwargs): # type: (bytes, **Any) -> bytes # UPER has no BER-style TLV tagging. @@ -757,8 +756,9 @@ def dec_from_decoder(cls, return cls.asn1_object(value) @classmethod - def enc(cls, i, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False, **_kwargs): - # type: (int, Optional[int], Optional[int], Optional[int], bool) -> bytes + def enc(cls, i, size_len=0, uper_min=None, uper_max=None, + oer_unsigned=False, **_kwargs): + # type: (int, Optional[int], Optional[int], Optional[int], bool, **Any) -> bytes return _uper_enc_via_encode_into( cls, i, size_len, uper_min, uper_max, oer_unsigned, ) @@ -809,8 +809,9 @@ def dec_from_decoder(cls, return cls.asn1_object(dec.read_bit()) @classmethod - def enc(cls, i, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False, **_kwargs): - # type: (int, Optional[int], Optional[int], Optional[int], bool) -> bytes + def enc(cls, i, size_len=0, uper_min=None, uper_max=None, + oer_unsigned=False, **_kwargs): + # type: (int, Optional[int], Optional[int], Optional[int], bool, **Any) -> bytes return _uper_enc_via_encode_into( cls, i, size_len, uper_min, uper_max, oer_unsigned, ) @@ -917,8 +918,9 @@ def dec_from_decoder(cls, return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)) @classmethod - def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False, **_kwargs): - # type: (Any, Optional[int], Optional[int], Optional[int], bool) -> bytes + def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, + oer_unsigned=False, **_kwargs): + # type: (Any, Optional[int], Optional[int], Optional[int], bool, **Any) -> bytes return _uper_enc_via_encode_into( cls, _s, size_len, uper_min, uper_max, oer_unsigned, ) @@ -994,8 +996,9 @@ def dec_from_decoder(cls, return cls.asn1_object(raw) @classmethod - def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False, **_kwargs): - # type: (Union[str, bytes], Optional[int], Optional[int], Optional[int], bool) -> bytes # noqa: E501 + def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, + oer_unsigned=False, **_kwargs): + # type: (Union[str, bytes], Optional[int], Optional[int], Optional[int], bool, **Any) -> bytes # noqa: E501 return _uper_enc_via_encode_into( cls, _s, size_len, uper_min, uper_max, oer_unsigned, ) @@ -1045,8 +1048,9 @@ def dec_from_decoder(cls, return cls.asn1_object(None) @classmethod - def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False, **_kwargs): - # type: (Any, Optional[int], Optional[int], Optional[int], bool) -> bytes + def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, + oer_unsigned=False, **_kwargs): + # type: (Any, Optional[int], Optional[int], Optional[int], bool, **Any) -> bytes return b"" @classmethod @@ -1068,7 +1072,7 @@ class UPERcodec_OID(UPERcodec_Object[bytes]): @classmethod def enc(cls, _oid, size_len=0, uper_min=None, uper_max=None, **_kwargs): - # type: (AnyStr, Optional[int], Optional[int], Optional[int]) -> bytes + # type: (AnyStr, Optional[int], Optional[int], Optional[int], **Any) -> bytes oid = bytes_encode(_oid) if oid: lst = [int(x) for x in oid.split(b".")] @@ -1250,8 +1254,9 @@ def encode_into(cls, UPER_append_encoded(enc, _ll) @classmethod - def enc(cls, _ll, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False, **_kwargs): - # type: (Union[bytes, List[UPERcodec_Object[Any]]], Optional[int], Optional[int], Optional[int], bool) -> bytes # noqa: E501 + def enc(cls, _ll, size_len=0, uper_min=None, uper_max=None, + oer_unsigned=False, **_kwargs): + # type: (Union[bytes, List[UPERcodec_Object[Any]]], Optional[int], Optional[int], Optional[int], bool, **Any) -> bytes # noqa: E501 if isinstance(_ll, bytes): return _ll raise UPER_Encoding_Error( @@ -1284,7 +1289,7 @@ class UPERcodec_IPADDRESS(UPERcodec_STRING): @classmethod def enc(cls, ipaddr_ascii, size_len=0, uper_min=None, uper_max=None, **_kwargs): - # type: (str, Optional[int], Optional[int], Optional[int]) -> bytes + # type: (str, Optional[int], Optional[int], Optional[int], **Any) -> bytes try: s = inet_aton(ipaddr_ascii) except Exception: @@ -1739,7 +1744,7 @@ def set_absent(self, pkt): def m2i_from_decoder(self, pkt, dec): # type: (Any, Any, Any) -> Any codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - return codec.dec_from_decoder( # type: ignore[attr-defined] + return codec.dec_from_decoder( # type: ignore[attr-defined] # noqa: E501 dec, **self._codec_kwargs(pkt), ) @@ -1767,14 +1772,14 @@ def encode_into(self, enc, pkt, value=None): ) else: raw = value - codec.encode_into( # type: ignore[attr-defined] + codec.encode_into( # type: ignore[attr-defined] # noqa: E501 enc, raw, **self._codec_kwargs(pkt), ) - af.ASN1F_field.m2i_from_decoder = m2i_from_decoder # type: ignore[attr-defined] - af.ASN1F_field.dissect_from_decoder = dissect_from_decoder # type: ignore[attr-defined] - af.ASN1F_field.encode_into = encode_into # type: ignore[attr-defined] - af.ASN1F_field._uper_encode_into = encode_into # type: ignore[attr-defined] + af.ASN1F_field.m2i_from_decoder = m2i_from_decoder # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_field.dissect_from_decoder = dissect_from_decoder # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_field.encode_into = encode_into # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_field._uper_encode_into = encode_into # type: ignore[attr-defined] # noqa: E501 def seq_dissect_from_decoder(self, pkt, dec): # type: (Any, Any, Any) -> None @@ -1784,9 +1789,9 @@ def seq_encode_into(self, enc, pkt, value=None): # type: (Any, Any, Any, Any) -> None return _UPER_FieldHooks.sequence_encode_into(self, enc, pkt, value) - af.ASN1F_SEQUENCE.dissect_from_decoder = seq_dissect_from_decoder # type: ignore[attr-defined] - af.ASN1F_SEQUENCE.encode_into = seq_encode_into # type: ignore[attr-defined] - af.ASN1F_SEQUENCE._uper_encode_into = seq_encode_into # type: ignore[attr-defined] + af.ASN1F_SEQUENCE.dissect_from_decoder = seq_dissect_from_decoder # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_SEQUENCE.encode_into = seq_encode_into # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_SEQUENCE._uper_encode_into = seq_encode_into # type: ignore[attr-defined] # noqa: E501 def seqof_m2i_from_decoder(self, pkt, dec): # type: (Any, Any, Any) -> Any @@ -1796,9 +1801,9 @@ def seqof_encode_into(self, enc, pkt, value=None): # type: (Any, Any, Any, Any) -> None return _UPER_FieldHooks.sequence_of_encode_into(self, enc, pkt, value) - af.ASN1F_SEQUENCE_OF.m2i_from_decoder = seqof_m2i_from_decoder # type: ignore[attr-defined] - af.ASN1F_SEQUENCE_OF.encode_into = seqof_encode_into # type: ignore[attr-defined] - af.ASN1F_SEQUENCE_OF._uper_encode_into = seqof_encode_into # type: ignore[attr-defined] + af.ASN1F_SEQUENCE_OF.m2i_from_decoder = seqof_m2i_from_decoder # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_SEQUENCE_OF.encode_into = seqof_encode_into # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_SEQUENCE_OF._uper_encode_into = seqof_encode_into # type: ignore[attr-defined] # noqa: E501 def choice_m2i_from_decoder(self, pkt, dec): # type: (Any, Any, Any) -> Any @@ -1808,9 +1813,9 @@ def choice_encode_into(self, enc, pkt, value=None): # type: (Any, Any, Any, Any) -> None return _UPER_FieldHooks.choice_encode_into(self, enc, pkt, value) - af.ASN1F_CHOICE.m2i_from_decoder = choice_m2i_from_decoder # type: ignore[attr-defined] - af.ASN1F_CHOICE.encode_into = choice_encode_into # type: ignore[attr-defined] - af.ASN1F_CHOICE._uper_encode_into = choice_encode_into # type: ignore[attr-defined] + af.ASN1F_CHOICE.m2i_from_decoder = choice_m2i_from_decoder # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_CHOICE.encode_into = choice_encode_into # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_CHOICE._uper_encode_into = choice_encode_into # type: ignore[attr-defined] # noqa: E501 def packet_m2i_from_decoder(self, pkt, dec): # type: (Any, Any, Any) -> Any @@ -1820,9 +1825,9 @@ def packet_encode_into(self, enc, pkt, value=None): # type: (Any, Any, Any, Any) -> None return _UPER_FieldHooks.packet_encode_into(self, enc, pkt, value) - af.ASN1F_PACKET.m2i_from_decoder = packet_m2i_from_decoder # type: ignore[attr-defined] - af.ASN1F_PACKET.encode_into = packet_encode_into # type: ignore[attr-defined] - af.ASN1F_PACKET._uper_encode_into = packet_encode_into # type: ignore[attr-defined] + af.ASN1F_PACKET.m2i_from_decoder = packet_m2i_from_decoder # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_PACKET.encode_into = packet_encode_into # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_PACKET._uper_encode_into = packet_encode_into # type: ignore[attr-defined] # noqa: E501 def opt_set_absent(self, pkt): # type: (Any, Any) -> None @@ -1836,10 +1841,10 @@ def opt_encode_into(self, enc, pkt, value=None): # type: (Any, Any, Any, Any) -> None self._field.encode_into(enc, pkt, value) - af.ASN1F_optional.set_absent = opt_set_absent # type: ignore[attr-defined] - af.ASN1F_optional.dissect_from_decoder = opt_dissect_from_decoder # type: ignore[attr-defined] - af.ASN1F_optional.encode_into = opt_encode_into # type: ignore[attr-defined] - af.ASN1F_optional._uper_encode_into = opt_encode_into # type: ignore[attr-defined] + af.ASN1F_optional.set_absent = opt_set_absent # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_optional.dissect_from_decoder = opt_dissect_from_decoder # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_optional.encode_into = opt_encode_into # type: ignore[attr-defined] # noqa: E501 + af.ASN1F_optional._uper_encode_into = opt_encode_into # type: ignore[attr-defined] # noqa: E501 _orig_enum_init = af.ASN1F_enum_INTEGER.__init__ diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index f33e5763a95..8c089ed1bff 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -471,6 +471,9 @@ def _id_tagging_enc(s, **kwargs): def _id_tagging_dec(s, **kwargs): return None, s +# Save/restore: asn1.uts may already have loaded contrib UPER tagging. +_prev_tagging_enc = getattr(ASN1_Codecs.PER, "_tagging_enc", None) +_prev_tagging_dec = getattr(ASN1_Codecs.PER, "_tagging_dec", None) ASN1_Codecs.PER.register_tagging(_id_tagging_enc, _id_tagging_dec) try: assert ASN1_Codecs.PER.tagging_enc(b"\x02\x01\x05", implicit_tag=0xA0) == b"\x02\x01\x05" @@ -479,8 +482,11 @@ try: ) assert diff is None and payload == b"\x02\x01\x05" finally: - del ASN1_Codecs.PER._tagging_enc - del ASN1_Codecs.PER._tagging_dec + if _prev_tagging_enc is not None and _prev_tagging_dec is not None: + ASN1_Codecs.PER.register_tagging(_prev_tagging_enc, _prev_tagging_dec) + else: + del ASN1_Codecs.PER._tagging_enc + del ASN1_Codecs.PER._tagging_dec = field _codec_kwargs and object-enc hooks class P(ASN1_Packet): From 5ca64213bae60f0247c4c976169dbd960cd5cfd8 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Mon, 10 Aug 2026 14:33:56 +0200 Subject: [PATCH 07/46] Cleanup asn1fields AI-Assisted: yes (Cursor) --- scapy/asn1/asn1.py | 16 + scapy/asn1/ber.py | 3 - scapy/asn1fields.py | 59 ++- scapy/contrib/uper.py | 31 +- test/{scapy/layers => contrib}/oer.uts | 613 ++++++++++------------ test/{scapy/layers => contrib}/uper.uts | 650 ++++++++---------------- test/scapy/layers/ber.uts | 7 +- 7 files changed, 539 insertions(+), 840 deletions(-) rename test/{scapy/layers => contrib}/oer.uts (88%) rename test/{scapy/layers => contrib}/uper.uts (89%) diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index c2b556c855d..69b923fbd1b 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -137,6 +137,22 @@ def register_field_hooks(cls, hooks): # Optional compound-field helpers (SEQUENCE/CHOICE/…) for contrib codecs. cls._field_hooks = hooks + def unregister_field_hooks(cls): + # type: () -> Any + # Returns the previous hooks, so that callers can restore them. + hooks = getattr(cls, "_field_hooks", None) + try: + del cls._field_hooks + except AttributeError: + pass + return hooks + + def field_hook(cls, name): + # type: (str) -> Any + # Hooks are optional and may be partial: missing entries mean that + # asn1fields keeps its default (BER-style) implementation. + return getattr(getattr(cls, "_field_hooks", None), name, None) + def tagging_enc(cls, s, **kwargs): # type: (bytes, **Any) -> bytes return cls._tagging_enc(s, **kwargs) # type: ignore diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index cb9503d2f23..2f675964992 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -297,9 +297,6 @@ def __new__(cls, class BERcodec_Object(Generic[_K], metaclass=BERcodec_metaclass): codec = ASN1_Codecs.BER tag = ASN1_Class_UNIVERSAL.ANY - skip_tagging = False - tagging_enc = staticmethod(BER_tagging_enc) - tagging_dec = staticmethod(BER_tagging_dec) @classmethod def asn1_object(cls, val): diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index ba43478f95f..364bde87a90 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -70,6 +70,13 @@ class ASN1F_element(object): pass +def _field_hook(pkt, name): + # type: (Any, str) -> Any + # Contrib codecs (OER/UPER/…) may override compound field operations. + # Returns None when the codec keeps the default BER behaviour. + return pkt.ASN1_codec.field_hook(name) + + ########################## # Basic ASN1 Field # ########################## @@ -108,8 +115,6 @@ def __init__(self, # Contrib codecs (OER/UPER/…) pass constraints here, e.g. # oer_unsigned=, uper_min=/uper_max=, uper_extensible=. self.codec_opts = codec_opts # type: Dict[str, Any] - for key, val in codec_opts.items(): - setattr(self, key, val) self.flexible_tag = flexible_tag if (implicit_tag is not None) and (explicit_tag is not None): err_msg = "field cannot be both implicitly and explicitly tagged" @@ -171,9 +176,9 @@ def _codec_kwargs(self, pkt): def _use_object_enc(self, pkt, item): # type: (ASN1_Packet, ASN1_Object[Any]) -> bool # Contrib codecs may force codec.enc(**kwargs) via field hooks. - hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) - if hooks is not None and hasattr(hooks, "use_object_enc"): - return cast(bool, hooks.use_object_enc(self, pkt, item)) + hook = _field_hook(pkt, "use_object_enc") + if hook is not None: + return cast(bool, hook(self, pkt, item)) return self.size_len is None and not self.codec_opts def _encode_item(self, pkt, item): @@ -549,9 +554,9 @@ def m2i(self, pkt, s): Thus m2i returns an empty list (along with the proper remainder). It is discarded by dissect() and should not be missed elsewhere. """ - hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) - if hooks is not None: - return cast(Tuple[Any, bytes], hooks.sequence_m2i(self, pkt, s)) + hook = _field_hook(pkt, "sequence_m2i") + if hook is not None: + return cast(Tuple[Any, bytes], hook(self, pkt, s)) s = self._apply_tagging_dec(s, pkt, _fname=pkt.name) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) i, s, remain = codec.check_type_check_len(s) @@ -570,9 +575,9 @@ def dissect(self, pkt, s): def build(self, pkt): # type: (ASN1_Packet) -> bytes - hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) - if hooks is not None: - return cast(bytes, hooks.sequence_build(self, pkt)) + hook = _field_hook(pkt, "sequence_build") + if hook is not None: + return cast(bytes, hook(self, pkt)) s = reduce(lambda x, y: x + y.build(pkt), self.seq, b"") return super(ASN1F_SEQUENCE, self).i2m(pkt, s) @@ -641,10 +646,9 @@ def m2i(self, s, # type: bytes ): # type: (...) -> Tuple[List[Any], bytes] - hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) - if hooks is not None: - return cast(Tuple[List[Any], bytes], - hooks.sequence_of_m2i(self, pkt, s)) + hook = _field_hook(pkt, "sequence_of_m2i") + if hook is not None: + return cast(Tuple[List[Any], bytes], hook(self, pkt, s)) s = self._apply_tagging_dec(s, pkt) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) i, s, remain = codec.check_type_check_len(s) @@ -662,9 +666,9 @@ def m2i(self, def build(self, pkt): # type: (ASN1_Packet) -> bytes - hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) - if hooks is not None: - return cast(bytes, hooks.sequence_of_build(self, pkt)) + hook = _field_hook(pkt, "sequence_of_build") + if hook is not None: + return cast(bytes, hook(self, pkt)) val = getattr(pkt, self.name) if isinstance(val, ASN1_Object) and \ val.tag == ASN1_Class_UNIVERSAL.RAW: @@ -846,10 +850,9 @@ def m2i(self, pkt, s): """ if len(s) == 0: raise ASN1_Error("ASN1F_CHOICE: got empty string") - hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) - if hooks is not None: - return cast(Tuple[ASN1_Object[Any], bytes], - hooks.choice_m2i(self, pkt, s)) + hook = _field_hook(pkt, "choice_m2i") + if hook is not None: + return cast(Tuple[ASN1_Object[Any], bytes], hook(self, pkt, s)) s = self._apply_tagging_dec(s, pkt) tag, _ = BER_id_dec(s) if tag in self.choices: @@ -875,9 +878,9 @@ def m2i(self, pkt, s): def i2m(self, pkt, x): # type: (ASN1_Packet, Any) -> bytes - hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) - if hooks is not None: - return cast(bytes, hooks.choice_i2m(self, pkt, x)) + hook = _field_hook(pkt, "choice_i2m") + if hook is not None: + return cast(bytes, hook(self, pkt, x)) if x is None: s = b"" else: @@ -963,9 +966,9 @@ def i2m(self, x # type: Union[bytes, ASN1_Packet, None, ASN1_Object[Optional[ASN1_Packet]]] # noqa: E501 ): # type: (...) -> bytes - hooks = getattr(pkt.ASN1_codec, "_field_hooks", None) - if hooks is not None and hasattr(hooks, "packet_i2m"): - return cast(bytes, hooks.packet_i2m(self, pkt, x)) + hook = _field_hook(pkt, "packet_i2m") + if hook is not None: + return cast(bytes, hook(self, pkt, x)) if x is None: s = b"" elif isinstance(x, bytes): diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index d1637c3f529..41a2d09bf52 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -1383,12 +1383,13 @@ class UPERcodec_BMP_STRING(UPERcodec_STRING): def _field_extensible(field): # type: (Any) -> bool - return bool(getattr(field, "uper_extensible", False)) + return bool(getattr(field, "codec_opts", {}).get("uper_extensible", False)) def _field_range(field): # type: (Any) -> Tuple[Optional[int], Optional[int]] - return getattr(field, "uper_min", None), getattr(field, "uper_max", None) + opts = getattr(field, "codec_opts", {}) + return opts.get("uper_min"), opts.get("uper_max") class _UPER_FieldHooks(object): @@ -1846,22 +1847,20 @@ def opt_encode_into(self, enc, pkt, value=None): af.ASN1F_optional.encode_into = opt_encode_into # type: ignore[attr-defined] # noqa: E501 af.ASN1F_optional._uper_encode_into = opt_encode_into # type: ignore[attr-defined] # noqa: E501 - _orig_enum_init = af.ASN1F_enum_INTEGER.__init__ + _orig_enum_codec_kwargs = af.ASN1F_enum_INTEGER._codec_kwargs - def enum_init(self, name, default, enum, context=None, - implicit_tag=None, explicit_tag=None): - # type: (Any, str, Any, Any, Any, Any, Any) -> None - _orig_enum_init( - self, name, default, enum, context=context, - implicit_tag=implicit_tag, explicit_tag=explicit_tag, - ) - values = list(self.i2s) - self.uper_enum_values = values - opts = dict(getattr(self, "codec_opts", {})) - opts["uper_enum_values"] = values - self.codec_opts = opts + def enum_codec_kwargs(self, pkt): + # type: (Any, Any) -> Any + kwargs = _orig_enum_codec_kwargs(self, pkt) + # The permitted values belong to the UPER encoding, not to the field + # definition, so they are only added for PER packets. Other codecs + # keep an empty codec_opts and their item.enc() fast path. + codec = getattr(pkt, "ASN1_codec", None) + if getattr(codec, "_field_hooks", None) is _UPER_FieldHooks: + kwargs.setdefault("uper_enum_values", list(self.i2s)) + return kwargs - af.ASN1F_enum_INTEGER.__init__ = enum_init # type: ignore[assignment] + af.ASN1F_enum_INTEGER._codec_kwargs = enum_codec_kwargs # type: ignore[assignment] # noqa: E501 _install_uper_asn1fields() diff --git a/test/scapy/layers/oer.uts b/test/contrib/oer.uts similarity index 88% rename from test/scapy/layers/oer.uts rename to test/contrib/oer.uts index 5ef6dfdaf39..4d355f67d54 100644 --- a/test/scapy/layers/oer.uts +++ b/test/contrib/oer.uts @@ -5,139 +5,13 @@ # bash test/run_tests -t test/scapy/layers/oer.uts -F + ASN.1 OER load -= import contrib codecs += prepare helpers and packet classes import scapy.contrib.oer -from scapy.contrib.oer import * -from scapy.packet import raw - - -+ ASN.1 OER codec -= OER length determinant short form -OER_len_enc(3) == b"\x03" -= OER length determinant long form -OER_len_enc(200) == b"\x81\xc8" -= OER boolean false -OERcodec_BOOLEAN.enc(0) == b"\x00" -= OER boolean true -OERcodec_BOOLEAN.enc(1) == b"\xff" -= OER null -OERcodec_NULL.enc(None) == b"" -= OER unconstrained integer -OERcodec_INTEGER.enc(4) == b"\x01\x04" -= OER constrained unsigned integer -OERcodec_INTEGER.enc(4, size_len=1) == b"\x04" -= OER constrained signed integer -OERcodec_INTEGER.enc(4, size_len=2) == b"\x00\x04" -= OER enumerated short form -OERcodec_ENUMERATED.enc(6) == b"\x06" -= OER octet string -OERcodec_STRING.enc(b"ABC") == b"\x03ABC" -= OER OID -OERcodec_OID.enc("1.2.3") == b"\x02\x2a\x03" -= OER integer roundtrip -x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(12345)) -x.val == 12345 and r == b"" -= OER boolean roundtrip -x, r = OERcodec_BOOLEAN.do_dec(OERcodec_BOOLEAN.enc(1)) -x.val == 1 and r == b"" -= OER ASN1 object encoding -ASN1_INTEGER(42).enc(ASN1_Codecs.OER) == b"\x01*" -= OER codec registration -ASN1_Class_UNIVERSAL.INTEGER.get_codec(ASN1_Codecs.OER) is OERcodec_INTEGER - -+ ASN.1 OER codec (extended) -= OER length zero -OER_len_enc(0) == b"\x00" -= OER length boundary short form -OER_len_enc(127) == b"\x7f" -= OER length boundary long form -OER_len_enc(128) == b"\x81\x80" -= OER length roundtrip -l, r = OER_len_dec(OER_len_enc(999)) -l == 999 and r == b"" -= OER signed integer zero -OER_signed_integer_enc(0) == b"\x01\x00" -= OER signed integer negative -OER_signed_integer_enc(-255) == b"\x02\xff\x01" -= OER signed integer large -OER_signed_integer_enc(100000) == b"\x03\x01\x86\xa0" -= OER signed integer roundtrip -v, r = OER_signed_integer_dec(OER_signed_integer_enc(-1234567)) -v == -1234567 and r == b"" -= OER unsigned integer zero -OER_unsigned_integer_enc(0) == b"\x01\x00" -= OER unsigned integer roundtrip -v, r = OER_unsigned_integer_dec(OER_unsigned_integer_enc(65535)) -v == 65535 and r == b"" -= OER fixed unsigned 1 byte -OERcodec_INTEGER.enc(255, size_len=1) == b"\xff" -= OER fixed signed 2 bytes negative -OERcodec_INTEGER.enc(-2, size_len=2) == b"\xff\xfe" -= OER fixed signed 4 bytes -OERcodec_INTEGER.enc(-2, size_len=4) == b"\xff\xff\xff\xfe" -= OER enumerated long form -OERcodec_ENUMERATED.enc(128) == b"\x82\x00\x80" -= OER enumerated negative -OERcodec_ENUMERATED.enc(-1) == b"\x81\xff" -= OER enumerated roundtrip -x, r = OERcodec_ENUMERATED.do_dec(OERcodec_ENUMERATED.enc(128)) -x.val == 128 and r == b"" -= OER null roundtrip -x, r = OERcodec_NULL.do_dec(OERcodec_NULL.enc(None)) -x.val is None and r == b"" -= OER octet string empty -OERcodec_STRING.enc(b"") == b"\x00" -= OER octet string fixed size -OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3) == b"\x12\x34\x56" -= OER octet string roundtrip -x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34")) -x.val == b"\x12\x34" and r == b"" -= OER OID 1.2 -OERcodec_OID.enc("1.2") == b"\x01\x2a" -= OER OID roundtrip -x, r = OERcodec_OID.do_dec(OERcodec_OID.enc("1.2.3321")) -x.val == "1.2.3321" and r == b"" -= OER bit string variable size -OERcodec_BIT_STRING.enc("0100") == b"\x02\x04\x40" -= OER bit string roundtrip -x, r = OERcodec_BIT_STRING.do_dec(OERcodec_BIT_STRING.enc("01000001")) -x.val == "01000001" and r == b"" -= OER IA5 string -OERcodec_IA5_STRING.enc(b"ABC") == b"\x03ABC" -= OER tag short form -OER_tag_enc(1, OER_CLASS_CONTEXT) == b"\x81" -= OER tag roundtrip -cls, num, r = OER_tag_dec(OER_tag_enc(1, OER_CLASS_CONTEXT)) -cls == OER_CLASS_CONTEXT and num == 1 and r == b"" -= OER sequence concat -OERcodec_SEQUENCE.enc([ASN1_INTEGER(4), ASN1_INTEGER(5)]) == b"\x01\x04\x01\x05" -= OER ASN1 boolean object -ASN1_BOOLEAN(1).enc(ASN1_Codecs.OER) == b"\xff" -= OER ASN1 null object -ASN1_NULL(None).enc(ASN1_Codecs.OER) == b"" - -+ ASN.1 OER review fixes -= OER fixed integer decode roundtrip -x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(128, size_len=1), size_len=1, oer_unsigned=True) -x.val == 128 and r == b"" -= OER fixed integer signed decode -x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(-2, size_len=2), size_len=2) -x.val == -2 and r == b"" -= OER fixed octet string decode -x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3), size_len=3) -x.val == b"\x12\x34\x56" and r == b"" -= OER explicit null tagging -OER_tagging_enc(OERcodec_NULL.enc(None), explicit_tag=0x81) == b"\x81" -= OER choice id decode -tag, r = OER_id_dec(b"\x81\x01") -tag == 0x81 and r == b"\x01" -+ ASN.1 OER packets, interop and fuzz -= import contrib codecs -import scapy.contrib.oer from scapy.contrib.oer import * + from scapy.packet import raw -= prepare helpers and packet classes + class OERTaggedInteger(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_INTEGER("n", 0, explicit_tag=0xA1) @@ -290,19 +164,263 @@ _DECODE_ERRORS = ( IndexError, ) -class OERFuzzRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_BOOLEAN("flag", False), - ASN1F_STRING("label", ""), - ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), - ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), - ) +class OERFuzzRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + +def _fuzz_packets(): + # type: () -> Iterable[Type[ASN1_Packet]] + return (OERFuzzRecord,) + +def _record_kwargs(): + # type: () -> dict + return dict( + id=42, + flag=True, + label=b"hi", + extra=7, + values=[1, 2, 3], + ) + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + +def _assert_record(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + +def _assert_record_empty(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] + +def _dissect(cls, data_hex): + # type: (Type[ASN1_Packet], str) -> ASN1_Packet + return cls(bytes.fromhex(data_hex)) + +class _InnerRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("mode", ASN1_INTEGER(0), ["off", "on"]), + ) + +class _EncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_ENCAPS("payload", None, _InnerRecord), + ) + +class _FlagsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_FLAGS("f", "000", ["read", "write", "exec"]), + ) + +class _SetOfRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SET_OF("items", [], ASN1F_INTEGER) + +class _PacketFieldRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_PacketField("data", b""), + ) + +class _ExplicitPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2) + +class _BitEncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord), + ) + +def _raises(exc, func): + # type: (type, Any) -> None + try: + func() + except exc: + return + raise AssertionError("Expected %s" % exc.__name__) + +import scapy.contrib.uper + +class OEREmptySequenceOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + +class OEREnumField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_ENUMERATED("e", 0, {0: "a", 1: "b"}) + +class OERBitStringField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_BIT_STRING("b", "0101") + +class OERNullRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_NULL("z", 0), + ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ) + +class OEROidField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_OID("oid", "1.2.3") + +class OERInnerSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, size_len=1, oer_unsigned=True), + ) + +class OERPacketChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE("c", None, OERInnerSeq, ASN1F_INTEGER) + +class OERUnsignedField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER( + "n", 0, size_len=1, oer_unsigned=True, + ) + ++ ASN.1 OER codec += OER length determinant short form +OER_len_enc(3) == b"\x03" += OER length determinant long form +OER_len_enc(200) == b"\x81\xc8" += OER boolean false +OERcodec_BOOLEAN.enc(0) == b"\x00" += OER boolean true +OERcodec_BOOLEAN.enc(1) == b"\xff" += OER null +OERcodec_NULL.enc(None) == b"" += OER unconstrained integer +OERcodec_INTEGER.enc(4) == b"\x01\x04" += OER constrained unsigned integer +OERcodec_INTEGER.enc(4, size_len=1) == b"\x04" += OER constrained signed integer +OERcodec_INTEGER.enc(4, size_len=2) == b"\x00\x04" += OER enumerated short form +OERcodec_ENUMERATED.enc(6) == b"\x06" += OER octet string +OERcodec_STRING.enc(b"ABC") == b"\x03ABC" += OER OID +OERcodec_OID.enc("1.2.3") == b"\x02\x2a\x03" += OER integer roundtrip +x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(12345)) +x.val == 12345 and r == b"" += OER boolean roundtrip +x, r = OERcodec_BOOLEAN.do_dec(OERcodec_BOOLEAN.enc(1)) +x.val == 1 and r == b"" += OER ASN1 object encoding +ASN1_INTEGER(42).enc(ASN1_Codecs.OER) == b"\x01*" += OER codec registration +ASN1_Class_UNIVERSAL.INTEGER.get_codec(ASN1_Codecs.OER) is OERcodec_INTEGER + ++ ASN.1 OER codec (extended) += OER length zero +OER_len_enc(0) == b"\x00" += OER length boundary short form +OER_len_enc(127) == b"\x7f" += OER length boundary long form +OER_len_enc(128) == b"\x81\x80" += OER length roundtrip +l, r = OER_len_dec(OER_len_enc(999)) +l == 999 and r == b"" += OER signed integer zero +OER_signed_integer_enc(0) == b"\x01\x00" += OER signed integer negative +OER_signed_integer_enc(-255) == b"\x02\xff\x01" += OER signed integer large +OER_signed_integer_enc(100000) == b"\x03\x01\x86\xa0" += OER signed integer roundtrip +v, r = OER_signed_integer_dec(OER_signed_integer_enc(-1234567)) +v == -1234567 and r == b"" += OER unsigned integer zero +OER_unsigned_integer_enc(0) == b"\x01\x00" += OER unsigned integer roundtrip +v, r = OER_unsigned_integer_dec(OER_unsigned_integer_enc(65535)) +v == 65535 and r == b"" += OER fixed unsigned 1 byte +OERcodec_INTEGER.enc(255, size_len=1) == b"\xff" += OER fixed signed 2 bytes negative +OERcodec_INTEGER.enc(-2, size_len=2) == b"\xff\xfe" += OER fixed signed 4 bytes +OERcodec_INTEGER.enc(-2, size_len=4) == b"\xff\xff\xff\xfe" += OER enumerated long form +OERcodec_ENUMERATED.enc(128) == b"\x82\x00\x80" += OER enumerated negative +OERcodec_ENUMERATED.enc(-1) == b"\x81\xff" += OER enumerated roundtrip +x, r = OERcodec_ENUMERATED.do_dec(OERcodec_ENUMERATED.enc(128)) +x.val == 128 and r == b"" += OER null roundtrip +x, r = OERcodec_NULL.do_dec(OERcodec_NULL.enc(None)) +x.val is None and r == b"" += OER octet string empty +OERcodec_STRING.enc(b"") == b"\x00" += OER octet string fixed size +OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3) == b"\x12\x34\x56" += OER octet string roundtrip +x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34")) +x.val == b"\x12\x34" and r == b"" += OER OID 1.2 +OERcodec_OID.enc("1.2") == b"\x01\x2a" += OER OID roundtrip +x, r = OERcodec_OID.do_dec(OERcodec_OID.enc("1.2.3321")) +x.val == "1.2.3321" and r == b"" += OER bit string variable size +OERcodec_BIT_STRING.enc("0100") == b"\x02\x04\x40" += OER bit string roundtrip +x, r = OERcodec_BIT_STRING.do_dec(OERcodec_BIT_STRING.enc("01000001")) +x.val == "01000001" and r == b"" += OER IA5 string +OERcodec_IA5_STRING.enc(b"ABC") == b"\x03ABC" += OER tag short form +OER_tag_enc(1, OER_CLASS_CONTEXT) == b"\x81" += OER tag roundtrip +cls, num, r = OER_tag_dec(OER_tag_enc(1, OER_CLASS_CONTEXT)) +cls == OER_CLASS_CONTEXT and num == 1 and r == b"" += OER sequence concat +OERcodec_SEQUENCE.enc([ASN1_INTEGER(4), ASN1_INTEGER(5)]) == b"\x01\x04\x01\x05" += OER ASN1 boolean object +ASN1_BOOLEAN(1).enc(ASN1_Codecs.OER) == b"\xff" += OER ASN1 null object +ASN1_NULL(None).enc(ASN1_Codecs.OER) == b"" + ++ ASN.1 OER review fixes += OER fixed integer decode roundtrip +x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(128, size_len=1), size_len=1, oer_unsigned=True) +x.val == 128 and r == b"" += OER fixed integer signed decode +x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(-2, size_len=2), size_len=2) +x.val == -2 and r == b"" += OER fixed octet string decode +x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3), size_len=3) +x.val == b"\x12\x34\x56" and r == b"" += OER explicit null tagging +OER_tagging_enc(OERcodec_NULL.enc(None), explicit_tag=0x81) == b"\x81" += OER choice id decode +tag, r = OER_id_dec(b"\x81\x01") +tag == 0x81 and r == b"\x01" -def _fuzz_packets(): - # type: () -> Iterable[Type[ASN1_Packet]] - return (OERFuzzRecord,) ++ ASN.1 OER packets, interop and fuzz = oer field explicit tag pkt = OERTaggedInteger(n=5) @@ -577,121 +695,6 @@ for cls in _fuzz_packets(): True + ASN.1 OER build and dissect -= import contrib codecs -import scapy.contrib.oer -from scapy.contrib.oer import * -from scapy.packet import raw -= prepare helpers and packet classes -class OERTaggedInteger(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_INTEGER("n", 0, explicit_tag=0xA1) - -class OERFixedFields(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), - ASN1F_STRING("s", "", size_len=3), - ) - -class OEROptionalField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), - ) - -class OERSequenceOfIntegers(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) - -class OERChoiceField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - -class OERRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_BOOLEAN("flag", False), - ASN1F_STRING("label", ""), - ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), - ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), - ) - -class OERNestedSequence(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0), - ASN1F_BOOLEAN("y", False), - ), - ) - -class OERNestedSequenceTrailing(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0), - ASN1F_BOOLEAN("y", False), - ), - ASN1F_INTEGER("id", 0), - ) - -class OERSequenceOfWithTrailing(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), - ASN1F_INTEGER("id", 0), - ) - -def _roundtrip(cls, pkt): - # type: (type, ASN1_Packet) -> ASN1_Packet - return cls(raw(pkt)) - -def _roundtrip(cls, pkt): - # type: (type, ASN1_Packet) -> ASN1_Packet - return cls(raw(pkt)) - -def _record_kwargs(): - # type: () -> dict - return dict( - id=42, - flag=True, - label=b"hi", - extra=7, - values=[1, 2, 3], - ) - -def _asn1_int(val): - # type: (Any) -> int - return val.val if hasattr(val, "val") else val - -def _asn1_int(val): - # type: (Any) -> int - return val.val if hasattr(val, "val") else val - -def _assert_record(decoded): - # type: (ASN1_Packet) -> None - assert decoded.id.val == 42 - assert decoded.flag.val == 1 - assert decoded.label.val == b"hi" - assert decoded.extra.val == 7 - assert [x.val for x in decoded.values] == [1, 2, 3] - -def _assert_record_empty(decoded): - # type: (ASN1_Packet) -> None - assert decoded.id.val == 1 - assert decoded.flag.val == 0 - assert decoded.label.val == b"" - assert decoded.extra is None - assert [x.val for x in decoded.values] == [] - -def _dissect(cls, data_hex): - # type: (Type[ASN1_Packet], str) -> ASN1_Packet - return cls(bytes.fromhex(data_hex)) = oer record build roundtrip pkt = OERRecord(**_record_kwargs()) @@ -762,56 +765,6 @@ True + ASN.1 OER coverage -= import contrib codecs -import scapy.contrib.oer -from scapy.contrib.oer import * -from scapy.packet import raw -= prepare helpers and packet classes -class _InnerRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_enum_INTEGER("mode", ASN1_INTEGER(0), ["off", "on"]), - ) - -class _EncapsRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_STRING_ENCAPS("payload", None, _InnerRecord), - ) - -class _FlagsRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_FLAGS("f", "000", ["read", "write", "exec"]), - ) - -class _SetOfRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SET_OF("items", [], ASN1F_INTEGER) - -class _PacketFieldRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_STRING_PacketField("data", b""), - ) - -class _ExplicitPacket(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2) - -class _BitEncapsRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord), - ) - -def _raises(exc, func): - # type: (type, Any) -> None - try: - func() - except exc: - return - raise AssertionError("Expected %s" % exc.__name__) = oer error str obj = ASN1_INTEGER(1) @@ -862,54 +815,6 @@ True + ASN.1 OER field hooks and packet extras -= import contrib codecs -import scapy.contrib.oer -import scapy.contrib.uper -from scapy.contrib.oer import * -from scapy.packet import raw -= prepare helpers and packet classes -class OEREmptySequenceOf(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) - -class OEREnumField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_ENUMERATED("e", 0, {0: "a", 1: "b"}) - -class OERBitStringField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_BIT_STRING("b", "0101") - -class OERNullRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_NULL("z", 0), - ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), - ) - -class OEROidField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_OID("oid", "1.2.3") - -class OERInnerSeq(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0, size_len=1, oer_unsigned=True), - ) - -class OERPacketChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_CHOICE("c", None, OERInnerSeq, ASN1F_INTEGER) - -class OERUnsignedField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_INTEGER( - "n", 0, size_len=1, oer_unsigned=True, - ) - -def _roundtrip(cls, pkt): - # type: (type, ASN1_Packet) -> ASN1_Packet - return cls(raw(pkt)) = oer field hooks registered assert hasattr(ASN1_Codecs.OER, "_field_hooks") diff --git a/test/scapy/layers/uper.uts b/test/contrib/uper.uts similarity index 89% rename from test/scapy/layers/uper.uts rename to test/contrib/uper.uts index 4e5168c55a2..7e19761a8e9 100644 --- a/test/scapy/layers/uper.uts +++ b/test/contrib/uper.uts @@ -5,53 +5,13 @@ # bash test/run_tests -t test/scapy/layers/uper.uts -F + ASN.1 UPER load -= import contrib codecs += prepare helpers and packet classes import scapy.contrib.uper -from scapy.contrib.uper import * -from scapy.packet import raw - - -+ ASN.1 UPER codec -= UPER boolean true -UPERcodec_BOOLEAN.enc(1) == b"\x80" -= UPER boolean false -UPERcodec_BOOLEAN.enc(0) == b"\x00" -= UPER unconstrained integer -UPERcodec_INTEGER.enc(42) == b"\x01*" -= UPER constrained integer -UPERcodec_INTEGER.enc(200, uper_min=0, uper_max=255) == b"\xc8" -= UPER signed constrained integer -UPERcodec_INTEGER.enc(-1, uper_min=-128, uper_max=127) == b"\x7f" -= UPER octet string -UPERcodec_STRING.enc(b"AB") == b"\x02AB" -= UPER fixed octet string -UPERcodec_STRING.enc(b"\x12\x34\x56", size_len=3) == b"\x12\x34\x56" -= UPER null -UPERcodec_NULL.enc(None) == b"" -= UPER enumerated index -UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]) == b"\x80" -= UPER bit string variable size -UPERcodec_BIT_STRING.enc((b"\xab\xcd", 16), uper_min=1, uper_max=20) == bytes.fromhex("7d5e68") -= UPER enumerated roundtrip -x, r = UPERcodec_ENUMERATED.do_dec(UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), uper_enum_values=[1, 200]) -x.val == 200 and r == b"" -= UPER integer roundtrip -x, r = UPERcodec_INTEGER.do_dec(UPERcodec_INTEGER.enc(-1)) -x.val == -1 and r == b"" -= UPER boolean roundtrip -x, r = UPERcodec_BOOLEAN.do_dec(UPERcodec_BOOLEAN.enc(1)) -x.val == 1 and r == b"" -= UPER ASN1 object encoding -ASN1_INTEGER(42).enc(ASN1_Codecs.PER) == b"\x01*" -= UPER codec registration -ASN1_Class_UNIVERSAL.INTEGER.get_codec(ASN1_Codecs.PER) is UPERcodec_INTEGER -+ ASN.1 UPER packets, helpers, interop and fuzz -= import contrib codecs -import scapy.contrib.uper from scapy.contrib.uper import * + from scapy.packet import raw -= prepare helpers and packet classes + class UPERFixedFields(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( @@ -582,6 +542,201 @@ class UPERFuzzEnumerated(ASN1_Packet): def _fuzz_packets(): # type: () -> Iterable[Type[ASN1_Packet]] return (UPERFuzzRecord, UPERFuzzNested, UPERFuzzEnumerated) + +def _record_kwargs(): + # type: () -> dict + return dict( + id=42, + flag=True, + label=b"hi", + extra=7, + values=[1, 2, 3], + ) + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + +def _assert_record(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + +def _assert_record_empty(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] + +def _dissect(cls, data_hex): + # type: (Type[ASN1_Packet], str) -> ASN1_Packet + return cls(bytes.fromhex(data_hex)) + +from unittest import mock + +from scapy.asn1.ber import BER_Decoding_Error + +from scapy.contrib.oer import OER_Decoding_Error, OER_Encoding_Error + +from scapy.contrib.uper import ( + UPER_Decoding_Error, UPER_Encoding_Error, UPER_Decoder, UPER_Encoder, +) + +from scapy.packet import Raw, raw + +class _InnerRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("mode", ASN1_INTEGER(0), ["off", "on"]), + ) + +class _EncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_ENCAPS("payload", None, _InnerRecord), + ) + +class _FlagsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_FLAGS("f", "000", ["read", "write", "exec"]), + ) + +class _SetOfRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SET_OF("items", [], ASN1F_INTEGER) + +class _PacketFieldRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_PacketField("data", b""), + ) + +class _ExplicitPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2) + +class _BitEncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord), + ) + +def _raises(exc, func): + # type: (type, Any) -> None + try: + func() + except exc: + return + raise AssertionError("Expected %s" % exc.__name__) + +import scapy.contrib.oer + +from scapy.contrib.oer import * + +from scapy.contrib.uper import ASN1F_DEFAULT + +import scapy.asn1fields as asn1fields + +def _val(x): + # type: (Any) -> Any + return x.val if hasattr(x, "val") else x + +class UPERSmallDefaultRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_DEFAULT( + ASN1F_INTEGER("n", 5, uper_min=0, uper_max=10), + 5, + ), + ) + +class UPEREmptySeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", + [], + ASN1F_INTEGER("item", 0, uper_min=0, uper_max=7), + uper_min=0, + uper_max=3, + ) + +class UPERExtSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", + [], + ASN1F_INTEGER("item", 0, uper_min=0, uper_max=7), + uper_min=1, + uper_max=2, + uper_extensible=True, + ) + +class UPERFlagsField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_FLAGS( + "f", "101", ["a", "b", "c"], uper_min=3, uper_max=3, + ) + +class UPERInnerPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, uper_min=0, uper_max=15), + ) + +class UPERWrappedPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_PACKET("inner", None, UPERInnerPacket), + ) + +class UPERConstrainedInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=255) + ++ ASN.1 UPER codec += UPER boolean true +UPERcodec_BOOLEAN.enc(1) == b"\x80" += UPER boolean false +UPERcodec_BOOLEAN.enc(0) == b"\x00" += UPER unconstrained integer +UPERcodec_INTEGER.enc(42) == b"\x01*" += UPER constrained integer +UPERcodec_INTEGER.enc(200, uper_min=0, uper_max=255) == b"\xc8" += UPER signed constrained integer +UPERcodec_INTEGER.enc(-1, uper_min=-128, uper_max=127) == b"\x7f" += UPER octet string +UPERcodec_STRING.enc(b"AB") == b"\x02AB" += UPER fixed octet string +UPERcodec_STRING.enc(b"\x12\x34\x56", size_len=3) == b"\x12\x34\x56" += UPER null +UPERcodec_NULL.enc(None) == b"" += UPER enumerated index +UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]) == b"\x80" += UPER bit string variable size +UPERcodec_BIT_STRING.enc((b"\xab\xcd", 16), uper_min=1, uper_max=20) == bytes.fromhex("7d5e68") += UPER enumerated roundtrip +x, r = UPERcodec_ENUMERATED.do_dec(UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), uper_enum_values=[1, 200]) +x.val == 200 and r == b"" += UPER integer roundtrip +x, r = UPERcodec_INTEGER.do_dec(UPERcodec_INTEGER.enc(-1)) +x.val == -1 and r == b"" += UPER boolean roundtrip +x, r = UPERcodec_BOOLEAN.do_dec(UPERcodec_BOOLEAN.enc(1)) +x.val == 1 and r == b"" += UPER ASN1 object encoding +ASN1_INTEGER(42).enc(ASN1_Codecs.PER) == b"\x01*" += UPER codec registration +ASN1_Class_UNIVERSAL.INTEGER.get_codec(ASN1_Codecs.PER) is UPERcodec_INTEGER + ++ ASN.1 UPER packets, helpers, interop and fuzz = uper field fixed size pkt = UPERFixedFields(n=200, s=b"ABC") @@ -1377,209 +1532,10 @@ for cls in _fuzz_packets(): True + ASN.1 UPER build and dissect -= import contrib codecs -import scapy.contrib.uper -from scapy.contrib.uper import * -from scapy.packet import raw -= prepare helpers and packet classes -class UPERFixedFields(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), - ASN1F_STRING("s", "", size_len=3), - ) += per record build roundtrip +pkt = UPERRecord(**_record_kwargs()) -class UPERIntegerField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0) - -class UPERBooleanField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_BOOLEAN("b", False) - -class UPERStringField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_STRING("s", "") - -class UPERConstrainedInteger(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER( - "n", 0, size_len=1, oer_unsigned=True, - ) - -class UPEROptionalField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_BOOLEAN("flag", False), - ASN1F_optional(ASN1F_INTEGER("extra", 0)), - ) - -class UPERSequenceOfIntegers(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) - -class UPERChoiceField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - ) - -class UPERChoiceStringFirst(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_CHOICE( - "c", ASN1_STRING(b""), ASN1F_STRING, ASN1F_INTEGER, - ) - -class UPERRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_BOOLEAN("flag", False), - ASN1F_STRING("label", ""), - ASN1F_optional(ASN1F_INTEGER("extra", 0)), - ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), - ) - -class UPEREnumeratedField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_ENUMERATED( - "state", 1, {1: "alpha", 200: "beta"}, - ) - -class UPERBitStringField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_BIT_STRING( - "bits", "0", uper_min=1, uper_max=20, - ) - -class UPERMessagePrefix(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("msgId", 0), - ASN1F_INTEGER("myflag", 0), - ASN1F_STRING("szDescription", "", size_len=10), - ASN1F_BOOLEAN("isReady", False), - ) - -class UPERSequenceWithChoice(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_CHOICE("c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING), - ) - -class UPERNullPacket(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_NULL("n", None) - -class UPERVariableOctetString(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_STRING("data", "", uper_min=1, uper_max=20) - -class UPERConstrainedRangeInt(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=15) - -class UPERSequenceWithEnumerated(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_ENUMERATED("state", 1, {1: "alpha", 200: "beta"}), - ) - -class UPERSequenceOfStrings(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE_OF("items", [], ASN1F_STRING) - -class UPERNestedSequence(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0), - ASN1F_BOOLEAN("y", False), - ), - ) - -class UPERSequenceWithNull(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_NULL("n", None), - ) - -class UPERFixedBitString(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_BIT_STRING("b", "0", uper_min=16, uper_max=16) - -class UPERSequenceOfConstrainedInts(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE_OF( - "values", [], ASN1F_INTEGER("item", 0, uper_min=0, uper_max=255), - ) - -class UPERSignedInteger(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=-128, uper_max=127) - -class UPERMultiOptional(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0), - ASN1F_optional(ASN1F_INTEGER("a", 0)), - ASN1F_optional(ASN1F_STRING("b", "")), - ) - -def _roundtrip(cls, pkt): - # type: (type, ASN1_Packet) -> ASN1_Packet - return cls(raw(pkt)) - -def _roundtrip(cls, pkt): - # type: (type, ASN1_Packet) -> ASN1_Packet - return cls(raw(pkt)) - -def _record_kwargs(): - # type: () -> dict - return dict( - id=42, - flag=True, - label=b"hi", - extra=7, - values=[1, 2, 3], - ) - -def _asn1_int(val): - # type: (Any) -> int - return val.val if hasattr(val, "val") else val - -def _asn1_int(val): - # type: (Any) -> int - return val.val if hasattr(val, "val") else val - -def _assert_record(decoded): - # type: (ASN1_Packet) -> None - assert decoded.id.val == 42 - assert decoded.flag.val == 1 - assert decoded.label.val == b"hi" - assert decoded.extra.val == 7 - assert [x.val for x in decoded.values] == [1, 2, 3] - -def _assert_record_empty(decoded): - # type: (ASN1_Packet) -> None - assert decoded.id.val == 1 - assert decoded.flag.val == 0 - assert decoded.label.val == b"" - assert decoded.extra is None - assert [x.val for x in decoded.values] == [] - -def _dissect(cls, data_hex): - # type: (Type[ASN1_Packet], str) -> ASN1_Packet - return cls(bytes.fromhex(data_hex)) -= per record build roundtrip -pkt = UPERRecord(**_record_kwargs()) - -assert len(raw(pkt)) > 0 +assert len(raw(pkt)) > 0 decoded = _roundtrip(UPERRecord, pkt) @@ -1811,63 +1767,6 @@ assert [x.val for x in decoded.items] == [1, 2] True + ASN.1 UPER coverage -= import contrib codecs -import scapy.contrib.uper -from scapy.contrib.uper import * -from scapy.packet import raw -from unittest import mock -from scapy.asn1.ber import BER_Decoding_Error -from scapy.contrib.oer import OER_Decoding_Error, OER_Encoding_Error -from scapy.contrib.uper import ( - UPER_Decoding_Error, UPER_Encoding_Error, UPER_Decoder, UPER_Encoder, -) -from scapy.packet import Raw, raw -= prepare helpers and packet classes -class _InnerRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_enum_INTEGER("mode", ASN1_INTEGER(0), ["off", "on"]), - ) - -class _EncapsRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_STRING_ENCAPS("payload", None, _InnerRecord), - ) - -class _FlagsRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_FLAGS("f", "000", ["read", "write", "exec"]), - ) - -class _SetOfRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SET_OF("items", [], ASN1F_INTEGER) - -class _PacketFieldRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_STRING_PacketField("data", b""), - ) - -class _ExplicitPacket(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2) - -class _BitEncapsRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord), - ) - -def _raises(exc, func): - # type: (type, Any) -> None - try: - func() - except exc: - return - raise AssertionError("Expected %s" % exc.__name__) = uper error str obj = ASN1_INTEGER(2) @@ -1990,65 +1889,6 @@ _raises(UPER_Encoding_Error, lambda: UPERcodec_IPADDRESS.enc("bad-ip")) True + ASN.1 fields coverage -= import contrib codecs -import scapy.contrib.oer -import scapy.contrib.uper -from scapy.contrib.oer import * -from scapy.contrib.uper import * -from scapy.packet import raw -from unittest import mock -from scapy.asn1.ber import BER_Decoding_Error -from scapy.contrib.oer import OER_Decoding_Error, OER_Encoding_Error -from scapy.contrib.uper import ( - UPER_Decoding_Error, UPER_Encoding_Error, UPER_Decoder, UPER_Encoder, -) -from scapy.packet import Raw, raw -= prepare helpers and packet classes -class _InnerRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_enum_INTEGER("mode", ASN1_INTEGER(0), ["off", "on"]), - ) - -class _EncapsRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_STRING_ENCAPS("payload", None, _InnerRecord), - ) - -class _FlagsRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_FLAGS("f", "000", ["read", "write", "exec"]), - ) - -class _SetOfRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SET_OF("items", [], ASN1F_INTEGER) - -class _PacketFieldRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_STRING_PacketField("data", b""), - ) - -class _ExplicitPacket(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2) - -class _BitEncapsRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord), - ) - -def _raises(exc, func): - # type: (type, Any) -> None - try: - func() - except exc: - return - raise AssertionError("Expected %s" % exc.__name__) = asn1fields enum and flags pkt = _InnerRecord(mode="on") @@ -2829,74 +2669,6 @@ True + ASN.1 UPER field hooks and packet extras -= import contrib codecs -import scapy.contrib.uper -from scapy.contrib.uper import * -from scapy.contrib.uper import ASN1F_DEFAULT -from scapy.packet import raw -import scapy.asn1fields as asn1fields -= prepare helpers and packet classes -def _val(x): - # type: (Any) -> Any - return x.val if hasattr(x, "val") else x - -def _roundtrip(cls, pkt): - # type: (type, ASN1_Packet) -> ASN1_Packet - return cls(raw(pkt)) - -class UPERDefaultRecord(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), - ASN1F_DEFAULT( - ASN1F_INTEGER("n", 5, uper_min=0, uper_max=10), - 5, - ), - ) - -class UPEREmptySeqOf(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE_OF( - "values", - [], - ASN1F_INTEGER("item", 0, uper_min=0, uper_max=7), - uper_min=0, - uper_max=3, - ) - -class UPERExtSeqOf(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE_OF( - "values", - [], - ASN1F_INTEGER("item", 0, uper_min=0, uper_max=7), - uper_min=1, - uper_max=2, - uper_extensible=True, - ) - -class UPERFlagsField(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_FLAGS( - "f", "101", ["a", "b", "c"], uper_min=3, uper_max=3, - ) - -class UPERInnerPacket(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0, uper_min=0, uper_max=15), - ) - -class UPERWrappedPacket(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), - ASN1F_PACKET("inner", None, UPERInnerPacket), - ) - -class UPERConstrainedInt(ASN1_Packet): - ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=255) = uper field hooks registered assert hasattr(ASN1_Codecs.PER, "_field_hooks") @@ -2919,9 +2691,17 @@ assert issubclass(ASN1F_DEFAULT, ASN1F_optional) enum_fld = ASN1F_ENUMERATED("e", 0, {1: "a", 2: "b"}) -assert enum_fld.uper_enum_values == [1, 2] +# ENUMERATED values are added per-codec, so BER packets keep an empty +# codec_opts while PER packets get the permitted values. +assert enum_fld.codec_opts == {} + +assert "uper_enum_values" not in enum_fld._codec_kwargs( + type("P", (), {"ASN1_codec": ASN1_Codecs.BER})() +) -assert enum_fld.codec_opts["uper_enum_values"] == [1, 2] +assert enum_fld._codec_kwargs( + type("P", (), {"ASN1_codec": ASN1_Codecs.PER})() +)["uper_enum_values"] == [1, 2] assert hasattr(ASN1F_field, "encode_into") @@ -2943,9 +2723,9 @@ assert _val(_roundtrip(UPERConstrainedInt, UPERConstrainedInt(n=5)).n) == 5 True = uper DEFAULT presence bit -absent = UPERDefaultRecord(id=1, n=5) +absent = UPERSmallDefaultRecord(id=1, n=5) -present = UPERDefaultRecord(id=1, n=7) +present = UPERSmallDefaultRecord(id=1, n=7) assert raw(absent) == bytes.fromhex("0080") @@ -2953,9 +2733,9 @@ assert raw(present) == bytes.fromhex("80b8") assert raw(absent) != raw(present) -decoded_absent = _roundtrip(UPERDefaultRecord, absent) +decoded_absent = _roundtrip(UPERSmallDefaultRecord, absent) -decoded_present = _roundtrip(UPERDefaultRecord, present) +decoded_present = _roundtrip(UPERSmallDefaultRecord, present) assert _val(decoded_absent.n) == 5 diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index 8c089ed1bff..bcfd5a28b22 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -539,11 +539,10 @@ assert constrained.codec_opts == { "uper_max": 255, } -assert constrained.oer_unsigned is True +# Constraints live in codec_opts only: they must not become field attributes. +assert not hasattr(constrained, "oer_unsigned") -assert constrained.uper_min == 0 - -assert constrained.uper_max == 255 +assert not hasattr(constrained, "uper_min") kwargs = constrained._codec_kwargs( type("P", (), {"ASN1_codec": ASN1_Codecs.BER})() From e7aef32e9a9c44d5bb39c2e704224d301dcad986 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Mon, 10 Aug 2026 15:17:00 +0200 Subject: [PATCH 08/46] Cleanup asn1fields AI-Assisted: yes (Cursor) --- scapy/asn1/asn1.py | 12 +- scapy/contrib/uper.py | 474 +++++++++++------------------------------- test/contrib/uper.uts | 27 ++- 3 files changed, 154 insertions(+), 359 deletions(-) diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index 69b923fbd1b..44e10cb630e 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -122,6 +122,11 @@ class ASN1_BadTag_Decoding_Error(ASN1_Decoding_Error): class ASN1Codec(EnumElement): + # Class-level default: EnumElement.__getattr__ forwards unknown attributes + # to its int value, so a missing _field_hooks would raise (and swallow) an + # AttributeError on every field operation. + _field_hooks = None # type: Any + def register_stem(cls, stem): # type: (Type[BERcodec_Object[Any]]) -> None cls._stem = stem @@ -140,7 +145,7 @@ def register_field_hooks(cls, hooks): def unregister_field_hooks(cls): # type: () -> Any # Returns the previous hooks, so that callers can restore them. - hooks = getattr(cls, "_field_hooks", None) + hooks = cls._field_hooks try: del cls._field_hooks except AttributeError: @@ -151,7 +156,10 @@ def field_hook(cls, name): # type: (str) -> Any # Hooks are optional and may be partial: missing entries mean that # asn1fields keeps its default (BER-style) implementation. - return getattr(getattr(cls, "_field_hooks", None), name, None) + hooks = cls._field_hooks + if hooks is None: + return None + return getattr(hooks, name, None) def tagging_enc(cls, s, **kwargs): # type: (bytes, **Any) -> bytes diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index 41a2d09bf52..d4ed00c40cb 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -368,6 +368,15 @@ def remaining(self): value = self._read_bits_int(self.number_of_bits) return _uper_per_bits_to_bytes(value, self.number_of_bits) + def remaining_bytes(self): + # type: () -> bytes + # A standalone UPER encoding is padded to an octet boundary, so the + # bits left over inside the current octet are padding; only whole + # octets after it are actual remaining input. + pad = -self._read_offset() % 8 + self.number_of_bits = max(0, self.number_of_bits - pad) + return self.remaining() + def read_bytes(self, number_of_bytes): # type: (int) -> bytes return self.read_bits(8 * number_of_bytes) @@ -571,111 +580,65 @@ def asn1_object(cls, val): # type: (_K) -> ASN1_Object[_K] return cls.tag.asn1_object(val) + # The bit-oriented encode_into()/dec_from_decoder() pair is the primitive + # every codec implements; enc()/do_dec() below are the standalone (byte + # buffer) entry points, and pass every codec option straight through. + @classmethod - def check_string(cls, s): - # type: (bytes) -> None - if not s and cls.tag != ASN1_Class_UNIVERSAL.NULL: - raise UPER_Decoding_Error( - "%s: Got empty object while expecting %r" % - (cls.__name__, cls.tag), remaining=s + def encode_into(cls, enc, s, **kwargs): + # type: (UPER_Encoder, Any, **Any) -> None + # No schema information here (ANY): guess from the Python type. + if isinstance(s, (str, bytes)): + UPERcodec_STRING.encode_into(enc, s, **kwargs) + return + try: + UPERcodec_INTEGER.encode_into(enc, int(s), **kwargs) + except Exception: + raise UPER_Encoding_Error( + "Cannot encode value %r for %s" % (s, cls.__name__), + encoded=s ) @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - uper_enum_values=None, # type: Optional[List[int]] - ): - # type: (...) -> Tuple[ASN1_Object[Any], bytes] + def dec_from_decoder(cls, dec, **kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[Any] raise UPER_Decoding_Error( "%s: Cannot decode unknown UPER type without context" % - cls.__name__, remaining=s + cls.__name__, remaining=dec.remaining() ) @classmethod - def dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - uper_enum_values=None, # type: Optional[List[int]] - ): - # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] - dec_kwargs = {} # type: Dict[str, Any] - if uper_enum_values is not None: - dec_kwargs["uper_enum_values"] = uper_enum_values + def enc(cls, s, **kwargs): + # type: (Any, **Any) -> bytes + enc = UPER_Encoder() + cls.encode_into(enc, s, **kwargs) + return enc.as_bytes() + + @classmethod + def do_dec(cls, s, context=None, safe=False, **kwargs): + # type: (bytes, Optional[Type[ASN1_Class]], bool, **Any) -> Tuple[ASN1_Object[Any], bytes] # noqa: E501 + dec = UPER_Decoder(s) + return cls.dec_from_decoder(dec, **kwargs), dec.remaining_bytes() + + @classmethod + def dec(cls, s, context=None, safe=False, **kwargs): + # type: (bytes, Optional[Type[ASN1_Class]], bool, **Any) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] # noqa: E501 if not safe: - return cls.do_dec( - s, context, safe, size_len, uper_min, uper_max, oer_unsigned, - **dec_kwargs - ) + return cls.do_dec(s, context, safe, **kwargs) try: - return cls.do_dec( - s, context, safe, size_len, uper_min, uper_max, oer_unsigned, - **dec_kwargs - ) + return cls.do_dec(s, context, safe, **kwargs) except UPER_BadTag_Decoding_Error as e: o, remain = UPERcodec_Object.dec( - e.remaining, context, safe, size_len, uper_min, uper_max, - oer_unsigned, uper_enum_values=uper_enum_values, + e.remaining, context, safe, **kwargs ) return ASN1_BADTAG(o), remain - except UPER_Decoding_Error as e: - return ASN1_DECODING_ERROR(s, exc=e), b"" - except ASN1_Error as e: + except (UPER_Decoding_Error, ASN1_Error) as e: return ASN1_DECODING_ERROR(s, exc=e), b"" @classmethod - def safedec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - uper_enum_values=None, # type: Optional[List[int]] - ): - # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] - return cls.dec( - s, context, safe=True, - size_len=size_len, uper_min=uper_min, uper_max=uper_max, - oer_unsigned=oer_unsigned, uper_enum_values=uper_enum_values, - ) - - @classmethod - def enc(cls, s, size_len=0, uper_min=None, uper_max=None, **_kwargs): - # type: (_K, Optional[int], Optional[int], Optional[int], **Any) -> bytes - if isinstance(s, (str, bytes)): - return UPERcodec_STRING.enc(s, size_len=size_len, - uper_min=uper_min, uper_max=uper_max) - else: - try: - return UPERcodec_INTEGER.enc( - int(s), - size_len=size_len, - uper_min=uper_min, - uper_max=uper_max, - ) - except Exception: - raise UPER_Encoding_Error( - "Cannot encode value %r for %s" % (s, cls.__name__), - encoded=s - ) - - -def _uper_enc_via_encode_into(cls, *args, **kwargs): - # type: (Type[UPERcodec_Object[Any]], *Any, **Any) -> bytes - enc = UPER_Encoder() - cls.encode_into(enc, *args, **kwargs) - return enc.as_bytes() + def safedec(cls, s, context=None, **kwargs): + # type: (bytes, Optional[Type[ASN1_Class]], **Any) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] # noqa: E501 + return cls.dec(s, context, safe=True, **kwargs) def UPER_tagging_enc(s, **kwargs): @@ -719,6 +682,7 @@ def encode_into(cls, uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool uper_extensible=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> None minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) @@ -742,6 +706,7 @@ def dec_from_decoder(cls, uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool uper_extensible=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> ASN1_Object[int] minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) @@ -755,32 +720,6 @@ def dec_from_decoder(cls, value = dec.read_unconstrained_whole_number() return cls.asn1_object(value) - @classmethod - def enc(cls, i, size_len=0, uper_min=None, uper_max=None, - oer_unsigned=False, **_kwargs): - # type: (int, Optional[int], Optional[int], Optional[int], bool, **Any) -> bytes - return _uper_enc_via_encode_into( - cls, i, size_len, uper_min, uper_max, oer_unsigned, - ) - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[int], bytes] - minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) - if minimum is not None and maximum is not None: - x, t = UPER_constrained_int_dec(s, minimum, maximum) - else: - x, t = UPER_unconstrained_int_dec(s) - return cls.asn1_object(x), t - class UPERcodec_BOOLEAN(UPERcodec_Object[int]): tag = ASN1_Class_UNIVERSAL.BOOLEAN @@ -793,6 +732,7 @@ def encode_into(cls, uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> None UPER_boolean_enc(i, enc=enc) @@ -804,32 +744,11 @@ def dec_from_decoder(cls, uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> ASN1_Object[int] return cls.asn1_object(dec.read_bit()) - @classmethod - def enc(cls, i, size_len=0, uper_min=None, uper_max=None, - oer_unsigned=False, **_kwargs): - # type: (int, Optional[int], Optional[int], Optional[int], bool, **Any) -> bytes - return _uper_enc_via_encode_into( - cls, i, size_len, uper_min, uper_max, oer_unsigned, - ) - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[int], bytes] - x, t = UPER_boolean_dec(s) - return cls.asn1_object(x), t - def _uper_bytes_to_bitstr(data, nbits): # type: (bytes, int) -> str @@ -864,6 +783,7 @@ def encode_into(cls, uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> None s, nbits = _uper_bit_string_parts(_s) @@ -897,6 +817,7 @@ def dec_from_decoder(cls, uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> ASN1_Object[str] minimum = uper_min @@ -917,42 +838,6 @@ def dec_from_decoder(cls, raw = dec.read_bits(nbits) return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)) - @classmethod - def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, - oer_unsigned=False, **_kwargs): - # type: (Any, Optional[int], Optional[int], Optional[int], bool, **Any) -> bytes - return _uper_enc_via_encode_into( - cls, _s, size_len, uper_min, uper_max, oer_unsigned, - ) - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[str], bytes] - dec = UPER_Decoder(s) - minimum = uper_min - maximum = uper_max - if minimum is not None and maximum is not None and minimum == maximum: - nbits = minimum - elif minimum is not None and maximum is not None: - nbits = minimum + dec.read_non_negative_binary_integer( - UPER_bits_for_range(maximum - minimum) - ) - else: - nbytes = dec.read_length_determinant() - raw = dec.read_bytes(nbytes) - nbits = 8 * nbytes - return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)), dec.remaining() - raw = dec.read_bits(nbits) - return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)), dec.remaining() - def _uper_octet_string_bounds(size_len, uper_min, uper_max): # type: (Optional[int], Optional[int], Optional[int]) -> Tuple[Optional[int], Optional[int]] # noqa: E501 @@ -972,6 +857,7 @@ def encode_into(cls, uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> None s = bytes_encode(_s) @@ -987,6 +873,7 @@ def dec_from_decoder(cls, uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> ASN1_Object[Any] minimum, maximum = _uper_octet_string_bounds( @@ -995,31 +882,6 @@ def dec_from_decoder(cls, raw, _ = UPER_octet_string_dec(b"", minimum, maximum, dec=dec) return cls.asn1_object(raw) - @classmethod - def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, - oer_unsigned=False, **_kwargs): - # type: (Union[str, bytes], Optional[int], Optional[int], Optional[int], bool, **Any) -> bytes # noqa: E501 - return _uper_enc_via_encode_into( - cls, _s, size_len, uper_min, uper_max, oer_unsigned, - ) - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[Any], bytes] - minimum, maximum = _uper_octet_string_bounds( - size_len, uper_min, uper_max, - ) - raw, remain = UPER_octet_string_dec(s, minimum, maximum) - return cls.asn1_object(raw), remain - class UPERcodec_NULL(UPERcodec_Object[None]): tag = ASN1_Class_UNIVERSAL.NULL @@ -1032,6 +894,7 @@ def encode_into(cls, uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> None return @@ -1043,27 +906,15 @@ def dec_from_decoder(cls, uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> ASN1_Object[None] return cls.asn1_object(None) @classmethod - def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, - oer_unsigned=False, **_kwargs): - # type: (Any, Optional[int], Optional[int], Optional[int], bool, **Any) -> bytes - return b"" - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[None], bytes] + def do_dec(cls, s, context=None, safe=False, **kwargs): + # type: (bytes, Optional[Type[ASN1_Class]], bool, **Any) -> Tuple[ASN1_Object[None], bytes] # noqa: E501 + # NULL occupies no bits at all, so the input is left untouched. return cls.asn1_object(None), s @@ -1071,8 +922,8 @@ class UPERcodec_OID(UPERcodec_Object[bytes]): tag = ASN1_Class_UNIVERSAL.OID @classmethod - def enc(cls, _oid, size_len=0, uper_min=None, uper_max=None, **_kwargs): - # type: (AnyStr, Optional[int], Optional[int], Optional[int], **Any) -> bytes + def encode_into(cls, enc, _oid, **_kwargs): + # type: (UPER_Encoder, AnyStr, **Any) -> None oid = bytes_encode(_oid) if oid: lst = [int(x) for x in oid.split(b".")] @@ -1080,23 +931,12 @@ def enc(cls, _oid, size_len=0, uper_min=None, uper_max=None, **_kwargs): else: lst = [] body = b"".join(BER_num_enc(k) for k in lst) - enc = UPER_Encoder() enc.append_length_determinant(len(body)) enc.append_bytes(body) - return enc.as_bytes() @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[bytes], bytes] - dec = UPER_Decoder(s) + def dec_from_decoder(cls, dec, **_kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[bytes] length = dec.read_length_determinant() content = dec.read_bytes(length) lst = [] @@ -1106,10 +946,7 @@ def do_dec(cls, if len(lst) > 0: lst.insert(0, lst[0] // 40) lst[1] %= 40 - return ( - cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)), - dec.remaining(), - ) + return cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)) def UPER_enumerated_enc(value, @@ -1165,6 +1002,7 @@ def encode_into(cls, uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool uper_enum_values=None, # type: Optional[List[int]] + **_kwargs # type: Any ): # type: (...) -> None if uper_enum_values is not None: @@ -1184,6 +1022,7 @@ def dec_from_decoder(cls, uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool uper_enum_values=None, # type: Optional[List[int]] + **_kwargs # type: Any ): # type: (...) -> ASN1_Object[int] if uper_enum_values is not None: @@ -1198,44 +1037,6 @@ def dec_from_decoder(cls, ) + minimum return cls.asn1_object(value) - @classmethod - def enc(cls, - i, - size_len=0, - uper_min=None, - uper_max=None, - oer_unsigned=False, - uper_enum_values=None, - **_kwargs - ): - # type: (int, Optional[int], Optional[int], Optional[int], bool, Optional[List[int]], **Any) -> bytes # noqa: E501 - return _uper_enc_via_encode_into( - cls, i, size_len, uper_min, uper_max, oer_unsigned, - uper_enum_values=uper_enum_values, - ) - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - uper_enum_values=None, # type: Optional[List[int]] - ): - # type: (...) -> Tuple[ASN1_Object[int], bytes] - if uper_enum_values is not None: - x, t = UPER_enumerated_dec(s, uper_enum_values) - return cls.asn1_object(x), t - minimum = uper_min if uper_min is not None else 0 - maximum = uper_max if uper_max is not None else size_len - if maximum is None: - raise UPER_Decoding_Error("UPERcodec_ENUMERATED: missing range") - x, t = UPER_constrained_int_dec(s, minimum, maximum) - return cls.asn1_object(x), t - class UPERcodec_SEQUENCE(UPERcodec_Object[Union[bytes, List[Any]]]): tag = ASN1_Class_UNIVERSAL.SEQUENCE @@ -1248,15 +1049,15 @@ def encode_into(cls, uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool + **_kwargs # type: Any ): # type: (...) -> None if isinstance(_ll, bytes): UPER_append_encoded(enc, _ll) @classmethod - def enc(cls, _ll, size_len=0, uper_min=None, uper_max=None, - oer_unsigned=False, **_kwargs): - # type: (Union[bytes, List[UPERcodec_Object[Any]]], Optional[int], Optional[int], Optional[int], bool, **Any) -> bytes # noqa: E501 + def enc(cls, _ll, **_kwargs): + # type: (Union[bytes, List[UPERcodec_Object[Any]]], **Any) -> bytes if isinstance(_ll, bytes): return _ll raise UPER_Encoding_Error( @@ -1264,19 +1065,11 @@ def enc(cls, _ll, size_len=0, uper_min=None, uper_max=None, ) @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[Union[bytes, List[Any]]], bytes] + def dec_from_decoder(cls, dec, **_kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[Union[bytes, List[Any]]] raise UPER_Decoding_Error( "UPERcodec_SEQUENCE: decoding requires schema-defined field order", - remaining=s + remaining=dec.remaining() ) @@ -1288,28 +1081,23 @@ class UPERcodec_IPADDRESS(UPERcodec_STRING): tag = ASN1_Class_UNIVERSAL.IPADDRESS @classmethod - def enc(cls, ipaddr_ascii, size_len=0, uper_min=None, uper_max=None, **_kwargs): - # type: (str, Optional[int], Optional[int], Optional[int], **Any) -> bytes + def encode_into(cls, enc, ipaddr_ascii, **_kwargs): + # type: (UPER_Encoder, str, **Any) -> None try: s = inet_aton(ipaddr_ascii) except Exception: raise UPER_Encoding_Error("IPv4 address could not be encoded") - return UPER_octet_string_enc(s, 4, 4) + UPER_octet_string_enc(s, 4, 4, enc=enc) @classmethod - def do_dec(cls, s, context=None, safe=False, - size_len=0, uper_min=None, uper_max=None, - oer_unsigned=False): - # type: (bytes, Optional[Any], bool, Optional[int], Optional[int], Optional[int], bool) -> Tuple[ASN1_Object[str], bytes] # noqa: E501 - raw, remain = UPER_octet_string_dec(s, 4, 4) + def dec_from_decoder(cls, dec, **_kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[str] + raw, _ = UPER_octet_string_dec(b"", 4, 4, dec=dec) try: ipaddr_ascii = inet_ntoa(raw) except Exception: - raise UPER_Decoding_Error( - "IP address could not be decoded", - remaining=s, - ) - return cls.asn1_object(ipaddr_ascii), remain + raise UPER_Decoding_Error("IP address could not be decoded") + return cls.asn1_object(ipaddr_ascii) class UPERcodec_COUNTER32(UPERcodec_INTEGER): @@ -1745,7 +1533,7 @@ def set_absent(self, pkt): def m2i_from_decoder(self, pkt, dec): # type: (Any, Any, Any) -> Any codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - return codec.dec_from_decoder( # type: ignore[attr-defined] # noqa: E501 + return codec.dec_from_decoder( # type: ignore[attr-defined] dec, **self._codec_kwargs(pkt), ) @@ -1773,63 +1561,10 @@ def encode_into(self, enc, pkt, value=None): ) else: raw = value - codec.encode_into( # type: ignore[attr-defined] # noqa: E501 + codec.encode_into( # type: ignore[attr-defined] enc, raw, **self._codec_kwargs(pkt), ) - af.ASN1F_field.m2i_from_decoder = m2i_from_decoder # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_field.dissect_from_decoder = dissect_from_decoder # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_field.encode_into = encode_into # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_field._uper_encode_into = encode_into # type: ignore[attr-defined] # noqa: E501 - - def seq_dissect_from_decoder(self, pkt, dec): - # type: (Any, Any, Any) -> None - return _UPER_FieldHooks.sequence_dissect_from_decoder(self, pkt, dec) - - def seq_encode_into(self, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - return _UPER_FieldHooks.sequence_encode_into(self, enc, pkt, value) - - af.ASN1F_SEQUENCE.dissect_from_decoder = seq_dissect_from_decoder # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_SEQUENCE.encode_into = seq_encode_into # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_SEQUENCE._uper_encode_into = seq_encode_into # type: ignore[attr-defined] # noqa: E501 - - def seqof_m2i_from_decoder(self, pkt, dec): - # type: (Any, Any, Any) -> Any - return _UPER_FieldHooks.sequence_of_m2i_from_decoder(self, pkt, dec) - - def seqof_encode_into(self, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - return _UPER_FieldHooks.sequence_of_encode_into(self, enc, pkt, value) - - af.ASN1F_SEQUENCE_OF.m2i_from_decoder = seqof_m2i_from_decoder # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_SEQUENCE_OF.encode_into = seqof_encode_into # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_SEQUENCE_OF._uper_encode_into = seqof_encode_into # type: ignore[attr-defined] # noqa: E501 - - def choice_m2i_from_decoder(self, pkt, dec): - # type: (Any, Any, Any) -> Any - return _UPER_FieldHooks.choice_m2i_from_decoder(self, pkt, dec) - - def choice_encode_into(self, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - return _UPER_FieldHooks.choice_encode_into(self, enc, pkt, value) - - af.ASN1F_CHOICE.m2i_from_decoder = choice_m2i_from_decoder # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_CHOICE.encode_into = choice_encode_into # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_CHOICE._uper_encode_into = choice_encode_into # type: ignore[attr-defined] # noqa: E501 - - def packet_m2i_from_decoder(self, pkt, dec): - # type: (Any, Any, Any) -> Any - return _UPER_FieldHooks.packet_m2i_from_decoder(self, pkt, dec) - - def packet_encode_into(self, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - return _UPER_FieldHooks.packet_encode_into(self, enc, pkt, value) - - af.ASN1F_PACKET.m2i_from_decoder = packet_m2i_from_decoder # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_PACKET.encode_into = packet_encode_into # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_PACKET._uper_encode_into = packet_encode_into # type: ignore[attr-defined] # noqa: E501 - def opt_set_absent(self, pkt): # type: (Any, Any) -> None self.set_val(pkt, None) @@ -1842,10 +1577,37 @@ def opt_encode_into(self, enc, pkt, value=None): # type: (Any, Any, Any, Any) -> None self._field.encode_into(enc, pkt, value) - af.ASN1F_optional.set_absent = opt_set_absent # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_optional.dissect_from_decoder = opt_dissect_from_decoder # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_optional.encode_into = opt_encode_into # type: ignore[attr-defined] # noqa: E501 - af.ASN1F_optional._uper_encode_into = opt_encode_into # type: ignore[attr-defined] # noqa: E501 + hooks = _UPER_FieldHooks + for field_cls, methods in ( + (af.ASN1F_field, { + "m2i_from_decoder": m2i_from_decoder, + "dissect_from_decoder": dissect_from_decoder, + "encode_into": encode_into, + }), + (af.ASN1F_SEQUENCE, { + "dissect_from_decoder": hooks.sequence_dissect_from_decoder, + "encode_into": hooks.sequence_encode_into, + }), + (af.ASN1F_SEQUENCE_OF, { + "m2i_from_decoder": hooks.sequence_of_m2i_from_decoder, + "encode_into": hooks.sequence_of_encode_into, + }), + (af.ASN1F_CHOICE, { + "m2i_from_decoder": hooks.choice_m2i_from_decoder, + "encode_into": hooks.choice_encode_into, + }), + (af.ASN1F_PACKET, { + "m2i_from_decoder": hooks.packet_m2i_from_decoder, + "encode_into": hooks.packet_encode_into, + }), + (af.ASN1F_optional, { + "set_absent": opt_set_absent, + "dissect_from_decoder": opt_dissect_from_decoder, + "encode_into": opt_encode_into, + }), + ): + for method_name, func in methods.items(): + setattr(field_cls, method_name, func) _orig_enum_codec_kwargs = af.ASN1F_enum_INTEGER._codec_kwargs @@ -1860,7 +1622,7 @@ def enum_codec_kwargs(self, pkt): kwargs.setdefault("uper_enum_values", list(self.i2s)) return kwargs - af.ASN1F_enum_INTEGER._codec_kwargs = enum_codec_kwargs # type: ignore[assignment] # noqa: E501 + af.ASN1F_enum_INTEGER._codec_kwargs = enum_codec_kwargs # type: ignore[assignment] _install_uper_asn1fields() diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index 7e19761a8e9..11edfb5de23 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -1616,6 +1616,31 @@ assert decoded.n.val == 1706733817 True += per extensible integer as a bare root +~ per +class UPERBareExtInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER( + "n", 0, uper_min=0, uper_max=15, uper_extensible=True, + ) + +class UPERWrappedExtInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, uper_min=0, uper_max=15, uper_extensible=True), + ) + +# A bare root must encode the extension bit just like the nested field does. +assert raw(UPERBareExtInt(n=5)) == bytes.fromhex("28") + +assert raw(UPERBareExtInt(n=5)) == raw(UPERWrappedExtInt(n=5)) + +assert _dissect(UPERBareExtInt, "28").n.val == 5 + +assert _roundtrip(UPERBareExtInt, UPERBareExtInt(n=99)).n.val == 99 + +True + = per constrained sequence of build class UPERConstrainedSeqOf(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER @@ -2268,7 +2293,7 @@ _raises( _raises( ASN1_Error, - lambda: _PerChoice.ASN1_root._uper_encode_into( + lambda: _PerChoice.ASN1_root.encode_into( UPER_Encoder(), _PerChoice(), 42, ), ) From 666399614d20dede4156d50445a9292a765f7bfb Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 11 Aug 2026 10:04:22 +0200 Subject: [PATCH 09/46] oer: fix X.696 conformance of SEQUENCE and fixed-size strings A SEQUENCE with OPTIONAL/DEFAULT components was encoded without the preamble required by X.696 16.2.2, so peers could not tell which components were present. Fixed-size BIT STRINGs kept their length determinant and unused-bit count, and fixed-size OCTET STRINGs of 1, 2, 4 or 8 bytes encoded without a length determinant but were decoded expecting one, so they could be built but never parsed. Encodings now match asn1tools byte for byte in both directions. AI-Assisted: yes (Cursor) Co-authored-by: Cursor --- scapy/contrib/oer.py | 169 +++++++++++++++++++++++++++++++--- test/contrib/oer.uts | 180 +++++++++++++++++++++++++++++++++++-- test/scapy/layers/asn1.uts | 1 + 3 files changed, 330 insertions(+), 20 deletions(-) diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py index 45f5f22fc67..0ef38f64f5e 100644 --- a/scapy/contrib/oer.py +++ b/scapy/contrib/oer.py @@ -9,6 +9,14 @@ Octet Encoding Rules (OER) for ASN.1 Basic-OER as specified in ITU-T X.696 | ISO/IEC 8825-7. + +``ASN1F_SEQUENCE`` emits the preamble required by 16.2.2: a presence bit per +``ASN1F_optional``/``ASN1F_DEFAULT`` component, preceded by an extension bit +for sequences declared with ``oer_extensible=True``. Fixed size constraints +are expressed with ``size_len=`` (octets for strings, bits for BIT STRING). + +Not supported yet: extension additions (an encoding that carries them is +refused rather than misparsed), SET, REAL, and the canonical variant (C-OER). """ import struct @@ -266,6 +274,50 @@ def OER_enumerated_dec(s): return value, s[length + 1:] +def OER_preamble_enc(extensible, presence): + # type: (bool, List[bool]) -> bytes + # X.696 16.2.2: an extension bit (extensible types only) followed by one + # presence bit per OPTIONAL/DEFAULT component, zero-padded to a whole + # number of octets. A type with neither has no preamble at all. + bits = [0] if extensible else [] + bits += [1 if present else 0 for present in presence] + if not bits: + return b"" + number_of_bytes = (len(bits) + 7) // 8 + value = 0 + for bit in bits: + value = (value << 1) | bit + value <<= 8 * number_of_bytes - len(bits) + return value.to_bytes(number_of_bytes, "big") + + +def OER_preamble_dec(s, extensible, number_of_optionals): + # type: (bytes, bool, int) -> Tuple[List[bool], bytes] + number_of_bits = (1 if extensible else 0) + number_of_optionals + if number_of_bits == 0: + return [], s + number_of_bytes = (number_of_bits + 7) // 8 + if len(s) < number_of_bytes: + raise OER_Decoding_Error( + "OER_preamble_dec: Got %i bytes while expecting %i" % + (len(s), number_of_bytes), + remaining=s + ) + value = int.from_bytes(s[:number_of_bytes], "big") + bits = [ + bool((value >> (8 * number_of_bytes - 1 - i)) & 1) + for i in range(number_of_bits) + ] + if extensible: + if bits[0]: + raise OER_Decoding_Error( + "OER_preamble_dec: extension additions are not supported", + remaining=s + ) + bits = bits[1:] + return bits, s[number_of_bytes:] + + def OER_tag_enc(n, tag_class=OER_CLASS_CONTEXT): # type: (int, int) -> bytes if n < 63: @@ -537,6 +589,17 @@ def do_dec(cls, return cls.asn1_object(0 if orb(s[0]) == 0 else 1), s[1:] +def _oer_bitstr_to_bytes(bitstr): + # type: (bytes) -> bytes + padded = bitstr + b"0" * (-len(bitstr) % 8) + return bytes([int(padded[i:i + 8], 2) for i in range(0, len(padded), 8)]) + + +def _oer_bytes_to_bitstr(data): + # type: (bytes) -> str + return "".join(binrepr(orb(x)).zfill(8) for x in data) + + class OERcodec_BIT_STRING(OERcodec_Object[str]): tag = ASN1_Class_UNIVERSAL.BIT_STRING @@ -549,6 +612,20 @@ def do_dec(cls, oer_unsigned=False, # type: bool ): # type: (...) -> Tuple[ASN1_Object[str], bytes] + if size_len: + number_of_bytes = (size_len + 7) // 8 + if len(s) < number_of_bytes: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % + (cls.__name__, len(s), number_of_bytes), + remaining=s + ) + return ( + cls.tag.asn1_object( + _oer_bytes_to_bitstr(s[:number_of_bytes])[:size_len] + ), + s[number_of_bytes:], + ) length, s = OER_len_dec(s) if length == 0: return cls.tag.asn1_object(""), s @@ -563,7 +640,7 @@ def do_dec(cls, "OERcodec_BIT_STRING: too many unused_bits advertised", remaining=s ) - fs = "".join(binrepr(orb(x)).zfill(8) for x in s[1:length]) + fs = _oer_bytes_to_bitstr(s[1:length]) if unused_bits > 0: fs = fs[:-unused_bits] return cls.tag.asn1_object(fs), s[length:] @@ -572,14 +649,17 @@ def do_dec(cls, def enc(cls, _s, size_len=0, **_kwargs): # type: (AnyStr, Optional[int], **Any) -> bytes s = bytes_encode(_s) - if len(s) % 8 == 0: - unused_bits = 0 - else: - unused_bits = 8 - len(s) % 8 - s += b"0" * unused_bits - data = b"".join(chb(int(b"".join(chb(y) for y in x), 2)) - for x in zip(*[iter(s)] * 8)) - body = chb(unused_bits) + data + if size_len: + # X.696 13.3: a fixed size means the bits are written padded to a + # whole number of octets, without length or unused-bit count. + if len(s) != size_len: + raise OER_Encoding_Error( + "%s: got %i bits while expecting %i" % + (cls.__name__, len(s), size_len), + encoded=_s + ) + return _oer_bitstr_to_bytes(s) + body = chb(-len(s) % 8) + _oer_bitstr_to_bytes(s) return OER_len_enc(len(body)) + body @@ -590,7 +670,14 @@ class OERcodec_STRING(OERcodec_Object[str]): def enc(cls, _s, size_len=0, **_kwargs): # type: (Union[str, bytes], Optional[int], **Any) -> bytes s = bytes_encode(_s) - if size_len and size_len == len(s): + if size_len: + # X.696 16.1: a fixed size means no length determinant. + if len(s) != size_len: + raise OER_Encoding_Error( + "%s: got %i bytes while expecting %i" % + (cls.__name__, len(s), size_len), + encoded=_s + ) return s return OER_len_enc(len(s)) + s @@ -603,7 +690,7 @@ def do_dec(cls, oer_unsigned=False, # type: bool ): # type: (...) -> Tuple[ASN1_Object[Any], bytes] - if size_len and size_len not in (1, 2, 4, 8): + if size_len: if len(s) < size_len: raise OER_Decoding_Error( "%s: Got %i bytes while expecting %i" % @@ -838,6 +925,22 @@ class OERcodec_TIME_TICKS(OERcodec_INTEGER): # ASN1F field hooks # ########################## +def _field_extensible(field): + # type: (Any) -> bool + return bool(getattr(field, "codec_opts", {}).get("oer_extensible", False)) + + +def _set_absent(field, pkt): + # type: (Any, Any) -> None + # ASN1F_DEFAULT restores its default value; a plain optional clears itself. + # set_absent() only exists once scapy.contrib.uper has been imported. + set_absent = getattr(field, "set_absent", None) + if set_absent is not None: + set_absent(pkt) + else: + field.set_val(pkt, None) + + class _OER_FieldHooks(object): """Compound ASN1F_* helpers for OER (kept out of asn1fields.py).""" @@ -847,18 +950,56 @@ def use_object_enc(field, pkt, item): # Constraints (e.g. oer_unsigned) must go through codec.enc(**kwargs). return field.size_len is None and not field.codec_opts + @staticmethod + def _optionals(field): + # type: (Any) -> Tuple[Any, ...] + from scapy.asn1fields import ASN1F_optional + return tuple(f for f in field.seq if isinstance(f, ASN1F_optional)) + @staticmethod def sequence_m2i(field, pkt, s): # type: (Any, Any, bytes) -> Tuple[Any, bytes] + from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) - s = field._dissect_sequence_children(pkt, s) + if not s: + for obj in field.seq: + obj.set_val(pkt, None) + return [], s + presence, s = OER_preamble_dec( + s, _field_extensible(field), + len(_OER_FieldHooks._optionals(field)), + ) + opt_index = 0 + for obj in field.seq: + target = obj + if isinstance(obj, ASN1F_optional): + present = presence[opt_index] + opt_index += 1 + if not present: + _set_absent(obj, pkt) + continue + # The preamble already said the component is there, so dissect + # it directly: a failure is an error, not an absence. + target = obj._field + try: + s = target.dissect(pkt, s) + except ASN1F_badsequence: + break return [], s @staticmethod def sequence_build(field, pkt): # type: (Any, Any) -> bytes - from functools import reduce - s = reduce(lambda x, y: x + y.build(pkt), field.seq, b"") + from scapy.asn1fields import ASN1F_optional + optionals = _OER_FieldHooks._optionals(field) + s = OER_preamble_enc( + _field_extensible(field), + [not opt.is_empty(pkt) for opt in optionals], + ) + for obj in field.seq: + if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): + continue + s += obj.build(pkt) return ASN1F_field_i2m(field, pkt, s) @staticmethod diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index 4d355f67d54..748b08e6f9c 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -448,7 +448,8 @@ True = oer field optional present = OEROptionalField(id=1, extra=7) -assert raw(present) == b"\x01\x01\xa0\x01\x07" +# \x80: preamble with the presence bit set for the single OPTIONAL component +assert raw(present) == b"\x80\x01\x01\xa0\x01\x07" decoded = _roundtrip(OEROptionalField, present) @@ -458,7 +459,7 @@ assert decoded.extra.val == 7 absent = OEROptionalField(id=1, extra=None) -assert raw(absent) == b"\x01\x01" +assert raw(absent) == b"\x00\x01\x01" decoded = _roundtrip(OEROptionalField, absent) @@ -504,6 +505,7 @@ pkt = OERRecord( ) expected = ( + b"\x80" b"\x01*\xff\x02hi\xa0\x01\x07" b"\x01\x03\x01\x01\x01\x02\x01\x03" ) @@ -524,7 +526,7 @@ assert [x.val for x in decoded.values] == [1, 2, 3] empty = OERRecord(id=1, flag=False, label="", extra=None, values=[]) -assert raw(empty) == b"\x01\x01\x00\x00\x01\x00" +assert raw(empty) == b"\x00\x01\x01\x00\x00\x01\x00" decoded = _roundtrip(OERRecord, empty) @@ -725,13 +727,13 @@ assert fixed.n.val == 200 assert fixed.s.val == b"ABC" -present = _dissect(OEROptionalField, "0101a00107") +present = _dissect(OEROptionalField, "800101a00107") assert present.id.val == 1 assert present.extra.val == 7 -absent = _dissect(OEROptionalField, "0101") +absent = _dissect(OEROptionalField, "000101") assert absent.id.val == 1 @@ -754,11 +756,12 @@ True = oer record dissect decoded = _dissect( OERRecord, + "80" "012aff026869a00107" "0103010101020103", ) _assert_record(decoded) -empty = _dissect(OERRecord, "010100000100") +empty = _dissect(OERRecord, "00010100000100") _assert_record_empty(empty) True @@ -918,3 +921,168 @@ assert remain == b"" True + ++ ASN.1 OER X.696 conformance + += oer sequence preamble presence bits +# X.696 16.2.2: one presence bit per OPTIONAL/DEFAULT component, zero padded +# to a whole number of octets. Byte vectors checked against asn1tools. +class OERPreambleOne(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("a", 0, size_len=1, oer_unsigned=True), + ASN1F_optional(ASN1F_INTEGER("b", 0, size_len=1, oer_unsigned=True)), + ) + +assert raw(OERPreambleOne(a=1, b=2)) == bytes.fromhex("800102") + +assert raw(OERPreambleOne(a=1, b=None)) == bytes.fromhex("0001") + +assert _dissect(OERPreambleOne, "800102").b.val == 2 + +assert _dissect(OERPreambleOne, "0001").b is None + +class OERPreambleTwo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_INTEGER("a", 0, size_len=1, oer_unsigned=True)), + ASN1F_optional(ASN1F_BOOLEAN("b", False)), + ) + +assert raw(OERPreambleTwo(a=1, b=None)) == bytes.fromhex("8001") + +assert raw(OERPreambleTwo(a=None, b=True)) == bytes.fromhex("40ff") + +# Nine optionals need a two-octet preamble. +class OERPreambleNine(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE(*[ + ASN1F_optional(ASN1F_INTEGER(c, 0, size_len=1, oer_unsigned=True)) + for c in "abcdefghi" + ]) + +nine = OERPreambleNine(a=1, b=None, c=None, d=None, e=None, f=None, g=None, + h=None, i=9) + +assert raw(nine) == bytes.fromhex("80800109") + +assert _roundtrip(OERPreambleNine, nine).i.val == 9 + +# A sequence without OPTIONAL/DEFAULT components has no preamble at all. +class OERNoPreamble(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("a", 0, size_len=1, oer_unsigned=True), + ) + +assert raw(OERNoPreamble(a=1)) == bytes.fromhex("01") + +# A DEFAULT component takes a presence bit too, and is omitted when it holds +# the default value. +from scapy.contrib.uper import ASN1F_DEFAULT + +class OERDefault(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_DEFAULT( + ASN1F_INTEGER("a", 7, size_len=1, oer_unsigned=True), 7, + ), + ) + +assert raw(OERDefault(a=7)) == bytes.fromhex("00") + +assert raw(OERDefault(a=9)) == bytes.fromhex("8009") + +# An absent DEFAULT is restored as the raw default value handed to +# ASN1F_DEFAULT, while a present one is decoded into an ASN1_INTEGER. +assert _dissect(OERDefault, "00").a == 7 + +assert _dissect(OERDefault, "8009").a.val == 9 + +True + += oer extensible sequence +class OERExtSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("a", 0, size_len=1, oer_unsigned=True), + oer_extensible=True, + ) + +assert raw(OERExtSeq(a=1)) == bytes.fromhex("0001") + +assert _dissect(OERExtSeq, "0001").a.val == 1 + +class OERExtSeqOpt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("a", 0, size_len=1, oer_unsigned=True), + ASN1F_optional(ASN1F_BOOLEAN("b", False)), + oer_extensible=True, + ) + +assert raw(OERExtSeqOpt(a=1, b=True)) == bytes.fromhex("4001ff") + +# An encoding that actually carries extension additions is refused rather +# than silently misparsed. +_raises(OER_Decoding_Error, lambda: _dissect(OERExtSeq, "8001")) + +True + += oer fixed size bit string +# X.696 13.3: a fixed size drops both the length determinant and the +# unused-bit count. +class OERBits(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE(ASN1F_BIT_STRING("b", "", size_len=4)) + +assert raw(OERBits(b="1010")) == bytes.fromhex("a0") + +assert _dissect(OERBits, "a0").b.val == "1010" + +for bits, size, expected in [ + ("1010", 4, "a0"), + ("10100101", 8, "a5"), + ("101001010101", 12, "a550"), + ("1010010101011010", 16, "a55a"), +]: + assert OERcodec_BIT_STRING.enc(bits, size_len=size) == bytes.fromhex(expected) + obj, remain = OERcodec_BIT_STRING.do_dec(bytes.fromhex(expected), size_len=size) + assert obj.val == bits + assert remain == b"" + +# Unconstrained bit strings keep the length and unused-bit count. +assert OERcodec_BIT_STRING.enc("101") == bytes.fromhex("0205a0") + +# A value that does not match the declared size is refused. +_raises(OER_Encoding_Error, lambda: OERcodec_BIT_STRING.enc("101", size_len=4)) + +True + += oer fixed size octet string +# X.696 16.1: a fixed size means no length determinant. Sizes 1, 2, 4 and 8 +# used to encode without one but decode expecting one. +for size in (1, 2, 3, 4, 8): + value = b"x" * size + encoded = OERcodec_STRING.enc(value, size_len=size) + assert encoded == value + obj, remain = OERcodec_STRING.do_dec(encoded, size_len=size) + assert obj.val == value + assert remain == b"" + +class OERFixedOctets(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE(ASN1F_STRING("s", "", size_len=4)) + +pkt = OERFixedOctets(s="abcd") + +assert raw(pkt) == b"abcd" + +assert _roundtrip(OERFixedOctets, pkt).s.val == b"abcd" + +# Unconstrained octet strings keep their length determinant. +assert OERcodec_STRING.enc(b"abc") == bytes.fromhex("03616263") + +_raises(OER_Encoding_Error, lambda: OERcodec_STRING.enc(b"abc", size_len=4)) + +True diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index a00e8a79848..1cd942e6ab1 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -479,6 +479,7 @@ for cls, data_hex in [ ), ( OERRecord, + "80" "012aff026869a00107" "0103010101020103", ), From a599880ae5d6873d1ae05c594d6c5200677c945a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 11 Aug 2026 10:39:14 +0200 Subject: [PATCH 10/46] uper: fix bit string length, fragmentation and integer signedness An unconstrained BIT STRING counted its length determinant in octets instead of bits and padded the content to a whole octet, which also shifted every field encoded after it, so peers read back the wrong bits. Content of 16K units or more emitted a fragment header and then the whole content in one go, without the per-fragment determinants and the terminating one required by X.691 11.9.3.8: a conformant peer silently decoded a truncated value. Fragmentation is now implemented on both sides for OCTET STRING, BIT STRING, OBJECT IDENTIFIER and SEQUENCE OF, and append_length_determinant refuses lengths it cannot express rather than clamping them. A fixed or range constrained BIT STRING silently padded or truncated a value whose length violated the constraint, where OER already raised. On the OER side the integer encoder picked the width and the signedness from the value rather than from the declared type, so 200 in a field declared INTEGER (-128..127) encoded as 0xc8 and read back as -56, and an unbounded value with a zero lower bound gained a spurious leading zero octet. Out of range values now raise OER_Encoding_Error instead of struct.error quoting the bounds of the wrong format. Both decoders raised ValueError on an integer with an empty length determinant, which escaped the dissector as a non-ASN.1 exception. Encodings now match asn1tools byte for byte in both directions, over random schemas as well as the vectors added here. AI-Assisted: yes (Cursor) Co-authored-by: Cursor --- scapy/contrib/oer.py | 34 ++++-- scapy/contrib/uper.py | 276 ++++++++++++++++++++++++++++-------------- test/contrib/oer.uts | 92 ++++++++++++-- test/contrib/uper.uts | 159 ++++++++++++++++++++++-- 4 files changed, 435 insertions(+), 126 deletions(-) diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py index 0ef38f64f5e..44dbd22d105 100644 --- a/scapy/contrib/oer.py +++ b/scapy/contrib/oer.py @@ -186,6 +186,11 @@ def OER_signed_integer_dec(s): (len(s), number_of_bytes), remaining=s ) + if number_of_bytes == 0: + raise OER_Decoding_Error( + "OER_signed_integer_dec: got an empty length determinant", + remaining=s + ) value = int.from_bytes(s[:number_of_bytes], "big") number_of_bits = 8 * number_of_bytes if value & (1 << (number_of_bits - 1)): @@ -196,6 +201,10 @@ def OER_signed_integer_dec(s): def OER_unsigned_integer_enc(i): # type: (int) -> bytes + if i < 0: + raise OER_Encoding_Error( + "OER_unsigned_integer_enc: %i is negative" % i + ) number_of_bits = max(i.bit_length(), 1) number_of_bytes = (number_of_bits + 7) // 8 return OER_len_enc(number_of_bytes) + i.to_bytes(number_of_bytes, "big") @@ -225,6 +234,11 @@ def OER_fixed_integer_enc(i, length, signed=True): raise OER_Encoding_Error( "OER_fixed_integer_enc: invalid length %i" % length ) + except struct.error: + raise OER_Encoding_Error( + "OER_fixed_integer_enc: %i does not fit in %i %s octet(s)" % + (i, length, "signed" if signed else "unsigned") + ) def OER_fixed_integer_dec(s, length, signed=True): @@ -532,19 +546,15 @@ class OERcodec_INTEGER(OERcodec_Object[int]): tag = ASN1_Class_UNIVERSAL.INTEGER @classmethod - def enc(cls, i, size_len=0, **_kwargs): - # type: (int, Optional[int], **Any) -> bytes + def enc(cls, i, size_len=0, oer_unsigned=False, **_kwargs): + # type: (int, Optional[int], bool, **Any) -> bytes + # X.696 10: the width and the signedness follow the declared bounds of + # the type, never the value at hand, otherwise the decoder (which only + # knows the type) reads something else back. if size_len in (1, 2, 4, 8): - if i >= 0: - if size_len == 1 and 0 <= i <= 255: - return OER_fixed_integer_enc(i, 1, signed=False) - if size_len == 2 and 0 <= i <= 65535: - return OER_fixed_integer_enc(i, 2, signed=False) - if size_len == 4 and 0 <= i <= 4294967295: - return OER_fixed_integer_enc(i, 4, signed=False) - if size_len == 8 and 0 <= i <= 18446744073709551615: - return OER_fixed_integer_enc(i, 8, signed=False) - return OER_fixed_integer_enc(i, size_len, signed=True) + return OER_fixed_integer_enc(i, size_len, signed=not oer_unsigned) + if oer_unsigned: + return OER_unsigned_integer_enc(i) return OER_signed_integer_enc(i) @classmethod diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index d4ed00c40cb..847e54de7f2 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -12,10 +12,15 @@ UPER is registered on ``ASN1_Codecs.PER``. Schema-driven encoding and decoding (``ASN1F_SEQUENCE``, ``ASN1F_CHOICE``, ``ASN1F_SEQUENCE_OF``, -``ASN1F_ENUMERATED``) is supported for common field types. Not supported yet: -explicit/implicit tagging, SET, extension markers, -``ASN1F_CHOICE``/``ASN1F_SEQUENCE_OF`` with nested ``ASN1_Packet`` -alternatives, REAL, and PER-visible character string permuted alphabets. +``ASN1F_ENUMERATED``) is supported for common field types. Value ranges are +declared with ``uper_min=``/``uper_max=``, fixed sizes with ``size_len=``, and +an extension marker with ``uper_extensible=True``. Content of 16K units or +more is fragmented as required by 11.9.3.8. + +Not supported yet: extension additions (an encoding that carries them is +refused rather than misparsed), SET, REAL, and the known-multiplier character +string encodings, which are emitted as plain octets rather than 7 or 4 bits +per character. """ import binascii @@ -41,6 +46,7 @@ from typing import ( Any, AnyStr, + Callable, Dict, Generic, List, @@ -114,6 +120,11 @@ def UPER_bits_for_range(size): return size.bit_length() +# X.691 11.9.3.8: content of 16K units or more is split into fragments, each +# one holding a multiple of this many units. +UPER_FRAGMENT_SIZE = 16384 + + class UPER_Encoder(object): def __init__(self): # type: () -> None @@ -167,25 +178,38 @@ def append_bytes(self, data): self.append_bits(data, 8 * len(data)) def append_length_determinant(self, length): - # type: (int) -> int + # type: (int) -> None + # X.691 11.9.3.6/11.9.3.7 only define the one and two octet forms up + # to 16K. Longer content has to be fragmented, which requires slicing + # the content itself, so leave that to append_fragmented rather than + # silently emitting a determinant that does not match what follows. + if length >= UPER_FRAGMENT_SIZE: + raise UPER_Encoding_Error( + "UPER_Encoder: length %i requires fragmentation" % length + ) if length < 128: encoded = bytes([length]) - elif length < 16384: - encoded = bytes([(0x80 | (length >> 8)), (length & 0xff)]) - elif length < 32768: - encoded = b"\xc1" - length = 16384 - elif length < 49152: - encoded = b"\xc2" - length = 32768 - elif length < 65536: - encoded = b"\xc3" - length = 49152 else: - encoded = b"\xc4" - length = 65536 + encoded = bytes([(0x80 | (length >> 8)), (length & 0xff)]) self.append_bytes(encoded) - return length + + def append_fragmented(self, count, append_units): + # type: (int, Callable[[int, int], None]) -> None + # X.691 11.9.3.8: emit the content as fragments of at most 4 * 16K + # units, each preceded by its own determinant, and always terminate + # with a determinant below 16K (possibly zero). append_units(offset, + # size) appends the units of one fragment. + offset = 0 + remaining = count + while remaining >= UPER_FRAGMENT_SIZE: + number_of_fragments = min(remaining // UPER_FRAGMENT_SIZE, 4) + size = number_of_fragments * UPER_FRAGMENT_SIZE + self.append_bytes(bytes([0xc0 | number_of_fragments])) + append_units(offset, size) + offset += size + remaining -= size + self.append_length_determinant(remaining) + append_units(offset, remaining) def append_unconstrained_whole_number(self, value): # type: (int) -> None @@ -400,23 +424,50 @@ def align_always(self): raise UPER_Decoding_Error("UPER_Decoder: out of data") self.number_of_bits -= width - def read_length_determinant(self): - # type: () -> int + def _read_length_determinant(self): + # type: () -> Tuple[int, bool] + # Returns the number of units and whether more fragments follow. value = self.read_non_negative_binary_integer(8) if (value & 0x80) == 0x00: - return value + return value, False if (value & 0xc0) == 0x80: - return ((value & 0x7f) << 8) | self.read_non_negative_binary_integer(8) - mapping = {0xc1: 16384, 0xc2: 32768, 0xc3: 49152, 0xc4: 65536} - if value in mapping: - return mapping[value] + return ( + ((value & 0x7f) << 8) | + self.read_non_negative_binary_integer(8) + ), False + if 0xc1 <= value <= 0xc4: + return (value & 0x0f) * UPER_FRAGMENT_SIZE, True raise UPER_Decoding_Error( "UPER_Decoder: bad length determinant 0x%02x" % value ) + def read_length_determinant(self): + # type: () -> int + length, fragmented = self._read_length_determinant() + if fragmented: + raise UPER_Decoding_Error( + "UPER_Decoder: unexpected fragmented length determinant" + ) + return length + + def read_fragmented(self, read_units): + # type: (Callable[[int], None]) -> None + # Counterpart of UPER_Encoder.append_fragmented: read_units(size) is + # called once per fragment, the last one being the (possibly empty) + # fragment introduced by a determinant below 16K. + while True: + size, fragmented = self._read_length_determinant() + read_units(size) + if not fragmented: + return + def read_unconstrained_whole_number(self): # type: () -> int number_of_bytes = self.read_length_determinant() + if number_of_bytes == 0: + raise UPER_Decoding_Error( + "UPER_Decoder: integer with an empty length determinant" + ) enc = self.read_non_negative_binary_integer(8 * number_of_bytes) sign_bit = 1 << (8 * number_of_bytes - 1) if enc & sign_bit: @@ -493,38 +544,45 @@ def UPER_boolean_dec(s): def UPER_octet_string_enc(data, minimum=None, maximum=None, enc=None): # type: (bytes, Optional[int], Optional[int], Optional[UPER_Encoder]) -> bytes standalone = enc is None - if enc is None: - enc = UPER_Encoder() + encoder = UPER_Encoder() if enc is None else enc if minimum is not None and maximum is not None and minimum == maximum: - enc.append_bytes(data) + encoder.append_bytes(data) elif minimum is not None and maximum is not None: - enc.append_non_negative_binary_integer( + encoder.append_non_negative_binary_integer( len(data) - minimum, UPER_bits_for_range(maximum - minimum), ) - enc.append_bytes(data) + encoder.append_bytes(data) else: - enc.append_length_determinant(len(data)) - enc.append_bytes(data) - return enc.as_bytes() if standalone else b"" + encoder.append_fragmented( + len(data), + lambda offset, size: encoder.append_bytes( + data[offset:offset + size] + ), + ) + return encoder.as_bytes() if standalone else b"" def UPER_octet_string_dec(s, minimum=None, maximum=None, dec=None): # type: (bytes, Optional[int], Optional[int], Optional[UPER_Decoder]) -> Tuple[bytes, bytes] # noqa: E501 standalone = dec is None - if dec is None: - dec = UPER_Decoder(s) + decoder = UPER_Decoder(s) if dec is None else dec if minimum is not None and maximum is not None and minimum == maximum: - length = minimum + data = decoder.read_bytes(minimum) elif minimum is not None and maximum is not None: - length = minimum + dec.read_non_negative_binary_integer( - UPER_bits_for_range(maximum - minimum) + data = decoder.read_bytes( + minimum + decoder.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) ) else: - length = dec.read_length_determinant() - data = dec.read_bytes(length) + fragments = [] # type: List[bytes] + decoder.read_fragmented( + lambda size: fragments.append(decoder.read_bytes(size)) + ) + data = b"".join(fragments) if standalone: - return data, dec.remaining() + return data, decoder.remaining() return data, b"" @@ -791,24 +849,31 @@ def encode_into(cls, maximum = uper_max if size_len: minimum = maximum = size_len + if minimum is not None and maximum is not None: + if not minimum <= nbits <= maximum: + raise UPER_Encoding_Error( + "UPERcodec_BIT_STRING: got %i bits while expecting %s" % + (nbits, minimum if minimum == maximum + else "%i..%i" % (minimum, maximum)) + ) if minimum is not None and maximum is not None and minimum == maximum: - if nbits >= minimum: - value = int.from_bytes(s, "big") >> (8 * len(s) - minimum) - elif isinstance(_s, str) and _s and all(c in "01" for c in _s): - value = int(_s, 2) - elif nbits > 0: - value = int.from_bytes(s, "big") >> max(0, 8 * len(s) - nbits) - else: - value = 0 - enc.append_non_negative_binary_integer(value, minimum) + enc.append_bits(s, nbits) elif minimum is not None and maximum is not None: enc.append_non_negative_binary_integer( nbits - minimum, UPER_bits_for_range(maximum - minimum) ) enc.append_bits(s, nbits) else: - enc.append_length_determinant((nbits + 7) // 8) - enc.append_bytes(s) + # X.691 16.11: the determinant counts bits, not octets, and no + # padding is inserted before whatever follows the bit string. + enc.append_fragmented( + nbits, + # Fragments hold whole multiples of 16K bits, so every chunk + # but the last starts and ends on an octet boundary. + lambda offset, size: enc.append_bits( + s[offset // 8:(offset + size + 7) // 8], size + ), + ) @classmethod def dec_from_decoder(cls, @@ -831,10 +896,18 @@ def dec_from_decoder(cls, UPER_bits_for_range(maximum - minimum) ) else: - nbytes = dec.read_length_determinant() - raw = dec.read_bytes(nbytes) - nbits = 8 * nbytes - return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)) + fragments = [] # type: List[bytes] + sizes = [] # type: List[int] + + def read_fragment(size): + # type: (int) -> None + fragments.append(dec.read_bits(size)) + sizes.append(size) + + dec.read_fragmented(read_fragment) + return cls.asn1_object( + _uper_bytes_to_bitstr(b"".join(fragments), sum(sizes)) + ) raw = dec.read_bits(nbits) return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)) @@ -931,14 +1004,17 @@ def encode_into(cls, enc, _oid, **_kwargs): else: lst = [] body = b"".join(BER_num_enc(k) for k in lst) - enc.append_length_determinant(len(body)) - enc.append_bytes(body) + enc.append_fragmented( + len(body), + lambda offset, size: enc.append_bytes(body[offset:offset + size]), + ) @classmethod def dec_from_decoder(cls, dec, **_kwargs): # type: (UPER_Decoder, **Any) -> ASN1_Object[bytes] - length = dec.read_length_determinant() - content = dec.read_bytes(length) + fragments = [] # type: List[bytes] + dec.read_fragmented(lambda size: fragments.append(dec.read_bytes(size))) + content = b"".join(fragments) lst = [] while content: val, content = BER_num_dec(content) @@ -1256,15 +1332,19 @@ def sequence_encode_into(field, enc, pkt, value=None): def sequence_of_m2i(field, pkt, s): # type: (Any, Any, bytes) -> Tuple[list, bytes] dec = UPER_Decoder(s) + lst = [] + + def read_items(count): + # type: (int) -> None + for _ in range(count): + c, _ = _extract_packet_from_decoder(field, dec, pkt) + if c: + lst.append(c) + if _field_extensible(field) and dec.read_bit(): - count = dec.read_length_determinant() + dec.read_fragmented(read_items) else: - count = _uper_count_dec(field, dec) - lst = [] - for _ in range(count): - c, _ = _extract_packet_from_decoder(field, dec, pkt) - if c: - lst.append(c) + _uper_count_dec(field, dec, read_items) if UPER_has_unexpected_remainder(dec): raise UPER_Decoding_Error( "unexpected remainder", @@ -1292,14 +1372,18 @@ def sequence_of_build(field, pkt): @staticmethod def sequence_of_m2i_from_decoder(field, pkt, dec): # type: (Any, Any, Any) -> list + lst = [] + + def read_items(count): + # type: (int) -> None + for _ in range(count): + item, _ = _extract_packet_from_decoder(field, dec, pkt) + lst.append(item) + if _field_extensible(field) and dec.read_bit(): - count = dec.read_length_determinant() + dec.read_fragmented(read_items) else: - count = _uper_count_dec(field, dec) - lst = [] - for _ in range(count): - item, _ = _extract_packet_from_decoder(field, dec, pkt) - lst.append(item) + _uper_count_dec(field, dec, read_items) return lst @staticmethod @@ -1308,9 +1392,18 @@ def sequence_of_encode_into(field, enc, pkt, value=None): if value is None: value = getattr(pkt, field.name) if value is None: - _uper_count_enc(field, enc, 0) + _uper_count_enc(field, enc, 0, lambda offset, size: None) return count = len(value) + + def append_items(offset, size): + # type: (int, int) -> None + for item in value[offset:offset + size]: + if field.holds_packets: + item.ASN1_root.encode_into(enc, item) + else: + field.fld.encode_into(enc, pkt, item) + uper_min, uper_max = _field_range(field) if _field_extensible(field): if ( @@ -1320,19 +1413,9 @@ def sequence_of_encode_into(field, enc, pkt, value=None): enc.append_bit(0) else: enc.append_bit(1) - enc.append_length_determinant(count) - for item in value: - if field.holds_packets: - item.ASN1_root.encode_into(enc, item) - else: - field.fld.encode_into(enc, pkt, item) + enc.append_fragmented(count, append_items) return - _uper_count_enc(field, enc, count) - for item in value: - if field.holds_packets: - item.ASN1_root.encode_into(enc, item) - else: - field.fld.encode_into(enc, pkt, item) + _uper_count_enc(field, enc, count, append_items) @staticmethod def choice_m2i(field, pkt, s): @@ -1461,25 +1544,30 @@ def _choice_index_for(field, x): return None -def _uper_count_enc(field, enc, count): - # type: (Any, Any, int) -> None +def _uper_count_enc(field, enc, count, append_items): + # type: (Any, Any, int, Callable[[int, int], None]) -> None + # The count of a SEQUENCE OF is a constrained whole number when the field + # carries a size constraint; otherwise it is a length determinant, and the + # items themselves are what gets fragmented, hence the callback. uper_min, uper_max = _field_range(field) if uper_min is not None and uper_max is not None: UPER_constrained_int_enc(count, uper_min, uper_max, enc=enc) + append_items(0, count) else: - enc.append_length_determinant(count) + enc.append_fragmented(count, append_items) -def _uper_count_dec(field, dec): - # type: (Any, Any) -> int +def _uper_count_dec(field, dec, read_items): + # type: (Any, Any, Callable[[int], None]) -> None uper_min, uper_max = _field_range(field) if uper_min is not None and uper_max is not None: size = uper_max - uper_min - return ( + read_items( dec.read_non_negative_binary_integer(UPER_bits_for_range(size)) + uper_min ) - return dec.read_length_determinant() + else: + dec.read_fragmented(read_items) def _extract_packet_from_decoder(field, dec, pkt): diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index 748b08e6f9c..5ec01bac174 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -96,14 +96,36 @@ INTEGER_VECTORS = [ lambda v: OERcodec_INTEGER.enc(v, size_len=8), b"\xff\xff\xff\xff\xff\xff\xff\xfe", ), - ("F", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=1), b"\x80"), - ("G", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=2), b"\x00\x80"), - ("G", 1000, lambda v: OERcodec_INTEGER.enc(v, size_len=2), b"\x03\xe8"), - ("H", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=4), b"\x00\x00\x00\x80"), + # F to I have a lower bound of zero, so they are unsigned: the width and + # the signedness come from the declared type, not from the value. + ( + "F", + 128, + lambda v: OERcodec_INTEGER.enc(v, size_len=1, oer_unsigned=True), + b"\x80", + ), + ( + "G", + 128, + lambda v: OERcodec_INTEGER.enc(v, size_len=2, oer_unsigned=True), + b"\x00\x80", + ), + ( + "G", + 1000, + lambda v: OERcodec_INTEGER.enc(v, size_len=2, oer_unsigned=True), + b"\x03\xe8", + ), + ( + "H", + 128, + lambda v: OERcodec_INTEGER.enc(v, size_len=4, oer_unsigned=True), + b"\x00\x00\x00\x80", + ), ( "I", 128, - lambda v: OERcodec_INTEGER.enc(v, size_len=8), + lambda v: OERcodec_INTEGER.enc(v, size_len=8, oer_unsigned=True), b"\x00\x00\x00\x00\x00\x00\x00\x80", ), ("B", 1, lambda v: OERcodec_INTEGER.enc(v, size_len=1), b"\x01"), @@ -358,7 +380,7 @@ OER_unsigned_integer_enc(0) == b"\x01\x00" v, r = OER_unsigned_integer_dec(OER_unsigned_integer_enc(65535)) v == 65535 and r == b"" = OER fixed unsigned 1 byte -OERcodec_INTEGER.enc(255, size_len=1) == b"\xff" +OERcodec_INTEGER.enc(255, size_len=1, oer_unsigned=True) == b"\xff" = OER fixed signed 2 bytes negative OERcodec_INTEGER.enc(-2, size_len=2) == b"\xff\xfe" = OER fixed signed 4 bytes @@ -406,7 +428,7 @@ ASN1_NULL(None).enc(ASN1_Codecs.OER) == b"" + ASN.1 OER review fixes = OER fixed integer decode roundtrip -x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(128, size_len=1), size_len=1, oer_unsigned=True) +x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(128, size_len=1, oer_unsigned=True), size_len=1, oer_unsigned=True) x.val == 128 and r == b"" = OER fixed integer signed decode x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(-2, size_len=2), size_len=2) @@ -1086,3 +1108,59 @@ assert OERcodec_STRING.enc(b"abc") == bytes.fromhex("03616263") _raises(OER_Encoding_Error, lambda: OERcodec_STRING.enc(b"abc", size_len=4)) True + += oer integer signedness follows the declared type +# X.696 10: the encoder must not pick the width or the signedness from the +# value, or the decoder (which only knows the type) reads something else back. +class OERSignedByte(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE(ASN1F_INTEGER("a", 0, size_len=1)) + +class OERUnsignedByte(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE(ASN1F_INTEGER("a", 0, size_len=1, oer_unsigned=True)) + +for cls, value, expected in [ + (OERSignedByte, 127, "7f"), + (OERSignedByte, -128, "80"), + (OERUnsignedByte, 255, "ff"), + (OERUnsignedByte, 0, "00"), +]: + pkt = cls(a=value) + assert raw(pkt) == bytes.fromhex(expected), (value, raw(pkt).hex()) + assert _roundtrip(cls, pkt).a.val == value + +# 200 used to encode as an unsigned 0xc8 and read back as -56. +_raises(OER_Encoding_Error, lambda: raw(OERSignedByte(a=200))) + +_raises(OER_Encoding_Error, lambda: raw(OERUnsignedByte(a=256))) + +_raises(OER_Encoding_Error, lambda: raw(OERUnsignedByte(a=-1))) + +True + += oer unbounded unsigned integer +# X.696 10.2: a lower bound of zero means the value is encoded unsigned, with +# no leading zero octet. Byte vectors checked against asn1tools. +class OERUnboundedUnsigned(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE(ASN1F_INTEGER("a", 0, oer_unsigned=True)) + +for value, expected in [ + (0, "0100"), + (127, "017f"), + (128, "0180"), + (200, "01c8"), + (65535, "02ffff"), + (100000, "030186a0"), +]: + pkt = OERUnboundedUnsigned(a=value) + assert raw(pkt) == bytes.fromhex(expected), (value, raw(pkt).hex()) + assert _roundtrip(OERUnboundedUnsigned, pkt).a.val == value + +True + += oer integer with an empty length determinant +_raises(OER_Decoding_Error, lambda: OER_signed_integer_dec(b"\x00")) + +True diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index 11edfb5de23..df792f54162 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -1230,7 +1230,6 @@ for length, expected in [ (127, b"\x7f"), (128, b"\x80\x80"), (16383, b"\xbf\xff"), - (16384, b"\xc1"), ]: enc = UPER_Encoder() enc.append_length_determinant(length) @@ -1238,6 +1237,49 @@ for length, expected in [ True += uper length determinant refuses lengths that need fragmentation +# X.691 11.9.3.8: the caller has to split the content, so a bare determinant +# of 16K or more would not match what follows it. +enc = UPER_Encoder() +try: + enc.append_length_determinant(16384) + assert False +except UPER_Encoding_Error: + pass + +True + += uper fragmented length determinant +# X.691 11.9.3.8: fragments of 16K units, always closed by a determinant +# below 16K. Byte vectors checked against asn1tools. +for count, expected_header in [ + (16384, b"\xc1"), + (32768, b"\xc2"), + (49152, b"\xc3"), + (65536, b"\xc4"), + (81920, b"\xc4"), +]: + enc = UPER_Encoder() + seen = [] + enc.append_fragmented(count, lambda offset, size: seen.append((offset, size))) + got = enc.as_bytes() + assert got.startswith(expected_header), (count, got[:1]) + assert sum(size for _, size in seen) == count, (count, seen) + assert got.endswith(b"\x00"), (count, got[-1:]) + +True + += uper fragmented length determinant roundtrip +for count in [0, 127, 16383, 16384, 40000, 70000]: + enc = UPER_Encoder() + enc.append_fragmented(count, lambda offset, size: None) + dec = UPER_Decoder(enc.as_bytes()) + seen = [] + dec.read_fragmented(lambda size: seen.append(size)) + assert sum(seen) == count, (count, seen) + +True + = uper count roundtrip for count in [0, 1, 3, 127]: enc = UPER_Encoder() @@ -1806,23 +1848,23 @@ assert "Already decoded" in str(err2) True = uper length determinant extended -enc = UPER_Encoder() - -assert enc.append_length_determinant(32768) == 32768 - -assert enc.as_bytes() == b"\xc2" - -enc = UPER_Encoder() +# X.691 11.9.3.8: multiples of 16K units are emitted as 0xc1..0xc4 fragments +# and the sequence is closed by a determinant below 16K. +def _fragment_headers(count): + enc = UPER_Encoder() + sizes = [] + enc.append_fragmented(count, lambda offset, size: sizes.append(size)) + return enc.as_bytes(), sizes -assert enc.append_length_determinant(49152) == 49152 +assert _fragment_headers(32768) == (b"\xc2\x00", [32768, 0]) -assert enc.as_bytes() == b"\xc3" +assert _fragment_headers(49152) == (b"\xc3\x00", [49152, 0]) -enc = UPER_Encoder() +assert _fragment_headers(65535) == (b"\xc3\xbf\xff", [49152, 16383]) -assert enc.append_length_determinant(65535) == 49152 +assert _fragment_headers(65536) == (b"\xc4\x00", [65536, 0]) -assert enc.as_bytes() == b"\xc3" +assert _fragment_headers(81920) == (b"\xc4\xc1\x00", [65536, 16384, 0]) True @@ -2837,3 +2879,94 @@ assert _val(empty.inner.x) == 7 True += uper unconstrained bit string counts bits +# X.691 16.11: the length determinant of an unconstrained BIT STRING counts +# bits, not octets, and nothing is padded before the next field. Byte vectors +# checked against asn1tools. +class UPERFreeBitString(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BIT_STRING("b", ""), + ASN1F_INTEGER("tail", 0, uper_min=0, uper_max=255), + ) + +for bits, expected in [ + ("", "00a5"), + ("1", "01d280"), + ("10110", "05b528"), + ("10110011", "08b3a5"), + ("1" * 20, "14fffffa50"), +]: + pkt = UPERFreeBitString(b=bits, tail=0xa5) + assert raw(pkt) == bytes.fromhex(expected), (bits, raw(pkt).hex()) + decoded = _roundtrip(UPERFreeBitString, pkt) + assert decoded.b.val == bits, (bits, decoded.b.val) + assert _val(decoded.tail) == 0xa5 + +True + += uper fixed size bit string refuses a mismatched length +class UPERFixedBitString(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE(ASN1F_BIT_STRING("b", "0" * 8, size_len=8)) + +assert raw(UPERFixedBitString(b="10110011")) == bytes.fromhex("b3") + +_raises(UPER_Encoding_Error, lambda: raw(UPERFixedBitString(b="101"))) + +_raises(UPER_Encoding_Error, lambda: raw(UPERFixedBitString(b="1011001100"))) + +True + += uper octet string fragmentation +# X.691 11.9.3.8. Byte vectors checked against asn1tools: a fragment header, +# 16K octets, then the terminating determinant. +class UPERFreeOctetString(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE(ASN1F_STRING("s", "")) + +data = bytes(i % 256 for i in range(16384)) + +built = raw(UPERFreeOctetString(s=data)) + +assert built == b"\xc1" + data + b"\x00" + +assert _roundtrip(UPERFreeOctetString, UPERFreeOctetString(s=data)).s.val == data + +data = bytes(i % 256 for i in range(40000)) + +built = raw(UPERFreeOctetString(s=data)) + +assert built == b"\xc2" + data[:32768] + b"\x9c\x40" + data[32768:] + +assert _roundtrip(UPERFreeOctetString, UPERFreeOctetString(s=data)).s.val == data + +True + += uper sequence of fragmentation +class UPERFreeSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER("x", 0, uper_min=0, uper_max=255)), + ) + +items = [i % 256 for i in range(16385)] + +built = raw(UPERFreeSeqOf(values=items)) + +assert built == b"\xc1" + bytes(items[:16384]) + b"\x01" + bytes(items[16384:]) + +decoded = _roundtrip(UPERFreeSeqOf, UPERFreeSeqOf(values=items)) + +assert [_val(x) for x in decoded.values] == items + +True + += uper integer with an empty length determinant +_raises( + UPER_Decoding_Error, + lambda: UPER_Decoder(b"\x00").read_unconstrained_whole_number(), +) + +True + From 0ea540d4e8d192e74df679545c6e5aad9f4debdb Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 11 Aug 2026 12:45:06 +0200 Subject: [PATCH 11/46] oer, uper: remove dead helpers and deduplicate the codecs The UPER encoder and decoder each carried an align_always method, which is meaningless for an unaligned codec and had no caller. A family of standalone helpers (join_encodings, optional_presence_enc, count_enc, count_dec, constrained_int_dec, unconstrained_int_dec and boolean_dec) was likewise reachable only from its own tests, and OER kept copies of the BER check_type and check_type_get_len that only BER itself calls. UPERcodec_SEQUENCE spliced a raw byte string into the bitstream by guessing how many of its trailing zero bits were padding, which drops bits from a sequence that legitimately ends in zeroes. Nothing reaches it, as sequences are encoded through the ASN1F_SEQUENCE hooks, so it now refuses the input like its decoding counterpart already did rather than corrupting it silently. The surviving helpers took an optional encoder and returned either the finished bytes or b"", while every caller passed one and the decoding side passed b"" as a dummy first argument; they now take the encoder or the decoder directly, as encode_into already did. The minimal two's complement sizing, the bit to byte packing and the decode-and-check- remainder wrapper were each written out several times over, and codec methods declared options they never read, which hid the ones they do honour. No encoding changes: the ASN.1 suites, the asn1tools differential fuzzing and the malformed input fuzzing pass unchanged. AI-Assisted: yes (Cursor) Co-authored-by: Cursor --- scapy/contrib/oer.py | 81 +++---- scapy/contrib/uper.py | 552 +++++++++++------------------------------- test/contrib/uper.uts | 62 ++--- 3 files changed, 198 insertions(+), 497 deletions(-) diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py index 44dbd22d105..beb4079a058 100644 --- a/scapy/contrib/oer.py +++ b/scapy/contrib/oer.py @@ -158,22 +158,12 @@ def OER_len_dec(s): def OER_signed_integer_enc(i): # type: (int) -> bytes - if i < 0: - number_of_bits = i.bit_length() - number_of_bytes = (number_of_bits + 7) // 8 - value = (1 << (8 * number_of_bytes)) + i - if (value & (1 << (8 * number_of_bytes - 1))) == 0: - value |= (0xff << (8 * number_of_bytes)) - number_of_bytes += 1 - elif i > 0: - number_of_bits = i.bit_length() - number_of_bytes = (number_of_bits + 7) // 8 - if number_of_bits == (8 * number_of_bytes): - number_of_bytes += 1 - value = i - else: - number_of_bytes = 1 - value = 0 + # X.696 10.4: the shortest two's complement encoding. A negative value + # needs one bit less than its magnitude suggests, as -2**(8n-1) still + # fits in n octets, hence the increment before measuring. + magnitude = i + 1 if i < 0 else i + number_of_bytes = (magnitude.bit_length() + 8) // 8 + value = i & ((1 << (8 * number_of_bytes)) - 1) return OER_len_enc(number_of_bytes) + value.to_bytes(number_of_bytes, "big") @@ -223,11 +213,15 @@ def OER_unsigned_integer_dec(s): return value, s[number_of_bytes:] +_OER_FIXED_FORMATS = { + True: {1: ">b", 2: ">h", 4: ">i", 8: ">q"}, + False: {1: ">B", 2: ">H", 4: ">I", 8: ">Q"}, +} + + def OER_fixed_integer_enc(i, length, signed=True): # type: (int, int, bool) -> bytes - fmt = {1: ">b", 2: ">h", 4: ">i", 8: ">q"} if signed else { - 1: ">B", 2: ">H", 4: ">I", 8: ">Q" - } + fmt = _OER_FIXED_FORMATS[signed] try: return struct.pack(fmt[length], i) except KeyError: @@ -249,9 +243,7 @@ def OER_fixed_integer_dec(s, length, signed=True): (len(s), length), remaining=s ) - fmt = {1: ">b", 2: ">h", 4: ">i", 8: ">q"} if signed else { - 1: ">B", 2: ">H", 4: ">I", 8: ">Q" - } + fmt = _OER_FIXED_FORMATS[signed] try: return struct.unpack(fmt[length], s[:length])[0], s[length:] except KeyError: @@ -449,18 +441,6 @@ def check_string(cls, s): (cls.__name__, cls.tag), remaining=s ) - @classmethod - def check_type(cls, s): - # type: (bytes) -> bytes - cls.check_string(s) - return s - - @classmethod - def check_type_get_len(cls, s): - # type: (bytes) -> Tuple[int, bytes] - cls.check_string(s) - return len(s), s - @classmethod def check_type_check_len(cls, s): # type: (bytes) -> Tuple[int, bytes, bytes] @@ -582,8 +562,8 @@ class OERcodec_BOOLEAN(OERcodec_Object[int]): tag = ASN1_Class_UNIVERSAL.BOOLEAN @classmethod - def enc(cls, i, size_len=0, **_kwargs): - # type: (int, Optional[int], **Any) -> bytes + def enc(cls, i, **_kwargs): + # type: (int, **Any) -> bytes return chb(0xff if i else 0x00) @classmethod @@ -721,8 +701,8 @@ class OERcodec_NULL(OERcodec_Object[None]): tag = ASN1_Class_UNIVERSAL.NULL @classmethod - def enc(cls, i, size_len=0, **_kwargs): - # type: (Any, Optional[int], **Any) -> bytes + def enc(cls, i, **_kwargs): + # type: (Any, **Any) -> bytes return b"" @classmethod @@ -741,8 +721,8 @@ class OERcodec_OID(OERcodec_Object[bytes]): tag = ASN1_Class_UNIVERSAL.OID @classmethod - def enc(cls, _oid, size_len=0, **_kwargs): - # type: (AnyStr, Optional[int], **Any) -> bytes + def enc(cls, _oid, **_kwargs): + # type: (AnyStr, **Any) -> bytes oid = bytes_encode(_oid) if oid: lst = [int(x) for x in oid.strip(b".").split(b".")] @@ -787,8 +767,8 @@ class OERcodec_ENUMERATED(OERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.ENUMERATED @classmethod - def enc(cls, i, size_len=0, **_kwargs): - # type: (int, Optional[int], **Any) -> bytes + def enc(cls, i, **_kwargs): + # type: (int, **Any) -> bytes return OER_enumerated_enc(i) @classmethod @@ -856,8 +836,8 @@ class OERcodec_SEQUENCE(OERcodec_Object[Union[bytes, List['OERcodec_Object[Any]' tag = ASN1_Class_UNIVERSAL.SEQUENCE @classmethod - def enc(cls, _ll, size_len=0, **_kwargs): - # type: (Union[bytes, List[OERcodec_Object[Any]]], Optional[int], **Any) -> bytes # noqa: E501 + def enc(cls, _ll, **_kwargs): + # type: (Union[bytes, List[OERcodec_Object[Any]]], **Any) -> bytes if isinstance(_ll, bytes): return _ll return b"".join(x.enc(cls.codec) for x in _ll) @@ -1031,15 +1011,12 @@ def sequence_of_build(field, pkt): val = getattr(pkt, field.name) if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: s = val # type: Any - elif val is None: - s = OER_unsigned_integer_enc(0) - elif field.holds_packets: - s = OER_unsigned_integer_enc(len(val)) + b"".join(bytes(i) for i in val) else: - s = ( - OER_unsigned_integer_enc(len(val)) + - b"".join(field.fld.i2m(pkt, i) for i in val) - ) + items = [ + bytes(item) if field.holds_packets else field.fld.i2m(pkt, item) + for item in val or [] + ] + s = OER_unsigned_integer_enc(len(items)) + b"".join(items) return field.i2m(pkt, s) @staticmethod diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index 847e54de7f2..fb15b9285f6 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -23,8 +23,6 @@ per character. """ -import binascii - from scapy.error import warning from scapy.compat import orb, bytes_encode from scapy.utils import binrepr, inet_aton, inet_ntoa @@ -125,6 +123,16 @@ def UPER_bits_for_range(size): UPER_FRAGMENT_SIZE = 16384 +def _uper_bits_to_bytes(value, number_of_bits): + # type: (int, int) -> bytes + # X.691 11.1: an encoding is padded with zero bits up to an octet + # boundary. + if number_of_bits == 0: + return b"" + padding = -number_of_bits % 8 + return (value << padding).to_bytes((number_of_bits + padding) // 8, "big") + + class UPER_Encoder(object): def __init__(self): # type: () -> None @@ -137,15 +145,6 @@ def number_of_bytes(self): # type: () -> int return (self.chunks_number_of_bits + self.number_of_bits + 7) // 8 - def align_always(self): - # type: () -> None - width = 8 * self.number_of_bytes() - width -= self.chunks_number_of_bits - width -= self.number_of_bits - if width: - self.number_of_bits += width - self.value <<= width - def append_bit(self, bit): # type: (int) -> None self.number_of_bits += 1 @@ -213,23 +212,15 @@ def append_fragmented(self, count, append_units): def append_unconstrained_whole_number(self, value): # type: (int) -> None - number_of_bits = 0 if value == 0 else value.bit_length() - if value < 0: - number_of_bytes = (number_of_bits + 7) // 8 - enc = (1 << (8 * number_of_bytes)) + value - if enc & (1 << (8 * number_of_bytes - 1)) == 0: - enc |= (0xff << (8 * number_of_bytes)) - number_of_bytes += 1 - elif value > 0: - number_of_bytes = (number_of_bits + 7) // 8 - if number_of_bits == 8 * number_of_bytes: - number_of_bytes += 1 - enc = value - else: - number_of_bytes = 1 - enc = 0 + # X.691 11.4: the shortest two's complement encoding. A negative value + # needs one bit less than its magnitude suggests, as -2**(8n-1) still + # fits in n octets, hence the increment before measuring. + magnitude = value + 1 if value < 0 else value + number_of_bytes = (magnitude.bit_length() + 8) // 8 self.append_length_determinant(number_of_bytes) - self.append_non_negative_binary_integer(enc, 8 * number_of_bytes) + self.append_non_negative_binary_integer( + value & ((1 << (8 * number_of_bytes)) - 1), 8 * number_of_bytes + ) def as_bytes(self): # type: () -> bytes @@ -242,87 +233,7 @@ def as_bytes(self): value <<= self.number_of_bits value |= self.value number_of_bits += self.number_of_bits - if number_of_bits == 0: - return b"" - number_of_alignment_bits = (8 - (number_of_bits % 8)) % 8 - value <<= number_of_alignment_bits - number_of_bits += number_of_alignment_bits - value |= (0x80 << number_of_bits) - hexval = hex(value)[4:].rstrip("L") - if len(hexval) % 2: - hexval = "0" + hexval - return binascii.unhexlify(hexval) - - -def _uper_significant_bit_count(data): - # type: (bytes) -> int - if not data: - return 0 - total = 8 * len(data) - bits = int.from_bytes(data, "big") - end = total - while end > 0 and ((bits >> (total - end)) & 1) == 0: - end -= 1 - trimmed = total - end - if trimmed > 0 and trimmed <= 8: - return end - return total - - -def _uper_per_bits_to_bytes(bit_value, number_of_bits): - # type: (int, int) -> bytes - if number_of_bits == 0: - return b"" - bitstr = format(bit_value, "0%db" % number_of_bits) - value = "10000000" + bitstr - number_of_alignment_bits = (8 - (number_of_bits % 8)) - if number_of_alignment_bits != 8: - value += "0" * number_of_alignment_bits - hexval = hex(int(value, 2))[4:].rstrip("L") - if len(hexval) % 2: - hexval = "0" + hexval - return binascii.unhexlify(hexval) - - -def UPER_append_encoded(enc, data): - # type: (UPER_Encoder, bytes) -> None - if not data: - return - nbits = _uper_significant_bit_count(data) - if nbits == 0: - return - total = 8 * len(data) - bits = int.from_bytes(data, "big") - shift = total - nbits - value = (bits >> shift) & ((1 << nbits) - 1) - enc.append_non_negative_binary_integer(value, nbits) - - -def UPER_join_encodings(*parts): - # type: (*bytes) -> bytes - enc = UPER_Encoder() - for part in parts: - UPER_append_encoded(enc, part) - return enc.as_bytes() - - -def UPER_optional_presence_enc(bits, enc=None): - # type: (List[int], Optional[UPER_Encoder]) -> bytes - standalone = enc is None - if enc is None: - enc = UPER_Encoder() - for bit in bits: - enc.append_bit(bit) - return enc.as_bytes() if standalone else b"" - - -def UPER_count_enc(count, enc=None): - # type: (int, Optional[UPER_Encoder]) -> bytes - standalone = enc is None - if enc is None: - enc = UPER_Encoder() - enc.append_length_determinant(count) - return enc.as_bytes() if standalone else b"" + return _uper_bits_to_bytes(value, number_of_bits) def UPER_has_unexpected_remainder(dec): @@ -333,17 +244,6 @@ def UPER_has_unexpected_remainder(dec): return (dec._bits & mask) != 0 -def UPER_count_dec(s, dec=None): - # type: (bytes, Optional[UPER_Decoder]) -> Tuple[int, bytes] - standalone = dec is None - if dec is None: - dec = UPER_Decoder(s) - count = dec.read_length_determinant() - if standalone: - return count, dec.remaining() - return count, b"" - - class UPER_Decoder(object): def __init__(self, encoded): # type: (bytes) -> None @@ -383,14 +283,14 @@ def read_bits(self, number_of_bits): return b"" value = self._read_bits_int(number_of_bits) self.number_of_bits -= number_of_bits - return _uper_per_bits_to_bytes(value, number_of_bits) + return _uper_bits_to_bytes(value, number_of_bits) def remaining(self): # type: () -> bytes if self.number_of_bits == 0: return b"" value = self._read_bits_int(self.number_of_bits) - return _uper_per_bits_to_bytes(value, self.number_of_bits) + return _uper_bits_to_bytes(value, self.number_of_bits) def remaining_bytes(self): # type: () -> bytes @@ -415,15 +315,6 @@ def read_non_negative_binary_integer(self, number_of_bits): self.number_of_bits -= number_of_bits return value - def align_always(self): - # type: () -> None - consumed = self.total_number_of_bits - self.number_of_bits - width = (8 - (consumed % 8)) % 8 - if width: - if width > self.number_of_bits: - raise UPER_Decoding_Error("UPER_Decoder: out of data") - self.number_of_bits -= width - def _read_length_determinant(self): # type: () -> Tuple[int, bool] # Returns the number of units and whether more fragments follow. @@ -479,135 +370,63 @@ def consume_input(self): self.number_of_bits = 0 -def UPER_constrained_int_enc(value, minimum, maximum, enc=None): - # type: (int, int, int, Optional[UPER_Encoder]) -> bytes - standalone = enc is None - if enc is None: - enc = UPER_Encoder() - size = maximum - minimum +def UPER_constrained_int_enc(enc, value, minimum, maximum): + # type: (UPER_Encoder, int, int, int) -> None enc.append_non_negative_binary_integer( - value - minimum, UPER_bits_for_range(size) + value - minimum, UPER_bits_for_range(maximum - minimum) ) - return enc.as_bytes() if standalone else b"" -def UPER_constrained_int_dec(s, minimum, maximum): - # type: (bytes, int, int) -> Tuple[int, bytes] - dec = UPER_Decoder(s) - size = maximum - minimum - value = dec.read_non_negative_binary_integer(UPER_bits_for_range(size)) - dec.consume_input() - return value + minimum, b"" - - -def UPER_constrained_int_dec_from_decoder(dec, minimum, maximum): +def UPER_constrained_int_dec(dec, minimum, maximum): # type: (UPER_Decoder, int, int) -> int - size = maximum - minimum - value = dec.read_non_negative_binary_integer(UPER_bits_for_range(size)) + value = dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) return value + minimum -def UPER_unconstrained_int_enc(value, enc=None): - # type: (int, Optional[UPER_Encoder]) -> bytes - standalone = enc is None - if enc is None: - enc = UPER_Encoder() - enc.append_unconstrained_whole_number(value) - return enc.as_bytes() if standalone else b"" - - -def UPER_unconstrained_int_dec(s): - # type: (bytes) -> Tuple[int, bytes] - dec = UPER_Decoder(s) - value = dec.read_unconstrained_whole_number() - remain = dec.remaining() - return value, remain - - -def UPER_boolean_enc(value, enc=None): - # type: (int, Optional[UPER_Encoder]) -> bytes - standalone = enc is None - if enc is None: - enc = UPER_Encoder() - enc.append_bit(1 if value else 0) - return enc.as_bytes() if standalone else b"" - - -def UPER_boolean_dec(s): - # type: (bytes) -> Tuple[int, bytes] - dec = UPER_Decoder(s) - value = dec.read_bit() - dec.consume_input() - return value, b"" - - -def UPER_octet_string_enc(data, minimum=None, maximum=None, enc=None): - # type: (bytes, Optional[int], Optional[int], Optional[UPER_Encoder]) -> bytes - standalone = enc is None - encoder = UPER_Encoder() if enc is None else enc - if minimum is not None and maximum is not None and minimum == maximum: - encoder.append_bytes(data) - elif minimum is not None and maximum is not None: - encoder.append_non_negative_binary_integer( - len(data) - minimum, - UPER_bits_for_range(maximum - minimum), - ) - encoder.append_bytes(data) +def UPER_octet_string_enc(enc, data, minimum=None, maximum=None): + # type: (UPER_Encoder, bytes, Optional[int], Optional[int]) -> None + if minimum is not None and maximum is not None: + if minimum != maximum: + enc.append_non_negative_binary_integer( + len(data) - minimum, + UPER_bits_for_range(maximum - minimum), + ) + enc.append_bytes(data) else: - encoder.append_fragmented( + enc.append_fragmented( len(data), - lambda offset, size: encoder.append_bytes( - data[offset:offset + size] - ), + lambda offset, size: enc.append_bytes(data[offset:offset + size]), ) - return encoder.as_bytes() if standalone else b"" - - -def UPER_octet_string_dec(s, minimum=None, maximum=None, dec=None): - # type: (bytes, Optional[int], Optional[int], Optional[UPER_Decoder]) -> Tuple[bytes, bytes] # noqa: E501 - standalone = dec is None - decoder = UPER_Decoder(s) if dec is None else dec - if minimum is not None and maximum is not None and minimum == maximum: - data = decoder.read_bytes(minimum) - elif minimum is not None and maximum is not None: - data = decoder.read_bytes( - minimum + decoder.read_non_negative_binary_integer( + + +def UPER_octet_string_dec(dec, minimum=None, maximum=None): + # type: (UPER_Decoder, Optional[int], Optional[int]) -> bytes + if minimum is not None and maximum is not None: + length = minimum + if minimum != maximum: + length += dec.read_non_negative_binary_integer( UPER_bits_for_range(maximum - minimum) ) - ) - else: - fragments = [] # type: List[bytes] - decoder.read_fragmented( - lambda size: fragments.append(decoder.read_bytes(size)) - ) - data = b"".join(fragments) - if standalone: - return data, decoder.remaining() - return data, b"" + return dec.read_bytes(length) + fragments = [] # type: List[bytes] + dec.read_fragmented(lambda size: fragments.append(dec.read_bytes(size))) + return b"".join(fragments) -def UPER_choice_index_enc(index, number_of_choices, enc=None): - # type: (int, int, Optional[UPER_Encoder]) -> bytes - standalone = enc is None - if enc is None: - enc = UPER_Encoder() +def UPER_choice_index_enc(enc, index, number_of_choices): + # type: (UPER_Encoder, int, int) -> None enc.append_non_negative_binary_integer( index, UPER_bits_for_range(number_of_choices - 1) ) - return enc.as_bytes() if standalone else b"" -def UPER_choice_index_dec(s, number_of_choices, dec=None): - # type: (bytes, int, Optional[UPER_Decoder]) -> Tuple[int, bytes] - standalone = dec is None - if dec is None: - dec = UPER_Decoder(s) - index = dec.read_non_negative_binary_integer( +def UPER_choice_index_dec(dec, number_of_choices): + # type: (UPER_Decoder, int) -> int + return dec.read_non_negative_binary_integer( UPER_bits_for_range(number_of_choices - 1) ) - if standalone: - return index, dec.remaining() - return index, b"" class UPERcodec_metaclass(type): @@ -749,12 +568,12 @@ def encode_into(cls, enc.append_bit(0) else: enc.append_bit(1) - UPER_unconstrained_int_enc(i, enc=enc) + enc.append_unconstrained_whole_number(i) return if minimum is not None and maximum is not None: - UPER_constrained_int_enc(i, minimum, maximum, enc=enc) + UPER_constrained_int_enc(enc, i, minimum, maximum) else: - UPER_unconstrained_int_enc(i, enc=enc) + enc.append_unconstrained_whole_number(i) @classmethod def dec_from_decoder(cls, @@ -773,7 +592,7 @@ def dec_from_decoder(cls, value = dec.read_unconstrained_whole_number() return cls.asn1_object(value) if minimum is not None and maximum is not None: - value = UPER_constrained_int_dec_from_decoder(dec, minimum, maximum) + value = UPER_constrained_int_dec(dec, minimum, maximum) else: value = dec.read_unconstrained_whole_number() return cls.asn1_object(value) @@ -783,28 +602,13 @@ class UPERcodec_BOOLEAN(UPERcodec_Object[int]): tag = ASN1_Class_UNIVERSAL.BOOLEAN @classmethod - def encode_into(cls, - enc, # type: UPER_Encoder - i, # type: int - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - **_kwargs # type: Any - ): - # type: (...) -> None - UPER_boolean_enc(i, enc=enc) + def encode_into(cls, enc, i, **_kwargs): + # type: (UPER_Encoder, int, **Any) -> None + enc.append_bit(1 if i else 0) @classmethod - def dec_from_decoder(cls, - dec, # type: UPER_Decoder - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - **_kwargs # type: Any - ): - # type: (...) -> ASN1_Object[int] + def dec_from_decoder(cls, dec, **_kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[int] return cls.asn1_object(dec.read_bit()) @@ -830,6 +634,15 @@ def _uper_bit_string_parts(_s): return s, 8 * len(s) +def _uper_size_bounds(size_len, uper_min, uper_max): + # type: (Optional[int], Optional[int], Optional[int]) -> Tuple[Optional[int], Optional[int]] # noqa: E501 + # A SIZE constraint given as size_len is a fixed size, i.e. a range whose + # bounds coincide. + if size_len: + return size_len, size_len + return uper_min, uper_max + + class UPERcodec_BIT_STRING(UPERcodec_Object[str]): tag = ASN1_Class_UNIVERSAL.BIT_STRING @@ -840,15 +653,11 @@ def encode_into(cls, size_len=0, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool **_kwargs # type: Any ): # type: (...) -> None s, nbits = _uper_bit_string_parts(_s) - minimum = uper_min - maximum = uper_max - if size_len: - minimum = maximum = size_len + minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) if minimum is not None and maximum is not None: if not minimum <= nbits <= maximum: raise UPER_Encoding_Error( @@ -856,12 +665,10 @@ def encode_into(cls, (nbits, minimum if minimum == maximum else "%i..%i" % (minimum, maximum)) ) - if minimum is not None and maximum is not None and minimum == maximum: - enc.append_bits(s, nbits) - elif minimum is not None and maximum is not None: - enc.append_non_negative_binary_integer( - nbits - minimum, UPER_bits_for_range(maximum - minimum) - ) + if minimum != maximum: + enc.append_non_negative_binary_integer( + nbits - minimum, UPER_bits_for_range(maximum - minimum) + ) enc.append_bits(s, nbits) else: # X.691 16.11: the determinant counts bits, not octets, and no @@ -881,20 +688,16 @@ def dec_from_decoder(cls, size_len=0, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool **_kwargs # type: Any ): # type: (...) -> ASN1_Object[str] - minimum = uper_min - maximum = uper_max - if size_len: - minimum = maximum = size_len - if minimum is not None and maximum is not None and minimum == maximum: + minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) + if minimum is not None and maximum is not None: nbits = minimum - elif minimum is not None and maximum is not None: - nbits = minimum + dec.read_non_negative_binary_integer( - UPER_bits_for_range(maximum - minimum) - ) + if minimum != maximum: + nbits += dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) else: fragments = [] # type: List[bytes] sizes = [] # type: List[int] @@ -912,13 +715,6 @@ def read_fragment(size): return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)) -def _uper_octet_string_bounds(size_len, uper_min, uper_max): - # type: (Optional[int], Optional[int], Optional[int]) -> Tuple[Optional[int], Optional[int]] # noqa: E501 - if size_len: - return size_len, size_len - return uper_min, uper_max - - class UPERcodec_STRING(UPERcodec_Object[str]): tag = ASN1_Class_UNIVERSAL.STRING @@ -929,15 +725,12 @@ def encode_into(cls, size_len=0, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool **_kwargs # type: Any ): # type: (...) -> None s = bytes_encode(_s) - minimum, maximum = _uper_octet_string_bounds( - size_len, uper_min, uper_max, - ) - UPER_octet_string_enc(s, minimum, maximum, enc=enc) + minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) + UPER_octet_string_enc(enc, s, minimum, maximum) @classmethod def dec_from_decoder(cls, @@ -945,14 +738,11 @@ def dec_from_decoder(cls, size_len=0, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool **_kwargs # type: Any ): # type: (...) -> ASN1_Object[Any] - minimum, maximum = _uper_octet_string_bounds( - size_len, uper_min, uper_max, - ) - raw, _ = UPER_octet_string_dec(b"", minimum, maximum, dec=dec) + minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) + raw = UPER_octet_string_dec(dec, minimum, maximum) return cls.asn1_object(raw) @@ -960,28 +750,14 @@ class UPERcodec_NULL(UPERcodec_Object[None]): tag = ASN1_Class_UNIVERSAL.NULL @classmethod - def encode_into(cls, - enc, # type: UPER_Encoder - _s, # type: Any - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - **_kwargs # type: Any - ): - # type: (...) -> None + def encode_into(cls, enc, _s, **_kwargs): + # type: (UPER_Encoder, Any, **Any) -> None + # NULL has an empty encoding. return @classmethod - def dec_from_decoder(cls, - dec, # type: UPER_Decoder - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - **_kwargs # type: Any - ): - # type: (...) -> ASN1_Object[None] + def dec_from_decoder(cls, dec, **_kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[None] return cls.asn1_object(None) @classmethod @@ -1025,14 +801,8 @@ def dec_from_decoder(cls, dec, **_kwargs): return cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)) -def UPER_enumerated_enc(value, - enum_values, # type: List[int] - enc=None # type: Optional[UPER_Encoder] - ): - # type: (int, List[int], Optional[UPER_Encoder]) -> bytes - standalone = enc is None - if enc is None: - enc = UPER_Encoder() +def UPER_enumerated_enc(enc, value, enum_values): + # type: (UPER_Encoder, int, List[int]) -> None if not enum_values: raise UPER_Encoding_Error("UPER_enumerated_enc: empty enumeration") try: @@ -1041,29 +811,19 @@ def UPER_enumerated_enc(value, raise UPER_Encoding_Error( "UPER_enumerated_enc: unknown enumeration value %r" % value ) - UPER_choice_index_enc(index, len(enum_values), enc=enc) - return enc.as_bytes() if standalone else b"" + UPER_choice_index_enc(enc, index, len(enum_values)) -def UPER_enumerated_dec(s, - enum_values, # type: List[int] - dec=None # type: Optional[UPER_Decoder] - ): - # type: (bytes, List[int], Optional[UPER_Decoder]) -> Tuple[int, bytes] - standalone = dec is None - if dec is None: - dec = UPER_Decoder(s) +def UPER_enumerated_dec(dec, enum_values): + # type: (UPER_Decoder, List[int]) -> int if not enum_values: raise UPER_Decoding_Error("UPER_enumerated_dec: empty enumeration") - index, _ = UPER_choice_index_dec(b"", len(enum_values), dec=dec) + index = UPER_choice_index_dec(dec, len(enum_values)) if index >= len(enum_values): raise UPER_Decoding_Error( "UPER_enumerated_dec: index %i out of range" % index ) - if standalone: - dec.consume_input() - return enum_values[index], b"" - return enum_values[index], b"" + return enum_values[index] class UPERcodec_ENUMERATED(UPERcodec_INTEGER): @@ -1076,19 +836,18 @@ def encode_into(cls, size_len=0, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool uper_enum_values=None, # type: Optional[List[int]] **_kwargs # type: Any ): # type: (...) -> None if uper_enum_values is not None: - UPER_enumerated_enc(i, uper_enum_values, enc=enc) + UPER_enumerated_enc(enc, i, uper_enum_values) return minimum = uper_min if uper_min is not None else 0 maximum = uper_max if uper_max is not None else size_len if maximum is None: maximum = max(i, 0) - UPER_constrained_int_enc(i, minimum, maximum, enc=enc) + UPER_constrained_int_enc(enc, i, minimum, maximum) @classmethod def dec_from_decoder(cls, @@ -1096,13 +855,12 @@ def dec_from_decoder(cls, size_len=0, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool uper_enum_values=None, # type: Optional[List[int]] **_kwargs # type: Any ): # type: (...) -> ASN1_Object[int] if uper_enum_values is not None: - value, _ = UPER_enumerated_dec(b"", uper_enum_values, dec=dec) + value = UPER_enumerated_dec(dec, uper_enum_values) return cls.asn1_object(value) minimum = uper_min if uper_min is not None else 0 maximum = uper_max if uper_max is not None else size_len @@ -1118,18 +876,14 @@ class UPERcodec_SEQUENCE(UPERcodec_Object[Union[bytes, List[Any]]]): tag = ASN1_Class_UNIVERSAL.SEQUENCE @classmethod - def encode_into(cls, - enc, # type: UPER_Encoder - _ll, # type: Union[bytes, List[UPERcodec_Object[Any]]] - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - **_kwargs # type: Any - ): - # type: (...) -> None - if isinstance(_ll, bytes): - UPER_append_encoded(enc, _ll) + def encode_into(cls, enc, _ll, **_kwargs): + # type: (UPER_Encoder, Any, **Any) -> None + # A finished encoding is padded to an octet boundary, so its real bit + # length is lost and it cannot be spliced into a bitstream. Sequences + # are encoded through the ASN1F_SEQUENCE hooks instead. + raise UPER_Encoding_Error( + "UPERcodec_SEQUENCE: schema-defined field order required" + ) @classmethod def enc(cls, _ll, **_kwargs): @@ -1163,12 +917,12 @@ def encode_into(cls, enc, ipaddr_ascii, **_kwargs): s = inet_aton(ipaddr_ascii) except Exception: raise UPER_Encoding_Error("IPv4 address could not be encoded") - UPER_octet_string_enc(s, 4, 4, enc=enc) + UPER_octet_string_enc(enc, s, 4, 4) @classmethod def dec_from_decoder(cls, dec, **_kwargs): # type: (UPER_Decoder, **Any) -> ASN1_Object[str] - raw, _ = UPER_octet_string_dec(b"", 4, 4, dec=dec) + raw = UPER_octet_string_dec(dec, 4, 4) try: ipaddr_ascii = inet_ntoa(raw) except Exception: @@ -1256,6 +1010,20 @@ def _field_range(field): return opts.get("uper_min"), opts.get("uper_max") +def _uper_decode_all(s, read): + # type: (bytes, Callable[[UPER_Decoder], Any]) -> Any + # The field owns the whole substring it was handed, so any bit left set + # beyond the octet padding means the encoding did not match the schema. + dec = UPER_Decoder(s) + value = read(dec) + if UPER_has_unexpected_remainder(dec): + raise UPER_Decoding_Error( + "unexpected remainder", + remaining=dec.remaining(), + ) + return value + + class _UPER_FieldHooks(object): """Compound ASN1F_* helpers for UPER/PER (kept out of asn1fields.py).""" @@ -1268,13 +1036,9 @@ def use_object_enc(field, pkt, item): @staticmethod def sequence_m2i(field, pkt, s): # type: (Any, Any, bytes) -> Tuple[Any, bytes] - dec = UPER_Decoder(s) - _UPER_FieldHooks.sequence_dissect_from_decoder(field, pkt, dec) - if UPER_has_unexpected_remainder(dec): - raise UPER_Decoding_Error( - "unexpected remainder", - remaining=dec.remaining(), - ) + _uper_decode_all(s, lambda dec: ( + _UPER_FieldHooks.sequence_dissect_from_decoder(field, pkt, dec) + )) return [], b"" @staticmethod @@ -1331,26 +1095,9 @@ def sequence_encode_into(field, enc, pkt, value=None): @staticmethod def sequence_of_m2i(field, pkt, s): # type: (Any, Any, bytes) -> Tuple[list, bytes] - dec = UPER_Decoder(s) - lst = [] - - def read_items(count): - # type: (int) -> None - for _ in range(count): - c, _ = _extract_packet_from_decoder(field, dec, pkt) - if c: - lst.append(c) - - if _field_extensible(field) and dec.read_bit(): - dec.read_fragmented(read_items) - else: - _uper_count_dec(field, dec, read_items) - if UPER_has_unexpected_remainder(dec): - raise UPER_Decoding_Error( - "unexpected remainder", - remaining=dec.remaining(), - ) - return lst, b"" + return _uper_decode_all(s, lambda dec: ( + _UPER_FieldHooks.sequence_of_m2i_from_decoder(field, pkt, dec) + )), b"" @staticmethod def sequence_of_build(field, pkt): @@ -1377,7 +1124,7 @@ def sequence_of_m2i_from_decoder(field, pkt, dec): def read_items(count): # type: (int) -> None for _ in range(count): - item, _ = _extract_packet_from_decoder(field, dec, pkt) + item = _extract_packet_from_decoder(field, dec, pkt) lst.append(item) if _field_extensible(field) and dec.read_bit(): @@ -1420,14 +1167,9 @@ def append_items(offset, size): @staticmethod def choice_m2i(field, pkt, s): # type: (Any, Any, bytes) -> Tuple[Any, bytes] - dec = UPER_Decoder(s) - val = _UPER_FieldHooks.choice_m2i_from_decoder(field, pkt, dec) - if UPER_has_unexpected_remainder(dec): - raise UPER_Decoding_Error( - "unexpected remainder", - remaining=dec.remaining(), - ) - return val, b"" + return _uper_decode_all(s, lambda dec: ( + _UPER_FieldHooks.choice_m2i_from_decoder(field, pkt, dec) + )), b"" @staticmethod def choice_i2m(field, pkt, x): @@ -1451,7 +1193,7 @@ def choice_m2i_from_decoder(field, pkt, dec): ) order = field.choice_order if len(order) > 1: - index, _ = UPER_choice_index_dec(b"", len(order), dec=dec) + index = UPER_choice_index_dec(dec, len(order)) else: index = 0 if index >= len(order): @@ -1485,7 +1227,7 @@ def choice_encode_into(field, enc, pkt, value=None): enc.append_bit(0) order = field.choice_order if len(order) > 1: - UPER_choice_index_enc(index, len(order), enc=enc) + UPER_choice_index_enc(enc, index, len(order)) choice = field.choice_list[index] if hasattr(choice, "ASN1_root"): value.ASN1_root.encode_into(enc, value) @@ -1551,7 +1293,7 @@ def _uper_count_enc(field, enc, count, append_items): # items themselves are what gets fragmented, hence the callback. uper_min, uper_max = _field_range(field) if uper_min is not None and uper_max is not None: - UPER_constrained_int_enc(count, uper_min, uper_max, enc=enc) + UPER_constrained_int_enc(enc, count, uper_min, uper_max) append_items(0, count) else: enc.append_fragmented(count, append_items) @@ -1561,23 +1303,19 @@ def _uper_count_dec(field, dec, read_items): # type: (Any, Any, Callable[[int], None]) -> None uper_min, uper_max = _field_range(field) if uper_min is not None and uper_max is not None: - size = uper_max - uper_min - read_items( - dec.read_non_negative_binary_integer(UPER_bits_for_range(size)) + - uper_min - ) + read_items(UPER_constrained_int_dec(dec, uper_min, uper_max)) else: dec.read_fragmented(read_items) def _extract_packet_from_decoder(field, dec, pkt): - # type: (Any, Any, Any) -> Tuple[Any, bytes] + # type: (Any, Any, Any) -> Any if field.holds_packets: p = field.cls() p.add_underlayer(pkt) p.ASN1_root.dissect_from_decoder(p, dec) - return p, b"" - return field.fld.m2i_from_decoder(pkt, dec), b"" + return p + return field.fld.m2i_from_decoder(pkt, dec) # Populated by _install_uper_asn1fields() (also published on scapy.asn1fields). diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index df792f54162..3c043938841 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -360,7 +360,7 @@ def _encode_composite(typename, value): if typename == "Choice": alt, payload = value index = 0 if alt == "a" else 1 - UPER_choice_index_enc(index, 2, enc=enc) + UPER_choice_index_enc(enc, index, 2) if alt == "a": UPERcodec_INTEGER.encode_into(enc, payload) else: @@ -369,7 +369,7 @@ def _encode_composite(typename, value): if typename == "ChoiceC": alt, payload = value index = 0 if alt == "a" else 1 - UPER_choice_index_enc(index, 2, enc=enc) + UPER_choice_index_enc(enc, index, 2) if alt == "a": UPERcodec_INTEGER.encode_into( enc, payload, uper_min=0, uper_max=15, @@ -490,7 +490,7 @@ ASN1SCC_VECTORS = [ def _encode_choice_int1_10(): # type: () -> bytes enc = UPER_Encoder() - UPER_choice_index_enc(0, 5, enc=enc) + UPER_choice_index_enc(enc, 0, 5) UPERcodec_INTEGER.encode_into(enc, 10, uper_min=0, uper_max=15) return enc.as_bytes() @@ -1283,17 +1283,16 @@ True = uper count roundtrip for count in [0, 1, 3, 127]: enc = UPER_Encoder() - UPER_count_enc(count, enc=enc) - got, _ = UPER_count_dec(enc.as_bytes()) - assert got == count + enc.append_length_determinant(count) + assert UPER_Decoder(enc.as_bytes()).read_length_determinant() == count True = uper choice index roundtrip for index, choices in [(0, 2), (1, 5), (3, 5)]: enc = UPER_Encoder() - UPER_choice_index_enc(index, choices, enc=enc) - got, _ = UPER_choice_index_dec(enc.as_bytes(), choices) + UPER_choice_index_enc(enc, index, choices) + got = UPER_choice_index_dec(UPER_Decoder(enc.as_bytes()), choices) assert got == index True @@ -1301,30 +1300,29 @@ True = uper optional presence enc = UPER_Encoder() -UPER_optional_presence_enc([0, 1, 0], enc=enc) +for bit in [0, 1, 0]: + enc.append_bit(bit) assert enc.as_bytes() == b"\x40" True = uper constrained integer -data = UPER_constrained_int_enc(10, 0, 15) - -value, remain = UPER_constrained_int_dec(data, 0, 15) +enc = UPER_Encoder() -assert value == 10 +UPER_constrained_int_enc(enc, 10, 0, 15) -assert remain == b"" +assert UPER_constrained_int_dec(UPER_Decoder(enc.as_bytes()), 0, 15) == 10 True = uper constrained signed integer for value, expected in [(0, b"\x80"), (-1, b"\x7f"), (127, b"\xff"), (-128, b"\x00")]: - data = UPER_constrained_int_enc(value, -128, 127) - assert data == expected - decoded, remain = UPER_constrained_int_dec(data, -128, 127) - assert decoded == value - assert remain == b"" + enc = UPER_Encoder() + UPER_constrained_int_enc(enc, value, -128, 127) + assert enc.as_bytes() == expected + dec = UPER_Decoder(enc.as_bytes()) + assert UPER_constrained_int_dec(dec, -128, 127) == value True @@ -1334,10 +1332,10 @@ for data, minimum, maximum in [ (b"\x12\x34\x56", 3, 3), (bytes.fromhex("afbc4583"), 1, 20), ]: - encoded = UPER_octet_string_enc(data, minimum, maximum) - dec = UPER_Decoder(encoded) - decoded, _ = UPER_octet_string_dec(encoded, minimum, maximum, dec=dec) - assert decoded == data + enc = UPER_Encoder() + UPER_octet_string_enc(enc, data, minimum, maximum) + dec = UPER_Decoder(enc.as_bytes()) + assert UPER_octet_string_dec(dec, minimum, maximum) == data assert not UPER_has_unexpected_remainder(dec) True @@ -1349,21 +1347,6 @@ assert UPER_has_unexpected_remainder(UPER_Decoder(b"\x80")) is True True -= uper join encodings -a = UPERcodec_INTEGER.enc(1) - -b = UPERcodec_INTEGER.enc(2) - -joined = UPER_join_encodings(a, b) - -dec = UPER_Decoder(joined) - -assert dec.read_unconstrained_whole_number() == 1 - -assert dec.read_unconstrained_whole_number() == 2 - -True - = uper chained encode into enc = UPER_Encoder() @@ -1938,6 +1921,9 @@ _raises(UPER_Encoding_Error, lambda: UPERcodec_SEQUENCE.enc([ASN1_INTEGER(1)])) _raises(UPER_Decoding_Error, lambda: UPERcodec_SEQUENCE.do_dec(b"\x00")) +# A finished encoding is octet padded, so it cannot be spliced into a bitstream +_raises(UPER_Encoding_Error, lambda: UPERcodec_SEQUENCE.encode_into(UPER_Encoder(), b"raw")) + assert UPERcodec_SET.enc(b"raw") == b"raw" True From e59309dd52c83ca980791a054248dd4ff2957626 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 11 Aug 2026 15:02:53 +0200 Subject: [PATCH 12/46] uper: fix enumerated indexing and octet string size constraints The enumeration index followed the order the values were declared in rather than their ascending order, as X.691 14.1 requires, so an ENUMERATED { c(2), a(0), b(1) } encoded a as index 1 where a conformant peer reads b. Enumerations written in ascending order, which is the usual case, were already correct. An extensible enumerated dropped the one bit prefix of 14.3, shifting every field encoded after it. The option could not be reached anyway, as ASN1F_enum_INTEGER was the one field class that did not forward its codec options, so uper_extensible= raised a TypeError instead of constraining the field. An OCTET STRING ignored its SIZE constraint while encoding, where BIT STRING and OER already raised: a two octet value in a SIZE(4) field emitted two octets, and in a SIZE(2..4) field an eight octet value wrote a determinant that wrapped, so the peer read a different length and lost everything that followed. Without an enumeration list and without declared bounds, the enumerated encoder took the upper bound from the value at hand, making the width depend on the value while the decoder refused the same case; a size_len of zero was also read as an upper bound of zero, which encoded every value in no bits at all. Encodings match asn1tools byte for byte, over the vectors added here as well as random schemas. AI-Assisted: yes (Cursor) Co-authored-by: Cursor --- scapy/asn1fields.py | 4 ++- scapy/contrib/uper.py | 64 +++++++++++++++++++++++++++++++++++-------- test/contrib/uper.uts | 63 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 12 deletions(-) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 364bde87a90..7adea44fd88 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -341,12 +341,14 @@ def __init__(self, context=None, # type: Optional[Any] implicit_tag=None, # type: Optional[Any] explicit_tag=None, # type: Optional[Any] + **codec_opts # type: Any ): # type: (...) -> None super(ASN1F_enum_INTEGER, self).__init__( name, default, context=context, implicit_tag=implicit_tag, - explicit_tag=explicit_tag + explicit_tag=explicit_tag, + **codec_opts ) i2s = self.i2s = {} # type: Dict[int, str] s2i = self.s2i = {} # type: Dict[str, int] diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index fb15b9285f6..0d70e951b4f 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -21,6 +21,11 @@ refused rather than misparsed), SET, REAL, and the known-multiplier character string encodings, which are emitted as plain octets rather than 7 or 4 bits per character. + +``ASN1F_CHOICE`` alternatives are indexed in declaration order, where 10.2 +asks for the canonical order of their tags. The two coincide for a schema +compiled with AUTOMATIC TAGS, which assigns the tags in declaration order; +declare the alternatives in ascending tag order otherwise. """ from scapy.error import warning @@ -388,6 +393,15 @@ def UPER_constrained_int_dec(dec, minimum, maximum): def UPER_octet_string_enc(enc, data, minimum=None, maximum=None): # type: (UPER_Encoder, bytes, Optional[int], Optional[int]) -> None if minimum is not None and maximum is not None: + if not minimum <= len(data) <= maximum: + # The determinant is sized after the constraint, so a value that + # violates it cannot be expressed: refuse rather than emit + # something the peer reads as a different length. + raise UPER_Encoding_Error( + "UPER_octet_string_enc: got %i octets while expecting %s" % + (len(data), minimum if minimum == maximum + else "%i..%i" % (minimum, maximum)) + ) if minimum != maximum: enc.append_non_negative_binary_integer( len(data) - minimum, @@ -837,16 +851,25 @@ def encode_into(cls, uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] uper_enum_values=None, # type: Optional[List[int]] + uper_extensible=False, # type: bool **_kwargs # type: Any ): # type: (...) -> None if uper_enum_values is not None: + if uper_extensible: + # X.691 14.3: a one bit prefix says whether the value is an + # extension addition. Only root values can be encoded. + if i not in uper_enum_values: + raise UPER_Encoding_Error( + "UPERcodec_ENUMERATED: extension additions are not " + "supported" + ) + enc.append_bit(0) UPER_enumerated_enc(enc, i, uper_enum_values) return - minimum = uper_min if uper_min is not None else 0 - maximum = uper_max if uper_max is not None else size_len - if maximum is None: - maximum = max(i, 0) + minimum, maximum = cls._range( + size_len, uper_min, uper_max, UPER_Encoding_Error + ) UPER_constrained_int_enc(enc, i, minimum, maximum) @classmethod @@ -856,21 +879,38 @@ def dec_from_decoder(cls, uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] uper_enum_values=None, # type: Optional[List[int]] + uper_extensible=False, # type: bool **_kwargs # type: Any ): # type: (...) -> ASN1_Object[int] if uper_enum_values is not None: - value = UPER_enumerated_dec(dec, uper_enum_values) - return cls.asn1_object(value) - minimum = uper_min if uper_min is not None else 0 - maximum = uper_max if uper_max is not None else size_len - if maximum is None: - raise UPER_Decoding_Error("UPERcodec_ENUMERATED: missing range") + if uper_extensible and dec.read_bit(): + raise UPER_Decoding_Error( + "UPERcodec_ENUMERATED: extension additions are not " + "supported" + ) + return cls.asn1_object(UPER_enumerated_dec(dec, uper_enum_values)) + minimum, maximum = cls._range( + size_len, uper_min, uper_max, UPER_Decoding_Error + ) value = dec.read_non_negative_binary_integer( UPER_bits_for_range(maximum - minimum) ) + minimum return cls.asn1_object(value) + @staticmethod + def _range(size_len, uper_min, uper_max, error): + # type: (Optional[int], Optional[int], Optional[int], Any) -> Tuple[int, int] # noqa: E501 + # Without the enumeration itself the index range has to come from + # the declared bounds; deriving it from the value at hand would + # make the width depend on the value, which the decoder cannot + # reproduce. + minimum = uper_min if uper_min is not None else 0 + maximum = uper_max if uper_max is not None else (size_len or None) + if maximum is None: + raise error("UPERcodec_ENUMERATED: missing range") + return minimum, maximum + class UPERcodec_SEQUENCE(UPERcodec_Object[Union[bytes, List[Any]]]): tag = ASN1_Class_UNIVERSAL.SEQUENCE @@ -1445,7 +1485,9 @@ def enum_codec_kwargs(self, pkt): # keep an empty codec_opts and their item.enc() fast path. codec = getattr(pkt, "ASN1_codec", None) if getattr(codec, "_field_hooks", None) is _UPER_FieldHooks: - kwargs.setdefault("uper_enum_values", list(self.i2s)) + # X.691 14.1: the index follows the enumeration values in + # ascending order, whatever order they were declared in. + kwargs.setdefault("uper_enum_values", sorted(self.i2s)) return kwargs af.ASN1F_enum_INTEGER._codec_kwargs = enum_codec_kwargs # type: ignore[assignment] diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index 3c043938841..9c8994bdcb8 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -1916,6 +1916,69 @@ assert obj2.val == 2 True += uper enumerated without a range +# The width would otherwise follow the value, which the decoder cannot redo +_raises(UPER_Encoding_Error, lambda: UPERcodec_ENUMERATED.enc(3)) + +_raises(UPER_Decoding_Error, lambda: UPERcodec_ENUMERATED.do_dec(b"\x60")) + +True + += uper enumerated index follows the value order +# X.691 14.1: sort the enumeration by value, whatever order it was declared in +class UPERUnsortedEnum(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_ENUMERATED("e", 0, {2: "c", 0: "a", 1: "b"}), + ) + +# byte vectors from asn1tools for ENUMERATED { c(2), a(0), b(1) } +for value, expected in [(0, "00"), (1, "40"), (2, "80")]: + assert raw(UPERUnsortedEnum(e=value)) == bytes.fromhex(expected), value + assert _dissect(UPERUnsortedEnum, expected).e.val == value + +True + += uper extensible enumerated +# X.691 14.3: a one bit prefix, zero for a value of the extension root +class UPERExtEnum(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_ENUMERATED("e", 0, {0: "a", 1: "b", 2: "c"}, + uper_extensible=True), + ) + +# byte vectors from asn1tools for ENUMERATED { a(0), b(1), c(2), ... } +for value, expected in [(0, "00"), (1, "20"), (2, "40")]: + assert raw(UPERExtEnum(e=value)) == bytes.fromhex(expected), value + assert _dissect(UPERExtEnum, expected).e.val == value + +enc = UPER_Encoder() + +_raises(UPER_Encoding_Error, lambda: UPERcodec_ENUMERATED.encode_into( + enc, 7, uper_enum_values=[0, 1, 2], uper_extensible=True)) + +_raises(UPER_Decoding_Error, lambda: UPERcodec_ENUMERATED.dec_from_decoder( + UPER_Decoder(b"\x80"), uper_enum_values=[0, 1, 2], uper_extensible=True)) + +True + += uper octet string honours its size constraint +# A determinant sized after the constraint cannot express a violating length +_raises(UPER_Encoding_Error, lambda: UPERcodec_STRING.enc(b"AB", size_len=4)) + +_raises(UPER_Encoding_Error, lambda: UPERcodec_STRING.enc(b"ABCDEF", size_len=4)) + +assert UPERcodec_STRING.enc(b"ABCD", size_len=4) == b"ABCD" + +_raises(UPER_Encoding_Error, + lambda: UPERcodec_STRING.enc(b"A", uper_min=2, uper_max=4)) + +_raises(UPER_Encoding_Error, + lambda: UPERcodec_STRING.enc(b"ABCDEFGH", uper_min=2, uper_max=4)) + +True + = uper sequence errors _raises(UPER_Encoding_Error, lambda: UPERcodec_SEQUENCE.enc([ASN1_INTEGER(1)])) From e590a5797538df32430f400334b8e98706100ef8 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 11 Aug 2026 19:10:59 +0200 Subject: [PATCH 13/46] oer, uper: fix choice alternatives, OER tagging and DEFAULT components A CHOICE whose alternatives are ASN1F_PACKET instances, which is how a choice between tagged sequences is written and what BER already supports, could not be encoded: the alternative lookup only recognised packet classes and basic field classes. UPER refused the value outright and OER dropped the alternative tag, emitting bytes it could not read back. OER also let the BER constructed bit into the tag number it emitted, so an alternative tagged [0] went out as tag number 32 and an untagged SEQUENCE alternative as universal 48 instead of 16. The encoding round tripped with itself and with nothing else. Tags of components were encoded at all, where X.696 encodes none whatever the tagging environment of the module: the only tag on the wire is the one of the chosen CHOICE alternative. OER_tagging_enc and OER_tagging_dec are now the identity, and the alternative tag is emitted by the choice hook alone. ASN1F_DEFAULT was defined by importing scapy.contrib.uper, although a DEFAULT component is not specific to a codec and the OER documentation refers to it, which left OER users with a name they could not import and OER with a getattr fallback for the absent set_absent. It now lives in asn1fields, next to ASN1F_optional, and both codecs re-export it. BER gains from it too: ASN1F_optional.build asked the wrapped field whether it was empty, so a DEFAULT component holding its default value was encoded where DER omits it. Alternative tags and the encodings of the sequences behind them match asn1tools byte for byte. AI-Assisted: yes (Cursor) Co-authored-by: Cursor --- scapy/asn1fields.py | 41 ++++++++++++++- scapy/contrib/oer.py | 103 +++++++++++++++++++------------------ scapy/contrib/uper.py | 56 +++++--------------- test/contrib/oer.uts | 63 +++++++++++++++++++---- test/contrib/uper.uts | 40 ++++++++++++++ test/scapy/layers/asn1.uts | 33 +++++++++++- 6 files changed, 230 insertions(+), 106 deletions(-) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 7adea44fd88..ab9f2aa1f08 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -750,12 +750,22 @@ def dissect(self, pkt, s): try: return self._field.dissect(pkt, s) except (ASN1_Error, ASN1F_badsequence, ASN1_Decoding_Error): - self._field.set_val(pkt, None) + self.set_absent(pkt) return s + def set_absent(self, pkt): + # type: (ASN1_Packet) -> None + """Called when the encoding does not carry the component.""" + self._field.set_val(pkt, None) + + def is_empty(self, pkt): + # type: (ASN1_Packet) -> bool + return self._field.is_empty(pkt) + def build(self, pkt): # type: (ASN1_Packet) -> bytes - if self._field.is_empty(pkt): + # Through self, so that a DEFAULT component omits its default value. + if self.is_empty(pkt): return b"" return self._field.build(pkt) @@ -768,6 +778,33 @@ def i2repr(self, pkt, x): return self._field.i2repr(pkt, x) +class ASN1F_DEFAULT(ASN1F_optional): + """ + ASN.1 field holding a DEFAULT value: it is omitted from the encoding while + it holds that value, and restored when the encoding does not carry it. + """ + def __init__(self, field, default): + # type: (ASN1F_field[Any, Any], Any) -> None + super(ASN1F_DEFAULT, self).__init__(field) + self._default = default + + def is_empty(self, pkt): + # type: (ASN1_Packet) -> bool + val = getattr(pkt, self._field.name, None) + if val is None: + return True + if isinstance(val, ASN1_Object): + val = val.val + default = self._default + if isinstance(default, ASN1_Object): + default = default.val + return bool(val == default) + + def set_absent(self, pkt): + # type: (ASN1_Packet) -> None + self._field.set_val(pkt, self._default) + + class ASN1F_omit(ASN1F_field[None, None]): """ ASN.1 field that is not specified. This is simply omitted on the network. diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py index beb4079a058..346ae6e7fb1 100644 --- a/scapy/contrib/oer.py +++ b/scapy/contrib/oer.py @@ -15,6 +15,10 @@ for sequences declared with ``oer_extensible=True``. Fixed size constraints are expressed with ``size_len=`` (octets for strings, bits for BIT STRING). +Tags declared on a field are not encoded: OER only puts a tag on the wire for +the chosen alternative of an ``ASN1F_CHOICE`` (20.2), so the ``implicit_tag=`` +and ``explicit_tag=`` of the alternatives are what selects it. + Not supported yet: extension additions (an encoding that carries them is refused rather than misparsed), SET, REAL, and the canonical variant (C-OER). """ @@ -39,6 +43,8 @@ ASN1_Object, _ASN1_ERROR, ) +# Re-exported: DEFAULT components are what the preamble bits describe. +from scapy.asn1fields import ASN1F_DEFAULT # noqa: F401 from typing import ( Any, @@ -369,6 +375,15 @@ def OER_id_dec(s): return tag_class | tag_number, remainder +def _OER_tag_parts(identifier): + # type: (int) -> Tuple[int, int] + # ASN1F_* fields describe tags as BER identifier octets: class in the top + # two bits, constructed flag in 0x20 and tag number in the low five bits. + # X.696 8.7 only keeps the class and the number, so the constructed flag + # must not leak into the encoded tag number. + return identifier & 0xc0, identifier & 0x1f + + def OER_tagging_dec(s, # type: bytes hidden_tag=None, # type: Optional[int | ASN1Tag] implicit_tag=None, # type: Optional[int] @@ -377,30 +392,14 @@ def OER_tagging_dec(s, # type: bytes _fname="", # type: str ): # type: (...) -> Tuple[Optional[int], bytes] - # OER does not use implicit tagging. Explicit tags are encoded as choice - # alternatives (tag + value). - real_tag = None - if explicit_tag is not None and len(s) > 0: - err_msg = ( - "OER_tagging_dec: observed tag 0x%.02x does not " - "match expected tag 0x%.02x (%s)" - ) - tag_class, tag_number, remainder = OER_tag_dec(s) - observed = tag_class | tag_number - if observed != explicit_tag: - if not safe: - raise OER_Decoding_Error( - err_msg % (observed, explicit_tag, _fname), - remaining=s) - real_tag = observed - s = remainder - return real_tag, s + # X.696 encodes no tag for a component, whatever the tagging environment + # of the module: the only tag on the wire is the one of a chosen CHOICE + # alternative, which _OER_FieldHooks handles. + return None, s def OER_tagging_enc(s, implicit_tag=None, explicit_tag=None): # type: (bytes, Optional[int], Optional[int]) -> bytes - if explicit_tag is not None: - return OER_tag_enc(explicit_tag & 0x3f, explicit_tag & 0xc0) + s return s @@ -920,17 +919,6 @@ def _field_extensible(field): return bool(getattr(field, "codec_opts", {}).get("oer_extensible", False)) -def _set_absent(field, pkt): - # type: (Any, Any) -> None - # ASN1F_DEFAULT restores its default value; a plain optional clears itself. - # set_absent() only exists once scapy.contrib.uper has been imported. - set_absent = getattr(field, "set_absent", None) - if set_absent is not None: - set_absent(pkt) - else: - field.set_val(pkt, None) - - class _OER_FieldHooks(object): """Compound ASN1F_* helpers for OER (kept out of asn1fields.py).""" @@ -966,7 +954,7 @@ def sequence_m2i(field, pkt, s): present = presence[opt_index] opt_index += 1 if not present: - _set_absent(obj, pkt) + obj.set_absent(pkt) continue # The preamble already said the component is there, so dissect # it directly: a failure is an error, not an absence. @@ -1025,23 +1013,32 @@ def choice_m2i(field, pkt, s): from scapy.asn1fields import ASN1F_field from scapy.asn1.asn1 import ASN1_Error s = field._apply_tagging_dec(s, pkt) - tag, payload = OER_id_dec(s) - if tag in field.choices: - choice = field.choices[tag] - elif field.flexible_tag: - choice = ASN1F_field - else: - raise ASN1_Error( - "ASN1F_CHOICE: unexpected field in '%s' " - "(tag %s not in possible tags %s)" % ( - field.name, tag, list(field.choices.keys()) + tag_class, tag_number, payload = OER_tag_dec(s) + choice = None + for key, alternative in field.choices.items(): + if _OER_tag_parts(key) == (tag_class, tag_number): + choice = alternative + break + if choice is None: + if not field.flexible_tag: + raise ASN1_Error( + "ASN1F_CHOICE: unexpected field in '%s' " + "(tag %s not in possible tags %s)" % ( + field.name, tag_class | tag_number, + list(field.choices.keys()) + ) ) - ) + choice = ASN1F_field if hasattr(choice, "ASN1_root"): return field.extract_packet(choice, payload, _underlayer=pkt) if isinstance(choice, type): return choice(field.name, b"").m2i(pkt, payload) - return choice.m2i(pkt, payload) + # ASN1F_PACKET instance: X.696 20.2 puts the alternative tag in front + # of the value, so it was consumed above and must not be looked for + # again by the field itself. + return field.extract_packet( + choice._resolve_cls(pkt), payload, _underlayer=pkt, + ) @staticmethod def choice_i2m(field, pkt, x): @@ -1056,7 +1053,8 @@ def choice_i2m(field, pkt, x): s = bytes(x) alt_tag = _choice_tag_for(field, x) if alt_tag is not None: - s = OER_tag_enc(alt_tag & 0x3f, alt_tag & 0xc0) + s + tag_class, tag_number = _OER_tag_parts(alt_tag) + s = OER_tag_enc(tag_number, tag_class) + s return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) @@ -1064,11 +1062,16 @@ def _choice_index_for(field, x): # type: (Any, Any) -> Optional[int] from scapy.asn1.asn1 import ASN1_Object for index, choice in enumerate(field.choice_list): - if isinstance(choice, type) and hasattr(choice, "ASN1_root"): - if isinstance(x, choice): - return index - elif hasattr(choice, "ASN1_tag"): - if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + if isinstance(choice, type): + if hasattr(choice, "ASN1_root"): + if isinstance(x, choice): + return index + elif hasattr(choice, "ASN1_tag"): + if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + return index + elif getattr(choice, "cls", None) is not None: + # ASN1F_PACKET instance: the alternative is a tagged packet. + if isinstance(x, choice.cls): return index return None diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index 0d70e951b4f..de5605e386e 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -45,6 +45,8 @@ ASN1_Object, _ASN1_ERROR, ) +# Re-exported: DEFAULT components are what the preamble bits describe. +from scapy.asn1fields import ASN1F_DEFAULT # noqa: F401 from typing import ( Any, @@ -1317,11 +1319,16 @@ def _choice_index_for(field, x): # type: (Any, Any) -> Optional[int] from scapy.asn1.asn1 import ASN1_Object for index, choice in enumerate(field.choice_list): - if isinstance(choice, type) and hasattr(choice, "ASN1_root"): - if isinstance(x, choice): - return index - elif hasattr(choice, "ASN1_tag"): - if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + if isinstance(choice, type): + if hasattr(choice, "ASN1_root"): + if isinstance(x, choice): + return index + elif hasattr(choice, "ASN1_tag"): + if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + return index + elif getattr(choice, "cls", None) is not None: + # ASN1F_PACKET instance: the alternative is a tagged packet. + if isinstance(x, choice.cls): return index return None @@ -1358,44 +1365,12 @@ def _extract_packet_from_decoder(field, dec, pkt): return field.fld.m2i_from_decoder(pkt, dec) -# Populated by _install_uper_asn1fields() (also published on scapy.asn1fields). -ASN1F_DEFAULT = None # type: Any - - def _install_uper_asn1fields(): # type: () -> None - """Attach UPER bitstream helpers and DEFAULT onto asn1fields classes.""" + """Attach the UPER bitstream helpers onto the asn1fields classes.""" from scapy import asn1fields as af from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Error, ASN1_Object - class _ASN1F_DEFAULT(af.ASN1F_optional): - """ASN.1 field with a DEFAULT value (PER presence bit).""" - - def __init__(self, field, default): - # type: (Any, Any) -> None - super(_ASN1F_DEFAULT, self).__init__(field) - self._default = default - - def is_empty(self, pkt): - # type: (Any) -> bool - val = getattr(pkt, self._field.name, None) - if val is None: - return True - if isinstance(val, ASN1_Object): - val = val.val - default = self._default - if isinstance(default, ASN1_Object): - default = default.val - return bool(val == default) - - def set_absent(self, pkt): - # type: (Any) -> None - self.set_val(pkt, self._default) - - global ASN1F_DEFAULT - ASN1F_DEFAULT = _ASN1F_DEFAULT # type: ignore[misc,assignment] - af.ASN1F_DEFAULT = _ASN1F_DEFAULT - def m2i_from_decoder(self, pkt, dec): # type: (Any, Any, Any) -> Any codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) @@ -1431,10 +1406,6 @@ def encode_into(self, enc, pkt, value=None): enc, raw, **self._codec_kwargs(pkt), ) - def opt_set_absent(self, pkt): - # type: (Any, Any) -> None - self.set_val(pkt, None) - def opt_dissect_from_decoder(self, pkt, dec): # type: (Any, Any, Any) -> None return self._field.dissect_from_decoder(pkt, dec) @@ -1467,7 +1438,6 @@ def opt_encode_into(self, enc, pkt, value=None): "encode_into": hooks.packet_encode_into, }), (af.ASN1F_optional, { - "set_absent": opt_set_absent, "dissect_from_decoder": opt_dissect_from_decoder, "encode_into": opt_encode_into, }), diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index 5ec01bac174..778482884fb 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -315,6 +315,28 @@ class OERPacketChoice(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_CHOICE("c", None, OERInnerSeq, ASN1F_INTEGER) +class OERAltA(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("i", 0), + ) + +class OERAltB(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BOOLEAN("b", True), + ) + +class OERTaggedChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_CHOICE( + "c", None, + ASN1F_PACKET("a1", None, OERAltA, explicit_tag=0xA0), + ASN1F_PACKET("a2", None, OERAltB, explicit_tag=0xA1), + ), + ) + class OERUnsignedField(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_INTEGER( @@ -436,8 +458,8 @@ x.val == -2 and r == b"" = OER fixed octet string decode x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3), size_len=3) x.val == b"\x12\x34\x56" and r == b"" -= OER explicit null tagging -OER_tagging_enc(OERcodec_NULL.enc(None), explicit_tag=0x81) == b"\x81" += OER does not encode the tag of a component +OER_tagging_enc(b"\x05", explicit_tag=0x81) == b"\x05" and OER_tagging_dec(b"\x05", explicit_tag=0x81) == (None, b"\x05") = OER choice id decode tag, r = OER_id_dec(b"\x81\x01") tag == 0x81 and r == b"\x01" @@ -446,7 +468,8 @@ tag == 0x81 and r == b"\x01" = oer field explicit tag pkt = OERTaggedInteger(n=5) -assert raw(pkt) == b"\xa1\x01\x05" +# X.696 encodes no tag for a component, whatever the tagging environment +assert raw(pkt) == b"\x01\x05" decoded = _roundtrip(OERTaggedInteger, pkt) @@ -471,7 +494,7 @@ True present = OEROptionalField(id=1, extra=7) # \x80: preamble with the presence bit set for the single OPTIONAL component -assert raw(present) == b"\x80\x01\x01\xa0\x01\x07" +assert raw(present) == b"\x80\x01\x01\x01\x07" decoded = _roundtrip(OEROptionalField, present) @@ -528,7 +551,7 @@ pkt = OERRecord( expected = ( b"\x80" - b"\x01*\xff\x02hi\xa0\x01\x07" + b"\x01*\xff\x02hi\x01\x07" b"\x01\x03\x01\x01\x01\x02\x01\x03" ) @@ -739,7 +762,7 @@ assert [x.val for x in decoded.values] == [1, 2, 3] True = oer field dissect -tagged = _dissect(OERTaggedInteger, "a10105") +tagged = _dissect(OERTaggedInteger, "0105") assert tagged.n.val == 5 @@ -749,7 +772,7 @@ assert fixed.n.val == 200 assert fixed.s.val == b"ABC" -present = _dissect(OEROptionalField, "800101a00107") +present = _dissect(OEROptionalField, "8001010107") assert present.id.val == 1 @@ -779,7 +802,7 @@ True decoded = _dissect( OERRecord, "80" - "012aff026869a00107" + "012aff0268690107" "0103010101020103", ) _assert_record(decoded) @@ -915,7 +938,8 @@ True = oer choice with packet alternative pkt = OERPacketChoice(c=OERInnerSeq(x=3)) -assert raw(pkt) == b"\x30\x03" +# \x10: universal 16 (SEQUENCE), without the BER constructed bit +assert raw(pkt) == b"\x10\x03" decoded = _roundtrip(OERPacketChoice, pkt) @@ -931,6 +955,25 @@ assert decoded_int.c.val == 9 True += oer choice with tagged packet alternatives +# Reference (asn1tools) for +# Ch ::= SEQUENCE { c CHOICE { a1 A, a2 B } } +# A ::= SEQUENCE { i INTEGER }, B ::= SEQUENCE { b BOOLEAN } +# in an AUTOMATIC TAGS module: the alternative tag is the only one encoded. +assert raw(OERTaggedChoice(c=OERAltA(i=4))) == b"\x80\x01\x04" + +assert raw(OERTaggedChoice(c=OERAltB(b=False))) == b"\x81\x00" + +decoded = _roundtrip(OERTaggedChoice, OERTaggedChoice(c=OERAltA(i=4))) + +assert isinstance(decoded.c, OERAltA) and decoded.c.i.val == 4 + +decoded = _roundtrip(OERTaggedChoice, OERTaggedChoice(c=OERAltB(b=False))) + +assert isinstance(decoded.c, OERAltB) and decoded.c.b.val == 0 + +True + = oer dec ignores foreign codec kwargs # Shared field.codec_opts may include UPER keys after contrib.uper is loaded. x, remain = OERcodec_ENUMERATED.dec( @@ -1001,7 +1044,7 @@ assert raw(OERNoPreamble(a=1)) == bytes.fromhex("01") # A DEFAULT component takes a presence bit too, and is omitted when it holds # the default value. -from scapy.contrib.uper import ASN1F_DEFAULT +from scapy.asn1fields import ASN1F_DEFAULT class OERDefault(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index 9c8994bdcb8..28084e5da16 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -3019,3 +3019,43 @@ _raises( True += uper choice with tagged packet alternatives +class UPERAltA(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("i", 0), + ) + +class UPERAltB(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BOOLEAN("b", True), + ) + +class UPERTaggedChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_CHOICE( + "c", None, + ASN1F_PACKET("a1", None, UPERAltA, explicit_tag=0xA0), + ASN1F_PACKET("a2", None, UPERAltB, explicit_tag=0xA1), + ), + ) + +# Reference (asn1tools) for +# Ch ::= SEQUENCE { c CHOICE { a1 A, a2 B } } +# A ::= SEQUENCE { i INTEGER }, B ::= SEQUENCE { b BOOLEAN } +# The alternative is picked by the type of the value, tags are not encoded. +assert raw(UPERTaggedChoice(c=UPERAltA(i=4))) == b"\x00\x82\x00" + +assert raw(UPERTaggedChoice(c=UPERAltB(b=False))) == b"\x80" + +decoded = _roundtrip(UPERTaggedChoice, UPERTaggedChoice(c=UPERAltA(i=4))) + +assert isinstance(decoded.c, UPERAltA) and decoded.c.i.val == 4 + +decoded = _roundtrip(UPERTaggedChoice, UPERTaggedChoice(c=UPERAltB(b=False))) + +assert isinstance(decoded.c, UPERAltB) and decoded.c.b.val == 0 + +True diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 1cd942e6ab1..b0e7ae1609c 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -480,7 +480,7 @@ for cls, data_hex in [ ( OERRecord, "80" - "012aff026869a00107" + "012aff0268690107" "0103010101020103", ), ( @@ -563,3 +563,34 @@ assert ASN1_Codecs.PER._field_hooks is not None True += ber oer per default component +class _BerDefault(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("a", 1), + ASN1F_DEFAULT(ASN1F_INTEGER("b", 7), 7), + ) + +class _OerDefault(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("a", 1), + ASN1F_DEFAULT(ASN1F_INTEGER("b", 7), 7), + ) + +class _PerDefault(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("a", 1), + ASN1F_DEFAULT(ASN1F_INTEGER("b", 7), 7), + ) + +# A component holding its default value is not encoded, and comes back as the +# default when the encoding does not carry it. +for cls in (_BerDefault, _OerDefault, _PerDefault): + assert len(raw(cls(a=1, b=7))) < len(raw(cls(a=1, b=9))) + absent = _roundtrip(cls, cls(a=1, b=7)).b + assert getattr(absent, "val", absent) == 7 + assert _roundtrip(cls, cls(a=1, b=9)).b.val == 9 + +True From 22542470cdd95f4d041fdccb419face677b0d4c4 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 11 Aug 2026 20:00:57 +0200 Subject: [PATCH 14/46] uper: reject out of range constrained values, drop dead code A constrained INTEGER is written on the width of its range, so a value outside it cannot be expressed: 100 in an INTEGER (0..7) went out as a byte that reads back as 4, and -3 as one that reads back as 5. The same hole let a SEQUENCE OF with a SIZE(1..3) constraint encode an empty list as index -1. UPER_constrained_int_enc now refuses such a value, as the string and bit string encoders already do; an extensible type still takes its extension path before coming here. Building a SEQUENCE OF also had a branch of its own for an unset field, which wrote a length determinant of zero past the size constraint, where an empty list went through the constrained count. The two codecs each defined a BadTag decoding error that nothing raises, along with the except branch catching it, and OER kept a check_type_check_len that only the fields it hooks would call and an OER_id_dec merging the tag class into the tag number, the lossy pattern just removed from the choice path. The UPER encoder and decoder also carried a number_of_bytes and a consume_input with no caller. Neither OER nor PER puts the tag of a field on the wire, so ASN1Codec now defaults to identity tagging and only BER registers its own. The alternative lookup of a CHOICE, copied verbatim in both codecs, becomes ASN1F_CHOICE.alternative_index, and the scan for optional components becomes an ASN1F_SEQUENCE.optionals tuple built once. Eleven copies of the OER length check and the two UPER size checks each collapse into one helper, with the same messages, and the OER use_object_enc hook returned exactly what asn1fields does without it. Coverage of the three modules over the ASN.1 suites goes from 89% to 97% for OER and from 95% to 99% for UPER, the added tests covering the long form of an OER tag, the untyped codec fallbacks, a dissect of an empty encoding, a pre-encoded value, a choice with an unknown tag, an unknown index, a single alternative or packet class alternatives, and the two constraint fixes above. AI-Assisted: yes (Cursor) Co-authored-by: Cursor --- scapy/asn1/asn1.py | 17 +++- scapy/asn1fields.py | 30 +++++- scapy/contrib/oer.py | 196 +++++++------------------------------ scapy/contrib/uper.py | 108 ++++++-------------- test/contrib/oer.uts | 60 ++++++++++-- test/contrib/uper.uts | 149 ++++++++++++++++++++++++++++ test/scapy/layers/asn1.uts | 14 ++- 7 files changed, 328 insertions(+), 246 deletions(-) diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index 44e10cb630e..4d7dfbadbbd 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -121,11 +121,25 @@ class ASN1_BadTag_Decoding_Error(ASN1_Decoding_Error): pass +def _identity_tagging_enc(s, **kwargs): + # type: (bytes, **Any) -> bytes + return s + + +def _identity_tagging_dec(s, **kwargs): + # type: (bytes, **Any) -> Tuple[Optional[int], bytes] + return None, s + + class ASN1Codec(EnumElement): # Class-level default: EnumElement.__getattr__ forwards unknown attributes # to its int value, so a missing _field_hooks would raise (and swallow) an # AttributeError on every field operation. _field_hooks = None # type: Any + # Only BER puts the tag of a field on the wire; the other codecs keep + # these identity defaults. + _tagging_enc = staticmethod(_identity_tagging_enc) # type: Any + _tagging_dec = staticmethod(_identity_tagging_dec) # type: Any def register_stem(cls, stem): # type: (Type[BERcodec_Object[Any]]) -> None @@ -133,7 +147,8 @@ def register_stem(cls, stem): def register_tagging(cls, enc, dec): # type: (Any, Any) -> None - # Codec-level implicit/explicit tagging (BER/OER) or identity (UPER/PER). + # Only for the codecs that put the tag of a field on the wire (BER): + # the others keep the identity defaults below. cls._tagging_enc = enc cls._tagging_dec = dec diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index ab9f2aa1f08..83aeea98a75 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -140,8 +140,8 @@ def _apply_diff_tag(self, diff_tag): def _tagging_dec(self, pkt, s, **kwargs): # type: (ASN1_Packet, bytes, **Any) -> Tuple[Optional[int], bytes] - # Codec provides tagging_*; OER implements real tags, UPER/PER use - # identity helpers (no BER-style tagging). + # Codec provides tagging_*; only BER puts the tag of a field on the + # wire, the others keep the identity default of ASN1Codec. return pkt.ASN1_codec.tagging_dec(s, **kwargs) # type: ignore def _tagging_enc(self, pkt, s, **kwargs): @@ -518,6 +518,11 @@ def __init__(self, *seq, **kwargs): name, default, **kwargs ) self.seq = seq + # Codecs that describe presence out of band (OER/PER preambles) need + # the optional components in declaration order. + self.optionals = tuple( + f for f in seq if isinstance(f, ASN1F_optional) + ) # type: Tuple[ASN1F_optional, ...] self.islist = len(seq) > 1 def __repr__(self): @@ -782,6 +787,10 @@ class ASN1F_DEFAULT(ASN1F_optional): """ ASN.1 field holding a DEFAULT value: it is omitted from the encoding while it holds that value, and restored when the encoding does not carry it. + + As with OPTIONAL components, a BER encoding only tells the component apart + from the one that follows it by its tag, so the schema must give it a + distinct one. OER and PER describe presence in the preamble instead. """ def __init__(self, field, default): # type: (ASN1F_field[Any, Any], Any) -> None @@ -876,6 +885,23 @@ def choice_order(self): # type: () -> List[int] return list(self.choices.keys()) + def alternative_index(self, x): + # type: (Any) -> Optional[int] + """Position in choice_order of the alternative that carries x.""" + for index, choice in enumerate(self.choices.values()): + if isinstance(choice, type): + if hasattr(choice, "ASN1_root"): + # ASN1_Packet subclass + if isinstance(x, choice): + return index + elif isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + # ASN1F_field subclass + return index + elif isinstance(x, choice.cls): + # ASN1F_PACKET instance, holding a tagged packet + return index + return None + @property def choice_list(self): # type: () -> List[_CHOICE_T] diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py index 346ae6e7fb1..4a3cfdf3f8c 100644 --- a/scapy/contrib/oer.py +++ b/scapy/contrib/oer.py @@ -30,9 +30,6 @@ from scapy.utils import binrepr, inet_aton, inet_ntoa from scapy.asn1.ber import BER_num_dec, BER_num_enc from scapy.asn1.asn1 import ( - ASN1Tag, - ASN1_BADTAG, - ASN1_BadTag_Decoding_Error, ASN1_Class, ASN1_Class_UNIVERSAL, ASN1_Codecs, @@ -113,11 +110,6 @@ def __str__(self): return s -class OER_BadTag_Decoding_Error(OER_Decoding_Error, - ASN1_BadTag_Decoding_Error): - pass - - # OER tag classes (bits 8-7 of the first identifier octet) OER_CLASS_UNIVERSAL = 0x00 OER_CLASS_APPLICATION = 0x40 @@ -125,6 +117,18 @@ class OER_BadTag_Decoding_Error(OER_Decoding_Error, OER_CLASS_PRIVATE = 0xc0 +def _OER_check_len(name, s, number_of_bytes, offset=0): + # type: (str, bytes, int, int) -> None + """Raise unless s carries number_of_bytes octets past its first offset.""" + available = len(s) - offset + if available < number_of_bytes: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % + (name, available, number_of_bytes), + remaining=s + ) + + def OER_len_enc(ll): # type: (int) -> bytes if ll < 128: @@ -149,12 +153,7 @@ def OER_len_dec(s): if not tmp_len & 0x80: return tmp_len, s[1:] tmp_len &= 0x7f - if len(s) <= tmp_len: - raise OER_Decoding_Error( - "OER_len_dec: Got %i bytes while expecting %i" % - (len(s) - 1, tmp_len), - remaining=s - ) + _OER_check_len("OER_len_dec", s, tmp_len, offset=1) ll = 0 for c in s[1:tmp_len + 1]: ll <<= 8 @@ -176,12 +175,7 @@ def OER_signed_integer_enc(i): def OER_signed_integer_dec(s): # type: (bytes) -> Tuple[int, bytes] number_of_bytes, s = OER_len_dec(s) - if len(s) < number_of_bytes: - raise OER_Decoding_Error( - "OER_signed_integer_dec: Got %i bytes while expecting %i" % - (len(s), number_of_bytes), - remaining=s - ) + _OER_check_len("OER_signed_integer_dec", s, number_of_bytes) if number_of_bytes == 0: raise OER_Decoding_Error( "OER_signed_integer_dec: got an empty length determinant", @@ -209,12 +203,7 @@ def OER_unsigned_integer_enc(i): def OER_unsigned_integer_dec(s): # type: (bytes) -> Tuple[int, bytes] number_of_bytes, s = OER_len_dec(s) - if len(s) < number_of_bytes: - raise OER_Decoding_Error( - "OER_unsigned_integer_dec: Got %i bytes while expecting %i" % - (len(s), number_of_bytes), - remaining=s - ) + _OER_check_len("OER_unsigned_integer_dec", s, number_of_bytes) value = int.from_bytes(s[:number_of_bytes], "big") return value, s[number_of_bytes:] @@ -243,12 +232,7 @@ def OER_fixed_integer_enc(i, length, signed=True): def OER_fixed_integer_dec(s, length, signed=True): # type: (bytes, int, bool) -> Tuple[int, bytes] - if len(s) < length: - raise OER_Decoding_Error( - "OER_fixed_integer_dec: Got %i bytes while expecting %i" % - (len(s), length), - remaining=s - ) + _OER_check_len("OER_fixed_integer_dec", s, length) fmt = _OER_FIXED_FORMATS[signed] try: return struct.unpack(fmt[length], s[:length])[0], s[length:] @@ -276,12 +260,7 @@ def OER_enumerated_dec(s): if not (first & 0x80): return first, s[1:] length = first & 0x7f - if len(s) < length + 1: - raise OER_Decoding_Error( - "OER_enumerated_dec: Got %i bytes while expecting %i" % - (len(s) - 1, length), - remaining=s - ) + _OER_check_len("OER_enumerated_dec", s, length, offset=1) value = int.from_bytes(s[1:length + 1], "big", signed=True) return value, s[length + 1:] @@ -309,12 +288,7 @@ def OER_preamble_dec(s, extensible, number_of_optionals): if number_of_bits == 0: return [], s number_of_bytes = (number_of_bits + 7) // 8 - if len(s) < number_of_bytes: - raise OER_Decoding_Error( - "OER_preamble_dec: Got %i bytes while expecting %i" % - (len(s), number_of_bytes), - remaining=s - ) + _OER_check_len("OER_preamble_dec", s, number_of_bytes) value = int.from_bytes(s[:number_of_bytes], "big") bits = [ bool((value >> (8 * number_of_bytes - 1 - i)) & 1) @@ -369,12 +343,6 @@ def OER_tag_dec(s): return tag_class, tag_number, s[i:] -def OER_id_dec(s): - # type: (bytes) -> Tuple[int, bytes] - tag_class, tag_number, remainder = OER_tag_dec(s) - return tag_class | tag_number, remainder - - def _OER_tag_parts(identifier): # type: (int) -> Tuple[int, int] # ASN1F_* fields describe tags as BER identifier octets: class in the top @@ -384,25 +352,6 @@ def _OER_tag_parts(identifier): return identifier & 0xc0, identifier & 0x1f -def OER_tagging_dec(s, # type: bytes - hidden_tag=None, # type: Optional[int | ASN1Tag] - implicit_tag=None, # type: Optional[int] - explicit_tag=None, # type: Optional[int] - safe=False, # type: Optional[bool] - _fname="", # type: str - ): - # type: (...) -> Tuple[Optional[int], bytes] - # X.696 encodes no tag for a component, whatever the tagging environment - # of the module: the only tag on the wire is the one of a chosen CHOICE - # alternative, which _OER_FieldHooks handles. - return None, s - - -def OER_tagging_enc(s, implicit_tag=None, explicit_tag=None): - # type: (bytes, Optional[int], Optional[int]) -> bytes - return s - - class OERcodec_metaclass(type): def __new__(cls, name, # type: str @@ -440,12 +389,6 @@ def check_string(cls, s): (cls.__name__, cls.tag), remaining=s ) - @classmethod - def check_type_check_len(cls, s): - # type: (bytes) -> Tuple[int, bytes, bytes] - cls.check_string(s) - return len(s), s, b"" - @classmethod def do_dec(cls, s, # type: bytes @@ -477,11 +420,6 @@ def dec(cls, return cls.do_dec(s, context, safe, size_len, oer_unsigned) try: return cls.do_dec(s, context, safe, size_len, oer_unsigned) - except OER_BadTag_Decoding_Error as e: - o, remain = OERcodec_Object.dec( - e.remaining, context, safe, size_len, oer_unsigned - ) - return ASN1_BADTAG(o), remain except OER_Decoding_Error as e: return ASN1_DECODING_ERROR(s, exc=e), b"" except ASN1_Error as e: @@ -513,8 +451,11 @@ def enc(cls, s, size_len=0, **_kwargs): raise TypeError("Trying to encode an invalid value !") +# No register_tagging(): X.696 encodes no tag for a component, whatever the +# tagging environment of the module, so the identity default of ASN1Codec is +# what OER needs. The only tag on the wire is the one of a chosen CHOICE +# alternative, which _OER_FieldHooks writes itself. ASN1_Codecs.OER.register_stem(OERcodec_Object) -ASN1_Codecs.OER.register_tagging(OER_tagging_enc, OER_tagging_dec) ########################## @@ -603,12 +544,7 @@ def do_dec(cls, # type: (...) -> Tuple[ASN1_Object[str], bytes] if size_len: number_of_bytes = (size_len + 7) // 8 - if len(s) < number_of_bytes: - raise OER_Decoding_Error( - "%s: Got %i bytes while expecting %i" % - (cls.__name__, len(s), number_of_bytes), - remaining=s - ) + _OER_check_len(cls.__name__, s, number_of_bytes) return ( cls.tag.asn1_object( _oer_bytes_to_bitstr(s[:number_of_bytes])[:size_len] @@ -618,11 +554,7 @@ def do_dec(cls, length, s = OER_len_dec(s) if length == 0: return cls.tag.asn1_object(""), s - if len(s) < length: - raise OER_Decoding_Error( - "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), length), - remaining=s - ) + _OER_check_len(cls.__name__, s, length) unused_bits = orb(s[0]) if safe and unused_bits > 7: raise OER_Decoding_Error( @@ -680,19 +612,10 @@ def do_dec(cls, ): # type: (...) -> Tuple[ASN1_Object[Any], bytes] if size_len: - if len(s) < size_len: - raise OER_Decoding_Error( - "%s: Got %i bytes while expecting %i" % - (cls.__name__, len(s), size_len), - remaining=s - ) + _OER_check_len(cls.__name__, s, size_len) return cls.tag.asn1_object(s[:size_len]), s[size_len:] length, s = OER_len_dec(s) - if len(s) < length: - raise OER_Decoding_Error( - "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), length), - remaining=s - ) + _OER_check_len(cls.__name__, s, length) return cls.tag.asn1_object(s[:length]), s[length:] @@ -743,11 +666,7 @@ def do_dec(cls, ): # type: (...) -> Tuple[ASN1_Object[bytes], bytes] length, s = OER_len_dec(s) - if len(s) < length: - raise OER_Decoding_Error( - "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), length), - remaining=s - ) + _OER_check_len(cls.__name__, s, length) content, t = s[:length], s[length:] lst = [] while content: @@ -922,18 +841,6 @@ def _field_extensible(field): class _OER_FieldHooks(object): """Compound ASN1F_* helpers for OER (kept out of asn1fields.py).""" - @staticmethod - def use_object_enc(field, pkt, item): - # type: (Any, Any, Any) -> bool - # Constraints (e.g. oer_unsigned) must go through codec.enc(**kwargs). - return field.size_len is None and not field.codec_opts - - @staticmethod - def _optionals(field): - # type: (Any) -> Tuple[Any, ...] - from scapy.asn1fields import ASN1F_optional - return tuple(f for f in field.seq if isinstance(f, ASN1F_optional)) - @staticmethod def sequence_m2i(field, pkt, s): # type: (Any, Any, bytes) -> Tuple[Any, bytes] @@ -945,7 +852,7 @@ def sequence_m2i(field, pkt, s): return [], s presence, s = OER_preamble_dec( s, _field_extensible(field), - len(_OER_FieldHooks._optionals(field)), + len(field.optionals), ) opt_index = 0 for obj in field.seq: @@ -968,8 +875,8 @@ def sequence_m2i(field, pkt, s): @staticmethod def sequence_build(field, pkt): # type: (Any, Any) -> bytes - from scapy.asn1fields import ASN1F_optional - optionals = _OER_FieldHooks._optionals(field) + from scapy.asn1fields import ASN1F_field, ASN1F_optional + optionals = field.optionals s = OER_preamble_enc( _field_extensible(field), [not opt.is_empty(pkt) for opt in optionals], @@ -978,7 +885,8 @@ def sequence_build(field, pkt): if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): continue s += obj.build(pkt) - return ASN1F_field_i2m(field, pkt, s) + # Through ASN1F_field, as ASN1F_SEQUENCE.i2m is the hook above + return ASN1F_field.i2m(field, pkt, s) @staticmethod def sequence_of_m2i(field, pkt, s): @@ -1051,42 +959,14 @@ def choice_i2m(field, pkt, x): s = x.enc(pkt.ASN1_codec) else: s = bytes(x) - alt_tag = _choice_tag_for(field, x) - if alt_tag is not None: - tag_class, tag_number = _OER_tag_parts(alt_tag) + index = field.alternative_index(x) + if index is not None: + # X.696 20.2: the chosen alternative is prefixed with its tag + tag_class, tag_number = _OER_tag_parts( + field.choice_order[index] + ) s = OER_tag_enc(tag_number, tag_class) + s return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) -def _choice_index_for(field, x): - # type: (Any, Any) -> Optional[int] - from scapy.asn1.asn1 import ASN1_Object - for index, choice in enumerate(field.choice_list): - if isinstance(choice, type): - if hasattr(choice, "ASN1_root"): - if isinstance(x, choice): - return index - elif hasattr(choice, "ASN1_tag"): - if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: - return index - elif getattr(choice, "cls", None) is not None: - # ASN1F_PACKET instance: the alternative is a tagged packet. - if isinstance(x, choice.cls): - return index - return None - - -def _choice_tag_for(field, x): - # type: (Any, Any) -> Optional[int] - index = _choice_index_for(field, x) - return None if index is None else field.choice_order[index] - - -def ASN1F_field_i2m(field, pkt, s): - # type: (Any, Any, bytes) -> bytes - # Call ASN1F_field.i2m without compound overrides. - from scapy.asn1fields import ASN1F_field - return ASN1F_field.i2m(field, pkt, s) - - ASN1_Codecs.OER.register_field_hooks(_OER_FieldHooks) diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index de5605e386e..3750e8fa22c 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -33,8 +33,6 @@ from scapy.utils import binrepr, inet_aton, inet_ntoa from scapy.asn1.ber import BER_num_dec, BER_num_enc from scapy.asn1.asn1 import ( - ASN1_BADTAG, - ASN1_BadTag_Decoding_Error, ASN1_Class, ASN1_Class_UNIVERSAL, ASN1_Codecs, @@ -113,11 +111,6 @@ def __str__(self): return s -class UPER_BadTag_Decoding_Error(UPER_Decoding_Error, - ASN1_BadTag_Decoding_Error): - pass - - def UPER_bits_for_range(size): # type: (int) -> int if size <= 0: @@ -148,10 +141,6 @@ def __init__(self): self.chunks_number_of_bits = 0 self.chunks = [] # type: List[List[int]] - def number_of_bytes(self): - # type: () -> int - return (self.chunks_number_of_bits + self.number_of_bits + 7) // 8 - def append_bit(self, bit): # type: (int) -> None self.number_of_bits += 1 @@ -372,13 +361,16 @@ def read_unconstrained_whole_number(self): return enc - (1 << (8 * number_of_bytes)) return enc - def consume_input(self): - # type: () -> None - self.number_of_bits = 0 - def UPER_constrained_int_enc(enc, value, minimum, maximum): # type: (UPER_Encoder, int, int, int) -> None + # X.691 13.2.2: the field is sized after the range, so a value outside it + # cannot be expressed. Callers handle extensibility before coming here. + if not minimum <= value <= maximum: + raise UPER_Encoding_Error( + "UPER_constrained_int_enc: got %i while expecting %i..%i" % + (value, minimum, maximum) + ) enc.append_non_negative_binary_integer( value - minimum, UPER_bits_for_range(maximum - minimum) ) @@ -392,18 +384,25 @@ def UPER_constrained_int_dec(dec, minimum, maximum): return value + minimum +def _uper_check_size(name, unit, count, minimum, maximum): + # type: (str, str, int, int, int) -> None + # The determinant is sized after the constraint, so a value that violates + # it cannot be expressed: refuse rather than emit something the peer reads + # as a different length. + if not minimum <= count <= maximum: + raise UPER_Encoding_Error( + "%s: got %i %s while expecting %s" % + (name, count, unit, minimum if minimum == maximum + else "%i..%i" % (minimum, maximum)) + ) + + def UPER_octet_string_enc(enc, data, minimum=None, maximum=None): # type: (UPER_Encoder, bytes, Optional[int], Optional[int]) -> None if minimum is not None and maximum is not None: - if not minimum <= len(data) <= maximum: - # The determinant is sized after the constraint, so a value that - # violates it cannot be expressed: refuse rather than emit - # something the peer reads as a different length. - raise UPER_Encoding_Error( - "UPER_octet_string_enc: got %i octets while expecting %s" % - (len(data), minimum if minimum == maximum - else "%i..%i" % (minimum, maximum)) - ) + _uper_check_size( + "UPER_octet_string_enc", "octets", len(data), minimum, maximum, + ) if minimum != maximum: enc.append_non_negative_binary_integer( len(data) - minimum, @@ -520,11 +519,6 @@ def dec(cls, s, context=None, safe=False, **kwargs): return cls.do_dec(s, context, safe, **kwargs) try: return cls.do_dec(s, context, safe, **kwargs) - except UPER_BadTag_Decoding_Error as e: - o, remain = UPERcodec_Object.dec( - e.remaining, context, safe, **kwargs - ) - return ASN1_BADTAG(o), remain except (UPER_Decoding_Error, ASN1_Error) as e: return ASN1_DECODING_ERROR(s, exc=e), b"" @@ -534,19 +528,9 @@ def safedec(cls, s, context=None, **kwargs): return cls.dec(s, context, safe=True, **kwargs) -def UPER_tagging_enc(s, **kwargs): - # type: (bytes, **Any) -> bytes - # UPER has no BER-style TLV tagging. - return s - - -def UPER_tagging_dec(s, **kwargs): - # type: (bytes, **Any) -> Tuple[Optional[int], bytes] - return None, s - - +# No register_tagging(): PER encodes no tag at all, so the identity default +# of ASN1Codec is what UPER needs. ASN1_Codecs.PER.register_stem(UPERcodec_Object) -ASN1_Codecs.PER.register_tagging(UPER_tagging_enc, UPER_tagging_dec) ######################### @@ -675,12 +659,7 @@ def encode_into(cls, s, nbits = _uper_bit_string_parts(_s) minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) if minimum is not None and maximum is not None: - if not minimum <= nbits <= maximum: - raise UPER_Encoding_Error( - "UPERcodec_BIT_STRING: got %i bits while expecting %s" % - (nbits, minimum if minimum == maximum - else "%i..%i" % (minimum, maximum)) - ) + _uper_check_size(cls.__name__, "bits", nbits, minimum, maximum) if minimum != maximum: enc.append_non_negative_binary_integer( nbits - minimum, UPER_bits_for_range(maximum - minimum) @@ -1091,12 +1070,6 @@ def sequence_build(field, pkt): _UPER_FieldHooks.sequence_encode_into(field, enc, pkt) return ASN1F_field.i2m(field, pkt, enc.as_bytes()) - @staticmethod - def _optionals(field): - # type: (Any) -> Tuple[Any, ...] - from scapy.asn1fields import ASN1F_optional - return tuple(f for f in field.seq if isinstance(f, ASN1F_optional)) - @staticmethod def sequence_dissect_from_decoder(field, pkt, dec): # type: (Any, Any, Any) -> None @@ -1106,7 +1079,7 @@ def sequence_dissect_from_decoder(field, pkt, dec): raise UPER_Decoding_Error( "ASN1F_SEQUENCE: extension additions are not supported" ) - optionals = _UPER_FieldHooks._optionals(field) + optionals = field.optionals presence = [dec.read_bit() for _ in optionals] opt_idx = 0 for obj in field.seq: @@ -1127,7 +1100,7 @@ def sequence_encode_into(field, enc, pkt, value=None): from scapy.asn1fields import ASN1F_optional if _field_extensible(field): enc.append_bit(0) - for opt in _UPER_FieldHooks._optionals(field): + for opt in field.optionals: enc.append_bit(0 if opt.is_empty(pkt) else 1) for obj in field.seq: if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): @@ -1148,11 +1121,8 @@ def sequence_of_build(field, pkt): val = getattr(pkt, field.name) if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: s = val # type: Any - elif val is None: - enc = UPER_Encoder() - enc.append_length_determinant(0) - s = enc.as_bytes() else: + # An unset field counts as an empty one, size constraint included enc = UPER_Encoder() _UPER_FieldHooks.sequence_of_encode_into(field, enc, pkt, val) s = enc.as_bytes() @@ -1259,7 +1229,7 @@ def choice_encode_into(field, enc, pkt, value=None): from scapy.asn1.asn1 import ASN1_Error if value is None: value = getattr(pkt, field.name) - index = _choice_index_for(field, value) + index = field.alternative_index(value) if index is None: raise ASN1_Error( "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % @@ -1315,24 +1285,6 @@ def packet_encode_into(field, enc, pkt, value=None): value.ASN1_root.encode_into(enc, value) -def _choice_index_for(field, x): - # type: (Any, Any) -> Optional[int] - from scapy.asn1.asn1 import ASN1_Object - for index, choice in enumerate(field.choice_list): - if isinstance(choice, type): - if hasattr(choice, "ASN1_root"): - if isinstance(x, choice): - return index - elif hasattr(choice, "ASN1_tag"): - if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: - return index - elif getattr(choice, "cls", None) is not None: - # ASN1F_PACKET instance: the alternative is a tagged packet. - if isinstance(x, choice.cls): - return index - return None - - def _uper_count_enc(field, enc, count, append_items): # type: (Any, Any, int, Callable[[int, int], None]) -> None # The count of a SEQUENCE OF is a constrained whole number when the field diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index 778482884fb..2a449c79020 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -459,10 +459,20 @@ x.val == -2 and r == b"" x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3), size_len=3) x.val == b"\x12\x34\x56" and r == b"" = OER does not encode the tag of a component -OER_tagging_enc(b"\x05", explicit_tag=0x81) == b"\x05" and OER_tagging_dec(b"\x05", explicit_tag=0x81) == (None, b"\x05") -= OER choice id decode -tag, r = OER_id_dec(b"\x81\x01") -tag == 0x81 and r == b"\x01" +ASN1_Codecs.OER.tagging_enc(b"\x05", explicit_tag=0x81) == b"\x05" and ASN1_Codecs.OER.tagging_dec(b"\x05", explicit_tag=0x81) == (None, b"\x05") += OER tag long form +# X.696 8.7.2.2: a tag number of 63 or more spills into continuation octets +assert OER_tag_enc(100, OER_CLASS_CONTEXT) == b"\xbf\x64" + +assert OER_tag_dec(b"\xbf\x64\x01") == (OER_CLASS_CONTEXT, 100, b"\x01") + +assert OER_tag_dec(OER_tag_enc(16384, OER_CLASS_PRIVATE)) == (OER_CLASS_PRIVATE, 16384, b"") + +_raises(OER_Decoding_Error, lambda: OER_tag_dec(b"\xbf\x81")) + +_raises(OER_Decoding_Error, lambda: OER_tag_dec(b"")) + +True + ASN.1 OER packets, interop and fuzz = oer field explicit tag @@ -870,11 +880,11 @@ assert ASN1_Codecs.OER._field_hooks is not None assert hasattr(ASN1_Codecs.OER._field_hooks, "sequence_m2i") -assert hasattr(ASN1_Codecs.OER._field_hooks, "use_object_enc") +assert hasattr(ASN1_Codecs.OER._field_hooks, "choice_i2m") True -= oer use_object_enc via hooks += oer use_object_enc fld = OERUnsignedField.ASN1_root assert fld.codec_opts["oer_unsigned"] is True @@ -1207,3 +1217,41 @@ True _raises(OER_Decoding_Error, lambda: OER_signed_integer_dec(b"\x00")) True + += oer choice rejects an unknown alternative tag +_raises(ASN1_Error, lambda: OERPacketChoice(b"\x40\x00")) + +# An unset CHOICE encodes to nothing, as the field is then absent +assert raw(OERPacketChoice(c=None)) == b"" + +True + += oer untyped codec falls back on string and integer +assert OERcodec_Object.enc(b"hi") == b"\x02hi" + +assert OERcodec_Object.enc(5) == b"\x01\x05" + +_raises(TypeError, lambda: OERcodec_Object.enc(object())) + +# Without a schema there is nothing to tell one type from another +_raises(OER_Decoding_Error, lambda: OERcodec_Object.dec(b"\x01")) + +assert OERcodec_OID.enc(b"") == b"\x00" + +True + += oer sequence dissect of an empty encoding +# Nothing to read leaves every component unset, hence holding its default +decoded = _dissect(OERRecord, "") + +assert decoded.id.val == 0 and decoded.label.val == "" and decoded.values == [] + +True + += oer sequence of a pre-encoded value +# A RAW object is written as-is, without the quantity determinant +raw_items = ASN1_Class_UNIVERSAL.RAW.asn1_object(b"\x01\x01\x07") + +assert raw(OERSequenceOfIntegers(values=raw_items)) == b"\x01\x01\x07" + +True diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index 28084e5da16..d8ab2fb4b8d 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -3059,3 +3059,152 @@ decoded = _roundtrip(UPERTaggedChoice, UPERTaggedChoice(c=UPERAltB(b=False))) assert isinstance(decoded.c, UPERAltB) and decoded.c.b.val == 0 True + += uper choice with packet class alternatives +class UPERClassChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE("c", None, UPERAltA, ASN1F_INTEGER) + +pkt = UPERClassChoice(c=UPERAltA(i=5)) + +# index bit 0, then the sequence: an unconstrained integer of one octet +assert raw(pkt) == b"\x00\x82\x80" + +decoded = _roundtrip(UPERClassChoice, pkt) + +assert isinstance(decoded.c, UPERAltA) and decoded.c.i.val == 5 + +decoded = _roundtrip(UPERClassChoice, UPERClassChoice(c=ASN1_INTEGER(3))) + +assert decoded.c.val == 3 + +# An unset CHOICE encodes to nothing, as the field is then absent +assert raw(UPERClassChoice(c=None)) == b"" + +True + += uper enumerated bounds +_raises(UPER_Encoding_Error, lambda: UPER_enumerated_enc(UPER_Encoder(), 0, [])) + +_raises(UPER_Decoding_Error, lambda: UPER_enumerated_dec(UPER_Decoder(b"\x00"), [])) + +# Three values are indexed on two bits, which can carry an index they do not +# define +_raises(UPER_Decoding_Error, lambda: UPER_enumerated_dec(UPER_Decoder(b"\xc0"), [0, 1, 2])) + +assert UPER_enumerated_dec(UPER_Decoder(b"\x40"), [0, 1, 2]) == 1 + +True + += uper untyped codec falls back on string and integer +enc = UPER_Encoder() + +UPERcodec_Object.encode_into(enc, b"hi") + +assert enc.as_bytes() == b"\x02hi" + +enc = UPER_Encoder() + +UPERcodec_Object.encode_into(enc, 5) + +assert enc.as_bytes() == b"\x01\x05" + +_raises(UPER_Encoding_Error, lambda: UPERcodec_Object.encode_into(UPER_Encoder(), object())) + +_raises(UPER_Decoding_Error, lambda: UPERcodec_Object.dec_from_decoder(UPER_Decoder(b"\x01"))) + +True + += uper choice with a single alternative +class UPEROneAlt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE("c", None, ASN1F_INTEGER) + +# X.691 23.5: one alternative leaves nothing to choose, so no index is encoded +assert raw(UPEROneAlt(c=ASN1_INTEGER(4))) == b"\x01\x04" + +assert _roundtrip(UPEROneAlt, UPEROneAlt(c=ASN1_INTEGER(4))).c.val == 4 + +class UPERThreeAlt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE("c", None, ASN1F_INTEGER, ASN1F_STRING, ASN1F_BOOLEAN) + +# Three alternatives are indexed on two bits, which can carry a fourth index +_raises(ASN1_Error, lambda: UPERThreeAlt(b"\xc0")) + +True + += uper sequence of an unset field +class UPERUnsetSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER("x", 0, uper_min=0, uper_max=255)), + ) + +assert raw(UPERUnsetSeqOf(values=None)) == b"\x00" + +assert raw(UPERUnsetSeqOf(values=[])) == b"\x00" + +True + += uper field rejects a value of another type +class UPERIntOnly(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0), + ) + +_raises(ASN1_Error, lambda: raw(UPERIntOnly(n=ASN1_STRING(b"x")))) + +enc = UPER_Encoder() + +UPERcodec_OID.encode_into(enc, b"") + +assert enc.as_bytes() == b"\x00" + +True + += uper constrained values honour their range +class UPERSmallInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7), + ) + +assert raw(UPERSmallInt(n=5)) == b"\xa0" + +# The value is written on the width of the range, so one outside it would be +# read back as another value +_raises(UPER_Encoding_Error, lambda: raw(UPERSmallInt(n=100))) + +_raises(UPER_Encoding_Error, lambda: raw(UPERSmallInt(n=-3))) + +class UPERSmallExtInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7, uper_extensible=True), + ) + +# An extensible range does accept it, as an extension addition +assert _roundtrip(UPERSmallExtInt, UPERSmallExtInt(n=100)).n.val == 100 + +True + += uper sequence of honours its size constraint +class UPERSizedSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF( + "values", [], ASN1F_INTEGER("x", 0, uper_min=0, uper_max=255), + uper_min=1, uper_max=3, + ), + ) + +assert raw(UPERSizedSeqOf(values=[ASN1_INTEGER(7)])) == b"\x01\xc0" + +# An unset field is an empty one, which the constraint rules out here +_raises(UPER_Encoding_Error, lambda: raw(UPERSizedSeqOf(values=[]))) + +_raises(UPER_Encoding_Error, lambda: raw(UPERSizedSeqOf(values=None))) + +True diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index b0e7ae1609c..d40acd479dc 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -586,11 +586,23 @@ class _PerDefault(ASN1_Packet): ) # A component holding its default value is not encoded, and comes back as the -# default when the encoding does not carry it. +# default when the encoding does not carry it. An unset component counts as +# holding it, and the default may be given as an ASN.1 object. for cls in (_BerDefault, _OerDefault, _PerDefault): assert len(raw(cls(a=1, b=7))) < len(raw(cls(a=1, b=9))) absent = _roundtrip(cls, cls(a=1, b=7)).b assert getattr(absent, "val", absent) == 7 assert _roundtrip(cls, cls(a=1, b=9)).b.val == 9 + assert raw(cls(a=1, b=None)) == raw(cls(a=1, b=7)) + +class _AsnObjectDefault(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_DEFAULT(ASN1F_INTEGER("a", 7), ASN1_INTEGER(7)), + ) + +assert raw(_AsnObjectDefault(a=ASN1_INTEGER(7))) == raw(_AsnObjectDefault(a=7)) + +assert raw(_AsnObjectDefault(a=7)) == b"\x00" True From 08c960aefb3e7ef836c01eb7c4e21493261290c9 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 11 Aug 2026 20:12:19 +0200 Subject: [PATCH 15/46] asn1: let BER hook the tagging of a field, drop the codec-level one Tagging had a registration mechanism of its own, next to the field hooks, and every codec paid for it: ASN1Codec carried an identity tagging_enc and tagging_dec so that OER and PER, which encode no tag at all, would not have to register anything. Tagging is just another field operation a codec does its own way, so BER now registers it among its field hooks, where a missing entry already means the default behaviour, and asn1fields leaves the encoding alone when no codec hooks it. register_tagging, tagging_enc and tagging_dec go away with it, as does unregister_field_hooks, which nothing called. Dissecting and building a BER sequence of tagged fields takes the same time as before. AI-Assisted: yes (Cursor) Co-authored-by: Cursor --- scapy/asn1/asn1.py | 44 +++------------------------------------ scapy/asn1/ber.py | 9 +++++++- scapy/asn1fields.py | 14 +++++++++---- scapy/contrib/oer.py | 8 +++---- scapy/contrib/uper.py | 3 +-- test/contrib/oer.uts | 15 ++++++++++++- test/scapy/layers/ber.uts | 44 ++++++++++++++++----------------------- 7 files changed, 58 insertions(+), 79 deletions(-) diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index 4d7dfbadbbd..5ffb5dfdad6 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -121,69 +121,31 @@ class ASN1_BadTag_Decoding_Error(ASN1_Decoding_Error): pass -def _identity_tagging_enc(s, **kwargs): - # type: (bytes, **Any) -> bytes - return s - - -def _identity_tagging_dec(s, **kwargs): - # type: (bytes, **Any) -> Tuple[Optional[int], bytes] - return None, s - - class ASN1Codec(EnumElement): # Class-level default: EnumElement.__getattr__ forwards unknown attributes # to its int value, so a missing _field_hooks would raise (and swallow) an # AttributeError on every field operation. _field_hooks = None # type: Any - # Only BER puts the tag of a field on the wire; the other codecs keep - # these identity defaults. - _tagging_enc = staticmethod(_identity_tagging_enc) # type: Any - _tagging_dec = staticmethod(_identity_tagging_dec) # type: Any def register_stem(cls, stem): # type: (Type[BERcodec_Object[Any]]) -> None cls._stem = stem - def register_tagging(cls, enc, dec): - # type: (Any, Any) -> None - # Only for the codecs that put the tag of a field on the wire (BER): - # the others keep the identity defaults below. - cls._tagging_enc = enc - cls._tagging_dec = dec - def register_field_hooks(cls, hooks): # type: (Any) -> None - # Optional compound-field helpers (SEQUENCE/CHOICE/…) for contrib codecs. + # Field operations a codec does its own way: the tagging of a field + # (BER) and the compound fields (SEQUENCE/CHOICE/… in OER and PER). cls._field_hooks = hooks - def unregister_field_hooks(cls): - # type: () -> Any - # Returns the previous hooks, so that callers can restore them. - hooks = cls._field_hooks - try: - del cls._field_hooks - except AttributeError: - pass - return hooks - def field_hook(cls, name): # type: (str) -> Any # Hooks are optional and may be partial: missing entries mean that - # asn1fields keeps its default (BER-style) implementation. + # asn1fields keeps its default implementation. hooks = cls._field_hooks if hooks is None: return None return getattr(hooks, name, None) - def tagging_enc(cls, s, **kwargs): - # type: (bytes, **Any) -> bytes - return cls._tagging_enc(s, **kwargs) # type: ignore - - def tagging_dec(cls, s, **kwargs): - # type: (bytes, **Any) -> Tuple[Optional[int], bytes] - return cls._tagging_dec(s, **kwargs) # type: ignore - def dec(cls, s, context=None, _depth=0): # type: (bytes, Optional[Type[ASN1_Class]], int) -> ASN1_Object[Any] return cls._stem.dec(s, context=context, _depth=_depth) # type: ignore diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index 2f675964992..e5467d6f856 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -423,8 +423,15 @@ def enc(cls, s, size_len=0, **_kwargs): raise TypeError("Trying to encode an invalid value !") +class _BER_FieldHooks(object): + """ASN1F_* helpers for BER, the one codec that tags a field on the wire.""" + + tagging_enc = staticmethod(BER_tagging_enc) + tagging_dec = staticmethod(BER_tagging_dec) + + ASN1_Codecs.BER.register_stem(BERcodec_Object) -ASN1_Codecs.BER.register_tagging(BER_tagging_enc, BER_tagging_dec) +ASN1_Codecs.BER.register_field_hooks(_BER_FieldHooks) ########################## diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 83aeea98a75..5b46641aeb7 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -140,13 +140,19 @@ def _apply_diff_tag(self, diff_tag): def _tagging_dec(self, pkt, s, **kwargs): # type: (ASN1_Packet, bytes, **Any) -> Tuple[Optional[int], bytes] - # Codec provides tagging_*; only BER puts the tag of a field on the - # wire, the others keep the identity default of ASN1Codec. - return pkt.ASN1_codec.tagging_dec(s, **kwargs) # type: ignore + # Only BER puts the tag of a field on the wire: a codec that does not + # hook the tagging leaves the encoding alone. + hook = _field_hook(pkt, "tagging_dec") + if hook is None: + return None, s + return cast(Tuple[Optional[int], bytes], hook(s, **kwargs)) def _tagging_enc(self, pkt, s, **kwargs): # type: (ASN1_Packet, bytes, **Any) -> bytes - return pkt.ASN1_codec.tagging_enc(s, **kwargs) # type: ignore + hook = _field_hook(pkt, "tagging_enc") + if hook is None: + return s + return cast(bytes, hook(s, **kwargs)) def _apply_tagging_dec(self, s, pkt, hidden_tag=None, **kwargs): # type: (bytes, ASN1_Packet, Optional[Any], **Any) -> bytes diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py index 4a3cfdf3f8c..148905b5772 100644 --- a/scapy/contrib/oer.py +++ b/scapy/contrib/oer.py @@ -451,10 +451,10 @@ def enc(cls, s, size_len=0, **_kwargs): raise TypeError("Trying to encode an invalid value !") -# No register_tagging(): X.696 encodes no tag for a component, whatever the -# tagging environment of the module, so the identity default of ASN1Codec is -# what OER needs. The only tag on the wire is the one of a chosen CHOICE -# alternative, which _OER_FieldHooks writes itself. +# No tagging hook: X.696 encodes no tag for a component, whatever the tagging +# environment of the module, so a field is left alone. The only tag on the +# wire is the one of a chosen CHOICE alternative, which _OER_FieldHooks writes +# itself. ASN1_Codecs.OER.register_stem(OERcodec_Object) diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index 3750e8fa22c..2ffd92b1215 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -528,8 +528,7 @@ def safedec(cls, s, context=None, **kwargs): return cls.dec(s, context, safe=True, **kwargs) -# No register_tagging(): PER encodes no tag at all, so the identity default -# of ASN1Codec is what UPER needs. +# No tagging hook: PER encodes no tag at all, so a field is left alone. ASN1_Codecs.PER.register_stem(UPERcodec_Object) diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index 2a449c79020..b8182a50dae 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -459,7 +459,20 @@ x.val == -2 and r == b"" x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3), size_len=3) x.val == b"\x12\x34\x56" and r == b"" = OER does not encode the tag of a component -ASN1_Codecs.OER.tagging_enc(b"\x05", explicit_tag=0x81) == b"\x05" and ASN1_Codecs.OER.tagging_dec(b"\x05", explicit_tag=0x81) == (None, b"\x05") +# X.696 encodes none, so OER hooks no tagging and the field is left alone +assert ASN1_Codecs.OER.field_hook("tagging_enc") is None + +assert ASN1_Codecs.OER.field_hook("tagging_dec") is None + +fld = ASN1F_INTEGER("n", 0, explicit_tag=0xA0) + +pkt = OERTaggedInteger() + +assert fld._tagging_enc(pkt, b"\x05", explicit_tag=0xA0) == b"\x05" + +assert fld._tagging_dec(pkt, b"\x05", explicit_tag=0xA0) == (None, b"\x05") + +True = OER tag long form # X.696 8.7.2.2: a tag number of 63 or more spills into continuation octets assert OER_tag_enc(100, OER_CLASS_CONTEXT) == b"\xbf\x64" diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index bcfd5a28b22..a8ceea06473 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -457,36 +457,28 @@ assert BERcodec_STRING.enc(b"x", uper_max=10) == BERcodec_STRING.enc(b"x") BERcodec_SEQUENCE.enc(BERcodec_INTEGER.enc(1), uper_min=0) == BERcodec_SEQUENCE.enc(BERcodec_INTEGER.enc(1)) + ASN.1 codec tagging contract -= BER tagging is exposed on the codec -assert ASN1_Codecs.BER.tagging_enc(b"\x02\x01\x05", implicit_tag=0xA0) == b"\xa0\x01\x05" -diff, payload = ASN1_Codecs.BER.tagging_dec( - b"\xa0\x01\x05", hidden_tag=2, implicit_tag=0xA0 -) += BER hooks the tagging of a field +tagging_enc = ASN1_Codecs.BER.field_hook("tagging_enc") + +assert tagging_enc(b"\x02\x01\x05", implicit_tag=0xA0) == b"\xa0\x01\x05" + +tagging_dec = ASN1_Codecs.BER.field_hook("tagging_dec") + +diff, payload = tagging_dec(b"\xa0\x01\x05", hidden_tag=2, implicit_tag=0xA0) + diff is None and payload == b"\x02\x01\x05" -= identity tagging for PER-style codecs -def _id_tagging_enc(s, **kwargs): - return s += a codec that hooks no tagging leaves the encoding alone +class _NoHooks: + ASN1_codec = ASN1_Codecs.CER -def _id_tagging_dec(s, **kwargs): - return None, s +assert ASN1_Codecs.CER.field_hook("tagging_enc") is None -# Save/restore: asn1.uts may already have loaded contrib UPER tagging. -_prev_tagging_enc = getattr(ASN1_Codecs.PER, "_tagging_enc", None) -_prev_tagging_dec = getattr(ASN1_Codecs.PER, "_tagging_dec", None) -ASN1_Codecs.PER.register_tagging(_id_tagging_enc, _id_tagging_dec) -try: - assert ASN1_Codecs.PER.tagging_enc(b"\x02\x01\x05", implicit_tag=0xA0) == b"\x02\x01\x05" - diff, payload = ASN1_Codecs.PER.tagging_dec( - b"\x02\x01\x05", hidden_tag=2, explicit_tag=0xA1 - ) - assert diff is None and payload == b"\x02\x01\x05" -finally: - if _prev_tagging_enc is not None and _prev_tagging_dec is not None: - ASN1_Codecs.PER.register_tagging(_prev_tagging_enc, _prev_tagging_dec) - else: - del ASN1_Codecs.PER._tagging_enc - del ASN1_Codecs.PER._tagging_dec +fld = ASN1F_INTEGER("n", 0, explicit_tag=0xA0) + +assert fld._tagging_enc(_NoHooks(), b"\x02\x01\x05", explicit_tag=0xA0) == b"\x02\x01\x05" + +fld._tagging_dec(_NoHooks(), b"\x02\x01\x05", explicit_tag=0xA0) == (None, b"\x02\x01\x05") = field _codec_kwargs and object-enc hooks class P(ASN1_Packet): From 1e103e88e9807ff1c665e6118c1c07248a47a669 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 12 Aug 2026 23:00:16 +0200 Subject: [PATCH 16/46] asn1: register the field hooks of a codec by keyword, in one dictionary A codec that does a field operation its own way had to declare a class of static methods and hand it to register_field_hooks, where the attribute names of that class silently defined the hook points, and each codec kept its own class in a _field_hooks attribute. The functions are now named as keyword arguments of register_hooks, and they all land in ASN1_Codecs.hooks, a dictionary by codec then by field operation, so what a codec overrides reads at the call site and lives in one place. The three hook classes become plain module functions, which is what UPER already needed anyway to bolt its bitstream helpers onto the asn1fields classes, and the enumerated codec kwargs simply ask whether the packet is a PER one instead of comparing hook classes. Dissecting and building a BER sequence of tagged fields takes the same time as before. AI-Assisted: yes (Cursor) Co-authored-by: Cursor --- scapy/asn1/asn1.py | 21 +- scapy/asn1/ber.py | 13 +- scapy/asn1fields.py | 2 +- scapy/contrib/oer.py | 257 ++++++++++----------- scapy/contrib/uper.py | 456 +++++++++++++++++++------------------ test/contrib/oer.uts | 12 +- test/contrib/uper.uts | 10 +- test/scapy/layers/asn1.uts | 8 +- test/scapy/layers/ber.uts | 8 +- 9 files changed, 393 insertions(+), 394 deletions(-) diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index 5ffb5dfdad6..5c83530fb73 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -122,29 +122,21 @@ class ASN1_BadTag_Decoding_Error(ASN1_Decoding_Error): class ASN1Codec(EnumElement): - # Class-level default: EnumElement.__getattr__ forwards unknown attributes - # to its int value, so a missing _field_hooks would raise (and swallow) an - # AttributeError on every field operation. - _field_hooks = None # type: Any - def register_stem(cls, stem): # type: (Type[BERcodec_Object[Any]]) -> None cls._stem = stem - def register_field_hooks(cls, hooks): - # type: (Any) -> None + def register_hooks(cls, **hooks): + # type: (**Any) -> None # Field operations a codec does its own way: the tagging of a field # (BER) and the compound fields (SEQUENCE/CHOICE/… in OER and PER). - cls._field_hooks = hooks + ASN1_Codecs.hooks.setdefault(cls, {}).update(hooks) - def field_hook(cls, name): + def hook(cls, name): # type: (str) -> Any # Hooks are optional and may be partial: missing entries mean that # asn1fields keeps its default implementation. - hooks = cls._field_hooks - if hooks is None: - return None - return getattr(hooks, name, None) + return ASN1_Codecs.hooks.get(cls, {}).get(name) def dec(cls, s, context=None, _depth=0): # type: (bytes, Optional[Type[ASN1_Class]], int) -> ASN1_Object[Any] @@ -174,6 +166,9 @@ class ASN1_Codecs(metaclass=ASN1_Codecs_metaclass): SER = cast(ASN1Codec, 8) XER = cast(ASN1Codec, 9) + # The field hooks of every codec, by codec then by field operation. + hooks = {} # type: Dict[ASN1Codec, Dict[str, Any]] + class ASN1Tag(EnumElement): def __init__(self, diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index e5467d6f856..f1bed662e1c 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -423,15 +423,12 @@ def enc(cls, s, size_len=0, **_kwargs): raise TypeError("Trying to encode an invalid value !") -class _BER_FieldHooks(object): - """ASN1F_* helpers for BER, the one codec that tags a field on the wire.""" - - tagging_enc = staticmethod(BER_tagging_enc) - tagging_dec = staticmethod(BER_tagging_dec) - - ASN1_Codecs.BER.register_stem(BERcodec_Object) -ASN1_Codecs.BER.register_field_hooks(_BER_FieldHooks) +# BER is the one codec that puts the tag of a field on the wire. +ASN1_Codecs.BER.register_hooks( + tagging_enc=BER_tagging_enc, + tagging_dec=BER_tagging_dec, +) ########################## diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 5b46641aeb7..6681d715ab5 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -74,7 +74,7 @@ def _field_hook(pkt, name): # type: (Any, str) -> Any # Contrib codecs (OER/UPER/…) may override compound field operations. # Returns None when the codec keeps the default BER behaviour. - return pkt.ASN1_codec.field_hook(name) + return pkt.ASN1_codec.hook(name) ########################## diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py index 148905b5772..6195ce2fbff 100644 --- a/scapy/contrib/oer.py +++ b/scapy/contrib/oer.py @@ -453,8 +453,8 @@ def enc(cls, s, size_len=0, **_kwargs): # No tagging hook: X.696 encodes no tag for a component, whatever the tagging # environment of the module, so a field is left alone. The only tag on the -# wire is the one of a chosen CHOICE alternative, which _OER_FieldHooks writes -# itself. +# wire is the one of a chosen CHOICE alternative, which the CHOICE hooks below +# write themselves. ASN1_Codecs.OER.register_stem(OERcodec_Object) @@ -838,135 +838,138 @@ def _field_extensible(field): return bool(getattr(field, "codec_opts", {}).get("oer_extensible", False)) -class _OER_FieldHooks(object): - """Compound ASN1F_* helpers for OER (kept out of asn1fields.py).""" - - @staticmethod - def sequence_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional - s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) - if not s: - for obj in field.seq: - obj.set_val(pkt, None) - return [], s - presence, s = OER_preamble_dec( - s, _field_extensible(field), - len(field.optionals), - ) - opt_index = 0 +def _oer_sequence_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional + s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) + if not s: for obj in field.seq: - target = obj - if isinstance(obj, ASN1F_optional): - present = presence[opt_index] - opt_index += 1 - if not present: - obj.set_absent(pkt) - continue - # The preamble already said the component is there, so dissect - # it directly: a failure is an error, not an absence. - target = obj._field - try: - s = target.dissect(pkt, s) - except ASN1F_badsequence: - break + obj.set_val(pkt, None) return [], s - - @staticmethod - def sequence_build(field, pkt): - # type: (Any, Any) -> bytes - from scapy.asn1fields import ASN1F_field, ASN1F_optional - optionals = field.optionals - s = OER_preamble_enc( - _field_extensible(field), - [not opt.is_empty(pkt) for opt in optionals], - ) - for obj in field.seq: - if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): + presence, s = OER_preamble_dec( + s, _field_extensible(field), + len(field.optionals), + ) + opt_index = 0 + for obj in field.seq: + target = obj + if isinstance(obj, ASN1F_optional): + present = presence[opt_index] + opt_index += 1 + if not present: + obj.set_absent(pkt) continue - s += obj.build(pkt) - # Through ASN1F_field, as ASN1F_SEQUENCE.i2m is the hook above - return ASN1F_field.i2m(field, pkt, s) - - @staticmethod - def sequence_of_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[list, bytes] - s = field._apply_tagging_dec(s, pkt) - count, s = OER_unsigned_integer_dec(s) - lst = [] - for _ in range(count): - c, s = field._extract_packet(s, pkt) - if c: - lst.append(c) - return lst, s - - @staticmethod - def sequence_of_build(field, pkt): - # type: (Any, Any) -> bytes - from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Object - val = getattr(pkt, field.name) - if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: - s = val # type: Any - else: - items = [ - bytes(item) if field.holds_packets else field.fld.i2m(pkt, item) - for item in val or [] - ] - s = OER_unsigned_integer_enc(len(items)) + b"".join(items) - return field.i2m(pkt, s) - - @staticmethod - def choice_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - from scapy.asn1fields import ASN1F_field - from scapy.asn1.asn1 import ASN1_Error - s = field._apply_tagging_dec(s, pkt) - tag_class, tag_number, payload = OER_tag_dec(s) - choice = None - for key, alternative in field.choices.items(): - if _OER_tag_parts(key) == (tag_class, tag_number): - choice = alternative - break - if choice is None: - if not field.flexible_tag: - raise ASN1_Error( - "ASN1F_CHOICE: unexpected field in '%s' " - "(tag %s not in possible tags %s)" % ( - field.name, tag_class | tag_number, - list(field.choices.keys()) - ) + # The preamble already said the component is there, so dissect + # it directly: a failure is an error, not an absence. + target = obj._field + try: + s = target.dissect(pkt, s) + except ASN1F_badsequence: + break + return [], s + + +def _oer_sequence_build(field, pkt): + # type: (Any, Any) -> bytes + from scapy.asn1fields import ASN1F_field, ASN1F_optional + optionals = field.optionals + s = OER_preamble_enc( + _field_extensible(field), + [not opt.is_empty(pkt) for opt in optionals], + ) + for obj in field.seq: + if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): + continue + s += obj.build(pkt) + # Through ASN1F_field, as ASN1F_SEQUENCE.i2m is the hook above + return ASN1F_field.i2m(field, pkt, s) + + +def _oer_sequence_of_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[list, bytes] + s = field._apply_tagging_dec(s, pkt) + count, s = OER_unsigned_integer_dec(s) + lst = [] + for _ in range(count): + c, s = field._extract_packet(s, pkt) + if c: + lst.append(c) + return lst, s + + +def _oer_sequence_of_build(field, pkt): + # type: (Any, Any) -> bytes + from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Object + val = getattr(pkt, field.name) + if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: + s = val # type: Any + else: + items = [ + bytes(item) if field.holds_packets else field.fld.i2m(pkt, item) + for item in val or [] + ] + s = OER_unsigned_integer_enc(len(items)) + b"".join(items) + return field.i2m(pkt, s) + + +def _oer_choice_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + from scapy.asn1fields import ASN1F_field + from scapy.asn1.asn1 import ASN1_Error + s = field._apply_tagging_dec(s, pkt) + tag_class, tag_number, payload = OER_tag_dec(s) + choice = None + for key, alternative in field.choices.items(): + if _OER_tag_parts(key) == (tag_class, tag_number): + choice = alternative + break + if choice is None: + if not field.flexible_tag: + raise ASN1_Error( + "ASN1F_CHOICE: unexpected field in '%s' " + "(tag %s not in possible tags %s)" % ( + field.name, tag_class | tag_number, + list(field.choices.keys()) ) - choice = ASN1F_field - if hasattr(choice, "ASN1_root"): - return field.extract_packet(choice, payload, _underlayer=pkt) - if isinstance(choice, type): - return choice(field.name, b"").m2i(pkt, payload) - # ASN1F_PACKET instance: X.696 20.2 puts the alternative tag in front - # of the value, so it was consumed above and must not be looked for - # again by the field itself. - return field.extract_packet( - choice._resolve_cls(pkt), payload, _underlayer=pkt, - ) - - @staticmethod - def choice_i2m(field, pkt, x): - # type: (Any, Any, Any) -> bytes - from scapy.asn1.asn1 import ASN1_Object - if x is None: - s = b"" + ) + choice = ASN1F_field + if hasattr(choice, "ASN1_root"): + return field.extract_packet(choice, payload, _underlayer=pkt) + if isinstance(choice, type): + return choice(field.name, b"").m2i(pkt, payload) + # ASN1F_PACKET instance: X.696 20.2 puts the alternative tag in front + # of the value, so it was consumed above and must not be looked for + # again by the field itself. + return field.extract_packet( + choice._resolve_cls(pkt), payload, _underlayer=pkt, + ) + + +def _oer_choice_i2m(field, pkt, x): + # type: (Any, Any, Any) -> bytes + from scapy.asn1.asn1 import ASN1_Object + if x is None: + s = b"" + else: + if isinstance(x, ASN1_Object): + s = x.enc(pkt.ASN1_codec) else: - if isinstance(x, ASN1_Object): - s = x.enc(pkt.ASN1_codec) - else: - s = bytes(x) - index = field.alternative_index(x) - if index is not None: - # X.696 20.2: the chosen alternative is prefixed with its tag - tag_class, tag_number = _OER_tag_parts( - field.choice_order[index] - ) - s = OER_tag_enc(tag_number, tag_class) + s - return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) + s = bytes(x) + index = field.alternative_index(x) + if index is not None: + # X.696 20.2: the chosen alternative is prefixed with its tag + tag_class, tag_number = _OER_tag_parts( + field.choice_order[index] + ) + s = OER_tag_enc(tag_number, tag_class) + s + return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) -ASN1_Codecs.OER.register_field_hooks(_OER_FieldHooks) +ASN1_Codecs.OER.register_hooks( + sequence_m2i=_oer_sequence_m2i, + sequence_build=_oer_sequence_build, + sequence_of_m2i=_oer_sequence_of_m2i, + sequence_of_build=_oer_sequence_of_build, + choice_m2i=_oer_choice_m2i, + choice_i2m=_oer_choice_i2m, +) diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index 2ffd92b1215..b64dc1d8369 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -1044,244 +1044,240 @@ def _uper_decode_all(s, read): return value -class _UPER_FieldHooks(object): - """Compound ASN1F_* helpers for UPER/PER (kept out of asn1fields.py).""" +def _uper_use_object_enc(field, pkt, item): + # type: (Any, Any, Any) -> bool + # Always pass constraints through codec.enc(**kwargs). + return False - @staticmethod - def use_object_enc(field, pkt, item): - # type: (Any, Any, Any) -> bool - # Always pass constraints through codec.enc(**kwargs). - return False - @staticmethod - def sequence_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - _uper_decode_all(s, lambda dec: ( - _UPER_FieldHooks.sequence_dissect_from_decoder(field, pkt, dec) - )) - return [], b"" +def _uper_sequence_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + _uper_decode_all(s, lambda dec: ( + _uper_sequence_dissect_from_decoder(field, pkt, dec) + )) + return [], b"" - @staticmethod - def sequence_build(field, pkt): - # type: (Any, Any) -> bytes - from scapy.asn1fields import ASN1F_field - enc = UPER_Encoder() - _UPER_FieldHooks.sequence_encode_into(field, enc, pkt) - return ASN1F_field.i2m(field, pkt, enc.as_bytes()) - @staticmethod - def sequence_dissect_from_decoder(field, pkt, dec): - # type: (Any, Any, Any) -> None - from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional - if _field_extensible(field): - if dec.read_bit(): - raise UPER_Decoding_Error( - "ASN1F_SEQUENCE: extension additions are not supported" - ) - optionals = field.optionals - presence = [dec.read_bit() for _ in optionals] - opt_idx = 0 - for obj in field.seq: - if isinstance(obj, ASN1F_optional): - if not presence[opt_idx]: - obj.set_absent(pkt) - opt_idx += 1 - continue - opt_idx += 1 - try: - obj.dissect_from_decoder(pkt, dec) - except ASN1F_badsequence: - break +def _uper_sequence_build(field, pkt): + # type: (Any, Any) -> bytes + from scapy.asn1fields import ASN1F_field + enc = UPER_Encoder() + _uper_sequence_encode_into(field, enc, pkt) + return ASN1F_field.i2m(field, pkt, enc.as_bytes()) - @staticmethod - def sequence_encode_into(field, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - from scapy.asn1fields import ASN1F_optional - if _field_extensible(field): - enc.append_bit(0) - for opt in field.optionals: - enc.append_bit(0 if opt.is_empty(pkt) else 1) - for obj in field.seq: - if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): + +def _uper_sequence_dissect_from_decoder(field, pkt, dec): + # type: (Any, Any, Any) -> None + from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional + if _field_extensible(field): + if dec.read_bit(): + raise UPER_Decoding_Error( + "ASN1F_SEQUENCE: extension additions are not supported" + ) + optionals = field.optionals + presence = [dec.read_bit() for _ in optionals] + opt_idx = 0 + for obj in field.seq: + if isinstance(obj, ASN1F_optional): + if not presence[opt_idx]: + obj.set_absent(pkt) + opt_idx += 1 continue - obj.encode_into(enc, pkt) + opt_idx += 1 + try: + obj.dissect_from_decoder(pkt, dec) + except ASN1F_badsequence: + break + + +def _uper_sequence_encode_into(field, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + from scapy.asn1fields import ASN1F_optional + if _field_extensible(field): + enc.append_bit(0) + for opt in field.optionals: + enc.append_bit(0 if opt.is_empty(pkt) else 1) + for obj in field.seq: + if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): + continue + obj.encode_into(enc, pkt) + + +def _uper_sequence_of_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[list, bytes] + return _uper_decode_all(s, lambda dec: ( + _uper_sequence_of_m2i_from_decoder(field, pkt, dec) + )), b"" + + +def _uper_sequence_of_build(field, pkt): + # type: (Any, Any) -> bytes + from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Object + val = getattr(pkt, field.name) + if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: + s = val # type: Any + else: + # An unset field counts as an empty one, size constraint included + enc = UPER_Encoder() + _uper_sequence_of_encode_into(field, enc, pkt, val) + s = enc.as_bytes() + return field.i2m(pkt, s) - @staticmethod - def sequence_of_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[list, bytes] - return _uper_decode_all(s, lambda dec: ( - _UPER_FieldHooks.sequence_of_m2i_from_decoder(field, pkt, dec) - )), b"" - @staticmethod - def sequence_of_build(field, pkt): - # type: (Any, Any) -> bytes - from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Object - val = getattr(pkt, field.name) - if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: - s = val # type: Any - else: - # An unset field counts as an empty one, size constraint included - enc = UPER_Encoder() - _UPER_FieldHooks.sequence_of_encode_into(field, enc, pkt, val) - s = enc.as_bytes() - return field.i2m(pkt, s) +def _uper_sequence_of_m2i_from_decoder(field, pkt, dec): + # type: (Any, Any, Any) -> list + lst = [] - @staticmethod - def sequence_of_m2i_from_decoder(field, pkt, dec): - # type: (Any, Any, Any) -> list - lst = [] + def read_items(count): + # type: (int) -> None + for _ in range(count): + item = _extract_packet_from_decoder(field, dec, pkt) + lst.append(item) - def read_items(count): - # type: (int) -> None - for _ in range(count): - item = _extract_packet_from_decoder(field, dec, pkt) - lst.append(item) + if _field_extensible(field) and dec.read_bit(): + dec.read_fragmented(read_items) + else: + _uper_count_dec(field, dec, read_items) + return lst - if _field_extensible(field) and dec.read_bit(): - dec.read_fragmented(read_items) - else: - _uper_count_dec(field, dec, read_items) - return lst - @staticmethod - def sequence_of_encode_into(field, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - if value is None: - value = getattr(pkt, field.name) - if value is None: - _uper_count_enc(field, enc, 0, lambda offset, size: None) - return - count = len(value) - - def append_items(offset, size): - # type: (int, int) -> None - for item in value[offset:offset + size]: - if field.holds_packets: - item.ASN1_root.encode_into(enc, item) - else: - field.fld.encode_into(enc, pkt, item) - - uper_min, uper_max = _field_range(field) - if _field_extensible(field): - if ( - uper_min is not None and uper_max is not None and - uper_min <= count <= uper_max - ): - enc.append_bit(0) - else: - enc.append_bit(1) - enc.append_fragmented(count, append_items) - return - _uper_count_enc(field, enc, count, append_items) +def _uper_sequence_of_encode_into(field, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + if value is None: + value = getattr(pkt, field.name) + if value is None: + _uper_count_enc(field, enc, 0, lambda offset, size: None) + return + count = len(value) - @staticmethod - def choice_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - return _uper_decode_all(s, lambda dec: ( - _UPER_FieldHooks.choice_m2i_from_decoder(field, pkt, dec) - )), b"" + def append_items(offset, size): + # type: (int, int) -> None + for item in value[offset:offset + size]: + if field.holds_packets: + item.ASN1_root.encode_into(enc, item) + else: + field.fld.encode_into(enc, pkt, item) - @staticmethod - def choice_i2m(field, pkt, x): - # type: (Any, Any, Any) -> bytes - if x is None: - s = b"" + uper_min, uper_max = _field_range(field) + if _field_extensible(field): + if ( + uper_min is not None and uper_max is not None and + uper_min <= count <= uper_max + ): + enc.append_bit(0) else: - enc = UPER_Encoder() - _UPER_FieldHooks.choice_encode_into(field, enc, pkt, x) - s = enc.as_bytes() - return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) + enc.append_bit(1) + enc.append_fragmented(count, append_items) + return + _uper_count_enc(field, enc, count, append_items) - @staticmethod - def choice_m2i_from_decoder(field, pkt, dec): - # type: (Any, Any, Any) -> Any - from scapy.asn1.asn1 import ASN1_Error - if _field_extensible(field): - if dec.read_bit(): - raise UPER_Decoding_Error( - "ASN1F_CHOICE: extension additions are not supported" - ) - order = field.choice_order - if len(order) > 1: - index = UPER_choice_index_dec(dec, len(order)) - else: - index = 0 - if index >= len(order): - raise ASN1_Error( - "ASN1F_CHOICE: unexpected index %s in '%s'" % - (index, field.name) - ) - choice = field.choice_list[index] - if isinstance(choice, type) and hasattr(choice, "ASN1_root"): - p = choice() - p.add_underlayer(pkt) - p.ASN1_root.dissect_from_decoder(p, dec) - return p - if isinstance(choice, type): - return choice(field.name, b"").m2i_from_decoder(pkt, dec) - return choice.m2i_from_decoder(pkt, dec) - @staticmethod - def choice_encode_into(field, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - from scapy.asn1.asn1 import ASN1_Error - if value is None: - value = getattr(pkt, field.name) - index = field.alternative_index(value) - if index is None: - raise ASN1_Error( - "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % - field.name - ) - if _field_extensible(field): - enc.append_bit(0) - order = field.choice_order - if len(order) > 1: - UPER_choice_index_enc(enc, index, len(order)) - choice = field.choice_list[index] - if hasattr(choice, "ASN1_root"): - value.ASN1_root.encode_into(enc, value) - elif isinstance(choice, type): - choice(field.name, b"").encode_into(enc, pkt, value) - else: - choice.encode_into(enc, pkt, value) +def _uper_choice_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + return _uper_decode_all(s, lambda dec: ( + _uper_choice_m2i_from_decoder(field, pkt, dec) + )), b"" - @staticmethod - def packet_m2i_from_decoder(field, pkt, dec): - # type: (Any, Any, Any) -> Any - cls = field._resolve_cls(pkt) - p = cls() + +def _uper_choice_i2m(field, pkt, x): + # type: (Any, Any, Any) -> bytes + if x is None: + s = b"" + else: + enc = UPER_Encoder() + _uper_choice_encode_into(field, enc, pkt, x) + s = enc.as_bytes() + return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) + + +def _uper_choice_m2i_from_decoder(field, pkt, dec): + # type: (Any, Any, Any) -> Any + from scapy.asn1.asn1 import ASN1_Error + if _field_extensible(field): + if dec.read_bit(): + raise UPER_Decoding_Error( + "ASN1F_CHOICE: extension additions are not supported" + ) + order = field.choice_order + if len(order) > 1: + index = UPER_choice_index_dec(dec, len(order)) + else: + index = 0 + if index >= len(order): + raise ASN1_Error( + "ASN1F_CHOICE: unexpected index %s in '%s'" % + (index, field.name) + ) + choice = field.choice_list[index] + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + p = choice() p.add_underlayer(pkt) p.ASN1_root.dissect_from_decoder(p, dec) return p - - @staticmethod - def packet_i2m(field, pkt, x): - # type: (Any, Any, Any) -> bytes - if x is None: - s = b"" - else: - enc = UPER_Encoder() - _UPER_FieldHooks.packet_encode_into(field, enc, pkt, x) - s = enc.as_bytes() - return field._tagging_enc( - pkt, s, - implicit_tag=field.implicit_tag, - explicit_tag=field.explicit_tag, + if isinstance(choice, type): + return choice(field.name, b"").m2i_from_decoder(pkt, dec) + return choice.m2i_from_decoder(pkt, dec) + + +def _uper_choice_encode_into(field, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + from scapy.asn1.asn1 import ASN1_Error + if value is None: + value = getattr(pkt, field.name) + index = field.alternative_index(value) + if index is None: + raise ASN1_Error( + "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % + field.name ) - - @staticmethod - def packet_encode_into(field, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - from scapy.asn1.asn1 import ASN1_Object - if value is None: - value = getattr(pkt, field.name) - if value is None: - return - if isinstance(value, ASN1_Object): - value = value.val + if _field_extensible(field): + enc.append_bit(0) + order = field.choice_order + if len(order) > 1: + UPER_choice_index_enc(enc, index, len(order)) + choice = field.choice_list[index] + if hasattr(choice, "ASN1_root"): value.ASN1_root.encode_into(enc, value) + elif isinstance(choice, type): + choice(field.name, b"").encode_into(enc, pkt, value) + else: + choice.encode_into(enc, pkt, value) + + +def _uper_packet_m2i_from_decoder(field, pkt, dec): + # type: (Any, Any, Any) -> Any + cls = field._resolve_cls(pkt) + p = cls() + p.add_underlayer(pkt) + p.ASN1_root.dissect_from_decoder(p, dec) + return p + + +def _uper_packet_i2m(field, pkt, x): + # type: (Any, Any, Any) -> bytes + if x is None: + s = b"" + else: + enc = UPER_Encoder() + _uper_packet_encode_into(field, enc, pkt, x) + s = enc.as_bytes() + return field._tagging_enc( + pkt, s, + implicit_tag=field.implicit_tag, + explicit_tag=field.explicit_tag, + ) + + +def _uper_packet_encode_into(field, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + from scapy.asn1.asn1 import ASN1_Object + if value is None: + value = getattr(pkt, field.name) + if value is None: + return + if isinstance(value, ASN1_Object): + value = value.val + value.ASN1_root.encode_into(enc, value) def _uper_count_enc(field, enc, count, append_items): @@ -1365,7 +1361,6 @@ def opt_encode_into(self, enc, pkt, value=None): # type: (Any, Any, Any, Any) -> None self._field.encode_into(enc, pkt, value) - hooks = _UPER_FieldHooks for field_cls, methods in ( (af.ASN1F_field, { "m2i_from_decoder": m2i_from_decoder, @@ -1373,20 +1368,20 @@ def opt_encode_into(self, enc, pkt, value=None): "encode_into": encode_into, }), (af.ASN1F_SEQUENCE, { - "dissect_from_decoder": hooks.sequence_dissect_from_decoder, - "encode_into": hooks.sequence_encode_into, + "dissect_from_decoder": _uper_sequence_dissect_from_decoder, + "encode_into": _uper_sequence_encode_into, }), (af.ASN1F_SEQUENCE_OF, { - "m2i_from_decoder": hooks.sequence_of_m2i_from_decoder, - "encode_into": hooks.sequence_of_encode_into, + "m2i_from_decoder": _uper_sequence_of_m2i_from_decoder, + "encode_into": _uper_sequence_of_encode_into, }), (af.ASN1F_CHOICE, { - "m2i_from_decoder": hooks.choice_m2i_from_decoder, - "encode_into": hooks.choice_encode_into, + "m2i_from_decoder": _uper_choice_m2i_from_decoder, + "encode_into": _uper_choice_encode_into, }), (af.ASN1F_PACKET, { - "m2i_from_decoder": hooks.packet_m2i_from_decoder, - "encode_into": hooks.packet_encode_into, + "m2i_from_decoder": _uper_packet_m2i_from_decoder, + "encode_into": _uper_packet_encode_into, }), (af.ASN1F_optional, { "dissect_from_decoder": opt_dissect_from_decoder, @@ -1405,7 +1400,7 @@ def enum_codec_kwargs(self, pkt): # definition, so they are only added for PER packets. Other codecs # keep an empty codec_opts and their item.enc() fast path. codec = getattr(pkt, "ASN1_codec", None) - if getattr(codec, "_field_hooks", None) is _UPER_FieldHooks: + if codec is ASN1_Codecs.PER: # X.691 14.1: the index follows the enumeration values in # ascending order, whatever order they were declared in. kwargs.setdefault("uper_enum_values", sorted(self.i2s)) @@ -1415,4 +1410,13 @@ def enum_codec_kwargs(self, pkt): _install_uper_asn1fields() -ASN1_Codecs.PER.register_field_hooks(_UPER_FieldHooks) +ASN1_Codecs.PER.register_hooks( + use_object_enc=_uper_use_object_enc, + sequence_m2i=_uper_sequence_m2i, + sequence_build=_uper_sequence_build, + sequence_of_m2i=_uper_sequence_of_m2i, + sequence_of_build=_uper_sequence_of_build, + choice_m2i=_uper_choice_m2i, + choice_i2m=_uper_choice_i2m, + packet_i2m=_uper_packet_i2m, +) diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index b8182a50dae..7a4a61b3a40 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -460,9 +460,9 @@ x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3), x.val == b"\x12\x34\x56" and r == b"" = OER does not encode the tag of a component # X.696 encodes none, so OER hooks no tagging and the field is left alone -assert ASN1_Codecs.OER.field_hook("tagging_enc") is None +assert ASN1_Codecs.OER.hook("tagging_enc") is None -assert ASN1_Codecs.OER.field_hook("tagging_dec") is None +assert ASN1_Codecs.OER.hook("tagging_dec") is None fld = ASN1F_INTEGER("n", 0, explicit_tag=0xA0) @@ -887,13 +887,13 @@ True + ASN.1 OER field hooks and packet extras = oer field hooks registered -assert hasattr(ASN1_Codecs.OER, "_field_hooks") +assert ASN1_Codecs.hooks[ASN1_Codecs.OER] -assert ASN1_Codecs.OER._field_hooks is not None +assert ASN1_Codecs.OER.hook("sequence_m2i") is not None -assert hasattr(ASN1_Codecs.OER._field_hooks, "sequence_m2i") +assert ASN1_Codecs.OER.hook("choice_i2m") is not None -assert hasattr(ASN1_Codecs.OER._field_hooks, "choice_i2m") +assert ASN1_Codecs.OER.hook("no_such_hook") is None True diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index d8ab2fb4b8d..cf4e19e230b 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -2786,15 +2786,13 @@ True + ASN.1 UPER field hooks and packet extras = uper field hooks registered -assert hasattr(ASN1_Codecs.PER, "_field_hooks") +assert ASN1_Codecs.hooks[ASN1_Codecs.PER] -assert ASN1_Codecs.PER._field_hooks is not None +assert ASN1_Codecs.PER.hook("sequence_m2i") is not None -assert hasattr(ASN1_Codecs.PER._field_hooks, "sequence_m2i") +use_object_enc = ASN1_Codecs.PER.hook("use_object_enc") -assert hasattr(ASN1_Codecs.PER._field_hooks, "use_object_enc") - -assert ASN1_Codecs.PER._field_hooks.use_object_enc( +assert use_object_enc( UPERConstrainedInt.ASN1_root, UPERConstrainedInt(), ASN1_INTEGER(1), ) is False diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index d40acd479dc..a50e146b3b4 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -553,13 +553,13 @@ for cls in (BEREmptySeqOf, OEREmptySeqOf, PEREmptySeqOf): True = field hooks present after contrib load -assert hasattr(ASN1_Codecs.OER, "_field_hooks") +assert ASN1_Codecs.hooks[ASN1_Codecs.OER] -assert hasattr(ASN1_Codecs.PER, "_field_hooks") +assert ASN1_Codecs.hooks[ASN1_Codecs.PER] -assert ASN1_Codecs.OER._field_hooks is not None +assert ASN1_Codecs.OER.hook("sequence_m2i") is not None -assert ASN1_Codecs.PER._field_hooks is not None +assert ASN1_Codecs.PER.hook("sequence_m2i") is not None True diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index a8ceea06473..2e4704b9353 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -458,11 +458,13 @@ BERcodec_SEQUENCE.enc(BERcodec_INTEGER.enc(1), uper_min=0) == BERcodec_SEQUENCE. + ASN.1 codec tagging contract = BER hooks the tagging of a field -tagging_enc = ASN1_Codecs.BER.field_hook("tagging_enc") +tagging_enc = ASN1_Codecs.BER.hook("tagging_enc") assert tagging_enc(b"\x02\x01\x05", implicit_tag=0xA0) == b"\xa0\x01\x05" -tagging_dec = ASN1_Codecs.BER.field_hook("tagging_dec") +assert ASN1_Codecs.hooks[ASN1_Codecs.BER]["tagging_enc"] is tagging_enc + +tagging_dec = ASN1_Codecs.BER.hook("tagging_dec") diff, payload = tagging_dec(b"\xa0\x01\x05", hidden_tag=2, implicit_tag=0xA0) @@ -472,7 +474,7 @@ diff is None and payload == b"\x02\x01\x05" class _NoHooks: ASN1_codec = ASN1_Codecs.CER -assert ASN1_Codecs.CER.field_hook("tagging_enc") is None +assert ASN1_Codecs.CER.hook("tagging_enc") is None fld = ASN1F_INTEGER("n", 0, explicit_tag=0xA0) From b1fc78157dc382339508a6f66502ba8e05aeb913 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 1 Sep 2026 14:53:15 +0200 Subject: [PATCH 17/46] oer, uper: inline single-use encoding helpers into their callers Fold fixed-integer, enumerated and preamble helpers into the codec and SEQUENCE hooks that were their only users, and drop the thin UPER utilities that existed only to wrap a single call site. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/contrib/oer.py | 183 ++++++++++++++---------------------- scapy/contrib/uper.py | 212 +++++++++++++++++++----------------------- 2 files changed, 167 insertions(+), 228 deletions(-) diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py index 6195ce2fbff..88057a02405 100644 --- a/scapy/contrib/oer.py +++ b/scapy/contrib/oer.py @@ -208,102 +208,6 @@ def OER_unsigned_integer_dec(s): return value, s[number_of_bytes:] -_OER_FIXED_FORMATS = { - True: {1: ">b", 2: ">h", 4: ">i", 8: ">q"}, - False: {1: ">B", 2: ">H", 4: ">I", 8: ">Q"}, -} - - -def OER_fixed_integer_enc(i, length, signed=True): - # type: (int, int, bool) -> bytes - fmt = _OER_FIXED_FORMATS[signed] - try: - return struct.pack(fmt[length], i) - except KeyError: - raise OER_Encoding_Error( - "OER_fixed_integer_enc: invalid length %i" % length - ) - except struct.error: - raise OER_Encoding_Error( - "OER_fixed_integer_enc: %i does not fit in %i %s octet(s)" % - (i, length, "signed" if signed else "unsigned") - ) - - -def OER_fixed_integer_dec(s, length, signed=True): - # type: (bytes, int, bool) -> Tuple[int, bytes] - _OER_check_len("OER_fixed_integer_dec", s, length) - fmt = _OER_FIXED_FORMATS[signed] - try: - return struct.unpack(fmt[length], s[:length])[0], s[length:] - except KeyError: - raise OER_Decoding_Error( - "OER_fixed_integer_dec: invalid length %i" % length, - remaining=s - ) - - -def OER_enumerated_enc(i): - # type: (int) -> bytes - if 0 <= i <= 127: - return chb(i) - body = OER_signed_integer_enc(i)[1:] - return chb(0x80 | len(body)) + body - - -def OER_enumerated_dec(s): - # type: (bytes) -> Tuple[int, bytes] - if not s: - raise OER_Decoding_Error("OER_enumerated_dec: got empty string", - remaining=s) - first = orb(s[0]) - if not (first & 0x80): - return first, s[1:] - length = first & 0x7f - _OER_check_len("OER_enumerated_dec", s, length, offset=1) - value = int.from_bytes(s[1:length + 1], "big", signed=True) - return value, s[length + 1:] - - -def OER_preamble_enc(extensible, presence): - # type: (bool, List[bool]) -> bytes - # X.696 16.2.2: an extension bit (extensible types only) followed by one - # presence bit per OPTIONAL/DEFAULT component, zero-padded to a whole - # number of octets. A type with neither has no preamble at all. - bits = [0] if extensible else [] - bits += [1 if present else 0 for present in presence] - if not bits: - return b"" - number_of_bytes = (len(bits) + 7) // 8 - value = 0 - for bit in bits: - value = (value << 1) | bit - value <<= 8 * number_of_bytes - len(bits) - return value.to_bytes(number_of_bytes, "big") - - -def OER_preamble_dec(s, extensible, number_of_optionals): - # type: (bytes, bool, int) -> Tuple[List[bool], bytes] - number_of_bits = (1 if extensible else 0) + number_of_optionals - if number_of_bits == 0: - return [], s - number_of_bytes = (number_of_bits + 7) // 8 - _OER_check_len("OER_preamble_dec", s, number_of_bytes) - value = int.from_bytes(s[:number_of_bytes], "big") - bits = [ - bool((value >> (8 * number_of_bytes - 1 - i)) & 1) - for i in range(number_of_bits) - ] - if extensible: - if bits[0]: - raise OER_Decoding_Error( - "OER_preamble_dec: extension additions are not supported", - remaining=s - ) - bits = bits[1:] - return bits, s[number_of_bytes:] - - def OER_tag_enc(n, tag_class=OER_CLASS_CONTEXT): # type: (int, int) -> bytes if n < 63: @@ -465,6 +369,11 @@ def enc(cls, s, size_len=0, **_kwargs): class OERcodec_INTEGER(OERcodec_Object[int]): tag = ASN1_Class_UNIVERSAL.INTEGER + _FIXED_FORMATS = { + True: {1: ">b", 2: ">h", 4: ">i", 8: ">q"}, + False: {1: ">B", 2: ">H", 4: ">I", 8: ">Q"}, + } + @classmethod def enc(cls, i, size_len=0, oer_unsigned=False, **_kwargs): # type: (int, Optional[int], bool, **Any) -> bytes @@ -472,7 +381,15 @@ def enc(cls, i, size_len=0, oer_unsigned=False, **_kwargs): # the type, never the value at hand, otherwise the decoder (which only # knows the type) reads something else back. if size_len in (1, 2, 4, 8): - return OER_fixed_integer_enc(i, size_len, signed=not oer_unsigned) + signed = not oer_unsigned + try: + return struct.pack(cls._FIXED_FORMATS[signed][size_len], i) + except struct.error: + raise OER_Encoding_Error( + "%s: %i does not fit in %i %s octet(s)" % + (cls.__name__, i, size_len, + "signed" if signed else "unsigned") + ) if oer_unsigned: return OER_unsigned_integer_enc(i) return OER_signed_integer_enc(i) @@ -487,10 +404,11 @@ def do_dec(cls, ): # type: (...) -> Tuple[ASN1_Object[int], bytes] if size_len in (1, 2, 4, 8): - x, t = OER_fixed_integer_dec( - s, size_len, signed=not oer_unsigned - ) - return cls.asn1_object(x), t + _OER_check_len(cls.__name__, s, size_len) + x = struct.unpack( + cls._FIXED_FORMATS[not oer_unsigned][size_len], s[:size_len] + )[0] + return cls.asn1_object(x), s[size_len:] if oer_unsigned: x, t = OER_unsigned_integer_dec(s) else: @@ -687,7 +605,10 @@ class OERcodec_ENUMERATED(OERcodec_INTEGER): @classmethod def enc(cls, i, **_kwargs): # type: (int, **Any) -> bytes - return OER_enumerated_enc(i) + if 0 <= i <= 127: + return chb(i) + body = OER_signed_integer_enc(i)[1:] + return chb(0x80 | len(body)) + body @classmethod def do_dec(cls, @@ -698,8 +619,17 @@ def do_dec(cls, oer_unsigned=False, # type: bool ): # type: (...) -> Tuple[ASN1_Object[int], bytes] - x, t = OER_enumerated_dec(s) - return cls.asn1_object(x), t + if not s: + raise OER_Decoding_Error( + "%s: got empty string" % cls.__name__, remaining=s + ) + first = orb(s[0]) + if not (first & 0x80): + return cls.asn1_object(first), s[1:] + length = first & 0x7f + _OER_check_len(cls.__name__, s, length, offset=1) + value = int.from_bytes(s[1:length + 1], "big", signed=True) + return cls.asn1_object(value), s[length + 1:] class OERcodec_UTF8_STRING(OERcodec_STRING): @@ -846,10 +776,30 @@ def _oer_sequence_m2i(field, pkt, s): for obj in field.seq: obj.set_val(pkt, None) return [], s - presence, s = OER_preamble_dec( - s, _field_extensible(field), - len(field.optionals), - ) + # X.696 16.2.2: extension bit (if extensible) then one presence bit per + # OPTIONAL/DEFAULT component, zero-padded to a whole number of octets. + extensible = _field_extensible(field) + number_of_optionals = len(field.optionals) + number_of_bits = (1 if extensible else 0) + number_of_optionals + if number_of_bits == 0: + presence = [] # type: List[bool] + else: + number_of_bytes = (number_of_bits + 7) // 8 + _OER_check_len("ASN1F_SEQUENCE", s, number_of_bytes) + value = int.from_bytes(s[:number_of_bytes], "big") + bits = [ + bool((value >> (8 * number_of_bytes - 1 - i)) & 1) + for i in range(number_of_bits) + ] + if extensible: + if bits[0]: + raise OER_Decoding_Error( + "ASN1F_SEQUENCE: extension additions are not supported", + remaining=s + ) + bits = bits[1:] + presence = bits + s = s[number_of_bytes:] opt_index = 0 for obj in field.seq: target = obj @@ -873,10 +823,19 @@ def _oer_sequence_build(field, pkt): # type: (Any, Any) -> bytes from scapy.asn1fields import ASN1F_field, ASN1F_optional optionals = field.optionals - s = OER_preamble_enc( - _field_extensible(field), - [not opt.is_empty(pkt) for opt in optionals], - ) + # X.696 16.2.2: extension bit (if extensible) then one presence bit per + # OPTIONAL/DEFAULT component, zero-padded to a whole number of octets. + bits = [0] if _field_extensible(field) else [] + bits += [0 if opt.is_empty(pkt) else 1 for opt in optionals] + if not bits: + s = b"" + else: + number_of_bytes = (len(bits) + 7) // 8 + value = 0 + for bit in bits: + value = (value << 1) | bit + value <<= 8 * number_of_bytes - len(bits) + s = value.to_bytes(number_of_bytes, "big") for obj in field.seq: if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): continue diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index b64dc1d8369..f23389cf042 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -617,22 +617,6 @@ def _uper_bytes_to_bitstr(data, nbits): return bitstr[:nbits] -def _uper_bit_string_parts(_s): - # type: (Any) -> Tuple[bytes, int] - if isinstance(_s, tuple) and len(_s) == 2: - data, nbits = _s - return bytes_encode(data), nbits - if isinstance(_s, str) and _s and all(c in "01" for c in _s): - nbits = len(_s) - padded = _s + "0" * ((8 - nbits % 8) % 8) - data = int(padded or "0", 2).to_bytes( - max(1, len(padded) // 8), "big" - ) - return data, nbits - s = bytes_encode(_s) - return s, 8 * len(s) - - def _uper_size_bounds(size_len, uper_min, uper_max): # type: (Optional[int], Optional[int], Optional[int]) -> Tuple[Optional[int], Optional[int]] # noqa: E501 # A SIZE constraint given as size_len is a fixed size, i.e. a range whose @@ -655,7 +639,18 @@ def encode_into(cls, **_kwargs # type: Any ): # type: (...) -> None - s, nbits = _uper_bit_string_parts(_s) + if isinstance(_s, tuple) and len(_s) == 2: + data, nbits = _s + s = bytes_encode(data) + elif isinstance(_s, str) and _s and all(c in "01" for c in _s): + nbits = len(_s) + padded = _s + "0" * ((8 - nbits % 8) % 8) + s = int(padded or "0", 2).to_bytes( + max(1, len(padded) // 8), "big" + ) + else: + s = bytes_encode(_s) + nbits = 8 * len(s) minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) if minimum is not None and maximum is not None: _uper_check_size(cls.__name__, "bits", nbits, minimum, maximum) @@ -1044,28 +1039,6 @@ def _uper_decode_all(s, read): return value -def _uper_use_object_enc(field, pkt, item): - # type: (Any, Any, Any) -> bool - # Always pass constraints through codec.enc(**kwargs). - return False - - -def _uper_sequence_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - _uper_decode_all(s, lambda dec: ( - _uper_sequence_dissect_from_decoder(field, pkt, dec) - )) - return [], b"" - - -def _uper_sequence_build(field, pkt): - # type: (Any, Any) -> bytes - from scapy.asn1fields import ASN1F_field - enc = UPER_Encoder() - _uper_sequence_encode_into(field, enc, pkt) - return ASN1F_field.i2m(field, pkt, enc.as_bytes()) - - def _uper_sequence_dissect_from_decoder(field, pkt, dec): # type: (Any, Any, Any) -> None from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional @@ -1103,27 +1076,6 @@ def _uper_sequence_encode_into(field, enc, pkt, value=None): obj.encode_into(enc, pkt) -def _uper_sequence_of_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[list, bytes] - return _uper_decode_all(s, lambda dec: ( - _uper_sequence_of_m2i_from_decoder(field, pkt, dec) - )), b"" - - -def _uper_sequence_of_build(field, pkt): - # type: (Any, Any) -> bytes - from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Object - val = getattr(pkt, field.name) - if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: - s = val # type: Any - else: - # An unset field counts as an empty one, size constraint included - enc = UPER_Encoder() - _uper_sequence_of_encode_into(field, enc, pkt, val) - s = enc.as_bytes() - return field.i2m(pkt, s) - - def _uper_sequence_of_m2i_from_decoder(field, pkt, dec): # type: (Any, Any, Any) -> list lst = [] @@ -1131,13 +1083,22 @@ def _uper_sequence_of_m2i_from_decoder(field, pkt, dec): def read_items(count): # type: (int) -> None for _ in range(count): - item = _extract_packet_from_decoder(field, dec, pkt) - lst.append(item) + if field.holds_packets: + p = field.cls() + p.add_underlayer(pkt) + p.ASN1_root.dissect_from_decoder(p, dec) + lst.append(p) + else: + lst.append(field.fld.m2i_from_decoder(pkt, dec)) if _field_extensible(field) and dec.read_bit(): dec.read_fragmented(read_items) else: - _uper_count_dec(field, dec, read_items) + uper_min, uper_max = _field_range(field) + if uper_min is not None and uper_max is not None: + read_items(UPER_constrained_int_dec(dec, uper_min, uper_max)) + else: + dec.read_fragmented(read_items) return lst @@ -1172,24 +1133,6 @@ def append_items(offset, size): _uper_count_enc(field, enc, count, append_items) -def _uper_choice_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - return _uper_decode_all(s, lambda dec: ( - _uper_choice_m2i_from_decoder(field, pkt, dec) - )), b"" - - -def _uper_choice_i2m(field, pkt, x): - # type: (Any, Any, Any) -> bytes - if x is None: - s = b"" - else: - enc = UPER_Encoder() - _uper_choice_encode_into(field, enc, pkt, x) - s = enc.as_bytes() - return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) - - def _uper_choice_m2i_from_decoder(field, pkt, dec): # type: (Any, Any, Any) -> Any from scapy.asn1.asn1 import ASN1_Error @@ -1253,21 +1196,6 @@ def _uper_packet_m2i_from_decoder(field, pkt, dec): return p -def _uper_packet_i2m(field, pkt, x): - # type: (Any, Any, Any) -> bytes - if x is None: - s = b"" - else: - enc = UPER_Encoder() - _uper_packet_encode_into(field, enc, pkt, x) - s = enc.as_bytes() - return field._tagging_enc( - pkt, s, - implicit_tag=field.implicit_tag, - explicit_tag=field.explicit_tag, - ) - - def _uper_packet_encode_into(field, enc, pkt, value=None): # type: (Any, Any, Any, Any) -> None from scapy.asn1.asn1 import ASN1_Object @@ -1293,25 +1221,6 @@ def _uper_count_enc(field, enc, count, append_items): enc.append_fragmented(count, append_items) -def _uper_count_dec(field, dec, read_items): - # type: (Any, Any, Callable[[int], None]) -> None - uper_min, uper_max = _field_range(field) - if uper_min is not None and uper_max is not None: - read_items(UPER_constrained_int_dec(dec, uper_min, uper_max)) - else: - dec.read_fragmented(read_items) - - -def _extract_packet_from_decoder(field, dec, pkt): - # type: (Any, Any, Any) -> Any - if field.holds_packets: - p = field.cls() - p.add_underlayer(pkt) - p.ASN1_root.dissect_from_decoder(p, dec) - return p - return field.fld.m2i_from_decoder(pkt, dec) - - def _install_uper_asn1fields(): # type: () -> None """Attach the UPER bitstream helpers onto the asn1fields classes.""" @@ -1409,9 +1318,80 @@ def enum_codec_kwargs(self, pkt): af.ASN1F_enum_INTEGER._codec_kwargs = enum_codec_kwargs # type: ignore[assignment] +def _uper_sequence_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + _uper_decode_all( + s, lambda dec: _uper_sequence_dissect_from_decoder(field, pkt, dec) + ) + return [], b"" + + +def _uper_sequence_build(field, pkt): + # type: (Any, Any) -> bytes + from scapy.asn1fields import ASN1F_field + enc = UPER_Encoder() + _uper_sequence_encode_into(field, enc, pkt) + return ASN1F_field.i2m(field, pkt, enc.as_bytes()) + + +def _uper_sequence_of_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[list, bytes] + return _uper_decode_all( + s, lambda dec: _uper_sequence_of_m2i_from_decoder(field, pkt, dec) + ), b"" + + +def _uper_sequence_of_build(field, pkt): + # type: (Any, Any) -> bytes + from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Object + val = getattr(pkt, field.name) + if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: + s = val # type: Any + else: + # An unset field counts as an empty one, size constraint included + enc = UPER_Encoder() + _uper_sequence_of_encode_into(field, enc, pkt, val) + s = enc.as_bytes() + return field.i2m(pkt, s) + + +def _uper_choice_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + return _uper_decode_all( + s, lambda dec: _uper_choice_m2i_from_decoder(field, pkt, dec) + ), b"" + + +def _uper_choice_i2m(field, pkt, x): + # type: (Any, Any, Any) -> bytes + if x is None: + s = b"" + else: + enc = UPER_Encoder() + _uper_choice_encode_into(field, enc, pkt, x) + s = enc.as_bytes() + return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) + + +def _uper_packet_i2m(field, pkt, x): + # type: (Any, Any, Any) -> bytes + if x is None: + s = b"" + else: + enc = UPER_Encoder() + _uper_packet_encode_into(field, enc, pkt, x) + s = enc.as_bytes() + return field._tagging_enc( + pkt, s, + implicit_tag=field.implicit_tag, + explicit_tag=field.explicit_tag, + ) + + _install_uper_asn1fields() ASN1_Codecs.PER.register_hooks( - use_object_enc=_uper_use_object_enc, + # Constraints always go through codec.enc(**kwargs). + use_object_enc=lambda field, pkt, item: False, sequence_m2i=_uper_sequence_m2i, sequence_build=_uper_sequence_build, sequence_of_m2i=_uper_sequence_of_m2i, From 6ae31522d91cead12f59e2421b47192781e8e5a5 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 1 Sep 2026 20:06:47 +0200 Subject: [PATCH 18/46] asn1: move OER/UPER into core and unify compound encode/decode Finish the OER/UPER architectural cleanup by reading constraints from fields directly, routing build/dissect through encode_to/decode_from, and sharing SEQUENCE/CHOICE/SEQUENCE OF logic in compound.py. Contrib modules become thin re-exports. Co-authored-by: Cursor AI-Assisted: yes (Cursor Agent) --- scapy/asn1/asn1.py | 84 ++- scapy/asn1/ber.py | 22 +- scapy/asn1/compound.py | 597 +++++++++++++++ scapy/asn1/constraints.py | 195 +++++ scapy/asn1/context.py | 135 ++++ scapy/asn1/oer.py | 821 +++++++++++++++++++++ scapy/asn1/tag.py | 27 + scapy/asn1/uper.py | 1072 +++++++++++++++++++++++++++ scapy/asn1fields.py | 552 ++++++++------ scapy/asn1packet.py | 9 +- scapy/contrib/oer.py | 928 +----------------------- scapy/contrib/uper.py | 1396 +----------------------------------- test/contrib/oer.uts | 230 +++++- test/contrib/uper.uts | 441 +++++++++++- test/scapy/layers/asn1.uts | 144 +++- test/scapy/layers/ber.uts | 33 +- 16 files changed, 4037 insertions(+), 2649 deletions(-) create mode 100644 scapy/asn1/compound.py create mode 100644 scapy/asn1/constraints.py create mode 100644 scapy/asn1/context.py create mode 100644 scapy/asn1/oer.py create mode 100644 scapy/asn1/tag.py create mode 100644 scapy/asn1/uper.py diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index 5c83530fb73..c7fb725303b 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -110,33 +110,90 @@ class ASN1_Error(Scapy_Exception): class ASN1_Encoding_Error(ASN1_Error): - pass + codec_label = "ASN.1" + + def __init__(self, + msg, # type: str + encoded=None, # type: Any + remaining=b"" # type: bytes + ): + # type: (...) -> None + Scapy_Exception.__init__(self, msg) + self.remaining = remaining + self.encoded = encoded + + def __str__(self): + # type: () -> str + s = Scapy_Exception.__str__(self) + if self.encoded is not None: + if isinstance(self.encoded, ASN1_Object): + s += "\n### Already encoded ###\n%s" % self.encoded.strshow() + else: + s += "\n### Already encoded ###\n%r" % self.encoded + if self.remaining: + s += "\n### Remaining ###\n%r" % self.remaining + return s class ASN1_Decoding_Error(ASN1_Error): - pass + codec_label = "ASN.1" + + def __init__(self, + msg, # type: str + decoded=None, # type: Any + remaining=b"" # type: bytes + ): + # type: (...) -> None + Scapy_Exception.__init__(self, msg) + self.remaining = remaining + self.decoded = decoded + + def __str__(self): + # type: () -> str + s = Scapy_Exception.__str__(self) + if self.decoded is not None: + if isinstance(self.decoded, ASN1_Object): + s += "\n### Already decoded ###\n%s" % self.decoded.strshow() + else: + s += "\n### Already decoded ###\n%r" % self.decoded + if self.remaining: + s += "\n### Remaining ###\n%r" % self.remaining + return s class ASN1_BadTag_Decoding_Error(ASN1_Decoding_Error): pass +class ASN1Codec_metaclass(type): + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> type + c = super(ASN1Codec_metaclass, cls).__new__(cls, name, bases, dct) + try: + c.tag.register(c.codec, c) # type: ignore + except Exception: + warning("Error registering %r for %r" % (c.tag, c.codec)) # type: ignore + return c + + class ASN1Codec(EnumElement): def register_stem(cls, stem): # type: (Type[BERcodec_Object[Any]]) -> None cls._stem = stem - def register_hooks(cls, **hooks): - # type: (**Any) -> None - # Field operations a codec does its own way: the tagging of a field - # (BER) and the compound fields (SEQUENCE/CHOICE/… in OER and PER). - ASN1_Codecs.hooks.setdefault(cls, {}).update(hooks) + def new_encoder(cls): + # type: () -> Any + from scapy.asn1.context import new_encoder + return new_encoder(cls) - def hook(cls, name): - # type: (str) -> Any - # Hooks are optional and may be partial: missing entries mean that - # asn1fields keeps its default implementation. - return ASN1_Codecs.hooks.get(cls, {}).get(name) + def new_decoder(cls, data): + # type: (bytes) -> Any + from scapy.asn1.context import new_decoder + return new_decoder(cls, data) def dec(cls, s, context=None, _depth=0): # type: (bytes, Optional[Type[ASN1_Class]], int) -> ASN1_Object[Any] @@ -166,9 +223,6 @@ class ASN1_Codecs(metaclass=ASN1_Codecs_metaclass): SER = cast(ASN1Codec, 8) XER = cast(ASN1Codec, 9) - # The field hooks of every codec, by codec then by field operation. - hooks = {} # type: Dict[ASN1Codec, Dict[str, Any]] - class ASN1Tag(EnumElement): def __init__(self, diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index f1bed662e1c..ca984242325 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -16,6 +16,7 @@ from scapy.compat import chb, bytes_encode from scapy.utils import binrepr, inet_aton, inet_ntoa from scapy.asn1.asn1 import ( + ASN1Codec_metaclass, ASN1Tag, ASN1_BADTAG, ASN1_BadTag_Decoding_Error, @@ -275,20 +276,8 @@ def BER_tagging_enc(s, implicit_tag=None, explicit_tag=None): # [ BER classes ] # -class BERcodec_metaclass(type): - def __new__(cls, - name, # type: str - bases, # type: Tuple[type, ...] - dct # type: Dict[str, Any] - ): - # type: (...) -> Type[BERcodec_Object[Any]] - c = cast('Type[BERcodec_Object[Any]]', - super(BERcodec_metaclass, cls).__new__(cls, name, bases, dct)) - try: - c.tag.register(c.codec, c) - except Exception: - warning("Error registering %r for %r" % (c.tag, c.codec)) - return c +class BERcodec_metaclass(ASN1Codec_metaclass): + pass _K = TypeVar('_K') @@ -424,11 +413,6 @@ def enc(cls, s, size_len=0, **_kwargs): ASN1_Codecs.BER.register_stem(BERcodec_Object) -# BER is the one codec that puts the tag of a field on the wire. -ASN1_Codecs.BER.register_hooks( - tagging_enc=BER_tagging_enc, - tagging_dec=BER_tagging_dec, -) ########################## diff --git a/scapy/asn1/compound.py b/scapy/asn1/compound.py new file mode 100644 index 00000000000..695d3da8292 --- /dev/null +++ b/scapy/asn1/compound.py @@ -0,0 +1,597 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +"""Shared ASN.1 compound-type encode/decode (SEQUENCE, CHOICE, SEQUENCE OF). + +ASN.1 schema fields form a tree, not a flat ``fields_desc`` list. The +``encode_to`` / ``decode_from`` methods on ``ASN1F_*`` are the analogue of +Scapy ``Field.addfield`` / ``Field.getfield`` for that tree. +""" + +from functools import reduce +from typing import Any, Callable, List, Optional, Tuple + +from scapy.asn1.asn1 import ( + ASN1_Class_UNIVERSAL, + ASN1_Codecs, + ASN1_Error, + ASN1_Object, +) +from scapy.asn1.constraints import field_extensible, field_range + + +def sequence_presence_bits(field, pkt): + # type: (Any, Any) -> List[int] + bits = [0] if field_extensible(field) else [] + bits += [1 if opt.is_present(pkt) else 0 for opt in field.optionals] + return bits + + +def read_oer_presence_bits(s, field): + # type: (bytes, Any) -> Tuple[List[bool], bytes] + from scapy.asn1.oer import OER_Decoding_Error, _OER_check_len + + number_of_optionals = len(field.optionals) + number_of_bits = (1 if field_extensible(field) else 0) + number_of_optionals + if number_of_bits == 0: + return [], s + number_of_bytes = (number_of_bits + 7) // 8 + _OER_check_len("ASN1F_SEQUENCE", s, number_of_bytes) + value = int.from_bytes(s[:number_of_bytes], "big") + bits = [ + bool((value >> (8 * number_of_bytes - 1 - i)) & 1) + for i in range(number_of_bits) + ] + if field_extensible(field): + if bits[0]: + raise OER_Decoding_Error( + "ASN1F_SEQUENCE: extension additions are not supported", + remaining=s, + ) + bits = bits[1:] + return bits, s[number_of_bytes:] + + +def write_oer_presence_bits(bits): + # type: (List[int]) -> bytes + if not bits: + return b"" + number_of_bytes = (len(bits) + 7) // 8 + value = 0 + for bit in bits: + value = (value << 1) | bit + value <<= 8 * number_of_bytes - len(bits) + return value.to_bytes(number_of_bytes, "big") + + +def read_uper_presence_bits(dec, field): + # type: (Any, Any) -> List[bool] + from scapy.asn1.uper import UPER_Decoding_Error + + if field_extensible(field): + if dec.read_bit(): + raise UPER_Decoding_Error( + "ASN1F_SEQUENCE: extension additions are not supported" + ) + return [dec.read_bit() for _ in field.optionals] + + +def write_uper_presence_bits(enc, field, pkt): + # type: (Any, Any, Any) -> None + if field_extensible(field): + enc.append_bit(0) + for opt in field.optionals: + enc.append_bit(1 if opt.is_present(pkt) else 0) + + +def _sequence_decode_children(field, pkt, presence, dissect): + # type: (Any, Any, List[bool], Callable[[Any], None]) -> None + from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional + + opt_index = 0 + for obj in field.seq: + if isinstance(obj, ASN1F_optional): + if not presence[opt_index]: + obj.set_missing(pkt) + opt_index += 1 + continue + opt_index += 1 + try: + dissect(obj) + except ASN1F_badsequence: + break + + +def _sequence_encode_children(field, pkt, encode): + # type: (Any, Any, Callable[[Any], None]) -> None + from scapy.asn1fields import ASN1F_optional + + for obj in field.seq: + if isinstance(obj, ASN1F_optional) and not obj.is_present(pkt): + continue + encode(obj) + + +# ---- SEQUENCE ------------------------------------------------------------- + +def sequence_encode_to(field, pkt, enc): + # type: (Any, Any, Any) -> None + if enc.codec is ASN1_Codecs.PER: + write_uper_presence_bits(enc.inner, field, pkt) + _sequence_encode_children( + field, pkt, + lambda obj: obj.encode_to(pkt, enc), + ) + return + if enc.codec is ASN1_Codecs.OER: + parts = [write_oer_presence_bits(sequence_presence_bits(field, pkt))] + _sequence_encode_children(field, pkt, lambda obj: parts.append(obj.build(pkt))) + enc.write(b"".join(parts)) + return + s = reduce(lambda x, y: x + y.build(pkt), field.seq, b"") + enc.write(field.i2m(pkt, s)) + + +def sequence_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + if pkt.ASN1_codec is ASN1_Codecs.PER: + inner = dec.inner if hasattr(dec, "inner") else dec + presence = read_uper_presence_bits(inner, field) + _sequence_decode_children( + field, pkt, presence, + lambda obj: obj.decode_from(pkt, dec), + ) + return + if dec.codec is ASN1_Codecs.OER: + s = dec.remaining() + s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) + if not s: + for obj in field.seq: + obj.set_val(pkt, None) + dec.set_remainder(b"") + return + presence, s = read_oer_presence_bits(s, field) + from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional + opt_index = 0 + for obj in field.seq: + target = obj + if isinstance(obj, ASN1F_optional): + if not presence[opt_index]: + obj.set_missing(pkt) + opt_index += 1 + continue + opt_index += 1 + target = obj.fld + try: + s = target.dissect(pkt, s) + except ASN1F_badsequence: + break + dec.set_remainder(s) + return + s = dec.remaining() + s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) + from scapy.asn1.ber import BER_Decoding_Error + codec = field.ASN1_tag.get_codec(ASN1_Codecs.BER) + _i, s, remain = codec.check_type_check_len(s) + s = field._dissect_sequence_children(pkt, s) + if len(s) > 0: + raise BER_Decoding_Error( + "unexpected remainder in %s" % pkt.name, + remaining=s, + ) + dec.set_remainder(remain) + + +# ---- SEQUENCE OF ---------------------------------------------------------- + +def sequence_of_encode_to(field, pkt, enc): + # type: (Any, Any, Any) -> None + if enc.codec is ASN1_Codecs.PER: + uper_sequence_of_encode_into(field, enc.inner, pkt) + return + if enc.codec is ASN1_Codecs.OER: + enc.write(oer_sequence_of_bytes(field, pkt)) + return + val = getattr(pkt, field.name) + if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: + s = val # type: Any + elif val is None: + s = b"" + elif field.holds_packets: + s = b"".join(bytes(i) for i in val) + else: + s = b"".join(field.fld.i2m(pkt, i) for i in val) + enc.write(field.i2m(pkt, s)) + + +def sequence_of_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + if dec.codec is ASN1_Codecs.PER: + field.set_val( + pkt, + uper_sequence_of_decode_from_decoder(field, pkt, dec.inner), + ) + return + if dec.codec is ASN1_Codecs.OER: + val, remain = oer_sequence_of_decode(field, pkt, dec.remaining()) + field.set_val(pkt, val) + dec.set_remainder(remain) + return + from scapy.asn1.ber import BER_Decoding_Error + s = dec.remaining() + s = field._apply_tagging_dec(s, pkt) + codec = field.ASN1_tag.get_codec(ASN1_Codecs.BER) + _i, s, remain = codec.check_type_check_len(s) + lst = [] + while s: + c, s = field._extract_packet(s, pkt) + if c: + lst.append(c) + if len(s) > 0: + raise BER_Decoding_Error( + "unexpected remainder in %s" % pkt.name, + remaining=s, + ) + field.set_val(pkt, lst) + dec.set_remainder(remain) + + +def oer_sequence_of_bytes(field, pkt): + # type: (Any, Any) -> bytes + from scapy.asn1.oer import OER_unsigned_integer_enc + + val = getattr(pkt, field.name) + if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: + return field.i2m(pkt, val) + items = [ + bytes(item) if field.holds_packets else field.fld.i2m(pkt, item) + for item in val or [] + ] + return field.i2m( + pkt, OER_unsigned_integer_enc(len(items)) + b"".join(items), + ) + + +def oer_sequence_of_decode(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[list, bytes] + from scapy.asn1.oer import OER_unsigned_integer_dec + + s = field._apply_tagging_dec(s, pkt) + count, s = OER_unsigned_integer_dec(s) + lst = [] + for _ in range(count): + c, s = field._extract_packet(s, pkt) + if c: + lst.append(c) + return lst, s + + +def uper_sequence_of_encode_into(field, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + if value is None: + value = getattr(pkt, field.name) + if value is None: + _uper_count_enc(field, enc, 0, lambda offset, size: None) + return + count = len(value) + + def append_items(offset, size): + # type: (int, int) -> None + for item in value[offset:offset + size]: + if field.holds_packets: + item.ASN1_root.encode_into(enc, item) + else: + field.fld.encode_into(enc, pkt, item) + + uper_min, uper_max = field_range(field) + if field_extensible(field): + if ( + uper_min is not None and uper_max is not None and + uper_min <= count <= uper_max + ): + enc.append_bit(0) + else: + enc.append_bit(1) + enc.append_fragmented(count, append_items) + return + _uper_count_enc(field, enc, count, append_items) + + +def uper_sequence_of_decode_from_decoder(field, pkt, dec): + # type: (Any, Any, Any) -> list + from scapy.asn1.uper import UPER_constrained_int_dec + + lst = [] + + def read_items(count): + # type: (int) -> None + for _ in range(count): + if field.holds_packets: + p = field.cls() + p.add_parent(pkt) + p.ASN1_root.decode_from(p, dec) + lst.append(p) + else: + lst.append(field.fld.m2i_from_decoder(pkt, dec)) + + if field_extensible(field) and dec.read_bit(): + dec.read_fragmented(read_items) + else: + uper_min, uper_max = field_range(field) + if uper_min is not None and uper_max is not None: + read_items(UPER_constrained_int_dec(dec, uper_min, uper_max)) + else: + dec.read_fragmented(read_items) + return lst + + +def _uper_count_enc(field, enc, count, append_items): + # type: (Any, Any, int, Callable[[int, int], None]) -> None + from scapy.asn1.uper import UPER_constrained_int_enc + + uper_min, uper_max = field_range(field) + if uper_min is not None and uper_max is not None: + UPER_constrained_int_enc(enc, count, uper_min, uper_max) + append_items(0, count) + else: + enc.append_fragmented(count, append_items) + + +# ---- CHOICE ------------------------------------------------------------- + +def choice_encode_to(field, pkt, enc): + # type: (Any, Any, Any) -> None + value = getattr(pkt, field.name) + if enc.codec is ASN1_Codecs.PER: + uper_choice_encode_into(field, enc.inner, pkt, value) + return + if enc.codec is ASN1_Codecs.OER: + enc.write(oer_choice_bytes(field, pkt, value)) + return + enc.write(ber_choice_bytes(field, pkt, value)) + + +def choice_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + if dec.codec is ASN1_Codecs.PER: + field.set_val(pkt, uper_choice_decode_from_decoder(field, pkt, dec.inner)) + return + if dec.codec is ASN1_Codecs.OER: + val, remain = oer_choice_decode(field, pkt, dec.remaining()) + field.set_val(pkt, val) + dec.set_remainder(remain) + return + val, remain = ber_choice_decode(field, pkt, dec.remaining()) + field.set_val(pkt, val) + dec.set_remainder(remain) + + +def ber_choice_decode(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + from scapy.asn1.ber import BER_id_dec + from scapy.asn1fields import ASN1F_field + + if len(s) == 0: + raise ASN1_Error("ASN1F_CHOICE: got empty string") + s = field._apply_tagging_dec(s, pkt) + tag, _ = BER_id_dec(s) + if tag in field.choices: + choice = field.choices[tag] + elif field.flexible_tag: + choice = ASN1F_field + else: + raise ASN1_Error( + "ASN1F_CHOICE: unexpected field in '%s' " + "(tag %s not in possible tags %s)" % ( + field.name, tag, list(field.choices.keys()) + ) + ) + if hasattr(choice, "ASN1_root"): + return field.extract_packet(choice, s, _parent=pkt) + if isinstance(choice, type): + return choice(field.name, b"").m2i(pkt, s) + return choice.m2i(pkt, s) + + +def ber_choice_bytes(field, pkt, x): + # type: (Any, Any, Any) -> bytes + if x is None: + s = b"" + else: + if isinstance(x, ASN1_Object): + s = x.enc(pkt.ASN1_codec) + else: + s = bytes(x) + if type(x) in field.pktchoices: + imp, exp = field.pktchoices[type(x)] + s = field._tagging_enc(pkt, s, implicit_tag=imp, explicit_tag=exp) + _imp, exp = field._tagging_tags(pkt) + return field._tagging_enc(pkt, s, explicit_tag=exp) + + +def oer_choice_bytes(field, pkt, x): + # type: (Any, Any, Any) -> bytes + from scapy.asn1.oer import OER_tag_enc, OER_tag_parts + + if x is None: + s = b"" + else: + if isinstance(x, ASN1_Object): + s = x.enc(pkt.ASN1_codec) + else: + s = bytes(x) + index = field.alternative_index(x) + if index is not None: + tag_class, tag_number = OER_tag_parts(field.choice_order[index]) + s = OER_tag_enc(tag_number, tag_class) + s + return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) + + +def oer_choice_decode(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + from scapy.asn1fields import ASN1F_field + from scapy.asn1.oer import OER_tag_dec, OER_tag_parts + + s = field._apply_tagging_dec(s, pkt) + tag_class, tag_number, payload = OER_tag_dec(s) + choice = None + for key, alternative in field.choices.items(): + if OER_tag_parts(key) == (tag_class, tag_number): + choice = alternative + break + if choice is None: + if not field.flexible_tag: + raise ASN1_Error( + "ASN1F_CHOICE: unexpected field in '%s' " + "(tag %s not in possible tags %s)" % ( + field.name, tag_class | tag_number, + list(field.choices.keys()) + ) + ) + choice = ASN1F_field + if hasattr(choice, "ASN1_root"): + return field.extract_packet(choice, payload, _parent=pkt) + if isinstance(choice, type): + return choice(field.name, b"").m2i(pkt, payload) + cls = (choice.next_cls_cb(pkt) or choice.cls) if choice.next_cls_cb else choice.cls + return field.extract_packet(cls, payload, _parent=pkt) + + +def uper_choice_encode_into(field, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + from scapy.asn1.uper import UPER_choice_index_enc + + if value is None: + value = getattr(pkt, field.name) + if value is None: + return + index = field.alternative_index(value) + if index is None: + raise ASN1_Error( + "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % + field.name + ) + if field_extensible(field): + enc.append_bit(0) + order = field.canonical_order + tag = field.choice_order[index] + canon_idx = field.canonical_index[tag] + if len(order) > 1: + UPER_choice_index_enc(enc, canon_idx, len(order)) + choice = order[canon_idx] + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + value.ASN1_root.encode_into(enc, value) + elif hasattr(choice, "cls"): + uper_packet_encode_into(choice, enc, pkt, value) + elif isinstance(choice, type): + choice(field.name, b"").encode_into(enc, pkt, value) + else: + choice.encode_into(enc, pkt, value) + + +def uper_choice_decode_from_decoder(field, pkt, dec): + # type: (Any, Any, Any) -> Any + from scapy.asn1.uper import UPER_Decoding_Error, UPER_choice_index_dec + + if field_extensible(field): + if dec.read_bit(): + raise UPER_Decoding_Error( + "ASN1F_CHOICE: extension additions are not supported" + ) + order = field.canonical_order + if len(order) > 1: + index = UPER_choice_index_dec(dec, len(order)) + else: + index = 0 + if index >= len(order): + raise ASN1_Error( + "ASN1F_CHOICE: unexpected index %s in '%s'" % + (index, field.name) + ) + choice = order[index] + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + p = choice() + p.add_parent(pkt) + p.ASN1_root.decode_from(p, dec) + return p + if hasattr(choice, "cls"): + return uper_packet_decode_from_decoder(choice, pkt, dec) + if isinstance(choice, type): + return choice(field.name, b"").m2i_from_decoder(pkt, dec) + return choice.m2i_from_decoder(pkt, dec) + + +# ---- PACKET (nested ASN1_Packet) ------------------------------------------ + +def packet_encode_to(field, pkt, enc, value=None): + # type: (Any, Any, Any, Any) -> None + if value is None: + value = getattr(pkt, field.name) + if enc.codec is ASN1_Codecs.PER: + uper_packet_encode_into(field, enc.inner, pkt, value) + return + enc.write(ber_oer_packet_bytes(field, pkt, value)) + + +def packet_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + if dec.codec is ASN1_Codecs.PER: + field.set_val(pkt, uper_packet_decode_from_decoder(field, pkt, dec.inner)) + return + val, remain = ber_oer_packet_decode(field, pkt, dec.remaining()) + field.set_val(pkt, val) + dec.set_remainder(remain) + + +def ber_oer_packet_decode(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + cls = (field.next_cls_cb(pkt) or field.cls) if field.next_cls_cb else field.cls + from scapy.asn1packet import ASN1_Packet as _ASN1_Packet + if not issubclass(cls, _ASN1_Packet): + return field.extract_packet(cls, s, _parent=pkt) + s = field._apply_tagging_dec( + s, pkt, + hidden_tag=cls.ASN1_root.ASN1_tag, + _fname=field.name, + ) + if not s: + return None, s + return field.extract_packet(cls, s, _parent=pkt) + + +def ber_oer_packet_bytes(field, pkt, x): + # type: (Any, Any, Any) -> bytes + if x is None: + s = b"" + elif isinstance(x, bytes): + s = x + elif isinstance(x, ASN1_Object): + s = bytes(x.val) if x.val else b"" + else: + s = bytes(x) + from scapy.asn1packet import ASN1_Packet as _ASN1_Packet + if not isinstance(x, _ASN1_Packet): + return s + imp, exp = field._tagging_tags(pkt) + return field._tagging_enc(pkt, s, implicit_tag=imp, explicit_tag=exp) + + +def uper_packet_encode_into(field, enc, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + if value is None: + value = getattr(pkt, field.name) + if value is None: + return + if isinstance(value, ASN1_Object): + value = value.val + value.ASN1_root.encode_into(enc, value) + + +def uper_packet_decode_from_decoder(field, pkt, dec): + # type: (Any, Any, Any) -> Any + cls = (field.next_cls_cb(pkt) or field.cls) if field.next_cls_cb else field.cls + p = cls() + p.add_parent(pkt) + p.ASN1_root.decode_from(p, dec) + return p diff --git a/scapy/asn1/constraints.py b/scapy/asn1/constraints.py new file mode 100644 index 00000000000..ba3d31a4709 --- /dev/null +++ b/scapy/asn1/constraints.py @@ -0,0 +1,195 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +"""Codec-neutral ASN.1 schema constraints.""" + +import warnings +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple + + +@dataclass(frozen=True) +class ASN1Constraints: + minimum: Optional[int] = None + maximum: Optional[int] = None + size_min: Optional[int] = None + size_max: Optional[int] = None + extensible: bool = False + unsigned: bool = False + + +@dataclass +class EncodingParams: + """Wire-encoding parameters resolved from a field or explicit kwargs.""" + size_len: Optional[int] = None + minimum: Optional[int] = None + maximum: Optional[int] = None + size_min: Optional[int] = None + size_max: Optional[int] = None + extensible: bool = False + unsigned: bool = False + uper_enum_values: Optional[List[int]] = None + + @property + def uper_min(self): + # type: () -> Optional[int] + if self.minimum is not None: + return self.minimum + return self.size_min + + @property + def uper_max(self): + # type: () -> Optional[int] + if self.maximum is not None: + return self.maximum + return self.size_max + + @property + def oer_unsigned(self): + # type: () -> bool + return self.unsigned + + @property + def uper_extensible(self): + # type: () -> bool + return self.extensible + + @property + def oer_extensible(self): + # type: () -> bool + return self.extensible + + +_LEGACY_CODEC_OPTS = { + "uper_min": "minimum", + "uper_max": "maximum", + "uper_extensible": "extensible", + "oer_extensible": "extensible", + "oer_unsigned": "unsigned", + "minimum": "minimum", + "maximum": "maximum", + "size_min": "size_min", + "size_max": "size_max", + "extensible": "extensible", + "unsigned": "unsigned", +} + + +def normalize_constraints(codec_opts, size_len=None): + # type: (Dict[str, Any], Optional[int]) -> ASN1Constraints + """Build ASN1Constraints from field kwargs, with legacy alias support.""" + data = { + "minimum": None, + "maximum": None, + "size_min": None, + "size_max": None, + "extensible": False, + "unsigned": False, + } # type: Dict[str, Any] + for key, value in codec_opts.items(): + if key in _LEGACY_CODEC_OPTS: + if key.startswith(("uper_", "oer_")) and key not in ( + "uper_min", "uper_max", "uper_extensible", + "oer_extensible", "oer_unsigned", + ): + warnings.warn( + "Unknown codec-prefixed constraint %r" % key, + DeprecationWarning, + stacklevel=4, + ) + continue + if key.startswith(("uper_", "oer_")): + warnings.warn( + "codec-prefixed constraint %r is deprecated; use %r instead" % + (key, _LEGACY_CODEC_OPTS[key]), + DeprecationWarning, + stacklevel=4, + ) + data[_LEGACY_CODEC_OPTS[key]] = value + elif key in data: + data[key] = value + elif key.startswith(("uper_", "oer_")): + warnings.warn( + "Unknown codec-prefixed constraint %r" % key, + DeprecationWarning, + stacklevel=4, + ) + return ASN1Constraints(**data) + + +def field_extensible(field): + # type: (Any) -> bool + return bool(field.constraints.extensible) + + +def field_range(field): + # type: (Any) -> Tuple[Optional[int], Optional[int]] + c = field.constraints + minimum = c.minimum + maximum = c.maximum + if minimum is None and maximum is None: + minimum = c.size_min + maximum = c.size_max + return minimum, maximum + + +def encoding_params(field=None, pkt=None, **legacy): + # type: (Any, Any, **Any) -> EncodingParams + """Resolve encoding parameters from a field and/or legacy codec kwargs.""" + if field is not None: + c = field.constraints + params = EncodingParams( + size_len=field.size_len, + minimum=c.minimum, + maximum=c.maximum, + size_min=c.size_min, + size_max=c.size_max, + extensible=c.extensible, + unsigned=c.unsigned, + ) + if pkt is not None and hasattr(field, "uper_enum_values"): + from scapy.asn1.asn1 import ASN1_Codecs + if getattr(pkt, "ASN1_codec", None) is ASN1_Codecs.PER: + params.uper_enum_values = field.uper_enum_values() + return _merge_legacy(params, legacy) + params = EncodingParams() + return _merge_legacy(params, legacy) + + +def _merge_legacy(params, legacy): + # type: (EncodingParams, Dict[str, Any]) -> EncodingParams + if not legacy: + return params + if "size_len" in legacy and legacy["size_len"] is not None: + params.size_len = legacy["size_len"] + if "uper_min" in legacy and legacy["uper_min"] is not None: + params.minimum = legacy["uper_min"] + if "uper_max" in legacy and legacy["uper_max"] is not None: + params.maximum = legacy["uper_max"] + if legacy.get("uper_extensible"): + params.extensible = True + if legacy.get("oer_extensible"): + params.extensible = True + if "oer_unsigned" in legacy and legacy["oer_unsigned"] is not None: + params.unsigned = legacy["oer_unsigned"] + if legacy.get("uper_enum_values") is not None: + params.uper_enum_values = legacy["uper_enum_values"] + return params + + +def codec_kwargs(field=None, pkt=None, **legacy): + # type: (Any, Any, **Any) -> Dict[str, Any] + """Legacy keyword dict for codec methods under migration.""" + p = encoding_params(field, pkt=pkt, **legacy) + kw = { + "size_len": p.size_len, + "oer_unsigned": p.unsigned, + "uper_min": p.uper_min, + "uper_max": p.uper_max, + "uper_extensible": p.extensible, + "oer_extensible": p.extensible, + } # type: Dict[str, Any] + if p.uper_enum_values is not None: + kw["uper_enum_values"] = p.uper_enum_values + return kw diff --git a/scapy/asn1/context.py b/scapy/asn1/context.py new file mode 100644 index 00000000000..c1534efbaec --- /dev/null +++ b/scapy/asn1/context.py @@ -0,0 +1,135 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +"""ASN.1 encoder and decoder contexts.""" + +from typing import Any, TYPE_CHECKING + +if TYPE_CHECKING: + from scapy.asn1.uper import UPER_Decoder as _UPER_Decoder + from scapy.asn1.uper import UPER_Encoder as _UPER_Encoder + + +class ASN1Encoder(object): + codec = None # type: Any + + def finish(self): + # type: () -> bytes + raise NotImplementedError + + +class ASN1Decoder(object): + codec = None # type: Any + + def remaining(self): + # type: () -> bytes + raise NotImplementedError + + +class BER_Encoder(ASN1Encoder): + from scapy.asn1.asn1 import ASN1_Codecs + + def __init__(self, codec=None): + # type: (Any) -> None + from scapy.asn1.asn1 import ASN1_Codecs + self.codec = codec or ASN1_Codecs.BER + self._parts = [] # type: list[bytes] + + def write(self, data): + # type: (bytes) -> None + self._parts.append(data) + + def finish(self): + # type: () -> bytes + return b"".join(self._parts) + + +class BER_Decoder(ASN1Decoder): + from scapy.asn1.asn1 import ASN1_Codecs + + def __init__(self, data, codec=None): + # type: (bytes, Any) -> None + from scapy.asn1.asn1 import ASN1_Codecs + self.codec = codec or ASN1_Codecs.BER + self._data = data + self._offset = 0 + + def read_all(self): + # type: () -> bytes + return self._data[self._offset:] + + def consume(self, n): + # type: (int) -> bytes + chunk = self._data[self._offset:self._offset + n] + self._offset += n + return chunk + + def remaining(self): + # type: () -> bytes + return self._data[self._offset:] + + def set_remainder(self, remainder): + # type: (bytes) -> None + self._data = remainder + self._offset = 0 + + +class UPER_EncoderContext(ASN1Encoder): + from scapy.asn1.asn1 import ASN1_Codecs + + codec = ASN1_Codecs.PER + + def __init__(self): + # type: () -> None + from scapy.asn1.uper import UPER_Encoder + self._enc = UPER_Encoder() + + @property + def inner(self): + # type: () -> _UPER_Encoder + return self._enc + + def finish(self): + # type: () -> bytes + return self._enc.as_bytes() + + +class UPER_DecoderContext(ASN1Decoder): + from scapy.asn1.asn1 import ASN1_Codecs + + codec = ASN1_Codecs.PER + + def __init__(self, data): + # type: (bytes) -> None + from scapy.asn1.uper import UPER_Decoder + self._dec = UPER_Decoder(data) + + @property + def inner(self): + # type: () -> _UPER_Decoder + return self._dec + + def remaining(self): + # type: () -> bytes + return self._dec.remaining() + + +def new_encoder(codec): + # type: (Any) -> ASN1Encoder + from scapy.asn1.asn1 import ASN1_Codecs + if codec is ASN1_Codecs.PER: + return UPER_EncoderContext() + if codec is ASN1_Codecs.OER: + return BER_Encoder(codec=ASN1_Codecs.OER) + return BER_Encoder() + + +def new_decoder(codec, data): + # type: (Any, bytes) -> ASN1Decoder + from scapy.asn1.asn1 import ASN1_Codecs + if codec is ASN1_Codecs.PER: + return UPER_DecoderContext(data) + if codec is ASN1_Codecs.OER: + return BER_Decoder(data, codec=ASN1_Codecs.OER) + return BER_Decoder(data) diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py new file mode 100644 index 00000000000..accbce5305a --- /dev/null +++ b/scapy/asn1/oer.py @@ -0,0 +1,821 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +# scapy.contrib.description = ASN.1 Octet Encoding Rules (OER) +# scapy.contrib.status = loads + +""" +Octet Encoding Rules (OER) for ASN.1 + +Basic-OER as specified in ITU-T X.696 | ISO/IEC 8825-7. + +``ASN1F_SEQUENCE`` emits the preamble required by 16.2.2: a presence bit per +``ASN1F_optional``/``ASN1F_DEFAULT`` component, preceded by an extension bit +for sequences declared with ``oer_extensible=True``. Fixed size constraints +are expressed with ``size_len=`` (octets for strings, bits for BIT STRING). + +Tags declared on a field are not encoded: OER only puts a tag on the wire for +the chosen alternative of an ``ASN1F_CHOICE`` (20.2), so the ``implicit_tag=`` +and ``explicit_tag=`` of the alternatives are what selects it. + +Not supported yet: extension additions (an encoding that carries them is +refused rather than misparsed), SET, REAL, and the canonical variant (C-OER). +""" + +import struct + +from scapy.error import warning +from scapy.compat import chb, orb, bytes_encode +from scapy.utils import binrepr, inet_aton, inet_ntoa +from scapy.asn1.tag import asn1_tag_parts +from scapy.asn1.ber import BER_num_dec, BER_num_enc +from scapy.asn1.asn1 import ( + ASN1Codec_metaclass, + ASN1_Class, + ASN1_Class_UNIVERSAL, + ASN1_Codecs, + ASN1_DECODING_ERROR, + ASN1_Decoding_Error, + ASN1_Encoding_Error, + ASN1_Error, + ASN1_Object, + _ASN1_ERROR, +) +# DEFAULT components are described by the sequence preamble in OER/PER. + +from typing import ( + Any, + AnyStr, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +################## +# OER encoding # +################## + + +class OER_Exception(Exception): + pass + + +class OER_Encoding_Error(ASN1_Encoding_Error): + codec_label = "OER" + + +class OER_Decoding_Error(ASN1_Decoding_Error): + codec_label = "OER" + + +# OER tag classes (bits 8-7 of the first identifier octet) +OER_CLASS_UNIVERSAL = 0x00 +OER_CLASS_APPLICATION = 0x40 +OER_CLASS_CONTEXT = 0x80 +OER_CLASS_PRIVATE = 0xc0 + + +def _OER_check_len(name, s, number_of_bytes, offset=0): + # type: (str, bytes, int, int) -> None + """Raise unless s carries number_of_bytes octets past its first offset.""" + available = len(s) - offset + if available < number_of_bytes: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % + (name, available, number_of_bytes), + remaining=s + ) + + +def OER_len_enc(ll): + # type: (int) -> bytes + if ll < 128: + return chb(ll) + encoded = [] + value = ll + while value > 0: + encoded.insert(0, value & 0xff) + value >>= 8 + if len(encoded) > 127: + raise OER_Exception( + "OER_len_enc: Length too long (%i) to be encoded" % len(encoded) + ) + return chb(0x80 | len(encoded)) + bytes(encoded) + + +def OER_len_dec(s): + # type: (bytes) -> Tuple[int, bytes] + if not s: + raise OER_Decoding_Error("OER_len_dec: got empty string", remaining=s) + tmp_len = orb(s[0]) + if not tmp_len & 0x80: + return tmp_len, s[1:] + tmp_len &= 0x7f + _OER_check_len("OER_len_dec", s, tmp_len, offset=1) + ll = 0 + for c in s[1:tmp_len + 1]: + ll <<= 8 + ll |= orb(c) + return ll, s[tmp_len + 1:] + + +def OER_signed_integer_enc(i): + # type: (int) -> bytes + # X.696 10.4: the shortest two's complement encoding. A negative value + # needs one bit less than its magnitude suggests, as -2**(8n-1) still + # fits in n octets, hence the increment before measuring. + magnitude = i + 1 if i < 0 else i + number_of_bytes = (magnitude.bit_length() + 8) // 8 + value = i & ((1 << (8 * number_of_bytes)) - 1) + return OER_len_enc(number_of_bytes) + value.to_bytes(number_of_bytes, "big") + + +def OER_signed_integer_dec(s): + # type: (bytes) -> Tuple[int, bytes] + number_of_bytes, s = OER_len_dec(s) + _OER_check_len("OER_signed_integer_dec", s, number_of_bytes) + if number_of_bytes == 0: + raise OER_Decoding_Error( + "OER_signed_integer_dec: got an empty length determinant", + remaining=s + ) + value = int.from_bytes(s[:number_of_bytes], "big") + number_of_bits = 8 * number_of_bytes + if value & (1 << (number_of_bits - 1)): + value -= (1 << number_of_bits) - 1 + value -= 1 + return value, s[number_of_bytes:] + + +def OER_unsigned_integer_enc(i): + # type: (int) -> bytes + if i < 0: + raise OER_Encoding_Error( + "OER_unsigned_integer_enc: %i is negative" % i + ) + number_of_bits = max(i.bit_length(), 1) + number_of_bytes = (number_of_bits + 7) // 8 + return OER_len_enc(number_of_bytes) + i.to_bytes(number_of_bytes, "big") + + +def OER_unsigned_integer_dec(s): + # type: (bytes) -> Tuple[int, bytes] + number_of_bytes, s = OER_len_dec(s) + _OER_check_len("OER_unsigned_integer_dec", s, number_of_bytes) + value = int.from_bytes(s[:number_of_bytes], "big") + return value, s[number_of_bytes:] + + +def OER_tag_enc(n, tag_class=OER_CLASS_CONTEXT): + # type: (int, int) -> bytes + if n < 63: + return chb(tag_class | n) + tag = bytearray([tag_class | 0x3f]) + encoded = [] + value = n + while value > 0: + encoded.append(0x80 | (value & 0x7f)) + value >>= 7 + encoded[0] &= 0x7f + encoded.reverse() + tag.extend(encoded) + return bytes(tag) + + +def OER_tag_dec(s): + # type: (bytes) -> Tuple[int, int, bytes] + if not s: + raise OER_Decoding_Error("OER_tag_dec: got empty string", remaining=s) + first = orb(s[0]) + tag_class = first & 0xc0 + tag_number = first & 0x3f + if tag_number != 0x3f: + return tag_class, tag_number, s[1:] + tag_number = 0 + i = 1 + while i < len(s): + c = orb(s[i]) + tag_number <<= 7 + tag_number |= c & 0x7f + i += 1 + if not (c & 0x80): + break + else: + raise OER_Decoding_Error("OER_tag_dec: unfinished tag", remaining=s) + return tag_class, tag_number, s[i:] + + +def OER_tag_parts(identifier): + # type: (int) -> Tuple[int, int] + # X.696 8.7 only keeps the class and the number, so the constructed flag + # must not leak into the encoded tag number. + tag_class, tag_number, _constructed = asn1_tag_parts(identifier) + return tag_class, tag_number + + +class OERcodec_metaclass(ASN1Codec_metaclass): + pass + + +_K = TypeVar('_K') + + +class OERcodec_Object(Generic[_K], metaclass=OERcodec_metaclass): + codec = ASN1_Codecs.OER + tag = ASN1_Class_UNIVERSAL.ANY + + @classmethod + def asn1_object(cls, val): + # type: (_K) -> ASN1_Object[_K] + return cls.tag.asn1_object(val) + + @classmethod + def check_string(cls, s): + # type: (bytes) -> None + if not s: + raise OER_Decoding_Error( + "%s: Got empty object while expecting %r" % + (cls.__name__, cls.tag), remaining=s + ) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + **_kwargs # type: Any + ): + # type: (...) -> Tuple[ASN1_Object[Any], bytes] + raise OER_Decoding_Error( + "%s: Cannot decode unknown OER type without context" % + cls.__name__, remaining=s + ) + + @classmethod + def dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + field=None, # type: Any + pkt=None, # type: Any + size_len=None, # type: Optional[int] + oer_unsigned=None, # type: Optional[bool] + **_kwargs # type: Any + ): + # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] + call_kw = dict(_kwargs) + if field is not None: + call_kw["field"] = field + if pkt is not None: + call_kw["pkt"] = pkt + if size_len is not None: + call_kw["size_len"] = size_len + if oer_unsigned is not None: + call_kw["oer_unsigned"] = oer_unsigned + if not safe: + return cls.do_dec( + s, context=context, safe=safe, **call_kw, + ) + try: + return cls.do_dec( + s, context=context, safe=safe, **call_kw, + ) + except OER_Decoding_Error as e: + return ASN1_DECODING_ERROR(s, exc=e), b"" + except ASN1_Error as e: + return ASN1_DECODING_ERROR(s, exc=e), b"" + + @classmethod + def safedec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + field=None, # type: Any + pkt=None, # type: Any + size_len=None, # type: Optional[int] + oer_unsigned=None, # type: Optional[bool] + **_kwargs # type: Any + ): + # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] + return cls.dec( + s, context, safe=True, + field=field, pkt=pkt, + size_len=size_len, oer_unsigned=oer_unsigned, + **_kwargs, + ) + + @classmethod + def enc(cls, s, size_len=0, **_kwargs): + # type: (_K, Optional[int], **Any) -> bytes + if isinstance(s, (str, bytes)): + return OERcodec_STRING.enc(s, size_len=size_len) + else: + try: + return OERcodec_INTEGER.enc(int(s), size_len=size_len) # type: ignore + except TypeError: + raise TypeError("Trying to encode an invalid value !") + + +# No tagging hook: X.696 encodes no tag for a component, whatever the tagging +# environment of the module, so a field is left alone. The only tag on the +# wire is the one of a chosen CHOICE alternative, which the CHOICE hooks below +# write themselves. +ASN1_Codecs.OER.register_stem(OERcodec_Object) + + +########################## +# OERcodec objects # +########################## + +class OERcodec_INTEGER(OERcodec_Object[int]): + tag = ASN1_Class_UNIVERSAL.INTEGER + + _FIXED_FORMATS = { + True: {1: ">b", 2: ">h", 4: ">i", 8: ">q"}, + False: {1: ">B", 2: ">H", 4: ">I", 8: ">Q"}, + } + + @classmethod + def enc(cls, i, field=None, size_len=None, oer_unsigned=None, **_kwargs): + # type: (int, Any, Optional[int], Optional[bool], **Any) -> bytes + from scapy.asn1.constraints import codec_kwargs + legacy = dict(_kwargs) + if size_len is not None: + legacy["size_len"] = size_len + if oer_unsigned is not None: + legacy["oer_unsigned"] = oer_unsigned + kw = codec_kwargs(field, **legacy) + size_len = kw["size_len"] + oer_unsigned = kw["oer_unsigned"] + if oer_unsigned and i < 0: + raise OER_Encoding_Error( + "%s: %i is negative for an unsigned type" % (cls.__name__, i) + ) + # X.696 10: the width and the signedness follow the declared bounds of + # the type, never the value at hand, otherwise the decoder (which only + # knows the type) reads something else back. + if size_len in (1, 2, 4, 8): + signed = not oer_unsigned + try: + return struct.pack(cls._FIXED_FORMATS[signed][size_len], i) + except struct.error: + raise OER_Encoding_Error( + "%s: %i does not fit in %i %s octet(s)" % + (cls.__name__, i, size_len, + "signed" if signed else "unsigned") + ) + if oer_unsigned: + return OER_unsigned_integer_enc(i) + return OER_signed_integer_enc(i) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + field=None, # type: Any + size_len=None, # type: Optional[int] + oer_unsigned=None, # type: Optional[bool] + **_kwargs # type: Any + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + from scapy.asn1.constraints import codec_kwargs + legacy = dict(_kwargs) + if size_len is not None: + legacy["size_len"] = size_len + if oer_unsigned is not None: + legacy["oer_unsigned"] = oer_unsigned + kw = codec_kwargs(field, **legacy) + size_len = kw["size_len"] + oer_unsigned = kw["oer_unsigned"] + if size_len in (1, 2, 4, 8): + _OER_check_len(cls.__name__, s, size_len) + x = struct.unpack( + cls._FIXED_FORMATS[not oer_unsigned][size_len], s[:size_len] + )[0] + return cls.asn1_object(x), s[size_len:] + if oer_unsigned: + x, t = OER_unsigned_integer_dec(s) + else: + x, t = OER_signed_integer_dec(s) + return cls.asn1_object(x), t + + +class OERcodec_BOOLEAN(OERcodec_Object[int]): + tag = ASN1_Class_UNIVERSAL.BOOLEAN + + @classmethod + def enc(cls, i, **_kwargs): + # type: (int, **Any) -> bytes + return chb(0xff if i else 0x00) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + **_kwargs # type: Any + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + cls.check_string(s) + return cls.asn1_object(0 if orb(s[0]) == 0 else 1), s[1:] + + +def _oer_bitstr_to_bytes(bitstr): + # type: (bytes) -> bytes + padded = bitstr + b"0" * (-len(bitstr) % 8) + return bytes([int(padded[i:i + 8], 2) for i in range(0, len(padded), 8)]) + + +def _oer_bytes_to_bitstr(data): + # type: (bytes) -> str + return "".join(binrepr(orb(x)).zfill(8) for x in data) + + +class OERcodec_BIT_STRING(OERcodec_Object[str]): + tag = ASN1_Class_UNIVERSAL.BIT_STRING + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + field=None, # type: Any + size_len=None, # type: Optional[int] + oer_unsigned=None, # type: Optional[bool] + **_kwargs # type: Any + ): + # type: (...) -> Tuple[ASN1_Object[str], bytes] + from scapy.asn1.constraints import codec_kwargs + legacy = dict(_kwargs) + if size_len is not None: + legacy["size_len"] = size_len + kw = codec_kwargs(field, **legacy) + size_len = kw["size_len"] + if size_len: + number_of_bytes = (size_len + 7) // 8 + _OER_check_len(cls.__name__, s, number_of_bytes) + return ( + cls.tag.asn1_object( + _oer_bytes_to_bitstr(s[:number_of_bytes])[:size_len] + ), + s[number_of_bytes:], + ) + length, s = OER_len_dec(s) + if length == 0: + return cls.tag.asn1_object(""), s + _OER_check_len(cls.__name__, s, length) + unused_bits = orb(s[0]) + if safe and unused_bits > 7: + raise OER_Decoding_Error( + "OERcodec_BIT_STRING: too many unused_bits advertised", + remaining=s + ) + fs = _oer_bytes_to_bitstr(s[1:length]) + if unused_bits > 0: + fs = fs[:-unused_bits] + return cls.tag.asn1_object(fs), s[length:] + + @classmethod + def enc(cls, _s, field=None, size_len=None, **_kwargs): + # type: (AnyStr, Any, Optional[int], **Any) -> bytes + from scapy.asn1.constraints import codec_kwargs + legacy = dict(_kwargs) + if size_len is not None: + legacy["size_len"] = size_len + kw = codec_kwargs(field, **legacy) + size_len = kw["size_len"] + s = bytes_encode(_s) + if size_len: + # X.696 13.3: a fixed size means the bits are written padded to a + # whole number of octets, without length or unused-bit count. + if len(s) != size_len: + raise OER_Encoding_Error( + "%s: got %i bits while expecting %i" % + (cls.__name__, len(s), size_len), + encoded=_s + ) + return _oer_bitstr_to_bytes(s) + body = chb(-len(s) % 8) + _oer_bitstr_to_bytes(s) + return OER_len_enc(len(body)) + body + + +class OERcodec_STRING(OERcodec_Object[str]): + tag = ASN1_Class_UNIVERSAL.STRING + + @classmethod + def enc(cls, _s, field=None, size_len=None, **_kwargs): + # type: (Union[str, bytes], Any, Optional[int], **Any) -> bytes + from scapy.asn1.constraints import codec_kwargs + legacy = dict(_kwargs) + if size_len is not None: + legacy["size_len"] = size_len + kw = codec_kwargs(field, **legacy) + size_len = kw["size_len"] + s = bytes_encode(_s) + if size_len: + # X.696 16.1: a fixed size means no length determinant. + if len(s) != size_len: + raise OER_Encoding_Error( + "%s: got %i bytes while expecting %i" % + (cls.__name__, len(s), size_len), + encoded=_s + ) + return s + return OER_len_enc(len(s)) + s + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + field=None, # type: Any + size_len=None, # type: Optional[int] + oer_unsigned=None, # type: Optional[bool] + **_kwargs # type: Any + ): + # type: (...) -> Tuple[ASN1_Object[Any], bytes] + from scapy.asn1.constraints import codec_kwargs + legacy = dict(_kwargs) + if size_len is not None: + legacy["size_len"] = size_len + kw = codec_kwargs(field, **legacy) + size_len = kw["size_len"] + if size_len: + _OER_check_len(cls.__name__, s, size_len) + return cls.tag.asn1_object(s[:size_len]), s[size_len:] + length, s = OER_len_dec(s) + _OER_check_len(cls.__name__, s, length) + return cls.tag.asn1_object(s[:length]), s[length:] + + +class OERcodec_NULL(OERcodec_Object[None]): + tag = ASN1_Class_UNIVERSAL.NULL + + @classmethod + def enc(cls, i, **_kwargs): + # type: (Any, **Any) -> bytes + return b"" + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + **_kwargs # type: Any + ): + # type: (...) -> Tuple[ASN1_Object[None], bytes] + return cls.asn1_object(None), s + + +class OERcodec_OID(OERcodec_Object[bytes]): + tag = ASN1_Class_UNIVERSAL.OID + + @classmethod + def enc(cls, _oid, **_kwargs): + # type: (AnyStr, **Any) -> bytes + oid = bytes_encode(_oid) + if oid: + lst = [int(x) for x in oid.strip(b".").split(b".")] + else: + lst = list() + if len(lst) >= 2: + lst[1] += 40 * lst[0] + del lst[0] + body = b"".join(BER_num_enc(k) for k in lst) + return OER_len_enc(len(body)) + body + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + **_kwargs # type: Any + ): + # type: (...) -> Tuple[ASN1_Object[bytes], bytes] + length, s = OER_len_dec(s) + _OER_check_len(cls.__name__, s, length) + content, t = s[:length], s[length:] + lst = [] + while content: + val, content = BER_num_dec(content) + lst.append(val) + if len(lst) > 0: + lst.insert(0, lst[0] // 40) + lst[1] %= 40 + return ( + cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)), + t, + ) + + +class OERcodec_ENUMERATED(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.ENUMERATED + + @classmethod + def enc(cls, i, **_kwargs): + # type: (int, **Any) -> bytes + if 0 <= i <= 127: + return chb(i) + body = OER_signed_integer_enc(i)[1:] + return chb(0x80 | len(body)) + body + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + **_kwargs # type: Any + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + if not s: + raise OER_Decoding_Error( + "%s: got empty string" % cls.__name__, remaining=s + ) + first = orb(s[0]) + if not (first & 0x80): + return cls.asn1_object(first), s[1:] + length = first & 0x7f + _OER_check_len(cls.__name__, s, length, offset=1) + value = int.from_bytes(s[1:length + 1], "big", signed=True) + return cls.asn1_object(value), s[length + 1:] + + +class OERcodec_UTF8_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTF8_STRING + + +class OERcodec_NUMERIC_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.NUMERIC_STRING + + +class OERcodec_PRINTABLE_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING + + +class OERcodec_T61_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.T61_STRING + + +class OERcodec_VIDEOTEX_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.VIDEOTEX_STRING + + +class OERcodec_IA5_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.IA5_STRING + + +class OERcodec_GENERAL_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERAL_STRING + + +class OERcodec_UTC_TIME(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTC_TIME + + +class OERcodec_GENERALIZED_TIME(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME + + +class OERcodec_ISO646_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.ISO646_STRING + + +class OERcodec_UNIVERSAL_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UNIVERSAL_STRING + + +class OERcodec_BMP_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.BMP_STRING + + +class OERcodec_SEQUENCE(OERcodec_Object[Union[bytes, List['OERcodec_Object[Any]']]]): + tag = ASN1_Class_UNIVERSAL.SEQUENCE + + @classmethod + def enc(cls, _ll, **_kwargs): + # type: (Union[bytes, List[OERcodec_Object[Any]]], **Any) -> bytes + if isinstance(_ll, bytes): + return _ll + return b"".join(x.enc(cls.codec) for x in _ll) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + **_kwargs # type: Any + ): + # type: (...) -> Tuple[ASN1_Object[Union[bytes, List[Any]]], bytes] + raise OER_Decoding_Error( + "OERcodec_SEQUENCE: decoding requires schema-defined field order", + remaining=s + ) + + +class OERcodec_SET(OERcodec_SEQUENCE): + tag = ASN1_Class_UNIVERSAL.SET + + +class OERcodec_IPADDRESS(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.IPADDRESS + + @classmethod + def enc(cls, ipaddr_ascii, size_len=0, **_kwargs): # type: ignore + # type: (str, Optional[int], **Any) -> bytes + try: + s = inet_aton(ipaddr_ascii) + except Exception: + raise OER_Encoding_Error("IPv4 address could not be encoded") + if size_len == len(s): + return s + return OER_len_enc(len(s)) + s + + @classmethod + def do_dec(cls, s, context=None, safe=False, + size_len=0, oer_unsigned=False, **_kwargs): + # type: (bytes, Optional[Any], bool, Optional[int], bool, **Any) -> Tuple[ASN1_Object[str], bytes] # noqa: E501 + if size_len == 4: + raw, remain = s[:4], s[4:] + else: + length, remain = OER_len_dec(s) + if len(remain) < length: + raise OER_Decoding_Error("IP address could not be decoded", + remaining=s) + raw, remain = remain[:length], remain[length:] + try: + ipaddr_ascii = inet_ntoa(raw) + except Exception: + raise OER_Decoding_Error("IP address could not be decoded", + remaining=s) + return cls.asn1_object(ipaddr_ascii), remain + + +class OERcodec_COUNTER32(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER32 + + +class OERcodec_COUNTER64(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER64 + + +class OERcodec_GAUGE32(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.GAUGE32 + + +class OERcodec_TIME_TICKS(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.TIME_TICKS + + +# Re-export compound helpers for backward compatibility. +from scapy.asn1.compound import ( # noqa: E402 + oer_choice_bytes, + oer_choice_decode, + oer_sequence_of_bytes, + oer_sequence_of_decode, + sequence_encode_to, + sequence_decode_from as _oer_sequence_decode_from, +) + +oer_choice_i2m = oer_choice_bytes +oer_choice_m2i = oer_choice_decode +oer_sequence_of_build = oer_sequence_of_bytes +oer_sequence_of_m2i = oer_sequence_of_decode + + +def oer_sequence_build(field, pkt): + # type: (Any, Any) -> bytes + from scapy.asn1fields import ASN1F_field + from scapy.asn1.context import BER_Encoder + from scapy.asn1.asn1 import ASN1_Codecs + enc = BER_Encoder(codec=ASN1_Codecs.OER) + sequence_encode_to(field, pkt, enc) + return ASN1F_field.i2m(field, pkt, enc.finish()) + + +def oer_sequence_m2i(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + from scapy.asn1.context import BER_Decoder + from scapy.asn1.asn1 import ASN1_Codecs + dec = BER_Decoder(s, codec=ASN1_Codecs.OER) + _oer_sequence_decode_from(field, pkt, dec) + return [], dec.remaining() + diff --git a/scapy/asn1/tag.py b/scapy/asn1/tag.py new file mode 100644 index 00000000000..7fe12407f69 --- /dev/null +++ b/scapy/asn1/tag.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +"""Semantic ASN.1 tag decomposition for Scapy's legacy BER integer tags.""" + +from scapy.compat import orb +from scapy.asn1.ber import BER_id_enc + + +def asn1_tag_parts(identifier): + # type: (int) -> tuple + """Return (tag_class, tag_number, constructed) for a Scapy tag integer.""" + wire = BER_id_enc(identifier) + first = orb(wire[0]) + tag_class = first & 0xc0 + constructed = bool(first & 0x20) + if (first & 0x1f) != 0x1f: + return tag_class, first & 0x1f, constructed + tag_number = 0 + for c in wire[1:]: + c = orb(c) + tag_number <<= 7 + tag_number |= c & 0x7f + if not (c & 0x80): + break + return tag_class, tag_number, constructed diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py new file mode 100644 index 00000000000..09dd1f5e614 --- /dev/null +++ b/scapy/asn1/uper.py @@ -0,0 +1,1072 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +# scapy.contrib.description = ASN.1 Unaligned Packed Encoding Rules (UPER) +# scapy.contrib.status = loads + +""" +Unaligned Packed Encoding Rules (UPER) for ASN.1 + +As specified in ITU-T X.691 | ISO/IEC 8825-2. + +UPER is registered on ``ASN1_Codecs.PER``. Schema-driven encoding and decoding +(``ASN1F_SEQUENCE``, ``ASN1F_CHOICE``, ``ASN1F_SEQUENCE_OF``, +``ASN1F_ENUMERATED``) is supported for common field types. Value ranges are +declared with ``uper_min=``/``uper_max=``, fixed sizes with ``size_len=``, and +an extension marker with ``uper_extensible=True``. Content of 16K units or +more is fragmented as required by 11.9.3.8. + +Not supported yet: extension additions (an encoding that carries them is +refused rather than misparsed), SET, REAL, and the known-multiplier character +string encodings, which are emitted as plain octets rather than 7 or 4 bits +per character. + +``ASN1F_CHOICE`` alternatives are indexed in X.691 10.2 canonical tag order +(via ``ASN1F_CHOICE.canonical_order``). Declaration order is kept for +``alternative_index`` / BER tag lookup. +""" + +from scapy.error import warning +from scapy.compat import orb, bytes_encode +from scapy.utils import binrepr, inet_aton, inet_ntoa +from scapy.asn1.ber import BER_num_dec, BER_num_enc +from scapy.asn1.asn1 import ( + ASN1Codec_metaclass, + ASN1_Class, + ASN1_Class_UNIVERSAL, + ASN1_Codecs, + ASN1_DECODING_ERROR, + ASN1_Decoding_Error, + ASN1_Encoding_Error, + ASN1_Error, + ASN1_Object, + _ASN1_ERROR, +) +# DEFAULT components are described by the sequence preamble in OER/PER. + +from typing import ( + Any, + AnyStr, + Callable, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, +) + + +################### +# UPER encoding # +################### + + +class UPER_Encoding_Error(ASN1_Encoding_Error): + codec_label = "UPER" + + +class UPER_Decoding_Error(ASN1_Decoding_Error): + codec_label = "UPER" + + +def UPER_bits_for_range(size): + # type: (int) -> int + if size <= 0: + return 0 + return size.bit_length() + + +# X.691 11.9.3.8: content of 16K units or more is split into fragments, each +# one holding a multiple of this many units. +UPER_FRAGMENT_SIZE = 16384 + + +def _uper_bits_to_bytes(value, number_of_bits): + # type: (int, int) -> bytes + # X.691 11.1: an encoding is padded with zero bits up to an octet + # boundary. + if number_of_bits == 0: + return b"" + padding = -number_of_bits % 8 + return (value << padding).to_bytes((number_of_bits + padding) // 8, "big") + + +class UPER_Encoder(object): + def __init__(self): + # type: () -> None + self.number_of_bits = 0 + self.value = 0 + self.chunks_number_of_bits = 0 + self.chunks = [] # type: List[List[int]] + + def append_bit(self, bit): + # type: (int) -> None + self.number_of_bits += 1 + self.value <<= 1 + self.value |= 1 if bit else 0 + + def append_bits(self, data, number_of_bits): + # type: (bytes, int) -> None + if number_of_bits == 0: + return + value = int.from_bytes(data, "big") + value >>= (8 * len(data) - number_of_bits) + self.append_non_negative_binary_integer(value, number_of_bits) + + def append_non_negative_binary_integer(self, value, number_of_bits): + # type: (int, int) -> None + if number_of_bits == 0: + return + if self.number_of_bits > 4096: + self.chunks.append([self.value, self.number_of_bits]) + self.chunks_number_of_bits += self.number_of_bits + self.number_of_bits = 0 + self.value = 0 + self.number_of_bits += number_of_bits + self.value <<= number_of_bits + self.value |= value & ((1 << number_of_bits) - 1) + + def append_bytes(self, data): + # type: (bytes) -> None + self.append_bits(data, 8 * len(data)) + + def append_length_determinant(self, length): + # type: (int) -> None + # X.691 11.9.3.6/11.9.3.7 only define the one and two octet forms up + # to 16K. Longer content has to be fragmented, which requires slicing + # the content itself, so leave that to append_fragmented rather than + # silently emitting a determinant that does not match what follows. + if length >= UPER_FRAGMENT_SIZE: + raise UPER_Encoding_Error( + "UPER_Encoder: length %i requires fragmentation" % length + ) + if length < 128: + encoded = bytes([length]) + else: + encoded = bytes([(0x80 | (length >> 8)), (length & 0xff)]) + self.append_bytes(encoded) + + def append_fragmented(self, count, append_units): + # type: (int, Callable[[int, int], None]) -> None + # X.691 11.9.3.8: emit the content as fragments of at most 4 * 16K + # units, each preceded by its own determinant, and always terminate + # with a determinant below 16K (possibly zero). append_units(offset, + # size) appends the units of one fragment. + offset = 0 + remaining = count + while remaining >= UPER_FRAGMENT_SIZE: + number_of_fragments = min(remaining // UPER_FRAGMENT_SIZE, 4) + size = number_of_fragments * UPER_FRAGMENT_SIZE + self.append_bytes(bytes([0xc0 | number_of_fragments])) + append_units(offset, size) + offset += size + remaining -= size + self.append_length_determinant(remaining) + append_units(offset, remaining) + + def append_unconstrained_whole_number(self, value): + # type: (int) -> None + # X.691 11.4: the shortest two's complement encoding. A negative value + # needs one bit less than its magnitude suggests, as -2**(8n-1) still + # fits in n octets, hence the increment before measuring. + magnitude = value + 1 if value < 0 else value + number_of_bytes = (magnitude.bit_length() + 8) // 8 + self.append_length_determinant(number_of_bytes) + self.append_non_negative_binary_integer( + value & ((1 << (8 * number_of_bytes)) - 1), 8 * number_of_bytes + ) + + def as_bytes(self): + # type: () -> bytes + value = 0 + number_of_bits = 0 + for chunk_value, chunk_number_of_bits in self.chunks: + value <<= chunk_number_of_bits + value |= chunk_value + number_of_bits += chunk_number_of_bits + value <<= self.number_of_bits + value |= self.value + number_of_bits += self.number_of_bits + return _uper_bits_to_bytes(value, number_of_bits) + + +def UPER_has_unexpected_remainder(dec): + # type: (UPER_Decoder) -> bool + if dec.number_of_bits == 0: + return False + mask = (1 << dec.number_of_bits) - 1 + return (dec._bits & mask) != 0 + + +class UPER_Decoder(object): + def __init__(self, encoded): + # type: (bytes) -> None + self.total_number_of_bits = 8 * len(encoded) + self.number_of_bits = self.total_number_of_bits + if encoded: + self._bits = int.from_bytes(encoded, "big") + else: + self._bits = 0 + + def _read_offset(self): + # type: () -> int + return self.total_number_of_bits - self.number_of_bits + + def _read_bits_int(self, number_of_bits): + # type: (int) -> int + if number_of_bits == 0: + return 0 + consumed = self._read_offset() + shift = self.total_number_of_bits - consumed - number_of_bits + mask = (1 << number_of_bits) - 1 + return (self._bits >> shift) & mask + + def read_bit(self): + # type: () -> int + if self.number_of_bits == 0: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + bit = self._read_bits_int(1) + self.number_of_bits -= 1 + return bit + + def read_bits(self, number_of_bits): + # type: (int) -> bytes + if number_of_bits > self.number_of_bits: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + if number_of_bits == 0: + return b"" + value = self._read_bits_int(number_of_bits) + self.number_of_bits -= number_of_bits + return _uper_bits_to_bytes(value, number_of_bits) + + def remaining(self): + # type: () -> bytes + if self.number_of_bits == 0: + return b"" + value = self._read_bits_int(self.number_of_bits) + return _uper_bits_to_bytes(value, self.number_of_bits) + + def remaining_bytes(self): + # type: () -> bytes + # A standalone UPER encoding is padded to an octet boundary, so the + # bits left over inside the current octet are padding; only whole + # octets after it are actual remaining input. + pad = -self._read_offset() % 8 + self.number_of_bits = max(0, self.number_of_bits - pad) + return self.remaining() + + def read_bytes(self, number_of_bytes): + # type: (int) -> bytes + return self.read_bits(8 * number_of_bytes) + + def read_non_negative_binary_integer(self, number_of_bits): + # type: (int) -> int + if number_of_bits > self.number_of_bits: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + if number_of_bits == 0: + return 0 + value = self._read_bits_int(number_of_bits) + self.number_of_bits -= number_of_bits + return value + + def _read_length_determinant(self): + # type: () -> Tuple[int, bool] + # Returns the number of units and whether more fragments follow. + value = self.read_non_negative_binary_integer(8) + if (value & 0x80) == 0x00: + return value, False + if (value & 0xc0) == 0x80: + return ( + ((value & 0x7f) << 8) | + self.read_non_negative_binary_integer(8) + ), False + if 0xc1 <= value <= 0xc4: + return (value & 0x0f) * UPER_FRAGMENT_SIZE, True + raise UPER_Decoding_Error( + "UPER_Decoder: bad length determinant 0x%02x" % value + ) + + def read_length_determinant(self): + # type: () -> int + length, fragmented = self._read_length_determinant() + if fragmented: + raise UPER_Decoding_Error( + "UPER_Decoder: unexpected fragmented length determinant" + ) + return length + + def read_fragmented(self, read_units): + # type: (Callable[[int], None]) -> None + # Counterpart of UPER_Encoder.append_fragmented: read_units(size) is + # called once per fragment, the last one being the (possibly empty) + # fragment introduced by a determinant below 16K. + while True: + size, fragmented = self._read_length_determinant() + read_units(size) + if not fragmented: + return + + def read_unconstrained_whole_number(self): + # type: () -> int + number_of_bytes = self.read_length_determinant() + if number_of_bytes == 0: + raise UPER_Decoding_Error( + "UPER_Decoder: integer with an empty length determinant" + ) + enc = self.read_non_negative_binary_integer(8 * number_of_bytes) + sign_bit = 1 << (8 * number_of_bytes - 1) + if enc & sign_bit: + return enc - (1 << (8 * number_of_bytes)) + return enc + + +def UPER_constrained_int_enc(enc, value, minimum, maximum): + # type: (UPER_Encoder, int, int, int) -> None + # X.691 13.2.2: the field is sized after the range, so a value outside it + # cannot be expressed. Callers handle extensibility before coming here. + if not minimum <= value <= maximum: + raise UPER_Encoding_Error( + "UPER_constrained_int_enc: got %i while expecting %i..%i" % + (value, minimum, maximum) + ) + enc.append_non_negative_binary_integer( + value - minimum, UPER_bits_for_range(maximum - minimum) + ) + + +def UPER_constrained_int_dec(dec, minimum, maximum): + # type: (UPER_Decoder, int, int) -> int + value = dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + return value + minimum + + +def _uper_check_size(name, unit, count, minimum, maximum): + # type: (str, str, int, int, int) -> None + # The determinant is sized after the constraint, so a value that violates + # it cannot be expressed: refuse rather than emit something the peer reads + # as a different length. + if not minimum <= count <= maximum: + raise UPER_Encoding_Error( + "%s: got %i %s while expecting %s" % + (name, count, unit, minimum if minimum == maximum + else "%i..%i" % (minimum, maximum)) + ) + + +def UPER_octet_string_enc(enc, data, minimum=None, maximum=None): + # type: (UPER_Encoder, bytes, Optional[int], Optional[int]) -> None + if minimum is not None and maximum is not None: + _uper_check_size( + "UPER_octet_string_enc", "octets", len(data), minimum, maximum, + ) + if minimum != maximum: + enc.append_non_negative_binary_integer( + len(data) - minimum, + UPER_bits_for_range(maximum - minimum), + ) + enc.append_bytes(data) + else: + enc.append_fragmented( + len(data), + lambda offset, size: enc.append_bytes(data[offset:offset + size]), + ) + + +def UPER_octet_string_dec(dec, minimum=None, maximum=None): + # type: (UPER_Decoder, Optional[int], Optional[int]) -> bytes + if minimum is not None and maximum is not None: + length = minimum + if minimum != maximum: + length += dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + return dec.read_bytes(length) + fragments = [] # type: List[bytes] + dec.read_fragmented(lambda size: fragments.append(dec.read_bytes(size))) + return b"".join(fragments) + + +def UPER_choice_index_enc(enc, index, number_of_choices): + # type: (UPER_Encoder, int, int) -> None + enc.append_non_negative_binary_integer( + index, UPER_bits_for_range(number_of_choices - 1) + ) + + +def UPER_choice_index_dec(dec, number_of_choices): + # type: (UPER_Decoder, int) -> int + return dec.read_non_negative_binary_integer( + UPER_bits_for_range(number_of_choices - 1) + ) + + +class UPERcodec_metaclass(ASN1Codec_metaclass): + pass + + +_K = TypeVar('_K') + + +class UPERcodec_Object(Generic[_K], metaclass=UPERcodec_metaclass): + codec = ASN1_Codecs.PER + tag = ASN1_Class_UNIVERSAL.ANY + + @classmethod + def asn1_object(cls, val): + # type: (_K) -> ASN1_Object[_K] + return cls.tag.asn1_object(val) + + # The bit-oriented encode_into()/dec_from_decoder() pair is the primitive + # every codec implements; enc()/do_dec() below are the standalone (byte + # buffer) entry points, and pass every codec option straight through. + + @classmethod + def encode_into(cls, enc, s, **kwargs): + # type: (UPER_Encoder, Any, **Any) -> None + # No schema information here (ANY): guess from the Python type. + if isinstance(s, (str, bytes)): + UPERcodec_STRING.encode_into(enc, s, **kwargs) + return + try: + UPERcodec_INTEGER.encode_into(enc, int(s), **kwargs) + except Exception: + raise UPER_Encoding_Error( + "Cannot encode value %r for %s" % (s, cls.__name__), + encoded=s + ) + + @classmethod + def dec_from_decoder(cls, dec, **kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[Any] + raise UPER_Decoding_Error( + "%s: Cannot decode unknown UPER type without context" % + cls.__name__, remaining=dec.remaining() + ) + + @classmethod + def enc(cls, s, **kwargs): + # type: (Any, **Any) -> bytes + enc = UPER_Encoder() + cls.encode_into(enc, s, **kwargs) + return enc.as_bytes() + + @classmethod + def do_dec(cls, s, context=None, safe=False, **kwargs): + # type: (bytes, Optional[Type[ASN1_Class]], bool, **Any) -> Tuple[ASN1_Object[Any], bytes] # noqa: E501 + dec = UPER_Decoder(s) + return cls.dec_from_decoder(dec, **kwargs), dec.remaining_bytes() + + @classmethod + def dec(cls, s, context=None, safe=False, **kwargs): + # type: (bytes, Optional[Type[ASN1_Class]], bool, **Any) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] # noqa: E501 + if not safe: + return cls.do_dec(s, context, safe, **kwargs) + try: + return cls.do_dec(s, context, safe, **kwargs) + except (UPER_Decoding_Error, ASN1_Error) as e: + return ASN1_DECODING_ERROR(s, exc=e), b"" + + @classmethod + def safedec(cls, s, context=None, **kwargs): + # type: (bytes, Optional[Type[ASN1_Class]], **Any) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] # noqa: E501 + return cls.dec(s, context, safe=True, **kwargs) + + +# No tagging hook: PER encodes no tag at all, so a field is left alone. +ASN1_Codecs.PER.register_stem(UPERcodec_Object) + + +######################### +# UPERcodec objects # +######################### + + +def _uper_int_range(size_len, uper_min, uper_max, oer_unsigned=False): + # type: (Optional[int], Optional[int], Optional[int], bool) -> Tuple[Optional[int], Optional[int]] # noqa: E501 + if uper_min is not None or uper_max is not None: + return uper_min, uper_max + if size_len in (1, 2, 4, 8) and oer_unsigned: + return 0, (256 ** size_len) - 1 + return None, None + + +class UPERcodec_INTEGER(UPERcodec_Object[int]): + tag = ASN1_Class_UNIVERSAL.INTEGER + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + i, # type: int + field=None, # type: Any + size_len=None, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=None, # type: Optional[bool] + uper_extensible=None, # type: Optional[bool] + **_kwargs # type: Any + ): + # type: (...) -> None + from scapy.asn1.constraints import codec_kwargs + legacy = dict(_kwargs) + if size_len is not None: + legacy["size_len"] = size_len + if uper_min is not None: + legacy["uper_min"] = uper_min + if uper_max is not None: + legacy["uper_max"] = uper_max + if oer_unsigned is not None: + legacy["oer_unsigned"] = oer_unsigned + if uper_extensible is not None: + legacy["uper_extensible"] = uper_extensible + kw = codec_kwargs(field, **legacy) + minimum, maximum = _uper_int_range( + kw["size_len"], kw["uper_min"], kw["uper_max"], kw["oer_unsigned"], + ) + uper_extensible = kw["uper_extensible"] + if uper_extensible and minimum is not None and maximum is not None: + if minimum <= i <= maximum: + enc.append_bit(0) + else: + enc.append_bit(1) + enc.append_unconstrained_whole_number(i) + return + if minimum is not None and maximum is not None: + UPER_constrained_int_enc(enc, i, minimum, maximum) + else: + enc.append_unconstrained_whole_number(i) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + field=None, # type: Any + pkt=None, # type: Any + size_len=None, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=None, # type: Optional[bool] + uper_extensible=None, # type: Optional[bool] + **_kwargs # type: Any + ): + # type: (...) -> ASN1_Object[int] + from scapy.asn1.constraints import codec_kwargs + legacy = dict(_kwargs) + if size_len is not None: + legacy["size_len"] = size_len + if uper_min is not None: + legacy["uper_min"] = uper_min + if uper_max is not None: + legacy["uper_max"] = uper_max + if oer_unsigned is not None: + legacy["oer_unsigned"] = oer_unsigned + if uper_extensible is not None: + legacy["uper_extensible"] = uper_extensible + kw = codec_kwargs(field, pkt=pkt, **legacy) + minimum, maximum = _uper_int_range( + kw["size_len"], kw["uper_min"], kw["uper_max"], kw["oer_unsigned"], + ) + uper_extensible = kw["uper_extensible"] + if uper_extensible and minimum is not None and maximum is not None: + if dec.read_bit(): + value = dec.read_unconstrained_whole_number() + return cls.asn1_object(value) + if minimum is not None and maximum is not None: + value = UPER_constrained_int_dec(dec, minimum, maximum) + else: + value = dec.read_unconstrained_whole_number() + return cls.asn1_object(value) + + +class UPERcodec_BOOLEAN(UPERcodec_Object[int]): + tag = ASN1_Class_UNIVERSAL.BOOLEAN + + @classmethod + def encode_into(cls, enc, i, **_kwargs): + # type: (UPER_Encoder, int, **Any) -> None + enc.append_bit(1 if i else 0) + + @classmethod + def dec_from_decoder(cls, dec, **_kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[int] + return cls.asn1_object(dec.read_bit()) + + +def _uper_bytes_to_bitstr(data, nbits): + # type: (bytes, int) -> str + bitstr = "".join(binrepr(orb(x)).zfill(8) for x in data) + return bitstr[:nbits] + + +def _uper_size_bounds(size_len, uper_min, uper_max): + # type: (Optional[int], Optional[int], Optional[int]) -> Tuple[Optional[int], Optional[int]] # noqa: E501 + # A SIZE constraint given as size_len is a fixed size, i.e. a range whose + # bounds coincide. + if size_len: + return size_len, size_len + return uper_min, uper_max + + +class UPERcodec_BIT_STRING(UPERcodec_Object[str]): + tag = ASN1_Class_UNIVERSAL.BIT_STRING + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + _s, # type: Any + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + **_kwargs # type: Any + ): + # type: (...) -> None + if isinstance(_s, tuple) and len(_s) == 2: + data, nbits = _s + s = bytes_encode(data) + elif isinstance(_s, str) and _s and all(c in "01" for c in _s): + nbits = len(_s) + padded = _s + "0" * ((8 - nbits % 8) % 8) + s = int(padded or "0", 2).to_bytes( + max(1, len(padded) // 8), "big" + ) + else: + s = bytes_encode(_s) + nbits = 8 * len(s) + minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) + if minimum is not None and maximum is not None: + _uper_check_size(cls.__name__, "bits", nbits, minimum, maximum) + if minimum != maximum: + enc.append_non_negative_binary_integer( + nbits - minimum, UPER_bits_for_range(maximum - minimum) + ) + enc.append_bits(s, nbits) + else: + # X.691 16.11: the determinant counts bits, not octets, and no + # padding is inserted before whatever follows the bit string. + enc.append_fragmented( + nbits, + # Fragments hold whole multiples of 16K bits, so every chunk + # but the last starts and ends on an octet boundary. + lambda offset, size: enc.append_bits( + s[offset // 8:(offset + size + 7) // 8], size + ), + ) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + **_kwargs # type: Any + ): + # type: (...) -> ASN1_Object[str] + minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) + if minimum is not None and maximum is not None: + nbits = minimum + if minimum != maximum: + nbits += dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + else: + fragments = [] # type: List[bytes] + sizes = [] # type: List[int] + + def read_fragment(size): + # type: (int) -> None + fragments.append(dec.read_bits(size)) + sizes.append(size) + + dec.read_fragmented(read_fragment) + return cls.asn1_object( + _uper_bytes_to_bitstr(b"".join(fragments), sum(sizes)) + ) + raw = dec.read_bits(nbits) + return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)) + + +class UPERcodec_STRING(UPERcodec_Object[str]): + tag = ASN1_Class_UNIVERSAL.STRING + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + _s, # type: Union[str, bytes] + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + **_kwargs # type: Any + ): + # type: (...) -> None + s = bytes_encode(_s) + minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) + UPER_octet_string_enc(enc, s, minimum, maximum) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + **_kwargs # type: Any + ): + # type: (...) -> ASN1_Object[Any] + minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) + raw = UPER_octet_string_dec(dec, minimum, maximum) + return cls.asn1_object(raw) + + +class UPERcodec_NULL(UPERcodec_Object[None]): + tag = ASN1_Class_UNIVERSAL.NULL + + @classmethod + def encode_into(cls, enc, _s, **_kwargs): + # type: (UPER_Encoder, Any, **Any) -> None + # NULL has an empty encoding. + return + + @classmethod + def dec_from_decoder(cls, dec, **_kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[None] + return cls.asn1_object(None) + + @classmethod + def do_dec(cls, s, context=None, safe=False, **kwargs): + # type: (bytes, Optional[Type[ASN1_Class]], bool, **Any) -> Tuple[ASN1_Object[None], bytes] # noqa: E501 + # NULL occupies no bits at all, so the input is left untouched. + return cls.asn1_object(None), s + + +class UPERcodec_OID(UPERcodec_Object[bytes]): + tag = ASN1_Class_UNIVERSAL.OID + + @classmethod + def encode_into(cls, enc, _oid, **_kwargs): + # type: (UPER_Encoder, AnyStr, **Any) -> None + oid = bytes_encode(_oid) + if oid: + lst = [int(x) for x in oid.split(b".")] + lst = [40 * lst[0] + lst[1]] + lst[2:] + else: + lst = [] + body = b"".join(BER_num_enc(k) for k in lst) + enc.append_fragmented( + len(body), + lambda offset, size: enc.append_bytes(body[offset:offset + size]), + ) + + @classmethod + def dec_from_decoder(cls, dec, **_kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[bytes] + fragments = [] # type: List[bytes] + dec.read_fragmented(lambda size: fragments.append(dec.read_bytes(size))) + content = b"".join(fragments) + lst = [] + while content: + val, content = BER_num_dec(content) + lst.append(val) + if len(lst) > 0: + lst.insert(0, lst[0] // 40) + lst[1] %= 40 + return cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)) + + +def UPER_enumerated_enc(enc, value, enum_values): + # type: (UPER_Encoder, int, List[int]) -> None + if not enum_values: + raise UPER_Encoding_Error("UPER_enumerated_enc: empty enumeration") + try: + index = enum_values.index(value) + except ValueError: + raise UPER_Encoding_Error( + "UPER_enumerated_enc: unknown enumeration value %r" % value + ) + UPER_choice_index_enc(enc, index, len(enum_values)) + + +def UPER_enumerated_dec(dec, enum_values): + # type: (UPER_Decoder, List[int]) -> int + if not enum_values: + raise UPER_Decoding_Error("UPER_enumerated_dec: empty enumeration") + index = UPER_choice_index_dec(dec, len(enum_values)) + if index >= len(enum_values): + raise UPER_Decoding_Error( + "UPER_enumerated_dec: index %i out of range" % index + ) + return enum_values[index] + + +class UPERcodec_ENUMERATED(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.ENUMERATED + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + i, # type: int + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + uper_enum_values=None, # type: Optional[List[int]] + uper_extensible=False, # type: bool + **_kwargs # type: Any + ): + # type: (...) -> None + if uper_enum_values is not None: + if uper_extensible: + # X.691 14.3: a one bit prefix says whether the value is an + # extension addition. Only root values can be encoded. + if i not in uper_enum_values: + raise UPER_Encoding_Error( + "UPERcodec_ENUMERATED: extension additions are not " + "supported" + ) + enc.append_bit(0) + UPER_enumerated_enc(enc, i, uper_enum_values) + return + minimum, maximum = cls._range( + size_len, uper_min, uper_max, UPER_Encoding_Error + ) + UPER_constrained_int_enc(enc, i, minimum, maximum) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + uper_enum_values=None, # type: Optional[List[int]] + uper_extensible=False, # type: bool + **_kwargs # type: Any + ): + # type: (...) -> ASN1_Object[int] + if uper_enum_values is not None: + if uper_extensible and dec.read_bit(): + raise UPER_Decoding_Error( + "UPERcodec_ENUMERATED: extension additions are not " + "supported" + ) + return cls.asn1_object(UPER_enumerated_dec(dec, uper_enum_values)) + minimum, maximum = cls._range( + size_len, uper_min, uper_max, UPER_Decoding_Error + ) + value = dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + minimum + return cls.asn1_object(value) + + @staticmethod + def _range(size_len, uper_min, uper_max, error): + # type: (Optional[int], Optional[int], Optional[int], Any) -> Tuple[int, int] # noqa: E501 + # Without the enumeration itself the index range has to come from + # the declared bounds; deriving it from the value at hand would + # make the width depend on the value, which the decoder cannot + # reproduce. + minimum = uper_min if uper_min is not None else 0 + maximum = uper_max if uper_max is not None else (size_len or None) + if maximum is None: + raise error("UPERcodec_ENUMERATED: missing range") + return minimum, maximum + + +class UPERcodec_SEQUENCE(UPERcodec_Object[Union[bytes, List[Any]]]): + tag = ASN1_Class_UNIVERSAL.SEQUENCE + + @classmethod + def encode_into(cls, enc, _ll, **_kwargs): + # type: (UPER_Encoder, Any, **Any) -> None + # A finished encoding is padded to an octet boundary, so its real bit + # length is lost and it cannot be spliced into a bitstream. Sequences + # are encoded through the ASN1F_SEQUENCE hooks instead. + raise UPER_Encoding_Error( + "UPERcodec_SEQUENCE: schema-defined field order required" + ) + + @classmethod + def enc(cls, _ll, **_kwargs): + # type: (Union[bytes, List[UPERcodec_Object[Any]]], **Any) -> bytes + if isinstance(_ll, bytes): + return _ll + raise UPER_Encoding_Error( + "UPERcodec_SEQUENCE: schema-defined field order required" + ) + + @classmethod + def dec_from_decoder(cls, dec, **_kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[Union[bytes, List[Any]]] + raise UPER_Decoding_Error( + "UPERcodec_SEQUENCE: decoding requires schema-defined field order", + remaining=dec.remaining() + ) + + +class UPERcodec_SET(UPERcodec_SEQUENCE): + tag = ASN1_Class_UNIVERSAL.SET + + +class UPERcodec_IPADDRESS(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.IPADDRESS + + @classmethod + def encode_into(cls, enc, ipaddr_ascii, **_kwargs): + # type: (UPER_Encoder, str, **Any) -> None + try: + s = inet_aton(ipaddr_ascii) + except Exception: + raise UPER_Encoding_Error("IPv4 address could not be encoded") + UPER_octet_string_enc(enc, s, 4, 4) + + @classmethod + def dec_from_decoder(cls, dec, **_kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[str] + raw = UPER_octet_string_dec(dec, 4, 4) + try: + ipaddr_ascii = inet_ntoa(raw) + except Exception: + raise UPER_Decoding_Error("IP address could not be decoded") + return cls.asn1_object(ipaddr_ascii) + + +class UPERcodec_COUNTER32(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER32 + + +class UPERcodec_COUNTER64(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER64 + + +class UPERcodec_GAUGE32(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.GAUGE32 + + +class UPERcodec_TIME_TICKS(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.TIME_TICKS + + +# string aliases +class UPERcodec_UTF8_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTF8_STRING + + +class UPERcodec_NUMERIC_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.NUMERIC_STRING + + +class UPERcodec_PRINTABLE_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING + + +class UPERcodec_T61_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.T61_STRING + + +class UPERcodec_VIDEOTEX_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.VIDEOTEX_STRING + + +class UPERcodec_IA5_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.IA5_STRING + + +class UPERcodec_GENERAL_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERAL_STRING + + +class UPERcodec_UTC_TIME(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTC_TIME + + +class UPERcodec_GENERALIZED_TIME(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME + + +class UPERcodec_ISO646_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.ISO646_STRING + + +class UPERcodec_UNIVERSAL_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UNIVERSAL_STRING + + +class UPERcodec_BMP_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.BMP_STRING + + +################################ +# ASN1F compound helpers # +################################ + +from scapy.asn1.compound import ( # noqa: E402 + uper_choice_decode_from_decoder as uper_choice_m2i_from_decoder, + uper_choice_encode_into, + uper_packet_decode_from_decoder as uper_packet_m2i_from_decoder, + uper_packet_encode_into, + uper_sequence_of_decode_from_decoder as uper_sequence_of_m2i_from_decoder, + uper_sequence_of_encode_into, + write_uper_presence_bits, + read_uper_presence_bits, + sequence_decode_from as _uper_sequence_decode_from, + sequence_encode_to as _uper_sequence_encode_to, + sequence_of_decode_from as _uper_sequence_of_decode_from, + sequence_of_encode_to as _uper_sequence_of_encode_to, + choice_decode_from as _uper_choice_decode_from, + choice_encode_to as _uper_choice_encode_to, +) + +def uper_sequence_m2i(field, pkt, s): + from scapy.asn1.context import UPER_DecoderContext + dec = UPER_DecoderContext(s) + _uper_sequence_decode_from(field, pkt, dec) + return [], dec.remaining() + + +def uper_sequence_build(field, pkt): + from scapy.asn1fields import ASN1F_field + from scapy.asn1.context import UPER_EncoderContext + enc = UPER_EncoderContext() + _uper_sequence_encode_to(field, pkt, enc) + return ASN1F_field.i2m(field, pkt, enc.finish()) + + +def uper_sequence_of_m2i(field, pkt, s): + from scapy.asn1.context import UPER_DecoderContext + dec = UPER_DecoderContext(s) + _uper_sequence_of_decode_from(field, pkt, dec) + return getattr(pkt, field.name), dec.remaining() + + +def uper_sequence_of_build(field, pkt): + from scapy.asn1.context import UPER_EncoderContext + enc = UPER_EncoderContext() + _uper_sequence_of_encode_to(field, pkt, enc) + return field.i2m(pkt, enc.finish()) + + +def uper_choice_m2i(field, pkt, s): + from scapy.asn1.context import UPER_DecoderContext + dec = UPER_DecoderContext(s) + _uper_choice_decode_from(field, pkt, dec) + return getattr(pkt, field.name), dec.remaining() + + +def uper_choice_i2m(field, pkt, x): + from scapy.asn1.context import UPER_EncoderContext + enc = UPER_EncoderContext() + _uper_choice_encode_to(field, pkt, enc) + return field._tagging_enc(pkt, enc.finish(), explicit_tag=field.explicit_tag) + + +def uper_packet_i2m(field, pkt, x): + from scapy.asn1.compound import packet_encode_to + from scapy.asn1.context import UPER_EncoderContext + enc = UPER_EncoderContext() + packet_encode_to(field, pkt, enc, x) + return field._tagging_enc( + pkt, enc.finish(), + implicit_tag=field.implicit_tag, + explicit_tag=field.explicit_tag, + ) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 6681d715ab5..6d43bed875e 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -6,6 +6,12 @@ """ Classes that implement ASN.1 data structures. + +ASN.1 schema fields form a tree (``ASN1F_SEQUENCE``, ``ASN1F_CHOICE``, …), +not a flat ``fields_desc`` list like Scapy ``Field`` instances. The +``encode_to`` / ``decode_from`` methods are the tree analogue of +``Field.addfield`` / ``Field.getfield``; ``build`` / ``dissect`` delegate to +those entry points for backward compatibility. """ import copy @@ -17,6 +23,7 @@ ASN1_BOOLEAN, ASN1_Class, ASN1_Class_UNIVERSAL, + ASN1_Codecs, ASN1_Decoding_Error, ASN1_Error, ASN1_INTEGER, @@ -28,7 +35,11 @@ from scapy.asn1.ber import ( BER_Decoding_Error, BER_id_dec, + BER_tagging_dec, + BER_tagging_enc, ) +from scapy.asn1.constraints import encoding_params, normalize_constraints +from scapy.asn1.tag import asn1_tag_parts from scapy.base_classes import BasePacket from scapy.volatile import ( GeneralizedTime, @@ -70,13 +81,6 @@ class ASN1F_element(object): pass -def _field_hook(pkt, name): - # type: (Any, str) -> Any - # Contrib codecs (OER/UPER/…) may override compound field operations. - # Returns None when the codec keeps the default BER behaviour. - return pkt.ASN1_codec.hook(name) - - ########################## # Basic ASN1 Field # ########################## @@ -112,9 +116,8 @@ def __init__(self, else: self.default = self.ASN1_tag.asn1_object(default) # type: ignore self.size_len = size_len - # Contrib codecs (OER/UPER/…) pass constraints here, e.g. - # oer_unsigned=, uper_min=/uper_max=, uper_extensible=. - self.codec_opts = codec_opts # type: Dict[str, Any] + self._init_codec_opts = codec_opts # type: Dict[str, Any] + self.constraints = normalize_constraints(codec_opts, size_len=size_len) self.flexible_tag = flexible_tag if (implicit_tag is not None) and (explicit_tag is not None): err_msg = "field cannot be both implicitly and explicitly tagged" @@ -125,34 +128,65 @@ def __init__(self, self.network_tag = int(implicit_tag or explicit_tag or self.ASN1_tag) self.owners = [] # type: List[Type[ASN1_Packet]] + + @property + def codec_opts(self): + # type: () -> Dict[str, Any] + """Deprecated view of constraint kwargs for backward compatibility.""" + return dict(self._init_codec_opts) + + def _codec_kwargs(self, pkt=None): + # type: (Any) -> Dict[str, Any] + """Deprecated; use ``scapy.asn1.constraints.codec_kwargs``.""" + from scapy.asn1.constraints import codec_kwargs + return codec_kwargs(self, pkt=pkt) + + def _constraints_kwargs(self, pkt=None): + # type: (Any) -> Dict[str, Any] + """Deprecated alias of ``_codec_kwargs``.""" + return self._codec_kwargs(pkt) + def register_owner(self, cls): # type: (Type[ASN1_Packet]) -> None self.owners.append(cls) - def _apply_diff_tag(self, diff_tag): - # type: (Optional[int]) -> None - # this implies that flexible_tag was True + def _apply_diff_tag(self, pkt, diff_tag): + # type: (ASN1_Packet, Optional[int]) -> None + # flexible_tag was True: record the observed tag on the packet so + # shared field descriptors stay immutable across interleaved decodes. if diff_tag is not None: - if self.implicit_tag is not None: - self.implicit_tag = diff_tag - elif self.explicit_tag is not None: - self.explicit_tag = diff_tag + observed = getattr(pkt, "_asn1_observed_tags", None) + if observed is None: + pkt._asn1_observed_tags = {} # type: ignore[attr-defined] + observed = pkt._asn1_observed_tags # type: ignore[attr-defined] + observed[self.name] = diff_tag + + def _tagging_tags(self, pkt): + # type: (ASN1_Packet) -> Tuple[Optional[int], Optional[int]] + imp = self.implicit_tag + exp = self.explicit_tag + if self.flexible_tag: + observed = getattr(pkt, "_asn1_observed_tags", None) or {} + diff = observed.get(self.name) + if diff is not None: + if imp is not None: + imp = diff + elif exp is not None: + exp = diff + return imp, exp def _tagging_dec(self, pkt, s, **kwargs): # type: (ASN1_Packet, bytes, **Any) -> Tuple[Optional[int], bytes] - # Only BER puts the tag of a field on the wire: a codec that does not - # hook the tagging leaves the encoding alone. - hook = _field_hook(pkt, "tagging_dec") - if hook is None: - return None, s - return cast(Tuple[Optional[int], bytes], hook(s, **kwargs)) + # Only BER puts the tag of a field on the wire. + if pkt.ASN1_codec is ASN1_Codecs.BER: + return BER_tagging_dec(s, **kwargs) + return None, s def _tagging_enc(self, pkt, s, **kwargs): # type: (ASN1_Packet, bytes, **Any) -> bytes - hook = _field_hook(pkt, "tagging_enc") - if hook is None: - return s - return cast(bytes, hook(s, **kwargs)) + if pkt.ASN1_codec is ASN1_Codecs.BER: + return BER_tagging_enc(s, **kwargs) + return s def _apply_tagging_dec(self, s, pkt, hidden_tag=None, **kwargs): # type: (bytes, ASN1_Packet, Optional[Any], **Any) -> bytes @@ -168,24 +202,9 @@ def _apply_tagging_dec(self, s, pkt, hidden_tag=None, **kwargs): safe=self.flexible_tag, **kwargs, ) - self._apply_diff_tag(diff_tag) + self._apply_diff_tag(pkt, diff_tag) return s - def _codec_kwargs(self, pkt): - # type: (ASN1_Packet) -> Dict[str, Any] - # BER ignores unknown keys via **_kwargs. Contrib codecs read - # constraints from field.codec_opts. - kwargs = {"size_len": self.size_len} # type: Dict[str, Any] - kwargs.update(self.codec_opts) - return kwargs - - def _use_object_enc(self, pkt, item): - # type: (ASN1_Packet, ASN1_Object[Any]) -> bool - # Contrib codecs may force codec.enc(**kwargs) via field hooks. - hook = _field_hook(pkt, "use_object_enc") - if hook is not None: - return cast(bool, hook(self, pkt, item)) - return self.size_len is None and not self.codec_opts def _encode_item(self, pkt, item): # type: (ASN1_Packet, Any) -> bytes @@ -202,15 +221,18 @@ def _encode_item(self, pkt, item): "Encoding Error: got %r instead of an %r for field [%s]" % (item, self.ASN1_tag, self.name) ) - if self._use_object_enc(pkt, item): - return item.enc(pkt.ASN1_codec) item = item.val elif hasattr(item, "self_build"): # Packet values (e.g. ASN1F_STRING_PacketField) must still go through # the BER type codec so the universal tag/length are applied. item = item.self_build() codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - return codec.enc(item, **self._codec_kwargs(pkt)) + from scapy.asn1.constraints import codec_kwargs + kw = codec_kwargs(self, pkt=pkt) + legacy = {} # type: Dict[str, Any] + if kw.get("size_len") is not None: + legacy["size_len"] = kw["size_len"] + return codec.enc(item, field=self, pkt=pkt, **legacy) def i2repr(self, pkt, x): # type: (ASN1_Packet, _I) -> str @@ -237,17 +259,18 @@ def m2i(self, pkt, s): s = self._apply_tagging_dec(s, pkt, _fname=self.name) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) dec = codec.safedec if self.flexible_tag else codec.dec - return dec(s, context=self.context, **self._codec_kwargs(pkt)) # type: ignore + return dec(s, context=self.context, field=self, pkt=pkt) # type: ignore # noqa: E501 def i2m(self, pkt, x): # type: (ASN1_Packet, Union[bytes, _I, _A]) -> bytes if x is None: return b"" s = self._encode_item(pkt, x) + imp, exp = self._tagging_tags(pkt) return self._tagging_enc( pkt, s, - implicit_tag=self.implicit_tag, - explicit_tag=self.explicit_tag, + implicit_tag=imp, + explicit_tag=exp, ) def any2i(self, pkt, x): @@ -257,13 +280,13 @@ def any2i(self, pkt, x): def extract_packet(self, cls, # type: Type[ASN1_Packet] s, # type: bytes - _underlayer=None # type: Optional[ASN1_Packet] + _parent=None # type: Optional[ASN1_Packet] ): # type: (...) -> Tuple[ASN1_Packet, bytes] try: - c = cls(s, _underlayer=_underlayer) + c = cls(s, _parent=_parent) except ASN1F_badsequence: - c = packet.Raw(s, _underlayer=_underlayer) # type: ignore + c = packet.Raw(s, _parent=_parent) # type: ignore cpad = c.getlayer(packet.Raw) s = b"" if cpad is not None: @@ -272,15 +295,74 @@ def extract_packet(self, del cpad.underlayer.payload return c, s + def m2i_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, Any) -> Any + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + from scapy.asn1.constraints import codec_kwargs + kw = codec_kwargs(self, pkt=pkt) + legacy = {k: v for k, v in kw.items() if v is not None} + return codec.dec_from_decoder( # type: ignore[attr-defined] + dec, field=self, pkt=pkt, **legacy, + ) + + def dissect_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, Any) -> None + self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) + + def encode_into(self, enc, pkt, value=None): + # type: (Any, ASN1_Packet, Any) -> None + if value is None: + value = getattr(pkt, self.name) + if value is None: + return + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + if isinstance(value, ASN1_Object): + if (self.ASN1_tag == ASN1_Class_UNIVERSAL.ANY or + value.tag == ASN1_Class_UNIVERSAL.RAW or + value.tag == ASN1_Class_UNIVERSAL.ERROR or + self.ASN1_tag == value.tag): + raw = value.val + else: + raise ASN1_Error( + "Encoding Error: got %r instead of an %r for field [%s]" % + (value, self.ASN1_tag, self.name) + ) + else: + raw = value + from scapy.asn1.constraints import codec_kwargs + kw = codec_kwargs(self, pkt=pkt) + legacy = {k: v for k, v in kw.items() if v is not None} + codec.encode_into( # type: ignore[attr-defined] + enc, raw, field=self, pkt=pkt, **legacy, + ) + + def encode_to(self, pkt, enc): + # type: (ASN1_Packet, Any) -> None + if pkt.ASN1_codec is ASN1_Codecs.PER: + self.encode_into(getattr(enc, "inner", enc), pkt) + else: + enc.write(self.i2m(pkt, getattr(pkt, self.name))) + + def decode_from(self, pkt, dec): + # type: (ASN1_Packet, Any) -> None + if pkt.ASN1_codec is ASN1_Codecs.PER: + self.dissect_from_decoder(pkt, getattr(dec, "inner", dec)) + else: + val, remain = self.m2i(pkt, dec.remaining()) + self.set_val(pkt, val) + dec.set_remainder(remain) + def build(self, pkt): # type: (ASN1_Packet) -> bytes - return self.i2m(pkt, getattr(pkt, self.name)) + enc = pkt.ASN1_codec.new_encoder() + self.encode_to(pkt, enc) + return enc.finish() def dissect(self, pkt, s): # type: (ASN1_Packet, bytes) -> bytes - v, s = self.m2i(pkt, s) - self.set_val(pkt, v) - return s + dec = pkt.ASN1_codec.new_decoder(s) + self.decode_from(pkt, dec) + return dec.remaining() def do_copy(self, x): # type: (Any) -> Any @@ -368,6 +450,11 @@ def __init__(self, i2s[k] = enum[k] s2i[enum[k]] = k + def uper_enum_values(self): + # type: () -> List[int] + return sorted(self.i2s) + + def i2m(self, pkt, # type: ASN1_Packet s, # type: Union[bytes, str, int, ASN1_INTEGER] @@ -519,7 +606,14 @@ class ASN1F_SEQUENCE(ASN1F_field[List[Any], List[Any]]): def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None name = "dummy_seq_name" - default = [field.default for field in seq] + default = [] + for field in seq: + if isinstance(field, ASN1F_DEFAULT): + default.append(field._default) + elif isinstance(field, ASN1F_optional): + default.append(None) + else: + default.append(field.default) super(ASN1F_SEQUENCE, self).__init__( name, default, **kwargs ) @@ -559,41 +653,43 @@ def _dissect_sequence_children(self, pkt, s): def m2i(self, pkt, s): # type: (Any, bytes) -> Tuple[Any, bytes] - """ - ASN1F_SEQUENCE behaves transparently, with nested ASN1_objects being - dissected one by one. Because we use obj.dissect (see loop below) - instead of obj.m2i (as we trust dissect to do the appropriate set_vals) - we do not directly retrieve the list of nested objects. - Thus m2i returns an empty list (along with the proper remainder). - It is discarded by dissect() and should not be missed elsewhere. - """ - hook = _field_hook(pkt, "sequence_m2i") - if hook is not None: - return cast(Tuple[Any, bytes], hook(self, pkt, s)) - s = self._apply_tagging_dec(s, pkt, _fname=pkt.name) - codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - i, s, remain = codec.check_type_check_len(s) - s = self._dissect_sequence_children(pkt, s) - if len(s) > 0: - raise BER_Decoding_Error( - "unexpected remainder in %s" % pkt.name, - remaining=s, + dec = pkt.ASN1_codec.new_decoder(s) + self.decode_from(pkt, dec) + remain = dec.remaining() + if pkt.ASN1_codec is ASN1_Codecs.PER and remain: + from scapy.asn1.uper import UPER_Decoding_Error + raise UPER_Decoding_Error( + "unexpected remainder in %s" % pkt.__class__.__name__, ) return [], remain - def dissect(self, pkt, s): - # type: (Any, bytes) -> bytes - _, x = self.m2i(pkt, s) - return x + def encode_to(self, pkt, enc): + # type: (ASN1_Packet, Any) -> None + from scapy.asn1.compound import sequence_encode_to + sequence_encode_to(self, pkt, enc) + + def encode_into(self, enc, pkt, value=None): + # type: (Any, ASN1_Packet, Any) -> None + from scapy.asn1.compound import sequence_encode_to + if pkt.ASN1_codec is ASN1_Codecs.PER: + class _Ctx(object): + codec = ASN1_Codecs.PER + inner = enc + sequence_encode_to(self, pkt, _Ctx()) + return + super(ASN1F_SEQUENCE, self).encode_into(enc, pkt, value) + + def dissect_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, Any) -> None + class _Ctx(object): + codec = pkt.ASN1_codec + inner = dec + self.decode_from(pkt, _Ctx()) - def build(self, pkt): - # type: (ASN1_Packet) -> bytes - hook = _field_hook(pkt, "sequence_build") - if hook is not None: - return cast(bytes, hook(self, pkt)) - s = reduce(lambda x, y: x + y.build(pkt), - self.seq, b"") - return super(ASN1F_SEQUENCE, self).i2m(pkt, s) + def decode_from(self, pkt, dec): + # type: (ASN1_Packet, Any) -> None + from scapy.asn1.compound import sequence_decode_from + sequence_decode_from(self, pkt, dec) class ASN1F_SET(ASN1F_SEQUENCE): @@ -637,7 +733,7 @@ def __init__(self, elif hasattr(cls, "ASN1_root") or callable(cls): self.cls = cast("Type[ASN1_Packet]", cls) self._extract_packet = lambda s, pkt: self.extract_packet( - self.cls, s, _underlayer=pkt) + self.cls, s, _parent=pkt) self.holds_packets = 1 else: raise ValueError("cls should be an ASN1_Packet or ASN1_field") @@ -654,47 +750,21 @@ def is_empty(self, # type: (...) -> bool return ASN1F_field.is_empty(self, pkt) - def m2i(self, - pkt, # type: ASN1_Packet - s, # type: bytes - ): - # type: (...) -> Tuple[List[Any], bytes] - hook = _field_hook(pkt, "sequence_of_m2i") - if hook is not None: - return cast(Tuple[List[Any], bytes], hook(self, pkt, s)) - s = self._apply_tagging_dec(s, pkt) - codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - i, s, remain = codec.check_type_check_len(s) - lst = [] - while s: - c, s = self._extract_packet(s, pkt) # type: ignore - if c: - lst.append(c) - if len(s) > 0: - raise BER_Decoding_Error( - "unexpected remainder in %s" % pkt.name, - remaining=s, - ) - return lst, remain + def m2i(self, pkt, s): + # type: (ASN1_Packet, bytes) -> Tuple[List[Any], bytes] + dec = pkt.ASN1_codec.new_decoder(s) + self.decode_from(pkt, dec) + return getattr(pkt, self.name), dec.remaining() - def build(self, pkt): - # type: (ASN1_Packet) -> bytes - hook = _field_hook(pkt, "sequence_of_build") - if hook is not None: - return cast(bytes, hook(self, pkt)) - val = getattr(pkt, self.name) - if isinstance(val, ASN1_Object) and \ - val.tag == ASN1_Class_UNIVERSAL.RAW: - s = cast(Union[List[_SEQ_T], bytes], val) - elif val is None: - s = b"" - elif self.holds_packets: - s = b"".join(bytes(i) for i in val) - else: - # BER: element fields may carry implicit/explicit tags; i2m - # matches m2i()/fld.m2i(). (Packet elements use bytes() above.) - s = b"".join(self.fld.i2m(pkt, i) for i in val) - return self.i2m(pkt, s) + def encode_to(self, pkt, enc): + # type: (ASN1_Packet, Any) -> None + from scapy.asn1.compound import sequence_of_encode_to + sequence_of_encode_to(self, pkt, enc) + + def decode_from(self, pkt, dec): + # type: (ASN1_Packet, Any) -> None + from scapy.asn1.compound import sequence_of_decode_from + sequence_of_decode_from(self, pkt, dec) def i2repr(self, pkt, x): # type: (ASN1_Packet, _I) -> str @@ -745,9 +815,26 @@ def __init__(self, field): self._field = field def __getattr__(self, attr): - # type: (str) -> Optional[Any] + # type: (str) -> Any + if attr.startswith("_"): + raise AttributeError(attr) return getattr(self._field, attr) + @property + def fld(self): + # type: () -> ASN1F_field[Any, Any] + return self._field + + + def get_fields_list(self): + # type: () -> List[ASN1F_field[Any, Any]] + inner = self._field.get_fields_list() + if inner == [self._field]: + field = self._field.copy() + field.default = None + return [field] + return inner + def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[Any, bytes] try: @@ -761,25 +848,49 @@ def dissect(self, pkt, s): try: return self._field.dissect(pkt, s) except (ASN1_Error, ASN1F_badsequence, ASN1_Decoding_Error): - self.set_absent(pkt) + self.set_missing(pkt) return s - def set_absent(self, pkt): + def is_present(self, pkt): + # type: (ASN1_Packet) -> bool + # Delegate to the wrapped field: an optional SEQUENCE uses a dummy + # name and is empty iff all of its children are. + return not self._field.is_empty(pkt) + + def set_missing(self, pkt): # type: (ASN1_Packet) -> None """Called when the encoding does not carry the component.""" self._field.set_val(pkt, None) def is_empty(self, pkt): # type: (ASN1_Packet) -> bool - return self._field.is_empty(pkt) + return not self.is_present(pkt) + def build(self, pkt): # type: (ASN1_Packet) -> bytes # Through self, so that a DEFAULT component omits its default value. - if self.is_empty(pkt): + if not self.is_present(pkt): return b"" return self._field.build(pkt) + def dissect_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, Any) -> None + self._field.dissect_from_decoder(pkt, dec) + + def encode_into(self, enc, pkt, value=None): + # type: (Any, ASN1_Packet, Any) -> None + self._field.encode_into(enc, pkt, value) + + def encode_to(self, pkt, enc): + # type: (ASN1_Packet, Any) -> None + if self.is_present(pkt): + self._field.encode_to(pkt, enc) + + def decode_from(self, pkt, dec): + # type: (ASN1_Packet, Any) -> None + self._field.decode_from(pkt, dec) + def any2i(self, pkt, x): # type: (ASN1_Packet, Any) -> Any return self._field.any2i(pkt, x) @@ -803,19 +914,26 @@ def __init__(self, field, default): super(ASN1F_DEFAULT, self).__init__(field) self._default = default - def is_empty(self, pkt): + def get_fields_list(self): + # type: () -> List[ASN1F_field[Any, Any]] + inner = self._field.get_fields_list() + if inner == [self._field]: + return [self._field.copy()] + return inner + + def is_present(self, pkt): # type: (ASN1_Packet) -> bool val = getattr(pkt, self._field.name, None) if val is None: - return True + return False if isinstance(val, ASN1_Object): val = val.val default = self._default if isinstance(default, ASN1_Object): default = default.val - return bool(val == default) + return bool(val != default) - def set_absent(self, pkt): + def set_missing(self, pkt): # type: (ASN1_Packet) -> None self._field.set_val(pkt, self._default) @@ -861,9 +979,8 @@ def __init__(self, name, default, *args, **kwargs): **kwargs ) self.default = default - self.current_choice = None self.choices = {} # type: Dict[int, _CHOICE_T] - self.pktchoices = {} + self.pktchoices = {} # type: Dict[type, Tuple[Optional[int], Optional[int]]] for p in args: if hasattr(p, "ASN1_root"): p = cast('ASN1_Packet', p) @@ -882,9 +999,19 @@ def __init__(self, name, default, *args, **kwargs): else: # should be ASN1F_PACKET instance self.choices[p.network_tag] = p - self.pktchoices[hash(p.cls)] = (p.implicit_tag, p.explicit_tag) # noqa: E501 + self.pktchoices[p.cls] = (p.implicit_tag, p.explicit_tag) else: raise ASN1_Error("ASN1F_CHOICE: no tag found for one field") + # X.691 10.2: PER indexes alternatives in canonical tag order. + decl_items = list(self.choices.items()) + canon_items = sorted( + decl_items, + key=lambda item: asn1_tag_parts(item[0])[:2], + ) + self.canonical_order = [alt for _tag, alt in canon_items] + self.canonical_index = { + tag: i for i, (tag, _alt) in enumerate(canon_items) + } # type: Dict[int, int] @property def choice_order(self): @@ -915,60 +1042,38 @@ def choice_list(self): def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] - """ - First we have to retrieve the appropriate choice. - Then we extract the field/packet, according to this choice. - """ if len(s) == 0: raise ASN1_Error("ASN1F_CHOICE: got empty string") - hook = _field_hook(pkt, "choice_m2i") - if hook is not None: - return cast(Tuple[ASN1_Object[Any], bytes], hook(self, pkt, s)) - s = self._apply_tagging_dec(s, pkt) - tag, _ = BER_id_dec(s) - if tag in self.choices: - choice = self.choices[tag] - else: - if self.flexible_tag: - choice = ASN1F_field - else: + dec = pkt.ASN1_codec.new_decoder(s) + self.decode_from(pkt, dec) + return getattr(pkt, self.name), dec.remaining() + + def encode_to(self, pkt, enc): + # type: (ASN1_Packet, Any) -> None + from scapy.asn1.compound import choice_encode_to + choice_encode_to(self, pkt, enc) + + def encode_into(self, enc, pkt, value=None): + # type: (Any, ASN1_Packet, Any) -> None + if value is None: + value = getattr(pkt, self.name) + if pkt.ASN1_codec is ASN1_Codecs.PER: + from scapy.asn1.compound import uper_choice_encode_into + if value is None: + return + if self.alternative_index(value) is None: raise ASN1_Error( - "ASN1F_CHOICE: unexpected field in '%s' " - "(tag %s not in possible tags %s)" % ( - self.name, tag, list(self.choices.keys()) - ) + "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % + self.name ) - if hasattr(choice, "ASN1_root"): - # we don't want to import ASN1_Packet in this module... - return self.extract_packet(choice, s, _underlayer=pkt) # type: ignore - elif isinstance(choice, type): - return choice(self.name, b"").m2i(pkt, s) - else: - # XXX check properly if this is an ASN1F_PACKET - return choice.m2i(pkt, s) + uper_choice_encode_into(self, enc, pkt, value) + return + super(ASN1F_CHOICE, self).encode_into(enc, pkt, value) - def i2m(self, pkt, x): - # type: (ASN1_Packet, Any) -> bytes - hook = _field_hook(pkt, "choice_i2m") - if hook is not None: - return cast(bytes, hook(self, pkt, x)) - if x is None: - s = b"" - else: - # Use the packet codec for ASN1_Object values; bytes(x) would - # follow conf.ASN1_default_codec instead. - if isinstance(x, ASN1_Object): - s = x.enc(pkt.ASN1_codec) - else: - s = bytes(x) - if hash(type(x)) in self.pktchoices: - imp, exp = self.pktchoices[hash(type(x))] - s = self._tagging_enc( - pkt, s, - implicit_tag=imp, - explicit_tag=exp, - ) - return self._tagging_enc(pkt, s, explicit_tag=self.explicit_tag) + def decode_from(self, pkt, dec): + # type: (ASN1_Packet, Any) -> None + from scapy.asn1.compound import choice_decode_from + choice_decode_from(self, pkt, dec) def randval(self): # type: () -> RandChoice @@ -1019,53 +1124,36 @@ def _resolve_cls(self, pkt): def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[Any, bytes] - cls = self._resolve_cls(pkt) - if not hasattr(cls, "ASN1_root"): - # A normal Packet (!= ASN1) - return self.extract_packet(cls, s, _underlayer=pkt) - s = self._apply_tagging_dec( - s, pkt, - hidden_tag=cls.ASN1_root.ASN1_tag, # noqa: E501 - _fname=self.name, - ) - if not s: - return None, s - return self.extract_packet(cls, s, _underlayer=pkt) + dec = pkt.ASN1_codec.new_decoder(s) + self.decode_from(pkt, dec) + return getattr(pkt, self.name), dec.remaining() - def i2m(self, - pkt, # type: ASN1_Packet - x # type: Union[bytes, ASN1_Packet, None, ASN1_Object[Optional[ASN1_Packet]]] # noqa: E501 - ): - # type: (...) -> bytes - hook = _field_hook(pkt, "packet_i2m") - if hook is not None: - return cast(bytes, hook(self, pkt, x)) - if x is None: - s = b"" - elif isinstance(x, bytes): - s = x - elif isinstance(x, ASN1_Object): - if x.val: - s = bytes(x.val) - else: - s = b"" - else: - s = bytes(x) - if not hasattr(x, "ASN1_root"): - # A normal Packet (!= ASN1) - return s - return self._tagging_enc( - pkt, s, - implicit_tag=self.implicit_tag, - explicit_tag=self.explicit_tag, - ) + def encode_to(self, pkt, enc): + # type: (ASN1_Packet, Any) -> None + from scapy.asn1.compound import packet_encode_to + packet_encode_to(self, pkt, enc) + + def encode_into(self, enc, pkt, value=None): + # type: (Any, ASN1_Packet, Any) -> None + if pkt.ASN1_codec is ASN1_Codecs.PER: + from scapy.asn1.compound import uper_packet_encode_into + uper_packet_encode_into(self, enc, pkt, value) + return + super(ASN1F_PACKET, self).encode_into(enc, pkt, value) + + def decode_from(self, pkt, dec): + # type: (ASN1_Packet, Any) -> None + from scapy.asn1.compound import packet_decode_from + packet_decode_from(self, pkt, dec) def any2i(self, pkt, # type: ASN1_Packet x # type: Union[bytes, ASN1_Packet, None, ASN1_Object[Optional[ASN1_Packet]]] # noqa: E501 ): # type: (...) -> 'ASN1_Packet' - if hasattr(x, "add_underlayer"): + if hasattr(x, "add_parent"): + x.add_parent(pkt) # type: ignore + elif hasattr(x, "add_underlayer"): x.add_underlayer(pkt) # type: ignore return super(ASN1F_PACKET, self).any2i(pkt, x) @@ -1106,7 +1194,7 @@ def m2i(self, pkt, s): # type: ignore raise BER_Decoding_Error("wrong bit string", remaining=s) if bit_string.val_readable: p, s = self.extract_packet(self.cls, bit_string.val_readable, - _underlayer=pkt) + _parent=pkt) else: return None, bit_string.val_readable if len(s) > 0: @@ -1218,4 +1306,4 @@ def __init__(self, def m2i(self, pkt, s): # type: ignore # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Packet, bytes] val = super(ASN1F_STRING_ENCAPS, self).m2i(pkt, s) - return self.cls(val[0].val, _underlayer=pkt), val[1] + return self.cls(val[0].val, _parent=pkt), val[1] diff --git a/scapy/asn1packet.py b/scapy/asn1packet.py index 058aecc0edb..ac5117bfba9 100644 --- a/scapy/asn1packet.py +++ b/scapy/asn1packet.py @@ -48,8 +48,13 @@ def self_build(self): # type: () -> bytes if self.raw_packet_cache is not None: return self.raw_packet_cache - return self.ASN1_root.build(self) + enc = self.ASN1_codec.new_encoder() + self.ASN1_root.encode_to(self, enc) + return enc.finish() def do_dissect(self, x): # type: (bytes) -> bytes - return self.ASN1_root.dissect(self, x) + self._asn1_observed_tags = {} # type: ignore[attr-defined] + dec = self.ASN1_codec.new_decoder(x) + self.ASN1_root.decode_from(self, dec) + return dec.remaining() diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py index 88057a02405..ef1e16b9964 100644 --- a/scapy/contrib/oer.py +++ b/scapy/contrib/oer.py @@ -5,930 +5,6 @@ # scapy.contrib.description = ASN.1 Octet Encoding Rules (OER) # scapy.contrib.status = loads -""" -Octet Encoding Rules (OER) for ASN.1 +"""Compat re-export of ``scapy.asn1.oer``.""" -Basic-OER as specified in ITU-T X.696 | ISO/IEC 8825-7. - -``ASN1F_SEQUENCE`` emits the preamble required by 16.2.2: a presence bit per -``ASN1F_optional``/``ASN1F_DEFAULT`` component, preceded by an extension bit -for sequences declared with ``oer_extensible=True``. Fixed size constraints -are expressed with ``size_len=`` (octets for strings, bits for BIT STRING). - -Tags declared on a field are not encoded: OER only puts a tag on the wire for -the chosen alternative of an ``ASN1F_CHOICE`` (20.2), so the ``implicit_tag=`` -and ``explicit_tag=`` of the alternatives are what selects it. - -Not supported yet: extension additions (an encoding that carries them is -refused rather than misparsed), SET, REAL, and the canonical variant (C-OER). -""" - -import struct - -from scapy.error import warning -from scapy.compat import chb, orb, bytes_encode -from scapy.utils import binrepr, inet_aton, inet_ntoa -from scapy.asn1.ber import BER_num_dec, BER_num_enc -from scapy.asn1.asn1 import ( - ASN1_Class, - ASN1_Class_UNIVERSAL, - ASN1_Codecs, - ASN1_DECODING_ERROR, - ASN1_Decoding_Error, - ASN1_Encoding_Error, - ASN1_Error, - ASN1_Object, - _ASN1_ERROR, -) -# Re-exported: DEFAULT components are what the preamble bits describe. -from scapy.asn1fields import ASN1F_DEFAULT # noqa: F401 - -from typing import ( - Any, - AnyStr, - Dict, - Generic, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, - cast, -) - -################## -# OER encoding # -################## - - -class OER_Exception(Exception): - pass - - -class OER_Encoding_Error(ASN1_Encoding_Error): - def __init__(self, - msg, # type: str - encoded=None, # type: Optional[Union['OERcodec_Object[Any]', str]] - remaining=b"" # type: bytes - ): - # type: (...) -> None - Exception.__init__(self, msg) - self.remaining = remaining - self.encoded = encoded - - def __str__(self): - # type: () -> str - s = Exception.__str__(self) - if isinstance(self.encoded, ASN1_Object): - s += "\n### Already encoded ###\n%s" % self.encoded.strshow() - else: - s += "\n### Already encoded ###\n%r" % self.encoded - s += "\n### Remaining ###\n%r" % self.remaining - return s - - -class OER_Decoding_Error(ASN1_Decoding_Error): - def __init__(self, - msg, # type: str - decoded=None, # type: Optional[Any] - remaining=b"" # type: bytes - ): - # type: (...) -> None - Exception.__init__(self, msg) - self.remaining = remaining - self.decoded = decoded - - def __str__(self): - # type: () -> str - s = Exception.__str__(self) - if isinstance(self.decoded, ASN1_Object): - s += "\n### Already decoded ###\n%s" % self.decoded.strshow() - else: - s += "\n### Already decoded ###\n%r" % self.decoded - s += "\n### Remaining ###\n%r" % self.remaining - return s - - -# OER tag classes (bits 8-7 of the first identifier octet) -OER_CLASS_UNIVERSAL = 0x00 -OER_CLASS_APPLICATION = 0x40 -OER_CLASS_CONTEXT = 0x80 -OER_CLASS_PRIVATE = 0xc0 - - -def _OER_check_len(name, s, number_of_bytes, offset=0): - # type: (str, bytes, int, int) -> None - """Raise unless s carries number_of_bytes octets past its first offset.""" - available = len(s) - offset - if available < number_of_bytes: - raise OER_Decoding_Error( - "%s: Got %i bytes while expecting %i" % - (name, available, number_of_bytes), - remaining=s - ) - - -def OER_len_enc(ll): - # type: (int) -> bytes - if ll < 128: - return chb(ll) - encoded = [] - value = ll - while value > 0: - encoded.insert(0, value & 0xff) - value >>= 8 - if len(encoded) > 127: - raise OER_Exception( - "OER_len_enc: Length too long (%i) to be encoded" % len(encoded) - ) - return chb(0x80 | len(encoded)) + bytes(encoded) - - -def OER_len_dec(s): - # type: (bytes) -> Tuple[int, bytes] - if not s: - raise OER_Decoding_Error("OER_len_dec: got empty string", remaining=s) - tmp_len = orb(s[0]) - if not tmp_len & 0x80: - return tmp_len, s[1:] - tmp_len &= 0x7f - _OER_check_len("OER_len_dec", s, tmp_len, offset=1) - ll = 0 - for c in s[1:tmp_len + 1]: - ll <<= 8 - ll |= orb(c) - return ll, s[tmp_len + 1:] - - -def OER_signed_integer_enc(i): - # type: (int) -> bytes - # X.696 10.4: the shortest two's complement encoding. A negative value - # needs one bit less than its magnitude suggests, as -2**(8n-1) still - # fits in n octets, hence the increment before measuring. - magnitude = i + 1 if i < 0 else i - number_of_bytes = (magnitude.bit_length() + 8) // 8 - value = i & ((1 << (8 * number_of_bytes)) - 1) - return OER_len_enc(number_of_bytes) + value.to_bytes(number_of_bytes, "big") - - -def OER_signed_integer_dec(s): - # type: (bytes) -> Tuple[int, bytes] - number_of_bytes, s = OER_len_dec(s) - _OER_check_len("OER_signed_integer_dec", s, number_of_bytes) - if number_of_bytes == 0: - raise OER_Decoding_Error( - "OER_signed_integer_dec: got an empty length determinant", - remaining=s - ) - value = int.from_bytes(s[:number_of_bytes], "big") - number_of_bits = 8 * number_of_bytes - if value & (1 << (number_of_bits - 1)): - value -= (1 << number_of_bits) - 1 - value -= 1 - return value, s[number_of_bytes:] - - -def OER_unsigned_integer_enc(i): - # type: (int) -> bytes - if i < 0: - raise OER_Encoding_Error( - "OER_unsigned_integer_enc: %i is negative" % i - ) - number_of_bits = max(i.bit_length(), 1) - number_of_bytes = (number_of_bits + 7) // 8 - return OER_len_enc(number_of_bytes) + i.to_bytes(number_of_bytes, "big") - - -def OER_unsigned_integer_dec(s): - # type: (bytes) -> Tuple[int, bytes] - number_of_bytes, s = OER_len_dec(s) - _OER_check_len("OER_unsigned_integer_dec", s, number_of_bytes) - value = int.from_bytes(s[:number_of_bytes], "big") - return value, s[number_of_bytes:] - - -def OER_tag_enc(n, tag_class=OER_CLASS_CONTEXT): - # type: (int, int) -> bytes - if n < 63: - return chb(tag_class | n) - tag = bytearray([tag_class | 0x3f]) - encoded = [] - value = n - while value > 0: - encoded.append(0x80 | (value & 0x7f)) - value >>= 7 - encoded[0] &= 0x7f - encoded.reverse() - tag.extend(encoded) - return bytes(tag) - - -def OER_tag_dec(s): - # type: (bytes) -> Tuple[int, int, bytes] - if not s: - raise OER_Decoding_Error("OER_tag_dec: got empty string", remaining=s) - first = orb(s[0]) - tag_class = first & 0xc0 - tag_number = first & 0x3f - if tag_number != 0x3f: - return tag_class, tag_number, s[1:] - tag_number = 0 - i = 1 - while i < len(s): - c = orb(s[i]) - tag_number <<= 7 - tag_number |= c & 0x7f - i += 1 - if not (c & 0x80): - break - else: - raise OER_Decoding_Error("OER_tag_dec: unfinished tag", remaining=s) - return tag_class, tag_number, s[i:] - - -def _OER_tag_parts(identifier): - # type: (int) -> Tuple[int, int] - # ASN1F_* fields describe tags as BER identifier octets: class in the top - # two bits, constructed flag in 0x20 and tag number in the low five bits. - # X.696 8.7 only keeps the class and the number, so the constructed flag - # must not leak into the encoded tag number. - return identifier & 0xc0, identifier & 0x1f - - -class OERcodec_metaclass(type): - def __new__(cls, - name, # type: str - bases, # type: Tuple[type, ...] - dct # type: Dict[str, Any] - ): - # type: (...) -> Type['OERcodec_Object[Any]'] - c = cast('Type[OERcodec_Object[Any]]', - super(OERcodec_metaclass, cls).__new__(cls, name, bases, dct)) - try: - c.tag.register(c.codec, c) - except Exception: - warning("Error registering %r for %r" % (c.tag, c.codec)) - return c - - -_K = TypeVar('_K') - - -class OERcodec_Object(Generic[_K], metaclass=OERcodec_metaclass): - codec = ASN1_Codecs.OER - tag = ASN1_Class_UNIVERSAL.ANY - - @classmethod - def asn1_object(cls, val): - # type: (_K) -> ASN1_Object[_K] - return cls.tag.asn1_object(val) - - @classmethod - def check_string(cls, s): - # type: (bytes) -> None - if not s: - raise OER_Decoding_Error( - "%s: Got empty object while expecting %r" % - (cls.__name__, cls.tag), remaining=s - ) - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - oer_unsigned=False, # type: bool - **_kwargs # type: Any - ): - # type: (...) -> Tuple[ASN1_Object[Any], bytes] - raise OER_Decoding_Error( - "%s: Cannot decode unknown OER type without context" % - cls.__name__, remaining=s - ) - - @classmethod - def dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - oer_unsigned=False, # type: bool - **_kwargs # type: Any - ): - # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] - # Ignore unknown kwargs so shared field._codec_kwargs() dicts (UPER - # keys) do not TypeError on OER packets. - if not safe: - return cls.do_dec(s, context, safe, size_len, oer_unsigned) - try: - return cls.do_dec(s, context, safe, size_len, oer_unsigned) - except OER_Decoding_Error as e: - return ASN1_DECODING_ERROR(s, exc=e), b"" - except ASN1_Error as e: - return ASN1_DECODING_ERROR(s, exc=e), b"" - - @classmethod - def safedec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - size_len=0, # type: Optional[int] - oer_unsigned=False, # type: bool - **_kwargs # type: Any - ): - # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] - return cls.dec( - s, context, safe=True, - size_len=size_len, oer_unsigned=oer_unsigned, - ) - - @classmethod - def enc(cls, s, size_len=0, **_kwargs): - # type: (_K, Optional[int], **Any) -> bytes - if isinstance(s, (str, bytes)): - return OERcodec_STRING.enc(s, size_len=size_len) - else: - try: - return OERcodec_INTEGER.enc(int(s), size_len=size_len) # type: ignore - except TypeError: - raise TypeError("Trying to encode an invalid value !") - - -# No tagging hook: X.696 encodes no tag for a component, whatever the tagging -# environment of the module, so a field is left alone. The only tag on the -# wire is the one of a chosen CHOICE alternative, which the CHOICE hooks below -# write themselves. -ASN1_Codecs.OER.register_stem(OERcodec_Object) - - -########################## -# OERcodec objects # -########################## - -class OERcodec_INTEGER(OERcodec_Object[int]): - tag = ASN1_Class_UNIVERSAL.INTEGER - - _FIXED_FORMATS = { - True: {1: ">b", 2: ">h", 4: ">i", 8: ">q"}, - False: {1: ">B", 2: ">H", 4: ">I", 8: ">Q"}, - } - - @classmethod - def enc(cls, i, size_len=0, oer_unsigned=False, **_kwargs): - # type: (int, Optional[int], bool, **Any) -> bytes - # X.696 10: the width and the signedness follow the declared bounds of - # the type, never the value at hand, otherwise the decoder (which only - # knows the type) reads something else back. - if size_len in (1, 2, 4, 8): - signed = not oer_unsigned - try: - return struct.pack(cls._FIXED_FORMATS[signed][size_len], i) - except struct.error: - raise OER_Encoding_Error( - "%s: %i does not fit in %i %s octet(s)" % - (cls.__name__, i, size_len, - "signed" if signed else "unsigned") - ) - if oer_unsigned: - return OER_unsigned_integer_enc(i) - return OER_signed_integer_enc(i) - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[int], bytes] - if size_len in (1, 2, 4, 8): - _OER_check_len(cls.__name__, s, size_len) - x = struct.unpack( - cls._FIXED_FORMATS[not oer_unsigned][size_len], s[:size_len] - )[0] - return cls.asn1_object(x), s[size_len:] - if oer_unsigned: - x, t = OER_unsigned_integer_dec(s) - else: - x, t = OER_signed_integer_dec(s) - return cls.asn1_object(x), t - - -class OERcodec_BOOLEAN(OERcodec_Object[int]): - tag = ASN1_Class_UNIVERSAL.BOOLEAN - - @classmethod - def enc(cls, i, **_kwargs): - # type: (int, **Any) -> bytes - return chb(0xff if i else 0x00) - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[int], bytes] - cls.check_string(s) - return cls.asn1_object(0 if orb(s[0]) == 0 else 1), s[1:] - - -def _oer_bitstr_to_bytes(bitstr): - # type: (bytes) -> bytes - padded = bitstr + b"0" * (-len(bitstr) % 8) - return bytes([int(padded[i:i + 8], 2) for i in range(0, len(padded), 8)]) - - -def _oer_bytes_to_bitstr(data): - # type: (bytes) -> str - return "".join(binrepr(orb(x)).zfill(8) for x in data) - - -class OERcodec_BIT_STRING(OERcodec_Object[str]): - tag = ASN1_Class_UNIVERSAL.BIT_STRING - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[str], bytes] - if size_len: - number_of_bytes = (size_len + 7) // 8 - _OER_check_len(cls.__name__, s, number_of_bytes) - return ( - cls.tag.asn1_object( - _oer_bytes_to_bitstr(s[:number_of_bytes])[:size_len] - ), - s[number_of_bytes:], - ) - length, s = OER_len_dec(s) - if length == 0: - return cls.tag.asn1_object(""), s - _OER_check_len(cls.__name__, s, length) - unused_bits = orb(s[0]) - if safe and unused_bits > 7: - raise OER_Decoding_Error( - "OERcodec_BIT_STRING: too many unused_bits advertised", - remaining=s - ) - fs = _oer_bytes_to_bitstr(s[1:length]) - if unused_bits > 0: - fs = fs[:-unused_bits] - return cls.tag.asn1_object(fs), s[length:] - - @classmethod - def enc(cls, _s, size_len=0, **_kwargs): - # type: (AnyStr, Optional[int], **Any) -> bytes - s = bytes_encode(_s) - if size_len: - # X.696 13.3: a fixed size means the bits are written padded to a - # whole number of octets, without length or unused-bit count. - if len(s) != size_len: - raise OER_Encoding_Error( - "%s: got %i bits while expecting %i" % - (cls.__name__, len(s), size_len), - encoded=_s - ) - return _oer_bitstr_to_bytes(s) - body = chb(-len(s) % 8) + _oer_bitstr_to_bytes(s) - return OER_len_enc(len(body)) + body - - -class OERcodec_STRING(OERcodec_Object[str]): - tag = ASN1_Class_UNIVERSAL.STRING - - @classmethod - def enc(cls, _s, size_len=0, **_kwargs): - # type: (Union[str, bytes], Optional[int], **Any) -> bytes - s = bytes_encode(_s) - if size_len: - # X.696 16.1: a fixed size means no length determinant. - if len(s) != size_len: - raise OER_Encoding_Error( - "%s: got %i bytes while expecting %i" % - (cls.__name__, len(s), size_len), - encoded=_s - ) - return s - return OER_len_enc(len(s)) + s - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[Any], bytes] - if size_len: - _OER_check_len(cls.__name__, s, size_len) - return cls.tag.asn1_object(s[:size_len]), s[size_len:] - length, s = OER_len_dec(s) - _OER_check_len(cls.__name__, s, length) - return cls.tag.asn1_object(s[:length]), s[length:] - - -class OERcodec_NULL(OERcodec_Object[None]): - tag = ASN1_Class_UNIVERSAL.NULL - - @classmethod - def enc(cls, i, **_kwargs): - # type: (Any, **Any) -> bytes - return b"" - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[None], bytes] - return cls.asn1_object(None), s - - -class OERcodec_OID(OERcodec_Object[bytes]): - tag = ASN1_Class_UNIVERSAL.OID - - @classmethod - def enc(cls, _oid, **_kwargs): - # type: (AnyStr, **Any) -> bytes - oid = bytes_encode(_oid) - if oid: - lst = [int(x) for x in oid.strip(b".").split(b".")] - else: - lst = list() - if len(lst) >= 2: - lst[1] += 40 * lst[0] - del lst[0] - body = b"".join(BER_num_enc(k) for k in lst) - return OER_len_enc(len(body)) + body - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[bytes], bytes] - length, s = OER_len_dec(s) - _OER_check_len(cls.__name__, s, length) - content, t = s[:length], s[length:] - lst = [] - while content: - val, content = BER_num_dec(content) - lst.append(val) - if len(lst) > 0: - lst.insert(0, lst[0] // 40) - lst[1] %= 40 - return ( - cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)), - t, - ) - - -class OERcodec_ENUMERATED(OERcodec_INTEGER): - tag = ASN1_Class_UNIVERSAL.ENUMERATED - - @classmethod - def enc(cls, i, **_kwargs): - # type: (int, **Any) -> bytes - if 0 <= i <= 127: - return chb(i) - body = OER_signed_integer_enc(i)[1:] - return chb(0x80 | len(body)) + body - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[int], bytes] - if not s: - raise OER_Decoding_Error( - "%s: got empty string" % cls.__name__, remaining=s - ) - first = orb(s[0]) - if not (first & 0x80): - return cls.asn1_object(first), s[1:] - length = first & 0x7f - _OER_check_len(cls.__name__, s, length, offset=1) - value = int.from_bytes(s[1:length + 1], "big", signed=True) - return cls.asn1_object(value), s[length + 1:] - - -class OERcodec_UTF8_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.UTF8_STRING - - -class OERcodec_NUMERIC_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.NUMERIC_STRING - - -class OERcodec_PRINTABLE_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING - - -class OERcodec_T61_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.T61_STRING - - -class OERcodec_VIDEOTEX_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.VIDEOTEX_STRING - - -class OERcodec_IA5_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.IA5_STRING - - -class OERcodec_GENERAL_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.GENERAL_STRING - - -class OERcodec_UTC_TIME(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.UTC_TIME - - -class OERcodec_GENERALIZED_TIME(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME - - -class OERcodec_ISO646_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.ISO646_STRING - - -class OERcodec_UNIVERSAL_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.UNIVERSAL_STRING - - -class OERcodec_BMP_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.BMP_STRING - - -class OERcodec_SEQUENCE(OERcodec_Object[Union[bytes, List['OERcodec_Object[Any]']]]): - tag = ASN1_Class_UNIVERSAL.SEQUENCE - - @classmethod - def enc(cls, _ll, **_kwargs): - # type: (Union[bytes, List[OERcodec_Object[Any]]], **Any) -> bytes - if isinstance(_ll, bytes): - return _ll - return b"".join(x.enc(cls.codec) for x in _ll) - - @classmethod - def do_dec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - safe=False, # type: bool - size_len=0, # type: Optional[int] - oer_unsigned=False, # type: bool - ): - # type: (...) -> Tuple[ASN1_Object[Union[bytes, List[Any]]], bytes] - raise OER_Decoding_Error( - "OERcodec_SEQUENCE: decoding requires schema-defined field order", - remaining=s - ) - - -class OERcodec_SET(OERcodec_SEQUENCE): - tag = ASN1_Class_UNIVERSAL.SET - - -class OERcodec_IPADDRESS(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.IPADDRESS - - @classmethod - def enc(cls, ipaddr_ascii, size_len=0, **_kwargs): # type: ignore - # type: (str, Optional[int], **Any) -> bytes - try: - s = inet_aton(ipaddr_ascii) - except Exception: - raise OER_Encoding_Error("IPv4 address could not be encoded") - if size_len == len(s): - return s - return OER_len_enc(len(s)) + s - - @classmethod - def do_dec(cls, s, context=None, safe=False, - size_len=0, oer_unsigned=False): - # type: (bytes, Optional[Any], bool, Optional[int], bool) -> Tuple[ASN1_Object[str], bytes] # noqa: E501 - if size_len == 4: - raw, remain = s[:4], s[4:] - else: - length, remain = OER_len_dec(s) - if len(remain) < length: - raise OER_Decoding_Error("IP address could not be decoded", - remaining=s) - raw, remain = remain[:length], remain[length:] - try: - ipaddr_ascii = inet_ntoa(raw) - except Exception: - raise OER_Decoding_Error("IP address could not be decoded", - remaining=s) - return cls.asn1_object(ipaddr_ascii), remain - - -class OERcodec_COUNTER32(OERcodec_INTEGER): - tag = ASN1_Class_UNIVERSAL.COUNTER32 - - -class OERcodec_COUNTER64(OERcodec_INTEGER): - tag = ASN1_Class_UNIVERSAL.COUNTER64 - - -class OERcodec_GAUGE32(OERcodec_INTEGER): - tag = ASN1_Class_UNIVERSAL.GAUGE32 - - -class OERcodec_TIME_TICKS(OERcodec_INTEGER): - tag = ASN1_Class_UNIVERSAL.TIME_TICKS - - -########################## -# ASN1F field hooks # -########################## - -def _field_extensible(field): - # type: (Any) -> bool - return bool(getattr(field, "codec_opts", {}).get("oer_extensible", False)) - - -def _oer_sequence_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional - s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) - if not s: - for obj in field.seq: - obj.set_val(pkt, None) - return [], s - # X.696 16.2.2: extension bit (if extensible) then one presence bit per - # OPTIONAL/DEFAULT component, zero-padded to a whole number of octets. - extensible = _field_extensible(field) - number_of_optionals = len(field.optionals) - number_of_bits = (1 if extensible else 0) + number_of_optionals - if number_of_bits == 0: - presence = [] # type: List[bool] - else: - number_of_bytes = (number_of_bits + 7) // 8 - _OER_check_len("ASN1F_SEQUENCE", s, number_of_bytes) - value = int.from_bytes(s[:number_of_bytes], "big") - bits = [ - bool((value >> (8 * number_of_bytes - 1 - i)) & 1) - for i in range(number_of_bits) - ] - if extensible: - if bits[0]: - raise OER_Decoding_Error( - "ASN1F_SEQUENCE: extension additions are not supported", - remaining=s - ) - bits = bits[1:] - presence = bits - s = s[number_of_bytes:] - opt_index = 0 - for obj in field.seq: - target = obj - if isinstance(obj, ASN1F_optional): - present = presence[opt_index] - opt_index += 1 - if not present: - obj.set_absent(pkt) - continue - # The preamble already said the component is there, so dissect - # it directly: a failure is an error, not an absence. - target = obj._field - try: - s = target.dissect(pkt, s) - except ASN1F_badsequence: - break - return [], s - - -def _oer_sequence_build(field, pkt): - # type: (Any, Any) -> bytes - from scapy.asn1fields import ASN1F_field, ASN1F_optional - optionals = field.optionals - # X.696 16.2.2: extension bit (if extensible) then one presence bit per - # OPTIONAL/DEFAULT component, zero-padded to a whole number of octets. - bits = [0] if _field_extensible(field) else [] - bits += [0 if opt.is_empty(pkt) else 1 for opt in optionals] - if not bits: - s = b"" - else: - number_of_bytes = (len(bits) + 7) // 8 - value = 0 - for bit in bits: - value = (value << 1) | bit - value <<= 8 * number_of_bytes - len(bits) - s = value.to_bytes(number_of_bytes, "big") - for obj in field.seq: - if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): - continue - s += obj.build(pkt) - # Through ASN1F_field, as ASN1F_SEQUENCE.i2m is the hook above - return ASN1F_field.i2m(field, pkt, s) - - -def _oer_sequence_of_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[list, bytes] - s = field._apply_tagging_dec(s, pkt) - count, s = OER_unsigned_integer_dec(s) - lst = [] - for _ in range(count): - c, s = field._extract_packet(s, pkt) - if c: - lst.append(c) - return lst, s - - -def _oer_sequence_of_build(field, pkt): - # type: (Any, Any) -> bytes - from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Object - val = getattr(pkt, field.name) - if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: - s = val # type: Any - else: - items = [ - bytes(item) if field.holds_packets else field.fld.i2m(pkt, item) - for item in val or [] - ] - s = OER_unsigned_integer_enc(len(items)) + b"".join(items) - return field.i2m(pkt, s) - - -def _oer_choice_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - from scapy.asn1fields import ASN1F_field - from scapy.asn1.asn1 import ASN1_Error - s = field._apply_tagging_dec(s, pkt) - tag_class, tag_number, payload = OER_tag_dec(s) - choice = None - for key, alternative in field.choices.items(): - if _OER_tag_parts(key) == (tag_class, tag_number): - choice = alternative - break - if choice is None: - if not field.flexible_tag: - raise ASN1_Error( - "ASN1F_CHOICE: unexpected field in '%s' " - "(tag %s not in possible tags %s)" % ( - field.name, tag_class | tag_number, - list(field.choices.keys()) - ) - ) - choice = ASN1F_field - if hasattr(choice, "ASN1_root"): - return field.extract_packet(choice, payload, _underlayer=pkt) - if isinstance(choice, type): - return choice(field.name, b"").m2i(pkt, payload) - # ASN1F_PACKET instance: X.696 20.2 puts the alternative tag in front - # of the value, so it was consumed above and must not be looked for - # again by the field itself. - return field.extract_packet( - choice._resolve_cls(pkt), payload, _underlayer=pkt, - ) - - -def _oer_choice_i2m(field, pkt, x): - # type: (Any, Any, Any) -> bytes - from scapy.asn1.asn1 import ASN1_Object - if x is None: - s = b"" - else: - if isinstance(x, ASN1_Object): - s = x.enc(pkt.ASN1_codec) - else: - s = bytes(x) - index = field.alternative_index(x) - if index is not None: - # X.696 20.2: the chosen alternative is prefixed with its tag - tag_class, tag_number = _OER_tag_parts( - field.choice_order[index] - ) - s = OER_tag_enc(tag_number, tag_class) + s - return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) - - -ASN1_Codecs.OER.register_hooks( - sequence_m2i=_oer_sequence_m2i, - sequence_build=_oer_sequence_build, - sequence_of_m2i=_oer_sequence_of_m2i, - sequence_of_build=_oer_sequence_of_build, - choice_m2i=_oer_choice_m2i, - choice_i2m=_oer_choice_i2m, -) +from scapy.asn1.oer import * # noqa: F401, F403 diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py index f23389cf042..a375150f970 100644 --- a/scapy/contrib/uper.py +++ b/scapy/contrib/uper.py @@ -5,1398 +5,6 @@ # scapy.contrib.description = ASN.1 Unaligned Packed Encoding Rules (UPER) # scapy.contrib.status = loads -""" -Unaligned Packed Encoding Rules (UPER) for ASN.1 +"""Compat re-export of ``scapy.asn1.uper``.""" -As specified in ITU-T X.691 | ISO/IEC 8825-2. - -UPER is registered on ``ASN1_Codecs.PER``. Schema-driven encoding and decoding -(``ASN1F_SEQUENCE``, ``ASN1F_CHOICE``, ``ASN1F_SEQUENCE_OF``, -``ASN1F_ENUMERATED``) is supported for common field types. Value ranges are -declared with ``uper_min=``/``uper_max=``, fixed sizes with ``size_len=``, and -an extension marker with ``uper_extensible=True``. Content of 16K units or -more is fragmented as required by 11.9.3.8. - -Not supported yet: extension additions (an encoding that carries them is -refused rather than misparsed), SET, REAL, and the known-multiplier character -string encodings, which are emitted as plain octets rather than 7 or 4 bits -per character. - -``ASN1F_CHOICE`` alternatives are indexed in declaration order, where 10.2 -asks for the canonical order of their tags. The two coincide for a schema -compiled with AUTOMATIC TAGS, which assigns the tags in declaration order; -declare the alternatives in ascending tag order otherwise. -""" - -from scapy.error import warning -from scapy.compat import orb, bytes_encode -from scapy.utils import binrepr, inet_aton, inet_ntoa -from scapy.asn1.ber import BER_num_dec, BER_num_enc -from scapy.asn1.asn1 import ( - ASN1_Class, - ASN1_Class_UNIVERSAL, - ASN1_Codecs, - ASN1_DECODING_ERROR, - ASN1_Decoding_Error, - ASN1_Encoding_Error, - ASN1_Error, - ASN1_Object, - _ASN1_ERROR, -) -# Re-exported: DEFAULT components are what the preamble bits describe. -from scapy.asn1fields import ASN1F_DEFAULT # noqa: F401 - -from typing import ( - Any, - AnyStr, - Callable, - Dict, - Generic, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, - cast, -) - - -################### -# UPER encoding # -################### - - -class UPER_Encoding_Error(ASN1_Encoding_Error): - def __init__(self, - msg, # type: str - encoded=None, # type: Optional[Union['UPERcodec_Object[Any]', str]] - remaining=b"" # type: bytes - ): - # type: (...) -> None - Exception.__init__(self, msg) - self.remaining = remaining - self.encoded = encoded - - def __str__(self): - # type: () -> str - s = Exception.__str__(self) - if isinstance(self.encoded, ASN1_Object): - s += "\n### Already encoded ###\n%s" % self.encoded.strshow() - else: - s += "\n### Already encoded ###\n%r" % self.encoded - s += "\n### Remaining ###\n%r" % self.remaining - return s - - -class UPER_Decoding_Error(ASN1_Decoding_Error): - def __init__(self, - msg, # type: str - decoded=None, # type: Optional[Any] - remaining=b"" # type: bytes - ): - # type: (...) -> None - Exception.__init__(self, msg) - self.remaining = remaining - self.decoded = decoded - - def __str__(self): - # type: () -> str - s = Exception.__str__(self) - if isinstance(self.decoded, ASN1_Object): - s += "\n### Already decoded ###\n%s" % self.decoded.strshow() - else: - s += "\n### Already decoded ###\n%r" % self.decoded - s += "\n### Remaining ###\n%r" % self.remaining - return s - - -def UPER_bits_for_range(size): - # type: (int) -> int - if size <= 0: - return 0 - return size.bit_length() - - -# X.691 11.9.3.8: content of 16K units or more is split into fragments, each -# one holding a multiple of this many units. -UPER_FRAGMENT_SIZE = 16384 - - -def _uper_bits_to_bytes(value, number_of_bits): - # type: (int, int) -> bytes - # X.691 11.1: an encoding is padded with zero bits up to an octet - # boundary. - if number_of_bits == 0: - return b"" - padding = -number_of_bits % 8 - return (value << padding).to_bytes((number_of_bits + padding) // 8, "big") - - -class UPER_Encoder(object): - def __init__(self): - # type: () -> None - self.number_of_bits = 0 - self.value = 0 - self.chunks_number_of_bits = 0 - self.chunks = [] # type: List[List[int]] - - def append_bit(self, bit): - # type: (int) -> None - self.number_of_bits += 1 - self.value <<= 1 - self.value |= 1 if bit else 0 - - def append_bits(self, data, number_of_bits): - # type: (bytes, int) -> None - if number_of_bits == 0: - return - value = int.from_bytes(data, "big") - value >>= (8 * len(data) - number_of_bits) - self.append_non_negative_binary_integer(value, number_of_bits) - - def append_non_negative_binary_integer(self, value, number_of_bits): - # type: (int, int) -> None - if number_of_bits == 0: - return - if self.number_of_bits > 4096: - self.chunks.append([self.value, self.number_of_bits]) - self.chunks_number_of_bits += self.number_of_bits - self.number_of_bits = 0 - self.value = 0 - self.number_of_bits += number_of_bits - self.value <<= number_of_bits - self.value |= value & ((1 << number_of_bits) - 1) - - def append_bytes(self, data): - # type: (bytes) -> None - self.append_bits(data, 8 * len(data)) - - def append_length_determinant(self, length): - # type: (int) -> None - # X.691 11.9.3.6/11.9.3.7 only define the one and two octet forms up - # to 16K. Longer content has to be fragmented, which requires slicing - # the content itself, so leave that to append_fragmented rather than - # silently emitting a determinant that does not match what follows. - if length >= UPER_FRAGMENT_SIZE: - raise UPER_Encoding_Error( - "UPER_Encoder: length %i requires fragmentation" % length - ) - if length < 128: - encoded = bytes([length]) - else: - encoded = bytes([(0x80 | (length >> 8)), (length & 0xff)]) - self.append_bytes(encoded) - - def append_fragmented(self, count, append_units): - # type: (int, Callable[[int, int], None]) -> None - # X.691 11.9.3.8: emit the content as fragments of at most 4 * 16K - # units, each preceded by its own determinant, and always terminate - # with a determinant below 16K (possibly zero). append_units(offset, - # size) appends the units of one fragment. - offset = 0 - remaining = count - while remaining >= UPER_FRAGMENT_SIZE: - number_of_fragments = min(remaining // UPER_FRAGMENT_SIZE, 4) - size = number_of_fragments * UPER_FRAGMENT_SIZE - self.append_bytes(bytes([0xc0 | number_of_fragments])) - append_units(offset, size) - offset += size - remaining -= size - self.append_length_determinant(remaining) - append_units(offset, remaining) - - def append_unconstrained_whole_number(self, value): - # type: (int) -> None - # X.691 11.4: the shortest two's complement encoding. A negative value - # needs one bit less than its magnitude suggests, as -2**(8n-1) still - # fits in n octets, hence the increment before measuring. - magnitude = value + 1 if value < 0 else value - number_of_bytes = (magnitude.bit_length() + 8) // 8 - self.append_length_determinant(number_of_bytes) - self.append_non_negative_binary_integer( - value & ((1 << (8 * number_of_bytes)) - 1), 8 * number_of_bytes - ) - - def as_bytes(self): - # type: () -> bytes - value = 0 - number_of_bits = 0 - for chunk_value, chunk_number_of_bits in self.chunks: - value <<= chunk_number_of_bits - value |= chunk_value - number_of_bits += chunk_number_of_bits - value <<= self.number_of_bits - value |= self.value - number_of_bits += self.number_of_bits - return _uper_bits_to_bytes(value, number_of_bits) - - -def UPER_has_unexpected_remainder(dec): - # type: (UPER_Decoder) -> bool - if dec.number_of_bits == 0: - return False - mask = (1 << dec.number_of_bits) - 1 - return (dec._bits & mask) != 0 - - -class UPER_Decoder(object): - def __init__(self, encoded): - # type: (bytes) -> None - self.total_number_of_bits = 8 * len(encoded) - self.number_of_bits = self.total_number_of_bits - if encoded: - self._bits = int.from_bytes(encoded, "big") - else: - self._bits = 0 - - def _read_offset(self): - # type: () -> int - return self.total_number_of_bits - self.number_of_bits - - def _read_bits_int(self, number_of_bits): - # type: (int) -> int - if number_of_bits == 0: - return 0 - consumed = self._read_offset() - shift = self.total_number_of_bits - consumed - number_of_bits - mask = (1 << number_of_bits) - 1 - return (self._bits >> shift) & mask - - def read_bit(self): - # type: () -> int - if self.number_of_bits == 0: - raise UPER_Decoding_Error("UPER_Decoder: out of data") - bit = self._read_bits_int(1) - self.number_of_bits -= 1 - return bit - - def read_bits(self, number_of_bits): - # type: (int) -> bytes - if number_of_bits > self.number_of_bits: - raise UPER_Decoding_Error("UPER_Decoder: out of data") - if number_of_bits == 0: - return b"" - value = self._read_bits_int(number_of_bits) - self.number_of_bits -= number_of_bits - return _uper_bits_to_bytes(value, number_of_bits) - - def remaining(self): - # type: () -> bytes - if self.number_of_bits == 0: - return b"" - value = self._read_bits_int(self.number_of_bits) - return _uper_bits_to_bytes(value, self.number_of_bits) - - def remaining_bytes(self): - # type: () -> bytes - # A standalone UPER encoding is padded to an octet boundary, so the - # bits left over inside the current octet are padding; only whole - # octets after it are actual remaining input. - pad = -self._read_offset() % 8 - self.number_of_bits = max(0, self.number_of_bits - pad) - return self.remaining() - - def read_bytes(self, number_of_bytes): - # type: (int) -> bytes - return self.read_bits(8 * number_of_bytes) - - def read_non_negative_binary_integer(self, number_of_bits): - # type: (int) -> int - if number_of_bits > self.number_of_bits: - raise UPER_Decoding_Error("UPER_Decoder: out of data") - if number_of_bits == 0: - return 0 - value = self._read_bits_int(number_of_bits) - self.number_of_bits -= number_of_bits - return value - - def _read_length_determinant(self): - # type: () -> Tuple[int, bool] - # Returns the number of units and whether more fragments follow. - value = self.read_non_negative_binary_integer(8) - if (value & 0x80) == 0x00: - return value, False - if (value & 0xc0) == 0x80: - return ( - ((value & 0x7f) << 8) | - self.read_non_negative_binary_integer(8) - ), False - if 0xc1 <= value <= 0xc4: - return (value & 0x0f) * UPER_FRAGMENT_SIZE, True - raise UPER_Decoding_Error( - "UPER_Decoder: bad length determinant 0x%02x" % value - ) - - def read_length_determinant(self): - # type: () -> int - length, fragmented = self._read_length_determinant() - if fragmented: - raise UPER_Decoding_Error( - "UPER_Decoder: unexpected fragmented length determinant" - ) - return length - - def read_fragmented(self, read_units): - # type: (Callable[[int], None]) -> None - # Counterpart of UPER_Encoder.append_fragmented: read_units(size) is - # called once per fragment, the last one being the (possibly empty) - # fragment introduced by a determinant below 16K. - while True: - size, fragmented = self._read_length_determinant() - read_units(size) - if not fragmented: - return - - def read_unconstrained_whole_number(self): - # type: () -> int - number_of_bytes = self.read_length_determinant() - if number_of_bytes == 0: - raise UPER_Decoding_Error( - "UPER_Decoder: integer with an empty length determinant" - ) - enc = self.read_non_negative_binary_integer(8 * number_of_bytes) - sign_bit = 1 << (8 * number_of_bytes - 1) - if enc & sign_bit: - return enc - (1 << (8 * number_of_bytes)) - return enc - - -def UPER_constrained_int_enc(enc, value, minimum, maximum): - # type: (UPER_Encoder, int, int, int) -> None - # X.691 13.2.2: the field is sized after the range, so a value outside it - # cannot be expressed. Callers handle extensibility before coming here. - if not minimum <= value <= maximum: - raise UPER_Encoding_Error( - "UPER_constrained_int_enc: got %i while expecting %i..%i" % - (value, minimum, maximum) - ) - enc.append_non_negative_binary_integer( - value - minimum, UPER_bits_for_range(maximum - minimum) - ) - - -def UPER_constrained_int_dec(dec, minimum, maximum): - # type: (UPER_Decoder, int, int) -> int - value = dec.read_non_negative_binary_integer( - UPER_bits_for_range(maximum - minimum) - ) - return value + minimum - - -def _uper_check_size(name, unit, count, minimum, maximum): - # type: (str, str, int, int, int) -> None - # The determinant is sized after the constraint, so a value that violates - # it cannot be expressed: refuse rather than emit something the peer reads - # as a different length. - if not minimum <= count <= maximum: - raise UPER_Encoding_Error( - "%s: got %i %s while expecting %s" % - (name, count, unit, minimum if minimum == maximum - else "%i..%i" % (minimum, maximum)) - ) - - -def UPER_octet_string_enc(enc, data, minimum=None, maximum=None): - # type: (UPER_Encoder, bytes, Optional[int], Optional[int]) -> None - if minimum is not None and maximum is not None: - _uper_check_size( - "UPER_octet_string_enc", "octets", len(data), minimum, maximum, - ) - if minimum != maximum: - enc.append_non_negative_binary_integer( - len(data) - minimum, - UPER_bits_for_range(maximum - minimum), - ) - enc.append_bytes(data) - else: - enc.append_fragmented( - len(data), - lambda offset, size: enc.append_bytes(data[offset:offset + size]), - ) - - -def UPER_octet_string_dec(dec, minimum=None, maximum=None): - # type: (UPER_Decoder, Optional[int], Optional[int]) -> bytes - if minimum is not None and maximum is not None: - length = minimum - if minimum != maximum: - length += dec.read_non_negative_binary_integer( - UPER_bits_for_range(maximum - minimum) - ) - return dec.read_bytes(length) - fragments = [] # type: List[bytes] - dec.read_fragmented(lambda size: fragments.append(dec.read_bytes(size))) - return b"".join(fragments) - - -def UPER_choice_index_enc(enc, index, number_of_choices): - # type: (UPER_Encoder, int, int) -> None - enc.append_non_negative_binary_integer( - index, UPER_bits_for_range(number_of_choices - 1) - ) - - -def UPER_choice_index_dec(dec, number_of_choices): - # type: (UPER_Decoder, int) -> int - return dec.read_non_negative_binary_integer( - UPER_bits_for_range(number_of_choices - 1) - ) - - -class UPERcodec_metaclass(type): - def __new__(cls, - name, # type: str - bases, # type: Tuple[type, ...] - dct # type: Dict[str, Any] - ): - # type: (...) -> Type['UPERcodec_Object[Any]'] - c = cast('Type[UPERcodec_Object[Any]]', - super(UPERcodec_metaclass, cls).__new__(cls, name, bases, dct)) - try: - c.tag.register(c.codec, c) - except Exception: - warning("Error registering %r for %r" % (c.tag, c.codec)) - return c - - -_K = TypeVar('_K') - - -class UPERcodec_Object(Generic[_K], metaclass=UPERcodec_metaclass): - codec = ASN1_Codecs.PER - tag = ASN1_Class_UNIVERSAL.ANY - - @classmethod - def asn1_object(cls, val): - # type: (_K) -> ASN1_Object[_K] - return cls.tag.asn1_object(val) - - # The bit-oriented encode_into()/dec_from_decoder() pair is the primitive - # every codec implements; enc()/do_dec() below are the standalone (byte - # buffer) entry points, and pass every codec option straight through. - - @classmethod - def encode_into(cls, enc, s, **kwargs): - # type: (UPER_Encoder, Any, **Any) -> None - # No schema information here (ANY): guess from the Python type. - if isinstance(s, (str, bytes)): - UPERcodec_STRING.encode_into(enc, s, **kwargs) - return - try: - UPERcodec_INTEGER.encode_into(enc, int(s), **kwargs) - except Exception: - raise UPER_Encoding_Error( - "Cannot encode value %r for %s" % (s, cls.__name__), - encoded=s - ) - - @classmethod - def dec_from_decoder(cls, dec, **kwargs): - # type: (UPER_Decoder, **Any) -> ASN1_Object[Any] - raise UPER_Decoding_Error( - "%s: Cannot decode unknown UPER type without context" % - cls.__name__, remaining=dec.remaining() - ) - - @classmethod - def enc(cls, s, **kwargs): - # type: (Any, **Any) -> bytes - enc = UPER_Encoder() - cls.encode_into(enc, s, **kwargs) - return enc.as_bytes() - - @classmethod - def do_dec(cls, s, context=None, safe=False, **kwargs): - # type: (bytes, Optional[Type[ASN1_Class]], bool, **Any) -> Tuple[ASN1_Object[Any], bytes] # noqa: E501 - dec = UPER_Decoder(s) - return cls.dec_from_decoder(dec, **kwargs), dec.remaining_bytes() - - @classmethod - def dec(cls, s, context=None, safe=False, **kwargs): - # type: (bytes, Optional[Type[ASN1_Class]], bool, **Any) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] # noqa: E501 - if not safe: - return cls.do_dec(s, context, safe, **kwargs) - try: - return cls.do_dec(s, context, safe, **kwargs) - except (UPER_Decoding_Error, ASN1_Error) as e: - return ASN1_DECODING_ERROR(s, exc=e), b"" - - @classmethod - def safedec(cls, s, context=None, **kwargs): - # type: (bytes, Optional[Type[ASN1_Class]], **Any) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] # noqa: E501 - return cls.dec(s, context, safe=True, **kwargs) - - -# No tagging hook: PER encodes no tag at all, so a field is left alone. -ASN1_Codecs.PER.register_stem(UPERcodec_Object) - - -######################### -# UPERcodec objects # -######################### - - -def _uper_int_range(size_len, uper_min, uper_max, oer_unsigned=False): - # type: (Optional[int], Optional[int], Optional[int], bool) -> Tuple[Optional[int], Optional[int]] # noqa: E501 - if uper_min is not None or uper_max is not None: - return uper_min, uper_max - if size_len in (1, 2, 4, 8) and oer_unsigned: - return 0, (256 ** size_len) - 1 - return None, None - - -class UPERcodec_INTEGER(UPERcodec_Object[int]): - tag = ASN1_Class_UNIVERSAL.INTEGER - - @classmethod - def encode_into(cls, - enc, # type: UPER_Encoder - i, # type: int - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - uper_extensible=False, # type: bool - **_kwargs # type: Any - ): - # type: (...) -> None - minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) - if uper_extensible and minimum is not None and maximum is not None: - if minimum <= i <= maximum: - enc.append_bit(0) - else: - enc.append_bit(1) - enc.append_unconstrained_whole_number(i) - return - if minimum is not None and maximum is not None: - UPER_constrained_int_enc(enc, i, minimum, maximum) - else: - enc.append_unconstrained_whole_number(i) - - @classmethod - def dec_from_decoder(cls, - dec, # type: UPER_Decoder - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=False, # type: bool - uper_extensible=False, # type: bool - **_kwargs # type: Any - ): - # type: (...) -> ASN1_Object[int] - minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) - if uper_extensible and minimum is not None and maximum is not None: - if dec.read_bit(): - value = dec.read_unconstrained_whole_number() - return cls.asn1_object(value) - if minimum is not None and maximum is not None: - value = UPER_constrained_int_dec(dec, minimum, maximum) - else: - value = dec.read_unconstrained_whole_number() - return cls.asn1_object(value) - - -class UPERcodec_BOOLEAN(UPERcodec_Object[int]): - tag = ASN1_Class_UNIVERSAL.BOOLEAN - - @classmethod - def encode_into(cls, enc, i, **_kwargs): - # type: (UPER_Encoder, int, **Any) -> None - enc.append_bit(1 if i else 0) - - @classmethod - def dec_from_decoder(cls, dec, **_kwargs): - # type: (UPER_Decoder, **Any) -> ASN1_Object[int] - return cls.asn1_object(dec.read_bit()) - - -def _uper_bytes_to_bitstr(data, nbits): - # type: (bytes, int) -> str - bitstr = "".join(binrepr(orb(x)).zfill(8) for x in data) - return bitstr[:nbits] - - -def _uper_size_bounds(size_len, uper_min, uper_max): - # type: (Optional[int], Optional[int], Optional[int]) -> Tuple[Optional[int], Optional[int]] # noqa: E501 - # A SIZE constraint given as size_len is a fixed size, i.e. a range whose - # bounds coincide. - if size_len: - return size_len, size_len - return uper_min, uper_max - - -class UPERcodec_BIT_STRING(UPERcodec_Object[str]): - tag = ASN1_Class_UNIVERSAL.BIT_STRING - - @classmethod - def encode_into(cls, - enc, # type: UPER_Encoder - _s, # type: Any - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - **_kwargs # type: Any - ): - # type: (...) -> None - if isinstance(_s, tuple) and len(_s) == 2: - data, nbits = _s - s = bytes_encode(data) - elif isinstance(_s, str) and _s and all(c in "01" for c in _s): - nbits = len(_s) - padded = _s + "0" * ((8 - nbits % 8) % 8) - s = int(padded or "0", 2).to_bytes( - max(1, len(padded) // 8), "big" - ) - else: - s = bytes_encode(_s) - nbits = 8 * len(s) - minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) - if minimum is not None and maximum is not None: - _uper_check_size(cls.__name__, "bits", nbits, minimum, maximum) - if minimum != maximum: - enc.append_non_negative_binary_integer( - nbits - minimum, UPER_bits_for_range(maximum - minimum) - ) - enc.append_bits(s, nbits) - else: - # X.691 16.11: the determinant counts bits, not octets, and no - # padding is inserted before whatever follows the bit string. - enc.append_fragmented( - nbits, - # Fragments hold whole multiples of 16K bits, so every chunk - # but the last starts and ends on an octet boundary. - lambda offset, size: enc.append_bits( - s[offset // 8:(offset + size + 7) // 8], size - ), - ) - - @classmethod - def dec_from_decoder(cls, - dec, # type: UPER_Decoder - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - **_kwargs # type: Any - ): - # type: (...) -> ASN1_Object[str] - minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) - if minimum is not None and maximum is not None: - nbits = minimum - if minimum != maximum: - nbits += dec.read_non_negative_binary_integer( - UPER_bits_for_range(maximum - minimum) - ) - else: - fragments = [] # type: List[bytes] - sizes = [] # type: List[int] - - def read_fragment(size): - # type: (int) -> None - fragments.append(dec.read_bits(size)) - sizes.append(size) - - dec.read_fragmented(read_fragment) - return cls.asn1_object( - _uper_bytes_to_bitstr(b"".join(fragments), sum(sizes)) - ) - raw = dec.read_bits(nbits) - return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)) - - -class UPERcodec_STRING(UPERcodec_Object[str]): - tag = ASN1_Class_UNIVERSAL.STRING - - @classmethod - def encode_into(cls, - enc, # type: UPER_Encoder - _s, # type: Union[str, bytes] - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - **_kwargs # type: Any - ): - # type: (...) -> None - s = bytes_encode(_s) - minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) - UPER_octet_string_enc(enc, s, minimum, maximum) - - @classmethod - def dec_from_decoder(cls, - dec, # type: UPER_Decoder - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - **_kwargs # type: Any - ): - # type: (...) -> ASN1_Object[Any] - minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) - raw = UPER_octet_string_dec(dec, minimum, maximum) - return cls.asn1_object(raw) - - -class UPERcodec_NULL(UPERcodec_Object[None]): - tag = ASN1_Class_UNIVERSAL.NULL - - @classmethod - def encode_into(cls, enc, _s, **_kwargs): - # type: (UPER_Encoder, Any, **Any) -> None - # NULL has an empty encoding. - return - - @classmethod - def dec_from_decoder(cls, dec, **_kwargs): - # type: (UPER_Decoder, **Any) -> ASN1_Object[None] - return cls.asn1_object(None) - - @classmethod - def do_dec(cls, s, context=None, safe=False, **kwargs): - # type: (bytes, Optional[Type[ASN1_Class]], bool, **Any) -> Tuple[ASN1_Object[None], bytes] # noqa: E501 - # NULL occupies no bits at all, so the input is left untouched. - return cls.asn1_object(None), s - - -class UPERcodec_OID(UPERcodec_Object[bytes]): - tag = ASN1_Class_UNIVERSAL.OID - - @classmethod - def encode_into(cls, enc, _oid, **_kwargs): - # type: (UPER_Encoder, AnyStr, **Any) -> None - oid = bytes_encode(_oid) - if oid: - lst = [int(x) for x in oid.split(b".")] - lst = [40 * lst[0] + lst[1]] + lst[2:] - else: - lst = [] - body = b"".join(BER_num_enc(k) for k in lst) - enc.append_fragmented( - len(body), - lambda offset, size: enc.append_bytes(body[offset:offset + size]), - ) - - @classmethod - def dec_from_decoder(cls, dec, **_kwargs): - # type: (UPER_Decoder, **Any) -> ASN1_Object[bytes] - fragments = [] # type: List[bytes] - dec.read_fragmented(lambda size: fragments.append(dec.read_bytes(size))) - content = b"".join(fragments) - lst = [] - while content: - val, content = BER_num_dec(content) - lst.append(val) - if len(lst) > 0: - lst.insert(0, lst[0] // 40) - lst[1] %= 40 - return cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)) - - -def UPER_enumerated_enc(enc, value, enum_values): - # type: (UPER_Encoder, int, List[int]) -> None - if not enum_values: - raise UPER_Encoding_Error("UPER_enumerated_enc: empty enumeration") - try: - index = enum_values.index(value) - except ValueError: - raise UPER_Encoding_Error( - "UPER_enumerated_enc: unknown enumeration value %r" % value - ) - UPER_choice_index_enc(enc, index, len(enum_values)) - - -def UPER_enumerated_dec(dec, enum_values): - # type: (UPER_Decoder, List[int]) -> int - if not enum_values: - raise UPER_Decoding_Error("UPER_enumerated_dec: empty enumeration") - index = UPER_choice_index_dec(dec, len(enum_values)) - if index >= len(enum_values): - raise UPER_Decoding_Error( - "UPER_enumerated_dec: index %i out of range" % index - ) - return enum_values[index] - - -class UPERcodec_ENUMERATED(UPERcodec_INTEGER): - tag = ASN1_Class_UNIVERSAL.ENUMERATED - - @classmethod - def encode_into(cls, - enc, # type: UPER_Encoder - i, # type: int - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - uper_enum_values=None, # type: Optional[List[int]] - uper_extensible=False, # type: bool - **_kwargs # type: Any - ): - # type: (...) -> None - if uper_enum_values is not None: - if uper_extensible: - # X.691 14.3: a one bit prefix says whether the value is an - # extension addition. Only root values can be encoded. - if i not in uper_enum_values: - raise UPER_Encoding_Error( - "UPERcodec_ENUMERATED: extension additions are not " - "supported" - ) - enc.append_bit(0) - UPER_enumerated_enc(enc, i, uper_enum_values) - return - minimum, maximum = cls._range( - size_len, uper_min, uper_max, UPER_Encoding_Error - ) - UPER_constrained_int_enc(enc, i, minimum, maximum) - - @classmethod - def dec_from_decoder(cls, - dec, # type: UPER_Decoder - size_len=0, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - uper_enum_values=None, # type: Optional[List[int]] - uper_extensible=False, # type: bool - **_kwargs # type: Any - ): - # type: (...) -> ASN1_Object[int] - if uper_enum_values is not None: - if uper_extensible and dec.read_bit(): - raise UPER_Decoding_Error( - "UPERcodec_ENUMERATED: extension additions are not " - "supported" - ) - return cls.asn1_object(UPER_enumerated_dec(dec, uper_enum_values)) - minimum, maximum = cls._range( - size_len, uper_min, uper_max, UPER_Decoding_Error - ) - value = dec.read_non_negative_binary_integer( - UPER_bits_for_range(maximum - minimum) - ) + minimum - return cls.asn1_object(value) - - @staticmethod - def _range(size_len, uper_min, uper_max, error): - # type: (Optional[int], Optional[int], Optional[int], Any) -> Tuple[int, int] # noqa: E501 - # Without the enumeration itself the index range has to come from - # the declared bounds; deriving it from the value at hand would - # make the width depend on the value, which the decoder cannot - # reproduce. - minimum = uper_min if uper_min is not None else 0 - maximum = uper_max if uper_max is not None else (size_len or None) - if maximum is None: - raise error("UPERcodec_ENUMERATED: missing range") - return minimum, maximum - - -class UPERcodec_SEQUENCE(UPERcodec_Object[Union[bytes, List[Any]]]): - tag = ASN1_Class_UNIVERSAL.SEQUENCE - - @classmethod - def encode_into(cls, enc, _ll, **_kwargs): - # type: (UPER_Encoder, Any, **Any) -> None - # A finished encoding is padded to an octet boundary, so its real bit - # length is lost and it cannot be spliced into a bitstream. Sequences - # are encoded through the ASN1F_SEQUENCE hooks instead. - raise UPER_Encoding_Error( - "UPERcodec_SEQUENCE: schema-defined field order required" - ) - - @classmethod - def enc(cls, _ll, **_kwargs): - # type: (Union[bytes, List[UPERcodec_Object[Any]]], **Any) -> bytes - if isinstance(_ll, bytes): - return _ll - raise UPER_Encoding_Error( - "UPERcodec_SEQUENCE: schema-defined field order required" - ) - - @classmethod - def dec_from_decoder(cls, dec, **_kwargs): - # type: (UPER_Decoder, **Any) -> ASN1_Object[Union[bytes, List[Any]]] - raise UPER_Decoding_Error( - "UPERcodec_SEQUENCE: decoding requires schema-defined field order", - remaining=dec.remaining() - ) - - -class UPERcodec_SET(UPERcodec_SEQUENCE): - tag = ASN1_Class_UNIVERSAL.SET - - -class UPERcodec_IPADDRESS(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.IPADDRESS - - @classmethod - def encode_into(cls, enc, ipaddr_ascii, **_kwargs): - # type: (UPER_Encoder, str, **Any) -> None - try: - s = inet_aton(ipaddr_ascii) - except Exception: - raise UPER_Encoding_Error("IPv4 address could not be encoded") - UPER_octet_string_enc(enc, s, 4, 4) - - @classmethod - def dec_from_decoder(cls, dec, **_kwargs): - # type: (UPER_Decoder, **Any) -> ASN1_Object[str] - raw = UPER_octet_string_dec(dec, 4, 4) - try: - ipaddr_ascii = inet_ntoa(raw) - except Exception: - raise UPER_Decoding_Error("IP address could not be decoded") - return cls.asn1_object(ipaddr_ascii) - - -class UPERcodec_COUNTER32(UPERcodec_INTEGER): - tag = ASN1_Class_UNIVERSAL.COUNTER32 - - -class UPERcodec_COUNTER64(UPERcodec_INTEGER): - tag = ASN1_Class_UNIVERSAL.COUNTER64 - - -class UPERcodec_GAUGE32(UPERcodec_INTEGER): - tag = ASN1_Class_UNIVERSAL.GAUGE32 - - -class UPERcodec_TIME_TICKS(UPERcodec_INTEGER): - tag = ASN1_Class_UNIVERSAL.TIME_TICKS - - -# string aliases -class UPERcodec_UTF8_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.UTF8_STRING - - -class UPERcodec_NUMERIC_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.NUMERIC_STRING - - -class UPERcodec_PRINTABLE_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING - - -class UPERcodec_T61_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.T61_STRING - - -class UPERcodec_VIDEOTEX_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.VIDEOTEX_STRING - - -class UPERcodec_IA5_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.IA5_STRING - - -class UPERcodec_GENERAL_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.GENERAL_STRING - - -class UPERcodec_UTC_TIME(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.UTC_TIME - - -class UPERcodec_GENERALIZED_TIME(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME - - -class UPERcodec_ISO646_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.ISO646_STRING - - -class UPERcodec_UNIVERSAL_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.UNIVERSAL_STRING - - -class UPERcodec_BMP_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.BMP_STRING - - -########################## -# ASN1F field hooks # -########################## - -def _field_extensible(field): - # type: (Any) -> bool - return bool(getattr(field, "codec_opts", {}).get("uper_extensible", False)) - - -def _field_range(field): - # type: (Any) -> Tuple[Optional[int], Optional[int]] - opts = getattr(field, "codec_opts", {}) - return opts.get("uper_min"), opts.get("uper_max") - - -def _uper_decode_all(s, read): - # type: (bytes, Callable[[UPER_Decoder], Any]) -> Any - # The field owns the whole substring it was handed, so any bit left set - # beyond the octet padding means the encoding did not match the schema. - dec = UPER_Decoder(s) - value = read(dec) - if UPER_has_unexpected_remainder(dec): - raise UPER_Decoding_Error( - "unexpected remainder", - remaining=dec.remaining(), - ) - return value - - -def _uper_sequence_dissect_from_decoder(field, pkt, dec): - # type: (Any, Any, Any) -> None - from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional - if _field_extensible(field): - if dec.read_bit(): - raise UPER_Decoding_Error( - "ASN1F_SEQUENCE: extension additions are not supported" - ) - optionals = field.optionals - presence = [dec.read_bit() for _ in optionals] - opt_idx = 0 - for obj in field.seq: - if isinstance(obj, ASN1F_optional): - if not presence[opt_idx]: - obj.set_absent(pkt) - opt_idx += 1 - continue - opt_idx += 1 - try: - obj.dissect_from_decoder(pkt, dec) - except ASN1F_badsequence: - break - - -def _uper_sequence_encode_into(field, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - from scapy.asn1fields import ASN1F_optional - if _field_extensible(field): - enc.append_bit(0) - for opt in field.optionals: - enc.append_bit(0 if opt.is_empty(pkt) else 1) - for obj in field.seq: - if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): - continue - obj.encode_into(enc, pkt) - - -def _uper_sequence_of_m2i_from_decoder(field, pkt, dec): - # type: (Any, Any, Any) -> list - lst = [] - - def read_items(count): - # type: (int) -> None - for _ in range(count): - if field.holds_packets: - p = field.cls() - p.add_underlayer(pkt) - p.ASN1_root.dissect_from_decoder(p, dec) - lst.append(p) - else: - lst.append(field.fld.m2i_from_decoder(pkt, dec)) - - if _field_extensible(field) and dec.read_bit(): - dec.read_fragmented(read_items) - else: - uper_min, uper_max = _field_range(field) - if uper_min is not None and uper_max is not None: - read_items(UPER_constrained_int_dec(dec, uper_min, uper_max)) - else: - dec.read_fragmented(read_items) - return lst - - -def _uper_sequence_of_encode_into(field, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - if value is None: - value = getattr(pkt, field.name) - if value is None: - _uper_count_enc(field, enc, 0, lambda offset, size: None) - return - count = len(value) - - def append_items(offset, size): - # type: (int, int) -> None - for item in value[offset:offset + size]: - if field.holds_packets: - item.ASN1_root.encode_into(enc, item) - else: - field.fld.encode_into(enc, pkt, item) - - uper_min, uper_max = _field_range(field) - if _field_extensible(field): - if ( - uper_min is not None and uper_max is not None and - uper_min <= count <= uper_max - ): - enc.append_bit(0) - else: - enc.append_bit(1) - enc.append_fragmented(count, append_items) - return - _uper_count_enc(field, enc, count, append_items) - - -def _uper_choice_m2i_from_decoder(field, pkt, dec): - # type: (Any, Any, Any) -> Any - from scapy.asn1.asn1 import ASN1_Error - if _field_extensible(field): - if dec.read_bit(): - raise UPER_Decoding_Error( - "ASN1F_CHOICE: extension additions are not supported" - ) - order = field.choice_order - if len(order) > 1: - index = UPER_choice_index_dec(dec, len(order)) - else: - index = 0 - if index >= len(order): - raise ASN1_Error( - "ASN1F_CHOICE: unexpected index %s in '%s'" % - (index, field.name) - ) - choice = field.choice_list[index] - if isinstance(choice, type) and hasattr(choice, "ASN1_root"): - p = choice() - p.add_underlayer(pkt) - p.ASN1_root.dissect_from_decoder(p, dec) - return p - if isinstance(choice, type): - return choice(field.name, b"").m2i_from_decoder(pkt, dec) - return choice.m2i_from_decoder(pkt, dec) - - -def _uper_choice_encode_into(field, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - from scapy.asn1.asn1 import ASN1_Error - if value is None: - value = getattr(pkt, field.name) - index = field.alternative_index(value) - if index is None: - raise ASN1_Error( - "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % - field.name - ) - if _field_extensible(field): - enc.append_bit(0) - order = field.choice_order - if len(order) > 1: - UPER_choice_index_enc(enc, index, len(order)) - choice = field.choice_list[index] - if hasattr(choice, "ASN1_root"): - value.ASN1_root.encode_into(enc, value) - elif isinstance(choice, type): - choice(field.name, b"").encode_into(enc, pkt, value) - else: - choice.encode_into(enc, pkt, value) - - -def _uper_packet_m2i_from_decoder(field, pkt, dec): - # type: (Any, Any, Any) -> Any - cls = field._resolve_cls(pkt) - p = cls() - p.add_underlayer(pkt) - p.ASN1_root.dissect_from_decoder(p, dec) - return p - - -def _uper_packet_encode_into(field, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - from scapy.asn1.asn1 import ASN1_Object - if value is None: - value = getattr(pkt, field.name) - if value is None: - return - if isinstance(value, ASN1_Object): - value = value.val - value.ASN1_root.encode_into(enc, value) - - -def _uper_count_enc(field, enc, count, append_items): - # type: (Any, Any, int, Callable[[int, int], None]) -> None - # The count of a SEQUENCE OF is a constrained whole number when the field - # carries a size constraint; otherwise it is a length determinant, and the - # items themselves are what gets fragmented, hence the callback. - uper_min, uper_max = _field_range(field) - if uper_min is not None and uper_max is not None: - UPER_constrained_int_enc(enc, count, uper_min, uper_max) - append_items(0, count) - else: - enc.append_fragmented(count, append_items) - - -def _install_uper_asn1fields(): - # type: () -> None - """Attach the UPER bitstream helpers onto the asn1fields classes.""" - from scapy import asn1fields as af - from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Error, ASN1_Object - - def m2i_from_decoder(self, pkt, dec): - # type: (Any, Any, Any) -> Any - codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - return codec.dec_from_decoder( # type: ignore[attr-defined] - dec, **self._codec_kwargs(pkt), - ) - - def dissect_from_decoder(self, pkt, dec): - # type: (Any, Any, Any) -> None - self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) - - def encode_into(self, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - if value is None: - value = getattr(pkt, self.name) - if value is None: - return - codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - if isinstance(value, ASN1_Object): - if (self.ASN1_tag == ASN1_Class_UNIVERSAL.ANY or - value.tag == ASN1_Class_UNIVERSAL.RAW or - value.tag == ASN1_Class_UNIVERSAL.ERROR or - self.ASN1_tag == value.tag): - raw = value.val - else: - raise ASN1_Error( - "Encoding Error: got %r instead of an %r for field [%s]" % - (value, self.ASN1_tag, self.name) - ) - else: - raw = value - codec.encode_into( # type: ignore[attr-defined] - enc, raw, **self._codec_kwargs(pkt), - ) - - def opt_dissect_from_decoder(self, pkt, dec): - # type: (Any, Any, Any) -> None - return self._field.dissect_from_decoder(pkt, dec) - - def opt_encode_into(self, enc, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - self._field.encode_into(enc, pkt, value) - - for field_cls, methods in ( - (af.ASN1F_field, { - "m2i_from_decoder": m2i_from_decoder, - "dissect_from_decoder": dissect_from_decoder, - "encode_into": encode_into, - }), - (af.ASN1F_SEQUENCE, { - "dissect_from_decoder": _uper_sequence_dissect_from_decoder, - "encode_into": _uper_sequence_encode_into, - }), - (af.ASN1F_SEQUENCE_OF, { - "m2i_from_decoder": _uper_sequence_of_m2i_from_decoder, - "encode_into": _uper_sequence_of_encode_into, - }), - (af.ASN1F_CHOICE, { - "m2i_from_decoder": _uper_choice_m2i_from_decoder, - "encode_into": _uper_choice_encode_into, - }), - (af.ASN1F_PACKET, { - "m2i_from_decoder": _uper_packet_m2i_from_decoder, - "encode_into": _uper_packet_encode_into, - }), - (af.ASN1F_optional, { - "dissect_from_decoder": opt_dissect_from_decoder, - "encode_into": opt_encode_into, - }), - ): - for method_name, func in methods.items(): - setattr(field_cls, method_name, func) - - _orig_enum_codec_kwargs = af.ASN1F_enum_INTEGER._codec_kwargs - - def enum_codec_kwargs(self, pkt): - # type: (Any, Any) -> Any - kwargs = _orig_enum_codec_kwargs(self, pkt) - # The permitted values belong to the UPER encoding, not to the field - # definition, so they are only added for PER packets. Other codecs - # keep an empty codec_opts and their item.enc() fast path. - codec = getattr(pkt, "ASN1_codec", None) - if codec is ASN1_Codecs.PER: - # X.691 14.1: the index follows the enumeration values in - # ascending order, whatever order they were declared in. - kwargs.setdefault("uper_enum_values", sorted(self.i2s)) - return kwargs - - af.ASN1F_enum_INTEGER._codec_kwargs = enum_codec_kwargs # type: ignore[assignment] - - -def _uper_sequence_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - _uper_decode_all( - s, lambda dec: _uper_sequence_dissect_from_decoder(field, pkt, dec) - ) - return [], b"" - - -def _uper_sequence_build(field, pkt): - # type: (Any, Any) -> bytes - from scapy.asn1fields import ASN1F_field - enc = UPER_Encoder() - _uper_sequence_encode_into(field, enc, pkt) - return ASN1F_field.i2m(field, pkt, enc.as_bytes()) - - -def _uper_sequence_of_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[list, bytes] - return _uper_decode_all( - s, lambda dec: _uper_sequence_of_m2i_from_decoder(field, pkt, dec) - ), b"" - - -def _uper_sequence_of_build(field, pkt): - # type: (Any, Any) -> bytes - from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Object - val = getattr(pkt, field.name) - if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: - s = val # type: Any - else: - # An unset field counts as an empty one, size constraint included - enc = UPER_Encoder() - _uper_sequence_of_encode_into(field, enc, pkt, val) - s = enc.as_bytes() - return field.i2m(pkt, s) - - -def _uper_choice_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - return _uper_decode_all( - s, lambda dec: _uper_choice_m2i_from_decoder(field, pkt, dec) - ), b"" - - -def _uper_choice_i2m(field, pkt, x): - # type: (Any, Any, Any) -> bytes - if x is None: - s = b"" - else: - enc = UPER_Encoder() - _uper_choice_encode_into(field, enc, pkt, x) - s = enc.as_bytes() - return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) - - -def _uper_packet_i2m(field, pkt, x): - # type: (Any, Any, Any) -> bytes - if x is None: - s = b"" - else: - enc = UPER_Encoder() - _uper_packet_encode_into(field, enc, pkt, x) - s = enc.as_bytes() - return field._tagging_enc( - pkt, s, - implicit_tag=field.implicit_tag, - explicit_tag=field.explicit_tag, - ) - - -_install_uper_asn1fields() -ASN1_Codecs.PER.register_hooks( - # Constraints always go through codec.enc(**kwargs). - use_object_enc=lambda field, pkt, item: False, - sequence_m2i=_uper_sequence_m2i, - sequence_build=_uper_sequence_build, - sequence_of_m2i=_uper_sequence_of_m2i, - sequence_of_build=_uper_sequence_of_build, - choice_m2i=_uper_choice_m2i, - choice_i2m=_uper_choice_i2m, - packet_i2m=_uper_packet_i2m, -) +from scapy.asn1.uper import * # noqa: F401, F403 diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index 7a4a61b3a40..4c797f03d28 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -272,6 +272,10 @@ class _BitEncapsRecord(ASN1_Packet): ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord), ) +def _value(obj): + return getattr(obj, "val", obj) + + def _raises(exc, func): # type: (type, Any) -> None try: @@ -459,11 +463,7 @@ x.val == -2 and r == b"" x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3), size_len=3) x.val == b"\x12\x34\x56" and r == b"" = OER does not encode the tag of a component -# X.696 encodes none, so OER hooks no tagging and the field is left alone -assert ASN1_Codecs.OER.hook("tagging_enc") is None - -assert ASN1_Codecs.OER.hook("tagging_dec") is None - +# X.696 encodes none of the component tags; only CHOICE alternatives are tagged. fld = ASN1F_INTEGER("n", 0, explicit_tag=0xA0) pkt = OERTaggedInteger() @@ -885,24 +885,24 @@ assert remain == b"" True -+ ASN.1 OER field hooks and packet extras -= oer field hooks registered -assert ASN1_Codecs.hooks[ASN1_Codecs.OER] ++ ASN.1 OER field dispatch and packet extras += oer sequence helpers are module-level +from scapy.asn1 import oer as oer_mod -assert ASN1_Codecs.OER.hook("sequence_m2i") is not None +assert callable(oer_mod.oer_sequence_m2i) -assert ASN1_Codecs.OER.hook("choice_i2m") is not None +assert callable(oer_mod.oer_choice_i2m) -assert ASN1_Codecs.OER.hook("no_such_hook") is None +assert not hasattr(ASN1_Codecs, "hooks") True -= oer use_object_enc += oer constrained integer via codec_opts fld = OERUnsignedField.ASN1_root assert fld.codec_opts["oer_unsigned"] is True -assert fld._use_object_enc(OERUnsignedField(), ASN1_INTEGER(5)) is False +assert fld.constraints.unsigned is True assert raw(OERUnsignedField(n=5)) == b"\x05" @@ -1268,3 +1268,207 @@ raw_items = ASN1_Class_UNIVERSAL.RAW.asn1_object(b"\x01\x01\x07") assert raw(OERSequenceOfIntegers(values=raw_items)) == b"\x01\x01\x07" True + +% PR #5050 refactor characterization (OER) ++ OER refactor contracts - field schema owns value constraints += component tags do not alter OER component encoding +class RefactorOERPlainInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0) + +class RefactorOERTaggedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0, explicit_tag=0xA0) + +all( + raw(RefactorOERPlainInteger(n=value)) == raw( + RefactorOERTaggedInteger(n=value) + ) + for value in [0, 1, 127, 128, 255, -1, -128] +) + += constrained OER integer plain value and ASN1_INTEGER encode identically +class RefactorOERUnsignedByte(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True) + +all( + raw(RefactorOERUnsignedByte(n=value)) == bytes([value]) + and raw(RefactorOERUnsignedByte(n=ASN1_INTEGER(value))) == bytes([value]) + and _value(RefactorOERUnsignedByte(bytes([value])).n) == value + for value in [0, 1, 127, 128, 254, 255] +) + += constrained OER unsigned integer rejects out-of-range values +_raises(OER_Encoding_Error, lambda: raw(RefactorOERUnsignedByte(n=-1))) +_raises(OER_Encoding_Error, lambda: raw(RefactorOERUnsignedByte(n=256))) +True + ++ OER refactor contracts - OPTIONAL and DEFAULT += OPTIONAL and DEFAULT share the OER sequence preamble +class RefactorOERPresence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, size_len=1, oer_unsigned=True), + ASN1F_optional( + ASN1F_INTEGER("opt", 0, size_len=1, oer_unsigned=True) + ), + ASN1F_DEFAULT( + ASN1F_INTEGER("mode", 3, size_len=1, oer_unsigned=True), + 3, + ), + ) + +vectors = [ + (dict(id=1, opt=None, mode=3), bytes.fromhex("0001")), + (dict(id=1, opt=2, mode=3), bytes.fromhex("800102")), + (dict(id=1, opt=None, mode=4), bytes.fromhex("400104")), + (dict(id=1, opt=2, mode=4), bytes.fromhex("c0010204")), +] +all( + raw(RefactorOERPresence(**values)) == expected + and _value(RefactorOERPresence(expected).id) == 1 + and (_value(RefactorOERPresence(expected).opt) + if RefactorOERPresence(expected).opt is not None else None) == values["opt"] + and _value(RefactorOERPresence(expected).mode) == values["mode"] + for values, expected in vectors +) + += OER DEFAULT wire encoding is independent of plain versus ASN1 object default +plain = RefactorOERPresence(id=1, mode=3) +asn1_object = RefactorOERPresence(id=1, mode=ASN1_INTEGER(3)) +assert raw(plain) == raw(asn1_object) == bytes.fromhex("0001") +True + ++ OER refactor contracts - nested packets and dynamic selection += nested ASN1F_PACKET retains Scapy underlayer semantics +class RefactorOERInner(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, size_len=1, oer_unsigned=True), + ASN1F_BOOLEAN("flag", False), + ) + +class RefactorOEROuter(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, size_len=1, oer_unsigned=True), + ASN1F_PACKET("inner", None, RefactorOERInner), + ASN1F_INTEGER("tail", 0, size_len=1, oer_unsigned=True), + ) + +pkt = RefactorOEROuter( + id=1, + inner=RefactorOERInner(x=7, flag=True), + tail=9, +) +assert raw(pkt) == bytes.fromhex("0107ff09") +decoded = RefactorOEROuter(raw(pkt)) +assert _value(decoded.id) == 1 +assert _value(decoded.inner.x) == 7 +assert _value(decoded.inner.flag) == 1 +assert _value(decoded.tail) == 9 +assert decoded.inner.parent is decoded +True + += OER ASN1F_PACKET next_cls_cb selects child class during decode +class RefactorOERDynamicA(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, size_len=1, oer_unsigned=True), + ) + +class RefactorOERDynamicB(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BOOLEAN("flag", False), + ) + + +def _oer_dynamic_cls(pkt): + return RefactorOERDynamicA if _value(pkt.kind) == 0 else RefactorOERDynamicB + + +class RefactorOERDynamicOuter(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("kind", 0, size_len=1, oer_unsigned=True), + ASN1F_PACKET( + "inner", None, RefactorOERDynamicA, + next_cls_cb=_oer_dynamic_cls, + ), + ) + +packet_a = RefactorOERDynamicOuter(kind=0, inner=RefactorOERDynamicA(x=5)) +packet_b = RefactorOERDynamicOuter(kind=1, inner=RefactorOERDynamicB(flag=True)) +assert raw(packet_a) == bytes.fromhex("0005") +assert raw(packet_b) == bytes.fromhex("01ff") +decoded_a = RefactorOERDynamicOuter(raw(packet_a)) +decoded_b = RefactorOERDynamicOuter(raw(packet_b)) +assert isinstance(decoded_a.inner, RefactorOERDynamicA) +assert _value(decoded_a.inner.x) == 5 +assert decoded_a.inner.parent is decoded_a +assert isinstance(decoded_b.inner, RefactorOERDynamicB) +assert _value(decoded_b.inner.flag) == 1 +assert decoded_b.inner.parent is decoded_b +True + + +% PR #5050 refactor targets (OER) ++ Finding - OER CHOICE must preserve semantic high tag numbers += OER CHOICE context-specific tag 100 uses high-tag-number form +high_context_100, remain = BER_id_dec(b"\xbf\x64") +assert remain == b"" + +class RefactorOERHighContextAlt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BOOLEAN("flag", False), + ) + +class RefactorOERHighContextChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", None, + ASN1F_PACKET( + "high", None, RefactorOERHighContextAlt, + explicit_tag=high_context_100, + ), + ) + +expected = b"\xbf\x64\xff" +pkt = RefactorOERHighContextChoice(c=RefactorOERHighContextAlt(flag=True)) +assert raw(pkt) == expected +decoded = RefactorOERHighContextChoice(expected) +assert isinstance(decoded.c, RefactorOERHighContextAlt) +assert _value(decoded.c.flag) == 1 +True + += OER CHOICE private tag 100 keeps class and tag number +high_private_100, remain = BER_id_dec(b"\xff\x64") +assert remain == b"" + +class RefactorOERHighPrivateAlt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BOOLEAN("flag", False), + ) + +class RefactorOERHighPrivateChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", None, + ASN1F_PACKET( + "high", None, RefactorOERHighPrivateAlt, + explicit_tag=high_private_100, + ), + ) + +expected = b"\xff\x64\x00" +pkt = RefactorOERHighPrivateChoice(c=RefactorOERHighPrivateAlt(flag=False)) +assert raw(pkt) == expected +decoded = RefactorOERHighPrivateChoice(expected) +assert isinstance(decoded.c, RefactorOERHighPrivateAlt) +assert _value(decoded.c.flag) == 0 +True + diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index cf4e19e230b..52912839040 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -639,10 +639,10 @@ import scapy.contrib.oer from scapy.contrib.oer import * -from scapy.contrib.uper import ASN1F_DEFAULT - import scapy.asn1fields as asn1fields +from scapy.asn1fields import ASN1F_DEFAULT + def _val(x): # type: (Any) -> Any return x.val if hasattr(x, "val") else x @@ -867,10 +867,12 @@ assert decoded.c.val == b"AB" True -= uper field choice definition order += uper field choice canonical order +# X.691 10.2: INTEGER (UNIVERSAL 2) precedes OCTET STRING (UNIVERSAL 4), +# even when STRING is declared first. as_str = UPERChoiceStringFirst(c=ASN1_STRING(b"AB")) -assert raw(as_str) == bytes.fromhex("0120a100") +assert raw(as_str) == bytes.fromhex("8120a100") decoded = _roundtrip(UPERChoiceStringFirst, as_str) @@ -878,7 +880,7 @@ assert decoded.c.val == b"AB" as_int = UPERChoiceStringFirst(c=ASN1_INTEGER(99)) -assert raw(as_int) == bytes.fromhex("80b180") +assert raw(as_int) == bytes.fromhex("00b180") decoded = _roundtrip(UPERChoiceStringFirst, as_int) @@ -2529,7 +2531,7 @@ with mock.patch.object( _InnerRecord, "__init__", side_effect=ASN1F_badsequence, ): pkt_obj, remain = field.extract_packet( - _InnerRecord, b"\xab\xcd", _underlayer=None, + _InnerRecord, b"\xab\xcd", _parent=None, ) assert isinstance(pkt_obj, Raw) @@ -2784,17 +2786,15 @@ assert raw(oer_pkt_choice) True -+ ASN.1 UPER field hooks and packet extras -= uper field hooks registered -assert ASN1_Codecs.hooks[ASN1_Codecs.PER] ++ ASN.1 UPER field dispatch and packet extras += uper helpers are module-level not monkey-patched +from scapy.asn1 import uper as uper_mod -assert ASN1_Codecs.PER.hook("sequence_m2i") is not None +assert callable(uper_mod.uper_sequence_m2i) -use_object_enc = ASN1_Codecs.PER.hook("use_object_enc") +assert "_install_uper_asn1fields" not in uper_mod.__dict__ -assert use_object_enc( - UPERConstrainedInt.ASN1_root, UPERConstrainedInt(), ASN1_INTEGER(1), -) is False +assert not hasattr(ASN1_Codecs, "hooks") True @@ -2817,18 +2817,14 @@ assert enum_fld._codec_kwargs( type("P", (), {"ASN1_codec": ASN1_Codecs.PER})() )["uper_enum_values"] == [1, 2] -assert hasattr(ASN1F_field, "encode_into") - -assert hasattr(ASN1F_SEQUENCE, "dissect_from_decoder") - True -= uper use_object_enc and codec_opts += uper constraints and codec_opts fld = UPERConstrainedInt.ASN1_root assert fld.codec_opts == {"uper_min": 0, "uper_max": 255} -assert fld._use_object_enc(UPERConstrainedInt(), ASN1_INTEGER(5)) is False +assert fld.constraints.minimum == 0 and fld.constraints.maximum == 255 assert raw(UPERConstrainedInt(n=5)) == b"\x05" @@ -3065,8 +3061,8 @@ class UPERClassChoice(ASN1_Packet): pkt = UPERClassChoice(c=UPERAltA(i=5)) -# index bit 0, then the sequence: an unconstrained integer of one octet -assert raw(pkt) == b"\x00\x82\x80" +# X.691 canonical order: INTEGER before SEQUENCE, so AltA is index 1 +assert raw(pkt) == b"\x80\x82\x80" decoded = _roundtrip(UPERClassChoice, pkt) @@ -3206,3 +3202,404 @@ _raises(UPER_Encoding_Error, lambda: raw(UPERSizedSeqOf(values=[]))) _raises(UPER_Encoding_Error, lambda: raw(UPERSizedSeqOf(values=None))) True + +def _value(obj): + return getattr(obj, "val", obj) + + +% PR #5050 refactor characterization (UPER) ++ UPER refactor contracts - continuous bit stream += constrained fields pack across one octet without intermediate padding +class RefactorUPERFlatBits(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BOOLEAN("a", False), + ASN1F_INTEGER("b", 0, uper_min=0, uper_max=7), + ASN1F_BOOLEAN("c", False), + ASN1F_INTEGER("d", 0, uper_min=0, uper_max=7), + ) + +pkt = RefactorUPERFlatBits(a=True, b=5, c=False, d=3) +assert raw(pkt) == b"\xd3" +decoded = RefactorUPERFlatBits(raw(pkt)) +assert _value(decoded.a) == 1 +assert _value(decoded.b) == 5 +assert _value(decoded.c) == 0 +assert _value(decoded.d) == 3 +True + += nested ASN1F_SEQUENCE does not introduce a byte boundary +class RefactorUPERNestedBits(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BOOLEAN("a", False), + ASN1F_SEQUENCE( + ASN1F_INTEGER("b", 0, uper_min=0, uper_max=7), + ASN1F_BOOLEAN("c", False), + ), + ASN1F_INTEGER("d", 0, uper_min=0, uper_max=7), + ) + +pkt = RefactorUPERNestedBits(a=True, b=5, c=False, d=3) +assert raw(pkt) == b"\xd3" +decoded = RefactorUPERNestedBits(b"\xd3") +assert (_value(decoded.a), _value(decoded.b), _value(decoded.c), _value(decoded.d)) == (1, 5, 0, 3) +True + += nested ASN1F_PACKET shares the same UPER bit stream +class RefactorUPERInnerBits(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("b", 0, uper_min=0, uper_max=7), + ASN1F_BOOLEAN("c", False), + ) + +class RefactorUPERPacketBits(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BOOLEAN("a", False), + ASN1F_PACKET("inner", None, RefactorUPERInnerBits), + ASN1F_INTEGER("d", 0, uper_min=0, uper_max=7), + ) + +pkt = RefactorUPERPacketBits( + a=True, + inner=RefactorUPERInnerBits(b=5, c=False), + d=3, +) +assert raw(pkt) == b"\xd3" +decoded = RefactorUPERPacketBits(b"\xd3") +assert _value(decoded.a) == 1 +assert _value(decoded.inner.b) == 5 +assert _value(decoded.inner.c) == 0 +assert _value(decoded.d) == 3 +assert decoded.inner.parent is decoded +True + += repeated nested packet build is deterministic +pkt = RefactorUPERPacketBits( + a=True, + inner=RefactorUPERInnerBits(b=5, c=False), + d=3, +) +first = raw(pkt) +second = raw(pkt) +third = raw(RefactorUPERPacketBits(first)) +assert first == second == third == b"\xd3" +True + ++ UPER refactor contracts - constrained whole numbers += one bit constrained range exact vectors +class RefactorUPERRange01(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=1) + +assert raw(RefactorUPERRange01(n=0)) == b"\x00" +assert raw(RefactorUPERRange01(n=1)) == b"\x80" +assert _value(RefactorUPERRange01(b"\x00").n) == 0 +assert _value(RefactorUPERRange01(b"\x80").n) == 1 +True + += two bit constrained range exact vectors +class RefactorUPERRange03(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=3) + +all( + raw(RefactorUPERRange03(n=value)) == expected + and _value(RefactorUPERRange03(expected).n) == value + for value, expected in [ + (0, b"\x00"), + (1, b"\x40"), + (2, b"\x80"), + (3, b"\xc0"), + ] +) + += non-zero lower bound is encoded as an offset +class RefactorUPERRange512(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 5, uper_min=5, uper_max=12) + +all( + raw(RefactorUPERRange512(n=value)) == expected + and _value(RefactorUPERRange512(expected).n) == value + for value, expected in [ + (5, b"\x00"), + (6, b"\x20"), + (7, b"\x40"), + (8, b"\x60"), + (9, b"\x80"), + (10, b"\xa0"), + (11, b"\xc0"), + (12, b"\xe0"), + ] +) + += singleton constrained range consumes zero bits +class RefactorUPERConstant(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 7, uper_min=7, uper_max=7) + +assert raw(RefactorUPERConstant(n=7)) == b"" +assert _value(RefactorUPERConstant(b"").n) == 7 +True + += constrained values outside the root range are rejected +_raises(UPER_Encoding_Error, lambda: raw(RefactorUPERRange03(n=-1))) +_raises(UPER_Encoding_Error, lambda: raw(RefactorUPERRange03(n=4))) +_raises(UPER_Encoding_Error, lambda: raw(RefactorUPERRange512(n=4))) +_raises(UPER_Encoding_Error, lambda: raw(RefactorUPERRange512(n=13))) +True + += plain int and ASN1_INTEGER use identical field constraints +all( + raw(RefactorUPERRange03(n=value)) == raw( + RefactorUPERRange03(n=ASN1_INTEGER(value)) + ) + for value in [0, 1, 2, 3] +) + += legacy oer_unsigned inference matches an explicit UPER 0 to 255 constraint +class RefactorUPERLegacyUnsigned(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True) + +class RefactorUPERExplicitUnsigned(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=255) + +all( + raw(RefactorUPERLegacyUnsigned(n=value)) + == raw(RefactorUPERExplicitUnsigned(n=value)) + == bytes([value]) + for value in [0, 1, 127, 128, 254, 255] +) + ++ UPER refactor contracts - OPTIONAL and DEFAULT += OPTIONAL and DEFAULT presence bits are a single continuous preamble +class RefactorUPERPresence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_optional( + ASN1F_INTEGER("opt", 0, uper_min=0, uper_max=255) + ), + ASN1F_DEFAULT( + ASN1F_INTEGER("mode", 3, uper_min=0, uper_max=255), + 3, + ), + ) + +vectors = [ + (dict(id=1, opt=None, mode=3), bytes.fromhex("0040")), + (dict(id=1, opt=2, mode=3), bytes.fromhex("804080")), + (dict(id=1, opt=None, mode=4), bytes.fromhex("404100")), + (dict(id=1, opt=2, mode=4), bytes.fromhex("c0408100")), +] +all( + raw(RefactorUPERPresence(**values)) == expected + and _value(RefactorUPERPresence(expected).id) == 1 + and (_value(RefactorUPERPresence(expected).opt) + if RefactorUPERPresence(expected).opt is not None else None) == values["opt"] + and _value(RefactorUPERPresence(expected).mode) == values["mode"] + for values, expected in vectors +) + += DEFAULT omitted explicitly and DEFAULT omitted implicitly have identical wire form +implicit_default = RefactorUPERPresence(id=1, opt=None) +explicit_default = RefactorUPERPresence(id=1, opt=None, mode=3) +assert raw(implicit_default) == raw(explicit_default) == bytes.fromhex("0040") +True + += ASN1 object DEFAULT compares by semantic value +plain = RefactorUPERPresence(id=1, mode=3) +asn1_object = RefactorUPERPresence(id=1, mode=ASN1_INTEGER(3)) +assert raw(plain) == raw(asn1_object) == bytes.fromhex("0040") +True + ++ UPER refactor contracts - packet class selection += ASN1F_PACKET next_cls_cb selects child class during decode +class RefactorUPERDynamicA(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, uper_min=0, uper_max=7), + ) + +class RefactorUPERDynamicB(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BOOLEAN("flag", False), + ) + + +def _uper_dynamic_cls(pkt): + return RefactorUPERDynamicA if _value(pkt.kind) == 0 else RefactorUPERDynamicB + + +class RefactorUPERDynamicOuter(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("kind", 0, uper_min=0, uper_max=1), + ASN1F_PACKET( + "inner", None, RefactorUPERDynamicA, + next_cls_cb=_uper_dynamic_cls, + ), + ) + +packet_a = RefactorUPERDynamicOuter(kind=0, inner=RefactorUPERDynamicA(x=5)) +packet_b = RefactorUPERDynamicOuter(kind=1, inner=RefactorUPERDynamicB(flag=True)) +assert raw(packet_a) == b"\x50" +assert raw(packet_b) == b"\xc0" +decoded_a = RefactorUPERDynamicOuter(raw(packet_a)) +decoded_b = RefactorUPERDynamicOuter(raw(packet_b)) +assert isinstance(decoded_a.inner, RefactorUPERDynamicA) +assert _value(decoded_a.inner.x) == 5 +assert decoded_a.inner.parent is decoded_a +assert isinstance(decoded_b.inner, RefactorUPERDynamicB) +assert _value(decoded_b.inner.flag) == 1 +assert decoded_b.inner.parent is decoded_b +True + ++ UPER refactor contracts - SEQUENCE OF += fixed-size SEQUENCE OF packs elements without per-element padding +class RefactorUPERFixedSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", + [], + ASN1F_INTEGER("item", 0, uper_min=0, uper_max=3), + uper_min=4, + uper_max=4, + ) + +pkt = RefactorUPERFixedSeqOf(values=[0, 1, 2, 3]) +assert raw(pkt) == b"\x1b" +decoded = RefactorUPERFixedSeqOf(b"\x1b") +assert [_value(item) for item in decoded.values] == [0, 1, 2, 3] +True + += constrained SEQUENCE OF count is encoded before continuous element bits +class RefactorUPERVariableSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", + [], + ASN1F_INTEGER("item", 0, uper_min=0, uper_max=3), + uper_min=1, + uper_max=4, + ) + +# count=3 is offset 2 in range 1..4 -> '10', then 00 01 10 -> 10000110 +pkt = RefactorUPERVariableSeqOf(values=[0, 1, 2]) +assert raw(pkt) == b"\x86" +decoded = RefactorUPERVariableSeqOf(b"\x86") +assert [_value(item) for item in decoded.values] == [0, 1, 2] +True + + +% PR #5050 refactor targets (UPER) ++ Finding - UPER CHOICE indexes alternatives in canonical tag order += UPER CHOICE encode is independent of declaration order +class RefactorUPERChoiceTag0(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE(ASN1F_NULL("n", None)) + +class RefactorUPERChoiceTag1(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE(ASN1F_NULL("n", None)) + +class RefactorUPERReversedChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", None, + # Deliberately declare [1] before [0]. X.691 10.2/23.2 requires + # indexing root alternatives in canonical tag order. + ASN1F_PACKET( + "tag1", None, RefactorUPERChoiceTag1, + explicit_tag=0xA1, + ), + ASN1F_PACKET( + "tag0", None, RefactorUPERChoiceTag0, + explicit_tag=0xA0, + ), + ) + +# Canonical order is [0], [1], so tag0 has index 0 and tag1 has index 1. +assert raw(RefactorUPERReversedChoice(c=RefactorUPERChoiceTag0())) == b"\x00" +assert raw(RefactorUPERReversedChoice(c=RefactorUPERChoiceTag1())) == b"\x80" +True + += UPER CHOICE decode uses canonical tag order rather than declaration order +decoded_zero = RefactorUPERReversedChoice(b"\x00") +decoded_one = RefactorUPERReversedChoice(b"\x80") +assert isinstance(decoded_zero.c, RefactorUPERChoiceTag0) +assert isinstance(decoded_one.c, RefactorUPERChoiceTag1) +True + += same tagged CHOICE has same encoding when alternatives are declared canonically +class RefactorUPERCanonicalChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", None, + ASN1F_PACKET( + "tag0", None, RefactorUPERChoiceTag0, + explicit_tag=0xA0, + ), + ASN1F_PACKET( + "tag1", None, RefactorUPERChoiceTag1, + explicit_tag=0xA1, + ), + ) + +all( + raw(RefactorUPERReversedChoice(c=value_reversed)) == raw( + RefactorUPERCanonicalChoice(c=value_canonical) + ) + for value_reversed, value_canonical in [ + (RefactorUPERChoiceTag0(), RefactorUPERChoiceTag0()), + (RefactorUPERChoiceTag1(), RefactorUPERChoiceTag1()), + ] +) + + +% PR #5050 architecture guard ++ Refactor architecture guard - importing UPER must not monkey-patch core field classes += isolated UPER import does not add codec methods to ASN1 field class dictionaries +code = r''' +from scapy.asn1fields import ( + ASN1F_CHOICE, + ASN1F_PACKET, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_field, + ASN1F_optional, +) +classes = ( + ASN1F_field, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_CHOICE, + ASN1F_PACKET, + ASN1F_optional, +) +watched = ( + "encode_into", + "m2i_from_decoder", + "dissect_from_decoder", +) +before = { + cls.__name__: tuple(name for name in watched if name in cls.__dict__) + for cls in classes +} +import scapy.asn1.uper +after = { + cls.__name__: tuple(name for name in watched if name in cls.__dict__) + for cls in classes +} +assert before == after, (before, after) +''' +result = subprocess.run([sys.executable, "-c", code]) +assert result.returncode == 0 +True + diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index a50e146b3b4..b3c579b8d55 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -552,14 +552,15 @@ for cls in (BEREmptySeqOf, OEREmptySeqOf, PEREmptySeqOf): True -= field hooks present after contrib load -assert ASN1_Codecs.hooks[ASN1_Codecs.OER] += encode_to decode_from after contrib load +# OER/PER register as first-class codecs; compound fields dispatch statically. +assert hasattr(ASN1_Codecs.OER, "new_encoder") -assert ASN1_Codecs.hooks[ASN1_Codecs.PER] +assert hasattr(ASN1_Codecs.PER, "new_decoder") -assert ASN1_Codecs.OER.hook("sequence_m2i") is not None +assert hasattr(ASN1F_SEQUENCE, "encode_to") -assert ASN1_Codecs.PER.hook("sequence_m2i") is not None +assert hasattr(ASN1F_SEQUENCE, "decode_from") True @@ -606,3 +607,136 @@ assert raw(_AsnObjectDefault(a=ASN1_INTEGER(7))) == raw(_AsnObjectDefault(a=7)) assert raw(_AsnObjectDefault(a=7)) == b"\x00" True + +% PR #5050 refactor targets (shared ASN.1) + +def _value(obj): + return getattr(obj, "val", obj) + ++ Finding - flexible BER tags must not mutate shared field descriptors += flexible explicit tag decode keeps schema descriptor immutable +class RefactorFlexibleExplicit(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER( + "n", 0, + explicit_tag=0xA0, + flexible_tag=True, + ) + +field = RefactorFlexibleExplicit.ASN1_root +original_tag = field.explicit_tag +canonical = raw(RefactorFlexibleExplicit(n=5)) +assert canonical[0] == 0xA0 +foreign = b"\xa1" + canonical[1:] +decoded = RefactorFlexibleExplicit(foreign) +assert _value(decoded.n) == 5 +# Packet-local tolerance must not rewrite the class-level schema. +assert field.explicit_tag == original_tag == 0xA0 +# A later independent build must still use the declared tag. +assert raw(RefactorFlexibleExplicit(n=5)) == canonical +True + += flexible implicit tag decode keeps schema descriptor immutable +class RefactorFlexibleImplicit(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER( + "n", 0, + implicit_tag=0x80, + flexible_tag=True, + ) + +field = RefactorFlexibleImplicit.ASN1_root +original_tag = field.implicit_tag +canonical = raw(RefactorFlexibleImplicit(n=5)) +assert canonical[0] == 0x80 +foreign = b"\x81" + canonical[1:] +decoded = RefactorFlexibleImplicit(foreign) +assert _value(decoded.n) == 5 +assert field.implicit_tag == original_tag == 0x80 +assert raw(RefactorFlexibleImplicit(n=5)) == canonical +True + += two flexible decodes with different observed tags do not influence each other +class RefactorFlexibleSequence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER( + "n", 0, + explicit_tag=0xA0, + flexible_tag=True, + ), + ) + +field = RefactorFlexibleSequence.ASN1_root.seq[0] +canonical = raw(RefactorFlexibleSequence(n=7)) +# SEQUENCE wrapper is 0x30; component tag begins after the sequence length. +# Locate the declared explicit tag instead of assuming a BER length width. +pos = canonical.index(b"\xa0") +all( + _value(RefactorFlexibleSequence( + canonical[:pos] + bytes([observed_tag]) + canonical[pos + 1:] + ).n) == 7 + and field.explicit_tag == 0xA0 + and raw(RefactorFlexibleSequence(n=7)) == canonical + for observed_tag in [0xA1, 0xA2, 0xA3] +) + + ++ Finding - CHOICE packet classes must be dictionary keys, not hash(cls) += colliding class hashes do not corrupt BER CHOICE tagging +class RefactorCollidingASN1Meta(ASN1Packet_metaclass): + def __hash__(cls): + return 1 + +class RefactorCollisionA(ASN1_Packet, metaclass=RefactorCollidingASN1Meta): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE(ASN1F_INTEGER("x", 0)) + +class RefactorCollisionB(ASN1_Packet, metaclass=RefactorCollidingASN1Meta): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE(ASN1F_INTEGER("x", 0)) + +assert hash(RefactorCollisionA) == hash(RefactorCollisionB) + +class RefactorCollisionChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", None, + ASN1F_PACKET("a", None, RefactorCollisionA, explicit_tag=0xA0), + ASN1F_PACKET("b", None, RefactorCollisionB, explicit_tag=0xA1), + ) + +raw_a = raw(RefactorCollisionChoice(c=RefactorCollisionA(x=1))) +raw_b = raw(RefactorCollisionChoice(c=RefactorCollisionB(x=2))) +assert raw_a[0] == 0xA0 +assert raw_b[0] == 0xA1 +assert isinstance(RefactorCollisionChoice(raw_a).c, RefactorCollisionA) +assert isinstance(RefactorCollisionChoice(raw_b).c, RefactorCollisionB) +True + + ++ Cross-codec schema contracts += identical schema values can be assigned as Python values or ASN1 objects +class RefactorBERInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER("n", 0) + +class RefactorOERPlainInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0) + +all( + raw(cls(n=value)) == raw(cls(n=ASN1_INTEGER(value))) + for cls in [RefactorBERInt, RefactorOERPlainInteger] + for value in [-1, 0, 1, 127] +) + += importing OER and UPER leaves representative BER packet wire behavior intact +ber_before = raw(RefactorBERInt(n=5)) +import scapy.contrib.oer +import scapy.contrib.uper +ber_after = raw(RefactorBERInt(n=5)) +assert ber_before == ber_after +assert _value(RefactorBERInt(ber_after).n) == 5 +True + diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index 2e4704b9353..459a5eb9f02 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -457,39 +457,32 @@ assert BERcodec_STRING.enc(b"x", uper_max=10) == BERcodec_STRING.enc(b"x") BERcodec_SEQUENCE.enc(BERcodec_INTEGER.enc(1), uper_min=0) == BERcodec_SEQUENCE.enc(BERcodec_INTEGER.enc(1)) + ASN.1 codec tagging contract -= BER hooks the tagging of a field -tagging_enc = ASN1_Codecs.BER.hook("tagging_enc") += BER applies tagging on encode and decode +from scapy.asn1.ber import BER_tagging_enc, BER_tagging_dec -assert tagging_enc(b"\x02\x01\x05", implicit_tag=0xA0) == b"\xa0\x01\x05" +assert BER_tagging_enc(b"\x02\x01\x05", implicit_tag=0xA0) == b"\xa0\x01\x05" -assert ASN1_Codecs.hooks[ASN1_Codecs.BER]["tagging_enc"] is tagging_enc - -tagging_dec = ASN1_Codecs.BER.hook("tagging_dec") - -diff, payload = tagging_dec(b"\xa0\x01\x05", hidden_tag=2, implicit_tag=0xA0) +diff, payload = BER_tagging_dec(b"\xa0\x01\x05", hidden_tag=2, implicit_tag=0xA0) diff is None and payload == b"\x02\x01\x05" -= a codec that hooks no tagging leaves the encoding alone -class _NoHooks: += a non-BER codec leaves field tagging alone +class _NoTagging: ASN1_codec = ASN1_Codecs.CER -assert ASN1_Codecs.CER.hook("tagging_enc") is None - fld = ASN1F_INTEGER("n", 0, explicit_tag=0xA0) -assert fld._tagging_enc(_NoHooks(), b"\x02\x01\x05", explicit_tag=0xA0) == b"\x02\x01\x05" +assert fld._tagging_enc(_NoTagging(), b"\x02\x01\x05", explicit_tag=0xA0) == b"\x02\x01\x05" -fld._tagging_dec(_NoHooks(), b"\x02\x01\x05", explicit_tag=0xA0) == (None, b"\x02\x01\x05") +fld._tagging_dec(_NoTagging(), b"\x02\x01\x05", explicit_tag=0xA0) == (None, b"\x02\x01\x05") -= field _codec_kwargs and object-enc hooks += field _codec_kwargs and ASN1_Object unwrap class P(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_INTEGER("n", 0) fld = P.ASN1_root -assert fld._codec_kwargs(P()) == {"size_len": None} -assert fld._use_object_enc(P(), ASN1_INTEGER(5)) is True +assert fld._codec_kwargs(P())["size_len"] is None assert raw(P(n=ASN1_INTEGER(5))) == b"\x02\x01\x05" assert raw(P(n=5)) == b"\x02\x01\x05" @@ -498,7 +491,7 @@ class Sized(ASN1_Packet): ASN1_root = ASN1F_INTEGER("n", 0, size_len=1) sfld = Sized.ASN1_root -assert sfld._use_object_enc(Sized(), ASN1_INTEGER(5)) is False +assert sfld._constraints_kwargs(Sized())["size_len"] == 1 assert raw(Sized(n=ASN1_INTEGER(5))) == raw(Sized(n=5)) == b"\x02\x81\x01\x05" = field encode with extra kwargs via _codec_kwargs override @@ -519,9 +512,7 @@ plain = ASN1F_INTEGER("n", 0) assert plain.codec_opts == {} -assert plain._codec_kwargs(type("P", (), {"ASN1_codec": ASN1_Codecs.BER})()) == { - "size_len": None, -} +assert plain._codec_kwargs(type("P", (), {"ASN1_codec": ASN1_Codecs.BER})())["size_len"] is None constrained = ASN1F_INTEGER( "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, From de9494aeac5015937fbaf4b4afe8116d7a38a04c Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 1 Sep 2026 20:48:45 +0200 Subject: [PATCH 19/46] =?UTF-8?q?asn1:=20finish=20OER/UPER=20wire-path=20u?= =?UTF-8?q?nification=20(P0=E2=80=93P3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unify compound child dispatch on encode_to/decode_from, add OER encoder contexts and PER bit-stream helpers, and read field constraints directly in primitive codecs instead of round-tripping through codec_kwargs. Co-authored-by: Cursor AI-Assisted: yes (Cursor Agent) --- scapy/asn1/compound.py | 136 +++++++++++++++++------------ scapy/asn1/constraints.py | 56 +++++++++++- scapy/asn1/context.py | 75 +++++++++++++++- scapy/asn1/oer.py | 134 +++++++++-------------------- scapy/asn1/uper.py | 175 +++++++++++++++++++++----------------- scapy/asn1fields.py | 90 ++++++++------------ scapy/asn1packet.py | 2 +- 7 files changed, 384 insertions(+), 284 deletions(-) diff --git a/scapy/asn1/compound.py b/scapy/asn1/compound.py index 695d3da8292..fc6b19ebf65 100644 --- a/scapy/asn1/compound.py +++ b/scapy/asn1/compound.py @@ -19,6 +19,12 @@ ASN1_Object, ) from scapy.asn1.constraints import field_extensible, field_range +from scapy.asn1.context import ( + OER_Decoder, + OER_Encoder, + per_bit_decoder, + per_bit_encoder, +) def sequence_presence_bits(field, pkt): @@ -115,10 +121,18 @@ def _sequence_encode_children(field, pkt, encode): # ---- SEQUENCE ------------------------------------------------------------- +def _encode_child_to_bytes(pkt, obj): + # type: (Any, Any) -> bytes + child_enc = OER_Encoder() + obj.encode_to(pkt, child_enc) + return child_enc.finish() + + def sequence_encode_to(field, pkt, enc): # type: (Any, Any, Any) -> None - if enc.codec is ASN1_Codecs.PER: - write_uper_presence_bits(enc.inner, field, pkt) + bit_enc = per_bit_encoder(enc) + if bit_enc is not None: + write_uper_presence_bits(bit_enc, field, pkt) _sequence_encode_children( field, pkt, lambda obj: obj.encode_to(pkt, enc), @@ -126,7 +140,10 @@ def sequence_encode_to(field, pkt, enc): return if enc.codec is ASN1_Codecs.OER: parts = [write_oer_presence_bits(sequence_presence_bits(field, pkt))] - _sequence_encode_children(field, pkt, lambda obj: parts.append(obj.build(pkt))) + _sequence_encode_children( + field, pkt, + lambda obj: parts.append(_encode_child_to_bytes(pkt, obj)), + ) enc.write(b"".join(parts)) return s = reduce(lambda x, y: x + y.build(pkt), field.seq, b"") @@ -135,9 +152,9 @@ def sequence_encode_to(field, pkt, enc): def sequence_decode_from(field, pkt, dec): # type: (Any, Any, Any) -> None - if pkt.ASN1_codec is ASN1_Codecs.PER: - inner = dec.inner if hasattr(dec, "inner") else dec - presence = read_uper_presence_bits(inner, field) + bit_dec = per_bit_decoder(dec) + if bit_dec is not None: + presence = read_uper_presence_bits(bit_dec, field) _sequence_decode_children( field, pkt, presence, lambda obj: obj.decode_from(pkt, dec), @@ -152,22 +169,12 @@ def sequence_decode_from(field, pkt, dec): dec.set_remainder(b"") return presence, s = read_oer_presence_bits(s, field) - from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional - opt_index = 0 - for obj in field.seq: - target = obj - if isinstance(obj, ASN1F_optional): - if not presence[opt_index]: - obj.set_missing(pkt) - opt_index += 1 - continue - opt_index += 1 - target = obj.fld - try: - s = target.dissect(pkt, s) - except ASN1F_badsequence: - break - dec.set_remainder(s) + child_dec = OER_Decoder(s) + _sequence_decode_children( + field, pkt, presence, + lambda obj: obj.decode_from(pkt, child_dec), + ) + dec.set_remainder(child_dec.remaining()) return s = dec.remaining() s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) @@ -187,8 +194,8 @@ def sequence_decode_from(field, pkt, dec): def sequence_of_encode_to(field, pkt, enc): # type: (Any, Any, Any) -> None - if enc.codec is ASN1_Codecs.PER: - uper_sequence_of_encode_into(field, enc.inner, pkt) + if per_bit_encoder(enc) is not None: + uper_sequence_of_encode_into(field, enc, pkt) return if enc.codec is ASN1_Codecs.OER: enc.write(oer_sequence_of_bytes(field, pkt)) @@ -207,10 +214,11 @@ def sequence_of_encode_to(field, pkt, enc): def sequence_of_decode_from(field, pkt, dec): # type: (Any, Any, Any) -> None - if dec.codec is ASN1_Codecs.PER: + bit_dec = per_bit_decoder(dec) + if bit_dec is not None: field.set_val( pkt, - uper_sequence_of_decode_from_decoder(field, pkt, dec.inner), + uper_sequence_of_decode_from_decoder(field, pkt, dec), ) return if dec.codec is ASN1_Codecs.OER: @@ -269,10 +277,13 @@ def oer_sequence_of_decode(field, pkt, s): def uper_sequence_of_encode_into(field, enc, pkt, value=None): # type: (Any, Any, Any, Any) -> None + bit_enc = per_bit_encoder(enc) + if bit_enc is None: + raise ASN1_Error("uper_sequence_of_encode_into: PER encoder required") if value is None: value = getattr(pkt, field.name) if value is None: - _uper_count_enc(field, enc, 0, lambda offset, size: None) + _uper_count_enc(field, bit_enc, 0, lambda offset, size: None) return count = len(value) @@ -280,9 +291,9 @@ def append_items(offset, size): # type: (int, int) -> None for item in value[offset:offset + size]: if field.holds_packets: - item.ASN1_root.encode_into(enc, item) + item.ASN1_root.encode_to(item, enc) else: - field.fld.encode_into(enc, pkt, item) + field.fld.encode_into(bit_enc, pkt, item) uper_min, uper_max = field_range(field) if field_extensible(field): @@ -290,18 +301,23 @@ def append_items(offset, size): uper_min is not None and uper_max is not None and uper_min <= count <= uper_max ): - enc.append_bit(0) + bit_enc.append_bit(0) else: - enc.append_bit(1) - enc.append_fragmented(count, append_items) + bit_enc.append_bit(1) + bit_enc.append_fragmented(count, append_items) return - _uper_count_enc(field, enc, count, append_items) + _uper_count_enc(field, bit_enc, count, append_items) def uper_sequence_of_decode_from_decoder(field, pkt, dec): # type: (Any, Any, Any) -> list from scapy.asn1.uper import UPER_constrained_int_dec + bit_dec = per_bit_decoder(dec) + if bit_dec is None: + raise ASN1_Error( + "uper_sequence_of_decode_from_decoder: PER decoder required" + ) lst = [] def read_items(count): @@ -313,16 +329,16 @@ def read_items(count): p.ASN1_root.decode_from(p, dec) lst.append(p) else: - lst.append(field.fld.m2i_from_decoder(pkt, dec)) + lst.append(field.fld.m2i_from_decoder(pkt, bit_dec)) - if field_extensible(field) and dec.read_bit(): - dec.read_fragmented(read_items) + if field_extensible(field) and bit_dec.read_bit(): + bit_dec.read_fragmented(read_items) else: uper_min, uper_max = field_range(field) if uper_min is not None and uper_max is not None: - read_items(UPER_constrained_int_dec(dec, uper_min, uper_max)) + read_items(UPER_constrained_int_dec(bit_dec, uper_min, uper_max)) else: - dec.read_fragmented(read_items) + bit_dec.read_fragmented(read_items) return lst @@ -343,8 +359,8 @@ def _uper_count_enc(field, enc, count, append_items): def choice_encode_to(field, pkt, enc): # type: (Any, Any, Any) -> None value = getattr(pkt, field.name) - if enc.codec is ASN1_Codecs.PER: - uper_choice_encode_into(field, enc.inner, pkt, value) + if per_bit_encoder(enc) is not None: + uper_choice_encode_into(field, enc, pkt, value) return if enc.codec is ASN1_Codecs.OER: enc.write(oer_choice_bytes(field, pkt, value)) @@ -354,8 +370,8 @@ def choice_encode_to(field, pkt, enc): def choice_decode_from(field, pkt, dec): # type: (Any, Any, Any) -> None - if dec.codec is ASN1_Codecs.PER: - field.set_val(pkt, uper_choice_decode_from_decoder(field, pkt, dec.inner)) + if per_bit_decoder(dec) is not None: + field.set_val(pkt, uper_choice_decode_from_decoder(field, pkt, dec)) return if dec.codec is ASN1_Codecs.OER: val, remain = oer_choice_decode(field, pkt, dec.remaining()) @@ -462,6 +478,9 @@ def uper_choice_encode_into(field, enc, pkt, value=None): # type: (Any, Any, Any, Any) -> None from scapy.asn1.uper import UPER_choice_index_enc + bit_enc = per_bit_encoder(enc) + if bit_enc is None: + raise ASN1_Error("uper_choice_encode_into: PER encoder required") if value is None: value = getattr(pkt, field.name) if value is None: @@ -473,35 +492,40 @@ def uper_choice_encode_into(field, enc, pkt, value=None): field.name ) if field_extensible(field): - enc.append_bit(0) + bit_enc.append_bit(0) order = field.canonical_order tag = field.choice_order[index] canon_idx = field.canonical_index[tag] if len(order) > 1: - UPER_choice_index_enc(enc, canon_idx, len(order)) + UPER_choice_index_enc(bit_enc, canon_idx, len(order)) choice = order[canon_idx] if isinstance(choice, type) and hasattr(choice, "ASN1_root"): - value.ASN1_root.encode_into(enc, value) + value.ASN1_root.encode_to(value, enc) elif hasattr(choice, "cls"): uper_packet_encode_into(choice, enc, pkt, value) elif isinstance(choice, type): - choice(field.name, b"").encode_into(enc, pkt, value) + choice(field.name, b"").encode_into(bit_enc, pkt, value) else: - choice.encode_into(enc, pkt, value) + choice.encode_into(bit_enc, pkt, value) def uper_choice_decode_from_decoder(field, pkt, dec): # type: (Any, Any, Any) -> Any from scapy.asn1.uper import UPER_Decoding_Error, UPER_choice_index_dec + bit_dec = per_bit_decoder(dec) + if bit_dec is None: + raise ASN1_Error( + "uper_choice_decode_from_decoder: PER decoder required" + ) if field_extensible(field): - if dec.read_bit(): + if bit_dec.read_bit(): raise UPER_Decoding_Error( "ASN1F_CHOICE: extension additions are not supported" ) order = field.canonical_order if len(order) > 1: - index = UPER_choice_index_dec(dec, len(order)) + index = UPER_choice_index_dec(bit_dec, len(order)) else: index = 0 if index >= len(order): @@ -518,8 +542,8 @@ def uper_choice_decode_from_decoder(field, pkt, dec): if hasattr(choice, "cls"): return uper_packet_decode_from_decoder(choice, pkt, dec) if isinstance(choice, type): - return choice(field.name, b"").m2i_from_decoder(pkt, dec) - return choice.m2i_from_decoder(pkt, dec) + return choice(field.name, b"").m2i_from_decoder(pkt, bit_dec) + return choice.m2i_from_decoder(pkt, bit_dec) # ---- PACKET (nested ASN1_Packet) ------------------------------------------ @@ -528,16 +552,16 @@ def packet_encode_to(field, pkt, enc, value=None): # type: (Any, Any, Any, Any) -> None if value is None: value = getattr(pkt, field.name) - if enc.codec is ASN1_Codecs.PER: - uper_packet_encode_into(field, enc.inner, pkt, value) + if per_bit_encoder(enc) is not None: + uper_packet_encode_into(field, enc, pkt, value) return enc.write(ber_oer_packet_bytes(field, pkt, value)) def packet_decode_from(field, pkt, dec): # type: (Any, Any, Any) -> None - if dec.codec is ASN1_Codecs.PER: - field.set_val(pkt, uper_packet_decode_from_decoder(field, pkt, dec.inner)) + if per_bit_decoder(dec) is not None: + field.set_val(pkt, uper_packet_decode_from_decoder(field, pkt, dec)) return val, remain = ber_oer_packet_decode(field, pkt, dec.remaining()) field.set_val(pkt, val) @@ -585,7 +609,7 @@ def uper_packet_encode_into(field, enc, pkt, value=None): return if isinstance(value, ASN1_Object): value = value.val - value.ASN1_root.encode_into(enc, value) + value.ASN1_root.encode_to(value, enc) def uper_packet_decode_from_decoder(field, pkt, dec): diff --git a/scapy/asn1/constraints.py b/scapy/asn1/constraints.py index ba3d31a4709..35d156b6a9b 100644 --- a/scapy/asn1/constraints.py +++ b/scapy/asn1/constraints.py @@ -180,7 +180,7 @@ def _merge_legacy(params, legacy): def codec_kwargs(field=None, pkt=None, **legacy): # type: (Any, Any, **Any) -> Dict[str, Any] - """Legacy keyword dict for codec methods under migration.""" + """Deprecated legacy keyword dict; prefer reading ``field.constraints``.""" p = encoding_params(field, pkt=pkt, **legacy) kw = { "size_len": p.size_len, @@ -193,3 +193,57 @@ def codec_kwargs(field=None, pkt=None, **legacy): if p.uper_enum_values is not None: kw["uper_enum_values"] = p.uper_enum_values return kw + + +def oer_size_len(field=None, size_len=None): + # type: (Any, Optional[int]) -> Optional[int] + if size_len is not None: + return size_len + if field is not None: + return field.size_len + return None + + +def oer_unsigned(field=None, oer_unsigned=None): + # type: (Any, Optional[bool]) -> bool + if oer_unsigned is not None: + return oer_unsigned + if field is not None: + return field.constraints.unsigned + return False + + +def uper_size_len(field=None, size_len=None): + # type: (Any, Optional[int]) -> Optional[int] + return oer_size_len(field, size_len) + + +def uper_extensible(field=None, uper_extensible=None, oer_extensible=None): + # type: (Any, Optional[bool], Optional[bool]) -> bool + if uper_extensible is not None: + return uper_extensible + if oer_extensible: + return True + if field is not None: + return field.constraints.extensible + return False + + +def uper_int_range(field=None, uper_min=None, uper_max=None): + # type: (Any, Optional[int], Optional[int]) -> Tuple[Optional[int], Optional[int]] + if uper_min is not None or uper_max is not None: + return uper_min, uper_max + if field is not None: + return field_range(field) + return None, None + + +def uper_enum_values(field=None, pkt=None, uper_enum_values=None): + # type: (Any, Any, Optional[List[int]]) -> Optional[List[int]] + if uper_enum_values is not None: + return uper_enum_values + if field is not None and pkt is not None and hasattr(field, "uper_enum_values"): + from scapy.asn1.asn1 import ASN1_Codecs + if getattr(pkt, "ASN1_codec", None) is ASN1_Codecs.PER: + return field.uper_enum_values() + return None diff --git a/scapy/asn1/context.py b/scapy/asn1/context.py index c1534efbaec..62f37d364bd 100644 --- a/scapy/asn1/context.py +++ b/scapy/asn1/context.py @@ -26,6 +26,10 @@ def remaining(self): # type: () -> bytes raise NotImplementedError + def set_remainder(self, remainder): + # type: (bytes) -> None + raise NotImplementedError + class BER_Encoder(ASN1Encoder): from scapy.asn1.asn1 import ASN1_Codecs @@ -75,6 +79,26 @@ def set_remainder(self, remainder): self._offset = 0 +class OER_Encoder(BER_Encoder): + from scapy.asn1.asn1 import ASN1_Codecs + + codec = ASN1_Codecs.OER + + def __init__(self): + # type: () -> None + super(OER_Encoder, self).__init__(codec=self.codec) + + +class OER_Decoder(BER_Decoder): + from scapy.asn1.asn1 import ASN1_Codecs + + codec = ASN1_Codecs.OER + + def __init__(self, data): + # type: (bytes) -> None + super(OER_Decoder, self).__init__(data, codec=self.codec) + + class UPER_EncoderContext(ASN1Encoder): from scapy.asn1.asn1 import ASN1_Codecs @@ -85,6 +109,11 @@ def __init__(self): from scapy.asn1.uper import UPER_Encoder self._enc = UPER_Encoder() + @property + def bit_encoder(self): + # type: () -> _UPER_Encoder + return self._enc + @property def inner(self): # type: () -> _UPER_Encoder @@ -105,6 +134,11 @@ def __init__(self, data): from scapy.asn1.uper import UPER_Decoder self._dec = UPER_Decoder(data) + @property + def bit_decoder(self): + # type: () -> _UPER_Decoder + return self._dec + @property def inner(self): # type: () -> _UPER_Decoder @@ -114,6 +148,19 @@ def remaining(self): # type: () -> bytes return self._dec.remaining() + def set_remainder(self, remainder): + # type: (bytes) -> None + from scapy.asn1.uper import UPER_Decoder + self._dec = UPER_Decoder(remainder) + + def check_no_remainder(self, name): + # type: (str) -> None + from scapy.asn1.uper import UPER_Decoding_Error + if self._dec.remaining(): + raise UPER_Decoding_Error( + "unexpected remainder in %s" % name, + ) + def new_encoder(codec): # type: (Any) -> ASN1Encoder @@ -121,7 +168,7 @@ def new_encoder(codec): if codec is ASN1_Codecs.PER: return UPER_EncoderContext() if codec is ASN1_Codecs.OER: - return BER_Encoder(codec=ASN1_Codecs.OER) + return OER_Encoder() return BER_Encoder() @@ -131,5 +178,29 @@ def new_decoder(codec, data): if codec is ASN1_Codecs.PER: return UPER_DecoderContext(data) if codec is ASN1_Codecs.OER: - return BER_Decoder(data, codec=ASN1_Codecs.OER) + return OER_Decoder(data) return BER_Decoder(data) + + +def per_bit_encoder(enc): + # type: (Any) -> Any + """Return the PER bit encoder, or *None* for byte-oriented contexts.""" + bit_enc = getattr(enc, "bit_encoder", None) + if bit_enc is not None: + return bit_enc + from scapy.asn1.uper import UPER_Encoder + if isinstance(enc, UPER_Encoder): + return enc + return None + + +def per_bit_decoder(dec): + # type: (Any) -> Any + """Return the PER bit decoder, or *None* for byte-oriented contexts.""" + bit_dec = getattr(dec, "bit_decoder", None) + if bit_dec is not None: + return bit_dec + from scapy.asn1.uper import UPER_Decoder + if isinstance(dec, UPER_Decoder): + return dec + return None diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index accbce5305a..b42827550c4 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -346,15 +346,9 @@ class OERcodec_INTEGER(OERcodec_Object[int]): @classmethod def enc(cls, i, field=None, size_len=None, oer_unsigned=None, **_kwargs): # type: (int, Any, Optional[int], Optional[bool], **Any) -> bytes - from scapy.asn1.constraints import codec_kwargs - legacy = dict(_kwargs) - if size_len is not None: - legacy["size_len"] = size_len - if oer_unsigned is not None: - legacy["oer_unsigned"] = oer_unsigned - kw = codec_kwargs(field, **legacy) - size_len = kw["size_len"] - oer_unsigned = kw["oer_unsigned"] + from scapy.asn1.constraints import oer_size_len, oer_unsigned as _oer_unsigned + size_len = oer_size_len(field, size_len) + oer_unsigned = _oer_unsigned(field, oer_unsigned) if oer_unsigned and i < 0: raise OER_Encoding_Error( "%s: %i is negative for an unsigned type" % (cls.__name__, i) @@ -387,15 +381,9 @@ def do_dec(cls, **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[int], bytes] - from scapy.asn1.constraints import codec_kwargs - legacy = dict(_kwargs) - if size_len is not None: - legacy["size_len"] = size_len - if oer_unsigned is not None: - legacy["oer_unsigned"] = oer_unsigned - kw = codec_kwargs(field, **legacy) - size_len = kw["size_len"] - oer_unsigned = kw["oer_unsigned"] + from scapy.asn1.constraints import oer_size_len, oer_unsigned as _oer_unsigned + size_len = oer_size_len(field, size_len) + oer_unsigned = _oer_unsigned(field, oer_unsigned) if size_len in (1, 2, 4, 8): _OER_check_len(cls.__name__, s, size_len) x = struct.unpack( @@ -456,12 +444,8 @@ def do_dec(cls, **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[str], bytes] - from scapy.asn1.constraints import codec_kwargs - legacy = dict(_kwargs) - if size_len is not None: - legacy["size_len"] = size_len - kw = codec_kwargs(field, **legacy) - size_len = kw["size_len"] + from scapy.asn1.constraints import oer_size_len + size_len = oer_size_len(field, size_len) if size_len: number_of_bytes = (size_len + 7) // 8 _OER_check_len(cls.__name__, s, number_of_bytes) @@ -489,12 +473,8 @@ def do_dec(cls, @classmethod def enc(cls, _s, field=None, size_len=None, **_kwargs): # type: (AnyStr, Any, Optional[int], **Any) -> bytes - from scapy.asn1.constraints import codec_kwargs - legacy = dict(_kwargs) - if size_len is not None: - legacy["size_len"] = size_len - kw = codec_kwargs(field, **legacy) - size_len = kw["size_len"] + from scapy.asn1.constraints import oer_size_len + size_len = oer_size_len(field, size_len) s = bytes_encode(_s) if size_len: # X.696 13.3: a fixed size means the bits are written padded to a @@ -516,12 +496,8 @@ class OERcodec_STRING(OERcodec_Object[str]): @classmethod def enc(cls, _s, field=None, size_len=None, **_kwargs): # type: (Union[str, bytes], Any, Optional[int], **Any) -> bytes - from scapy.asn1.constraints import codec_kwargs - legacy = dict(_kwargs) - if size_len is not None: - legacy["size_len"] = size_len - kw = codec_kwargs(field, **legacy) - size_len = kw["size_len"] + from scapy.asn1.constraints import oer_size_len + size_len = oer_size_len(field, size_len) s = bytes_encode(_s) if size_len: # X.696 16.1: a fixed size means no length determinant. @@ -545,12 +521,8 @@ def do_dec(cls, **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[Any], bytes] - from scapy.asn1.constraints import codec_kwargs - legacy = dict(_kwargs) - if size_len is not None: - legacy["size_len"] = size_len - kw = codec_kwargs(field, **legacy) - size_len = kw["size_len"] + from scapy.asn1.constraints import oer_size_len + size_len = oer_size_len(field, size_len) if size_len: _OER_check_len(cls.__name__, s, size_len) return cls.tag.asn1_object(s[:size_len]), s[size_len:] @@ -657,52 +629,33 @@ def do_dec(cls, return cls.asn1_object(value), s[length + 1:] -class OERcodec_UTF8_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.UTF8_STRING - - -class OERcodec_NUMERIC_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.NUMERIC_STRING - - -class OERcodec_PRINTABLE_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING - - -class OERcodec_T61_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.T61_STRING - - -class OERcodec_VIDEOTEX_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.VIDEOTEX_STRING - - -class OERcodec_IA5_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.IA5_STRING - - -class OERcodec_GENERAL_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.GENERAL_STRING - - -class OERcodec_UTC_TIME(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.UTC_TIME - - -class OERcodec_GENERALIZED_TIME(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME - - -class OERcodec_ISO646_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.ISO646_STRING +_OER_STRING_TAGS = ( + "UTF8_STRING", + "NUMERIC_STRING", + "PRINTABLE_STRING", + "T61_STRING", + "VIDEOTEX_STRING", + "IA5_STRING", + "GENERAL_STRING", + "UTC_TIME", + "GENERALIZED_TIME", + "ISO646_STRING", + "UNIVERSAL_STRING", + "BMP_STRING", +) -class OERcodec_UNIVERSAL_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.UNIVERSAL_STRING +def _oer_string_codec(name): + # type: (str) -> type + return type( + "OERcodec_%s" % name, + (OERcodec_STRING,), + {"tag": getattr(ASN1_Class_UNIVERSAL, name)}, + ) -class OERcodec_BMP_STRING(OERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.BMP_STRING +for _tag_name in _OER_STRING_TAGS: + globals()["OERcodec_%s" % _tag_name] = _oer_string_codec(_tag_name) class OERcodec_SEQUENCE(OERcodec_Object[Union[bytes, List['OERcodec_Object[Any]']]]): @@ -804,18 +757,15 @@ class OERcodec_TIME_TICKS(OERcodec_INTEGER): def oer_sequence_build(field, pkt): # type: (Any, Any) -> bytes from scapy.asn1fields import ASN1F_field - from scapy.asn1.context import BER_Encoder - from scapy.asn1.asn1 import ASN1_Codecs - enc = BER_Encoder(codec=ASN1_Codecs.OER) + from scapy.asn1.context import OER_Encoder + enc = OER_Encoder() sequence_encode_to(field, pkt, enc) return ASN1F_field.i2m(field, pkt, enc.finish()) def oer_sequence_m2i(field, pkt, s): # type: (Any, Any, bytes) -> Tuple[Any, bytes] - from scapy.asn1.context import BER_Decoder - from scapy.asn1.asn1 import ASN1_Codecs - dec = BER_Decoder(s, codec=ASN1_Codecs.OER) + from scapy.asn1.context import OER_Decoder + dec = OER_Decoder(s) _oer_sequence_decode_from(field, pkt, dec) - return [], dec.remaining() - + return [], dec.remaining() \ No newline at end of file diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 09dd1f5e614..2acf7ed0c3e 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -513,24 +513,20 @@ def encode_into(cls, **_kwargs # type: Any ): # type: (...) -> None - from scapy.asn1.constraints import codec_kwargs - legacy = dict(_kwargs) - if size_len is not None: - legacy["size_len"] = size_len - if uper_min is not None: - legacy["uper_min"] = uper_min - if uper_max is not None: - legacy["uper_max"] = uper_max - if oer_unsigned is not None: - legacy["oer_unsigned"] = oer_unsigned - if uper_extensible is not None: - legacy["uper_extensible"] = uper_extensible - kw = codec_kwargs(field, **legacy) + from scapy.asn1.constraints import ( + oer_size_len, + oer_unsigned as _oer_unsigned, + uper_extensible as _uper_extensible, + uper_int_range, + ) + size_len = oer_size_len(field, size_len) + uper_min, uper_max = uper_int_range(field, uper_min, uper_max) + oer_unsigned = _oer_unsigned(field, oer_unsigned) + extensible = _uper_extensible(field, uper_extensible) minimum, maximum = _uper_int_range( - kw["size_len"], kw["uper_min"], kw["uper_max"], kw["oer_unsigned"], + size_len, uper_min, uper_max, oer_unsigned, ) - uper_extensible = kw["uper_extensible"] - if uper_extensible and minimum is not None and maximum is not None: + if extensible and minimum is not None and maximum is not None: if minimum <= i <= maximum: enc.append_bit(0) else: @@ -555,24 +551,20 @@ def dec_from_decoder(cls, **_kwargs # type: Any ): # type: (...) -> ASN1_Object[int] - from scapy.asn1.constraints import codec_kwargs - legacy = dict(_kwargs) - if size_len is not None: - legacy["size_len"] = size_len - if uper_min is not None: - legacy["uper_min"] = uper_min - if uper_max is not None: - legacy["uper_max"] = uper_max - if oer_unsigned is not None: - legacy["oer_unsigned"] = oer_unsigned - if uper_extensible is not None: - legacy["uper_extensible"] = uper_extensible - kw = codec_kwargs(field, pkt=pkt, **legacy) + from scapy.asn1.constraints import ( + oer_size_len, + oer_unsigned as _oer_unsigned, + uper_extensible as _uper_extensible, + uper_int_range, + ) + size_len = oer_size_len(field, size_len) + uper_min, uper_max = uper_int_range(field, uper_min, uper_max) + oer_unsigned = _oer_unsigned(field, oer_unsigned) + extensible = _uper_extensible(field, uper_extensible) minimum, maximum = _uper_int_range( - kw["size_len"], kw["uper_min"], kw["uper_max"], kw["oer_unsigned"], + size_len, uper_min, uper_max, oer_unsigned, ) - uper_extensible = kw["uper_extensible"] - if uper_extensible and minimum is not None and maximum is not None: + if extensible and minimum is not None and maximum is not None: if dec.read_bit(): value = dec.read_unconstrained_whole_number() return cls.asn1_object(value) @@ -619,12 +611,16 @@ class UPERcodec_BIT_STRING(UPERcodec_Object[str]): def encode_into(cls, enc, # type: UPER_Encoder _s, # type: Any + field=None, # type: Any size_len=0, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] **_kwargs # type: Any ): # type: (...) -> None + from scapy.asn1.constraints import oer_size_len, uper_int_range + size_len = oer_size_len(field, size_len) or 0 + uper_min, uper_max = uper_int_range(field, uper_min, uper_max) if isinstance(_s, tuple) and len(_s) == 2: data, nbits = _s s = bytes_encode(data) @@ -660,12 +656,16 @@ def encode_into(cls, @classmethod def dec_from_decoder(cls, dec, # type: UPER_Decoder + field=None, # type: Any size_len=0, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] **_kwargs # type: Any ): # type: (...) -> ASN1_Object[str] + from scapy.asn1.constraints import oer_size_len, uper_int_range + size_len = oer_size_len(field, size_len) or 0 + uper_min, uper_max = uper_int_range(field, uper_min, uper_max) minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) if minimum is not None and maximum is not None: nbits = minimum @@ -697,12 +697,16 @@ class UPERcodec_STRING(UPERcodec_Object[str]): def encode_into(cls, enc, # type: UPER_Encoder _s, # type: Union[str, bytes] + field=None, # type: Any size_len=0, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] **_kwargs # type: Any ): # type: (...) -> None + from scapy.asn1.constraints import oer_size_len, uper_int_range + size_len = oer_size_len(field, size_len) or 0 + uper_min, uper_max = uper_int_range(field, uper_min, uper_max) s = bytes_encode(_s) minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) UPER_octet_string_enc(enc, s, minimum, maximum) @@ -710,12 +714,16 @@ def encode_into(cls, @classmethod def dec_from_decoder(cls, dec, # type: UPER_Decoder + field=None, # type: Any size_len=0, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] **_kwargs # type: Any ): # type: (...) -> ASN1_Object[Any] + from scapy.asn1.constraints import oer_size_len, uper_int_range + size_len = oer_size_len(field, size_len) or 0 + uper_min, uper_max = uper_int_range(field, uper_min, uper_max) minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) raw = UPER_octet_string_dec(dec, minimum, maximum) return cls.asn1_object(raw) @@ -808,16 +816,30 @@ class UPERcodec_ENUMERATED(UPERcodec_INTEGER): def encode_into(cls, enc, # type: UPER_Encoder i, # type: int + field=None, # type: Any + pkt=None, # type: Any size_len=0, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] uper_enum_values=None, # type: Optional[List[int]] - uper_extensible=False, # type: bool + uper_extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> None + from scapy.asn1.constraints import ( + oer_size_len, + uper_enum_values as _uper_enum_values, + uper_extensible as _uper_extensible, + uper_int_range, + ) + size_len = oer_size_len(field, size_len) or 0 + uper_min, uper_max = uper_int_range(field, uper_min, uper_max) + uper_enum_values = _uper_enum_values( + field, pkt, uper_enum_values, + ) + extensible = _uper_extensible(field, uper_extensible) if uper_enum_values is not None: - if uper_extensible: + if extensible: # X.691 14.3: a one bit prefix says whether the value is an # extension addition. Only root values can be encoded. if i not in uper_enum_values: @@ -836,16 +858,30 @@ def encode_into(cls, @classmethod def dec_from_decoder(cls, dec, # type: UPER_Decoder + field=None, # type: Any + pkt=None, # type: Any size_len=0, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] uper_enum_values=None, # type: Optional[List[int]] - uper_extensible=False, # type: bool + uper_extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> ASN1_Object[int] + from scapy.asn1.constraints import ( + oer_size_len, + uper_enum_values as _uper_enum_values, + uper_extensible as _uper_extensible, + uper_int_range, + ) + size_len = oer_size_len(field, size_len) or 0 + uper_min, uper_max = uper_int_range(field, uper_min, uper_max) + uper_enum_values = _uper_enum_values( + field, pkt, uper_enum_values, + ) + extensible = _uper_extensible(field, uper_extensible) if uper_enum_values is not None: - if uper_extensible and dec.read_bit(): + if extensible and dec.read_bit(): raise UPER_Decoding_Error( "UPERcodec_ENUMERATED: extension additions are not " "supported" @@ -947,53 +983,33 @@ class UPERcodec_TIME_TICKS(UPERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.TIME_TICKS -# string aliases -class UPERcodec_UTF8_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.UTF8_STRING - - -class UPERcodec_NUMERIC_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.NUMERIC_STRING - - -class UPERcodec_PRINTABLE_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING - - -class UPERcodec_T61_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.T61_STRING - - -class UPERcodec_VIDEOTEX_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.VIDEOTEX_STRING - - -class UPERcodec_IA5_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.IA5_STRING - - -class UPERcodec_GENERAL_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.GENERAL_STRING - - -class UPERcodec_UTC_TIME(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.UTC_TIME - - -class UPERcodec_GENERALIZED_TIME(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME - - -class UPERcodec_ISO646_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.ISO646_STRING +_UPER_STRING_TAGS = ( + "UTF8_STRING", + "NUMERIC_STRING", + "PRINTABLE_STRING", + "T61_STRING", + "VIDEOTEX_STRING", + "IA5_STRING", + "GENERAL_STRING", + "UTC_TIME", + "GENERALIZED_TIME", + "ISO646_STRING", + "UNIVERSAL_STRING", + "BMP_STRING", +) -class UPERcodec_UNIVERSAL_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.UNIVERSAL_STRING +def _uper_string_codec(name): + # type: (str) -> type + return type( + "UPERcodec_%s" % name, + (UPERcodec_STRING,), + {"tag": getattr(ASN1_Class_UNIVERSAL, name)}, + ) -class UPERcodec_BMP_STRING(UPERcodec_STRING): - tag = ASN1_Class_UNIVERSAL.BMP_STRING +for _tag_name in _UPER_STRING_TAGS: + globals()["UPERcodec_%s" % _tag_name] = _uper_string_codec(_tag_name) ################################ @@ -1017,6 +1033,7 @@ class UPERcodec_BMP_STRING(UPERcodec_STRING): choice_encode_to as _uper_choice_encode_to, ) + def uper_sequence_m2i(field, pkt, s): from scapy.asn1.context import UPER_DecoderContext dec = UPER_DecoderContext(s) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 6d43bed875e..0539420369f 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -34,11 +34,11 @@ ) from scapy.asn1.ber import ( BER_Decoding_Error, - BER_id_dec, BER_tagging_dec, BER_tagging_enc, ) -from scapy.asn1.constraints import encoding_params, normalize_constraints +from scapy.asn1.constraints import normalize_constraints +from scapy.asn1.context import per_bit_decoder, per_bit_encoder from scapy.asn1.tag import asn1_tag_parts from scapy.base_classes import BasePacket from scapy.volatile import ( @@ -227,12 +227,10 @@ def _encode_item(self, pkt, item): # the BER type codec so the universal tag/length are applied. item = item.self_build() codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - from scapy.asn1.constraints import codec_kwargs - kw = codec_kwargs(self, pkt=pkt) - legacy = {} # type: Dict[str, Any] - if kw.get("size_len") is not None: - legacy["size_len"] = kw["size_len"] - return codec.enc(item, field=self, pkt=pkt, **legacy) + kwargs = {"field": self, "pkt": pkt} # type: Dict[str, Any] + if self.size_len is not None: + kwargs["size_len"] = self.size_len + return codec.enc(item, **kwargs) def i2repr(self, pkt, x): # type: (ASN1_Packet, _I) -> str @@ -298,12 +296,10 @@ def extract_packet(self, def m2i_from_decoder(self, pkt, dec): # type: (ASN1_Packet, Any) -> Any codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - from scapy.asn1.constraints import codec_kwargs - kw = codec_kwargs(self, pkt=pkt) - legacy = {k: v for k, v in kw.items() if v is not None} - return codec.dec_from_decoder( # type: ignore[attr-defined] - dec, field=self, pkt=pkt, **legacy, - ) + kwargs = {"field": self, "pkt": pkt} # type: Dict[str, Any] + if self.size_len is not None: + kwargs["size_len"] = self.size_len + return codec.dec_from_decoder(dec, **kwargs) # type: ignore[attr-defined] def dissect_from_decoder(self, pkt, dec): # type: (ASN1_Packet, Any) -> None @@ -329,24 +325,28 @@ def encode_into(self, enc, pkt, value=None): ) else: raw = value - from scapy.asn1.constraints import codec_kwargs - kw = codec_kwargs(self, pkt=pkt) - legacy = {k: v for k, v in kw.items() if v is not None} - codec.encode_into( # type: ignore[attr-defined] - enc, raw, field=self, pkt=pkt, **legacy, - ) + bit_enc = per_bit_encoder(enc) + enc_kwargs = {"field": self, "pkt": pkt} # type: Dict[str, Any] + if self.size_len is not None: + enc_kwargs["size_len"] = self.size_len + if bit_enc is not None: + codec.encode_into( # type: ignore[attr-defined] + bit_enc, raw, **enc_kwargs, + ) + return + enc.write(codec.enc(raw, **enc_kwargs)) # type: ignore[attr-defined] def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None - if pkt.ASN1_codec is ASN1_Codecs.PER: - self.encode_into(getattr(enc, "inner", enc), pkt) + if per_bit_encoder(enc) is not None: + self.encode_into(enc, pkt) else: enc.write(self.i2m(pkt, getattr(pkt, self.name))) def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None - if pkt.ASN1_codec is ASN1_Codecs.PER: - self.dissect_from_decoder(pkt, getattr(dec, "inner", dec)) + if per_bit_decoder(dec) is not None: + self.dissect_from_decoder(pkt, per_bit_decoder(dec)) else: val, remain = self.m2i(pkt, dec.remaining()) self.set_val(pkt, val) @@ -656,7 +656,7 @@ def m2i(self, pkt, s): dec = pkt.ASN1_codec.new_decoder(s) self.decode_from(pkt, dec) remain = dec.remaining() - if pkt.ASN1_codec is ASN1_Codecs.PER and remain: + if per_bit_decoder(dec) is not None and remain: from scapy.asn1.uper import UPER_Decoding_Error raise UPER_Decoding_Error( "unexpected remainder in %s" % pkt.__class__.__name__, @@ -671,20 +671,11 @@ def encode_to(self, pkt, enc): def encode_into(self, enc, pkt, value=None): # type: (Any, ASN1_Packet, Any) -> None from scapy.asn1.compound import sequence_encode_to - if pkt.ASN1_codec is ASN1_Codecs.PER: - class _Ctx(object): - codec = ASN1_Codecs.PER - inner = enc - sequence_encode_to(self, pkt, _Ctx()) - return - super(ASN1F_SEQUENCE, self).encode_into(enc, pkt, value) + sequence_encode_to(self, pkt, enc) def dissect_from_decoder(self, pkt, dec): # type: (ASN1_Packet, Any) -> None - class _Ctx(object): - codec = pkt.ASN1_codec - inner = dec - self.decode_from(pkt, _Ctx()) + self.decode_from(pkt, dec) def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None @@ -1055,20 +1046,16 @@ def encode_to(self, pkt, enc): def encode_into(self, enc, pkt, value=None): # type: (Any, ASN1_Packet, Any) -> None + from scapy.asn1.compound import choice_encode_to if value is None: - value = getattr(pkt, self.name) - if pkt.ASN1_codec is ASN1_Codecs.PER: - from scapy.asn1.compound import uper_choice_encode_into - if value is None: - return - if self.alternative_index(value) is None: - raise ASN1_Error( - "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % - self.name - ) - uper_choice_encode_into(self, enc, pkt, value) + choice_encode_to(self, pkt, enc) return - super(ASN1F_CHOICE, self).encode_into(enc, pkt, value) + old = getattr(pkt, self.name, None) + setattr(pkt, self.name, value) + try: + choice_encode_to(self, pkt, enc) + finally: + setattr(pkt, self.name, old) def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None @@ -1135,11 +1122,8 @@ def encode_to(self, pkt, enc): def encode_into(self, enc, pkt, value=None): # type: (Any, ASN1_Packet, Any) -> None - if pkt.ASN1_codec is ASN1_Codecs.PER: - from scapy.asn1.compound import uper_packet_encode_into - uper_packet_encode_into(self, enc, pkt, value) - return - super(ASN1F_PACKET, self).encode_into(enc, pkt, value) + from scapy.asn1.compound import packet_encode_to + packet_encode_to(self, pkt, enc, value) def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None diff --git a/scapy/asn1packet.py b/scapy/asn1packet.py index ac5117bfba9..ebb138a0c66 100644 --- a/scapy/asn1packet.py +++ b/scapy/asn1packet.py @@ -42,7 +42,7 @@ def __new__(cls, class ASN1_Packet(Packet, metaclass=ASN1Packet_metaclass): ASN1_root = cast('ASN1F_field[Any, Any]', None) - ASN1_codec = None + ASN1_codec = cast(Any, None) def self_build(self): # type: () -> bytes From b43588208285882249a857c9cfd48a71ea0da339 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 1 Sep 2026 20:52:07 +0200 Subject: [PATCH 20/46] asn1: drop codec_kwargs legacy and read constraints from fields Remove EncodingParams, codec_kwargs, and field codec_opts/_codec_kwargs; codecs resolve size_len and constraints via field= directly. Co-authored-by: Cursor AI-Assisted: yes (Cursor Agent) --- scapy/asn1/ber.py | 61 +++++++++++++-------- scapy/asn1/constraints.py | 108 ------------------------------------- scapy/asn1/oer.py | 45 ++++++++-------- scapy/asn1/uper.py | 24 ++++----- scapy/asn1fields.py | 37 +++---------- test/contrib/oer.uts | 8 ++- test/contrib/uper.uts | 17 ++---- test/scapy/layers/asn1.uts | 8 +-- test/scapy/layers/ber.uts | 55 +++++++------------ 9 files changed, 111 insertions(+), 252 deletions(-) diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index ca984242325..dd2be30b6a5 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -399,15 +399,17 @@ def safedec(cls, return cls.dec(s, context, safe=True, _depth=_depth) @classmethod - def enc(cls, s, size_len=0, **_kwargs): - # type: (_K, Optional[int], **Any) -> bytes - # Ignore unknown kwargs so shared field._codec_kwargs() dicts (OER/UPER - # keys) do not TypeError on BER packets. + def enc(cls, s, field=None, pkt=None, size_len=None, **_kwargs): + # type: (_K, Any, Any, Optional[int], **Any) -> bytes if isinstance(s, (str, bytes)): - return BERcodec_STRING.enc(s, size_len=size_len) + return BERcodec_STRING.enc( + s, field=field, pkt=pkt, size_len=size_len, + ) else: try: - return BERcodec_INTEGER.enc(int(s), size_len=size_len) # type: ignore + return BERcodec_INTEGER.enc( + int(s), field=field, pkt=pkt, size_len=size_len, + ) # type: ignore except TypeError: raise TypeError("Trying to encode an invalid value !") @@ -415,6 +417,13 @@ def enc(cls, s, size_len=0, **_kwargs): ASN1_Codecs.BER.register_stem(BERcodec_Object) +def _ber_enc_size_len(field=None, size_len=None): + # type: (Any, Optional[int]) -> int + from scapy.asn1.constraints import oer_size_len + sl = oer_size_len(field, size_len) + return sl if sl is not None else 0 + + ########################## # BERcodec objects # ########################## @@ -423,8 +432,9 @@ class BERcodec_INTEGER(BERcodec_Object[int]): tag = ASN1_Class_UNIVERSAL.INTEGER @classmethod - def enc(cls, i, size_len=0, **_kwargs): # type: ignore[override] - # type: (int, Optional[int], **Any) -> bytes + def enc(cls, i, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] + # type: (int, Any, Any, Optional[int], **Any) -> bytes + size_len = _ber_enc_size_len(field, size_len) ls = [] while True: ls.append(i & 0xff) @@ -490,8 +500,9 @@ def do_dec(cls, ) @classmethod - def enc(cls, _s, size_len=0, **_kwargs): # type: ignore[override] - # type: (AnyStr, Optional[int], **Any) -> bytes + def enc(cls, _s, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] + # type: (AnyStr, Any, Any, Optional[int], **Any) -> bytes + size_len = _ber_enc_size_len(field, size_len) # /!\ this is DER encoding (bit strings are only zero-bit padded) s = bytes_encode(_s) if len(s) % 8 == 0: @@ -509,8 +520,9 @@ class BERcodec_STRING(BERcodec_Object[str]): tag = ASN1_Class_UNIVERSAL.STRING @classmethod - def enc(cls, _s, size_len=0, **_kwargs): # type: ignore[override] - # type: (Union[str, bytes], Optional[int], **Any) -> bytes + def enc(cls, _s, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] + # type: (Union[str, bytes], Any, Any, Optional[int], **Any) -> bytes + size_len = _ber_enc_size_len(field, size_len) s = bytes_encode(_s) # Be sure we are encoding bytes return chb(int(cls.tag)) + BER_len_enc(len(s), size=size_len) + s @@ -531,20 +543,23 @@ class BERcodec_NULL(BERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.NULL @classmethod - def enc(cls, i, size_len=0, **_kwargs): # type: ignore[override] - # type: (int, Optional[int], **Any) -> bytes + def enc(cls, i, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] + # type: (int, Any, Any, Optional[int], **Any) -> bytes if i == 0: return chb(int(cls.tag)) + b"\0" else: - return super(cls, cls).enc(i, size_len=size_len) + return super(cls, cls).enc( + i, field=field, pkt=pkt, size_len=size_len, + ) class BERcodec_OID(BERcodec_Object[bytes]): tag = ASN1_Class_UNIVERSAL.OID @classmethod - def enc(cls, _oid, size_len=0, **_kwargs): # type: ignore[override] - # type: (AnyStr, Optional[int], **Any) -> bytes + def enc(cls, _oid, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] + # type: (AnyStr, Any, Any, Optional[int], **Any) -> bytes + size_len = _ber_enc_size_len(field, size_len) oid = bytes_encode(_oid) if oid: lst = [int(x) for x in oid.strip(b".").split(b".")] @@ -635,13 +650,16 @@ class BERcodec_SEQUENCE(BERcodec_Object[Union[bytes, List[BERcodec_Object[Any]]] tag = ASN1_Class_UNIVERSAL.SEQUENCE @classmethod - def enc(cls, _ll, size_len=None, **_kwargs): # type: ignore[override] - # type: (Union[bytes, List[BERcodec_Object[Any]]], Optional[int], **Any) -> bytes # noqa: E501 + def enc(cls, _ll, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] + # type: (Union[bytes, List[BERcodec_Object[Any]]], Any, Any, Optional[int], **Any) -> bytes # noqa: E501 if isinstance(_ll, bytes): ll = _ll else: ll = b"".join(x.enc(cls.codec) for x in _ll) # None = apply conf; explicit 0 keeps short-form lengths. + if size_len is None: + from scapy.asn1.constraints import oer_size_len + size_len = oer_size_len(field, None) if size_len is None: size_len = conf.ASN1_default_long_size return chb(int(cls.tag)) + BER_len_enc(len(ll), size=size_len) + ll @@ -691,8 +709,9 @@ class BERcodec_IPADDRESS(BERcodec_STRING): tag = ASN1_Class_UNIVERSAL.IPADDRESS @classmethod - def enc(cls, ipaddr_ascii, size_len=0, **_kwargs): # type: ignore[override] - # type: (str, Optional[int], **Any) -> bytes + def enc(cls, ipaddr_ascii, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore + # type: (str, Any, Any, Optional[int], **Any) -> bytes + size_len = _ber_enc_size_len(field, size_len) try: s = inet_aton(ipaddr_ascii) except Exception: diff --git a/scapy/asn1/constraints.py b/scapy/asn1/constraints.py index 35d156b6a9b..1ebda9878c8 100644 --- a/scapy/asn1/constraints.py +++ b/scapy/asn1/constraints.py @@ -19,48 +19,6 @@ class ASN1Constraints: unsigned: bool = False -@dataclass -class EncodingParams: - """Wire-encoding parameters resolved from a field or explicit kwargs.""" - size_len: Optional[int] = None - minimum: Optional[int] = None - maximum: Optional[int] = None - size_min: Optional[int] = None - size_max: Optional[int] = None - extensible: bool = False - unsigned: bool = False - uper_enum_values: Optional[List[int]] = None - - @property - def uper_min(self): - # type: () -> Optional[int] - if self.minimum is not None: - return self.minimum - return self.size_min - - @property - def uper_max(self): - # type: () -> Optional[int] - if self.maximum is not None: - return self.maximum - return self.size_max - - @property - def oer_unsigned(self): - # type: () -> bool - return self.unsigned - - @property - def uper_extensible(self): - # type: () -> bool - return self.extensible - - @property - def oer_extensible(self): - # type: () -> bool - return self.extensible - - _LEGACY_CODEC_OPTS = { "uper_min": "minimum", "uper_max": "maximum", @@ -134,67 +92,6 @@ def field_range(field): return minimum, maximum -def encoding_params(field=None, pkt=None, **legacy): - # type: (Any, Any, **Any) -> EncodingParams - """Resolve encoding parameters from a field and/or legacy codec kwargs.""" - if field is not None: - c = field.constraints - params = EncodingParams( - size_len=field.size_len, - minimum=c.minimum, - maximum=c.maximum, - size_min=c.size_min, - size_max=c.size_max, - extensible=c.extensible, - unsigned=c.unsigned, - ) - if pkt is not None and hasattr(field, "uper_enum_values"): - from scapy.asn1.asn1 import ASN1_Codecs - if getattr(pkt, "ASN1_codec", None) is ASN1_Codecs.PER: - params.uper_enum_values = field.uper_enum_values() - return _merge_legacy(params, legacy) - params = EncodingParams() - return _merge_legacy(params, legacy) - - -def _merge_legacy(params, legacy): - # type: (EncodingParams, Dict[str, Any]) -> EncodingParams - if not legacy: - return params - if "size_len" in legacy and legacy["size_len"] is not None: - params.size_len = legacy["size_len"] - if "uper_min" in legacy and legacy["uper_min"] is not None: - params.minimum = legacy["uper_min"] - if "uper_max" in legacy and legacy["uper_max"] is not None: - params.maximum = legacy["uper_max"] - if legacy.get("uper_extensible"): - params.extensible = True - if legacy.get("oer_extensible"): - params.extensible = True - if "oer_unsigned" in legacy and legacy["oer_unsigned"] is not None: - params.unsigned = legacy["oer_unsigned"] - if legacy.get("uper_enum_values") is not None: - params.uper_enum_values = legacy["uper_enum_values"] - return params - - -def codec_kwargs(field=None, pkt=None, **legacy): - # type: (Any, Any, **Any) -> Dict[str, Any] - """Deprecated legacy keyword dict; prefer reading ``field.constraints``.""" - p = encoding_params(field, pkt=pkt, **legacy) - kw = { - "size_len": p.size_len, - "oer_unsigned": p.unsigned, - "uper_min": p.uper_min, - "uper_max": p.uper_max, - "uper_extensible": p.extensible, - "oer_extensible": p.extensible, - } # type: Dict[str, Any] - if p.uper_enum_values is not None: - kw["uper_enum_values"] = p.uper_enum_values - return kw - - def oer_size_len(field=None, size_len=None): # type: (Any, Optional[int]) -> Optional[int] if size_len is not None: @@ -213,11 +110,6 @@ def oer_unsigned(field=None, oer_unsigned=None): return False -def uper_size_len(field=None, size_len=None): - # type: (Any, Optional[int]) -> Optional[int] - return oer_size_len(field, size_len) - - def uper_extensible(field=None, uper_extensible=None, oer_extensible=None): # type: (Any, Optional[bool], Optional[bool]) -> bool if uper_extensible is not None: diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index b42827550c4..476ef43a448 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -250,7 +250,7 @@ def do_dec(cls, s, # type: bytes context=None, # type: Optional[Type[ASN1_Class]] safe=False, # type: bool - size_len=0, # type: Optional[int] + size_len=None, # type: Optional[int] oer_unsigned=False, # type: bool **_kwargs # type: Any ): @@ -267,8 +267,6 @@ def dec(cls, safe=False, # type: bool field=None, # type: Any pkt=None, # type: Any - size_len=None, # type: Optional[int] - oer_unsigned=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] @@ -277,10 +275,6 @@ def dec(cls, call_kw["field"] = field if pkt is not None: call_kw["pkt"] = pkt - if size_len is not None: - call_kw["size_len"] = size_len - if oer_unsigned is not None: - call_kw["oer_unsigned"] = oer_unsigned if not safe: return cls.do_dec( s, context=context, safe=safe, **call_kw, @@ -300,26 +294,27 @@ def safedec(cls, context=None, # type: Optional[Type[ASN1_Class]] field=None, # type: Any pkt=None, # type: Any - size_len=None, # type: Optional[int] - oer_unsigned=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] return cls.dec( s, context, safe=True, field=field, pkt=pkt, - size_len=size_len, oer_unsigned=oer_unsigned, **_kwargs, ) @classmethod - def enc(cls, s, size_len=0, **_kwargs): - # type: (_K, Optional[int], **Any) -> bytes + def enc(cls, s, field=None, pkt=None, size_len=None, **_kwargs): + # type: (_K, Any, Any, Optional[int], **Any) -> bytes if isinstance(s, (str, bytes)): - return OERcodec_STRING.enc(s, size_len=size_len) + return OERcodec_STRING.enc( + s, field=field, pkt=pkt, size_len=size_len, + ) else: try: - return OERcodec_INTEGER.enc(int(s), size_len=size_len) # type: ignore + return OERcodec_INTEGER.enc( + int(s), field=field, pkt=pkt, size_len=size_len, + ) # type: ignore except TypeError: raise TypeError("Trying to encode an invalid value !") @@ -410,7 +405,7 @@ def do_dec(cls, s, # type: bytes context=None, # type: Optional[Type[ASN1_Class]] safe=False, # type: bool - size_len=0, # type: Optional[int] + size_len=None, # type: Optional[int] oer_unsigned=False, # type: bool **_kwargs # type: Any ): @@ -544,7 +539,7 @@ def do_dec(cls, s, # type: bytes context=None, # type: Optional[Type[ASN1_Class]] safe=False, # type: bool - size_len=0, # type: Optional[int] + size_len=None, # type: Optional[int] oer_unsigned=False, # type: bool **_kwargs # type: Any ): @@ -574,7 +569,7 @@ def do_dec(cls, s, # type: bytes context=None, # type: Optional[Type[ASN1_Class]] safe=False, # type: bool - size_len=0, # type: Optional[int] + size_len=None, # type: Optional[int] oer_unsigned=False, # type: bool **_kwargs # type: Any ): @@ -611,7 +606,7 @@ def do_dec(cls, s, # type: bytes context=None, # type: Optional[Type[ASN1_Class]] safe=False, # type: bool - size_len=0, # type: Optional[int] + size_len=None, # type: Optional[int] oer_unsigned=False, # type: bool **_kwargs # type: Any ): @@ -673,7 +668,7 @@ def do_dec(cls, s, # type: bytes context=None, # type: Optional[Type[ASN1_Class]] safe=False, # type: bool - size_len=0, # type: Optional[int] + size_len=None, # type: Optional[int] oer_unsigned=False, # type: bool **_kwargs # type: Any ): @@ -692,8 +687,10 @@ class OERcodec_IPADDRESS(OERcodec_STRING): tag = ASN1_Class_UNIVERSAL.IPADDRESS @classmethod - def enc(cls, ipaddr_ascii, size_len=0, **_kwargs): # type: ignore - # type: (str, Optional[int], **Any) -> bytes + def enc(cls, ipaddr_ascii, field=None, size_len=None, **_kwargs): # type: ignore + # type: (str, Any, Optional[int], **Any) -> bytes + from scapy.asn1.constraints import oer_size_len + size_len = oer_size_len(field, size_len) try: s = inet_aton(ipaddr_ascii) except Exception: @@ -704,8 +701,10 @@ def enc(cls, ipaddr_ascii, size_len=0, **_kwargs): # type: ignore @classmethod def do_dec(cls, s, context=None, safe=False, - size_len=0, oer_unsigned=False, **_kwargs): - # type: (bytes, Optional[Any], bool, Optional[int], bool, **Any) -> Tuple[ASN1_Object[str], bytes] # noqa: E501 + field=None, size_len=None, oer_unsigned=False, **_kwargs): + # type: (bytes, Optional[Any], bool, Any, Optional[int], bool, **Any) -> Tuple[ASN1_Object[str], bytes] # noqa: E501 + from scapy.asn1.constraints import oer_size_len + size_len = oer_size_len(field, size_len) if size_len == 4: raw, remain = s[:4], s[4:] else: diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 2acf7ed0c3e..04cffd62f1b 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -612,14 +612,14 @@ def encode_into(cls, enc, # type: UPER_Encoder _s, # type: Any field=None, # type: Any - size_len=0, # type: Optional[int] + size_len=None, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] **_kwargs # type: Any ): # type: (...) -> None from scapy.asn1.constraints import oer_size_len, uper_int_range - size_len = oer_size_len(field, size_len) or 0 + size_len = oer_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) if isinstance(_s, tuple) and len(_s) == 2: data, nbits = _s @@ -657,14 +657,14 @@ def encode_into(cls, def dec_from_decoder(cls, dec, # type: UPER_Decoder field=None, # type: Any - size_len=0, # type: Optional[int] + size_len=None, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] **_kwargs # type: Any ): # type: (...) -> ASN1_Object[str] from scapy.asn1.constraints import oer_size_len, uper_int_range - size_len = oer_size_len(field, size_len) or 0 + size_len = oer_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) if minimum is not None and maximum is not None: @@ -698,14 +698,14 @@ def encode_into(cls, enc, # type: UPER_Encoder _s, # type: Union[str, bytes] field=None, # type: Any - size_len=0, # type: Optional[int] + size_len=None, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] **_kwargs # type: Any ): # type: (...) -> None from scapy.asn1.constraints import oer_size_len, uper_int_range - size_len = oer_size_len(field, size_len) or 0 + size_len = oer_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) s = bytes_encode(_s) minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) @@ -715,14 +715,14 @@ def encode_into(cls, def dec_from_decoder(cls, dec, # type: UPER_Decoder field=None, # type: Any - size_len=0, # type: Optional[int] + size_len=None, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] **_kwargs # type: Any ): # type: (...) -> ASN1_Object[Any] from scapy.asn1.constraints import oer_size_len, uper_int_range - size_len = oer_size_len(field, size_len) or 0 + size_len = oer_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) raw = UPER_octet_string_dec(dec, minimum, maximum) @@ -818,7 +818,7 @@ def encode_into(cls, i, # type: int field=None, # type: Any pkt=None, # type: Any - size_len=0, # type: Optional[int] + size_len=None, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] uper_enum_values=None, # type: Optional[List[int]] @@ -832,7 +832,7 @@ def encode_into(cls, uper_extensible as _uper_extensible, uper_int_range, ) - size_len = oer_size_len(field, size_len) or 0 + size_len = oer_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) uper_enum_values = _uper_enum_values( field, pkt, uper_enum_values, @@ -860,7 +860,7 @@ def dec_from_decoder(cls, dec, # type: UPER_Decoder field=None, # type: Any pkt=None, # type: Any - size_len=0, # type: Optional[int] + size_len=None, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] uper_enum_values=None, # type: Optional[List[int]] @@ -874,7 +874,7 @@ def dec_from_decoder(cls, uper_extensible as _uper_extensible, uper_int_range, ) - size_len = oer_size_len(field, size_len) or 0 + size_len = oer_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) uper_enum_values = _uper_enum_values( field, pkt, uper_enum_values, diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 0539420369f..20c1ac465b8 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -116,7 +116,6 @@ def __init__(self, else: self.default = self.ASN1_tag.asn1_object(default) # type: ignore self.size_len = size_len - self._init_codec_opts = codec_opts # type: Dict[str, Any] self.constraints = normalize_constraints(codec_opts, size_len=size_len) self.flexible_tag = flexible_tag if (implicit_tag is not None) and (explicit_tag is not None): @@ -129,23 +128,6 @@ def __init__(self, self.owners = [] # type: List[Type[ASN1_Packet]] - @property - def codec_opts(self): - # type: () -> Dict[str, Any] - """Deprecated view of constraint kwargs for backward compatibility.""" - return dict(self._init_codec_opts) - - def _codec_kwargs(self, pkt=None): - # type: (Any) -> Dict[str, Any] - """Deprecated; use ``scapy.asn1.constraints.codec_kwargs``.""" - from scapy.asn1.constraints import codec_kwargs - return codec_kwargs(self, pkt=pkt) - - def _constraints_kwargs(self, pkt=None): - # type: (Any) -> Dict[str, Any] - """Deprecated alias of ``_codec_kwargs``.""" - return self._codec_kwargs(pkt) - def register_owner(self, cls): # type: (Type[ASN1_Packet]) -> None self.owners.append(cls) @@ -227,10 +209,7 @@ def _encode_item(self, pkt, item): # the BER type codec so the universal tag/length are applied. item = item.self_build() codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - kwargs = {"field": self, "pkt": pkt} # type: Dict[str, Any] - if self.size_len is not None: - kwargs["size_len"] = self.size_len - return codec.enc(item, **kwargs) + return codec.enc(item, field=self, pkt=pkt) def i2repr(self, pkt, x): # type: (ASN1_Packet, _I) -> str @@ -296,10 +275,9 @@ def extract_packet(self, def m2i_from_decoder(self, pkt, dec): # type: (ASN1_Packet, Any) -> Any codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - kwargs = {"field": self, "pkt": pkt} # type: Dict[str, Any] - if self.size_len is not None: - kwargs["size_len"] = self.size_len - return codec.dec_from_decoder(dec, **kwargs) # type: ignore[attr-defined] + return codec.dec_from_decoder( # type: ignore[attr-defined] + dec, field=self, pkt=pkt, + ) def dissect_from_decoder(self, pkt, dec): # type: (ASN1_Packet, Any) -> None @@ -326,15 +304,12 @@ def encode_into(self, enc, pkt, value=None): else: raw = value bit_enc = per_bit_encoder(enc) - enc_kwargs = {"field": self, "pkt": pkt} # type: Dict[str, Any] - if self.size_len is not None: - enc_kwargs["size_len"] = self.size_len if bit_enc is not None: codec.encode_into( # type: ignore[attr-defined] - bit_enc, raw, **enc_kwargs, + bit_enc, raw, field=self, pkt=pkt, ) return - enc.write(codec.enc(raw, **enc_kwargs)) # type: ignore[attr-defined] + enc.write(codec.enc(raw, field=self, pkt=pkt)) # type: ignore[attr-defined] def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index 4c797f03d28..268f63f6f4b 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -897,11 +897,9 @@ assert not hasattr(ASN1_Codecs, "hooks") True -= oer constrained integer via codec_opts += oer constrained integer via constraints fld = OERUnsignedField.ASN1_root -assert fld.codec_opts["oer_unsigned"] is True - assert fld.constraints.unsigned is True assert raw(OERUnsignedField(n=5)) == b"\x05" @@ -997,8 +995,8 @@ assert isinstance(decoded.c, OERAltB) and decoded.c.b.val == 0 True -= oer dec ignores foreign codec kwargs -# Shared field.codec_opts may include UPER keys after contrib.uper is loaded. += oer dec reads constraints from field +# Direct codec calls without a field ignore schema constraints in kwargs. x, remain = OERcodec_ENUMERATED.dec( b"\x01", uper_enum_values=[0, 1], uper_min=0, ) diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index 52912839040..f946ca98fd9 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -2805,25 +2805,16 @@ assert issubclass(ASN1F_DEFAULT, ASN1F_optional) enum_fld = ASN1F_ENUMERATED("e", 0, {1: "a", 2: "b"}) -# ENUMERATED values are added per-codec, so BER packets keep an empty -# codec_opts while PER packets get the permitted values. -assert enum_fld.codec_opts == {} +# ENUMERATED values are resolved per-codec from the field schema. +assert enum_fld.constraints.extensible is False -assert "uper_enum_values" not in enum_fld._codec_kwargs( - type("P", (), {"ASN1_codec": ASN1_Codecs.BER})() -) - -assert enum_fld._codec_kwargs( - type("P", (), {"ASN1_codec": ASN1_Codecs.PER})() -)["uper_enum_values"] == [1, 2] +assert enum_fld.uper_enum_values() == [1, 2] True -= uper constraints and codec_opts += uper constraints fld = UPERConstrainedInt.ASN1_root -assert fld.codec_opts == {"uper_min": 0, "uper_max": 255} - assert fld.constraints.minimum == 0 and fld.constraints.maximum == 255 assert raw(UPERConstrainedInt(n=5)) == b"\x05" diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index b3c579b8d55..b2ff3558fcf 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -492,7 +492,7 @@ for cls, data_hex in [ True -= ber oer per constrained integer codec_opts += ber oer per constrained integer constraints class BERConstrained(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_INTEGER( @@ -519,9 +519,9 @@ for cls, expected in ( pkt = cls(n=200) assert raw(pkt) == expected assert _roundtrip(cls, pkt).n.val == 200 - assert cls.ASN1_root.codec_opts["oer_unsigned"] is True - assert cls.ASN1_root.codec_opts["uper_min"] == 0 - assert cls.ASN1_root.codec_opts["uper_max"] == 255 + assert cls.ASN1_root.constraints.unsigned is True + assert cls.ASN1_root.constraints.minimum == 0 + assert cls.ASN1_root.constraints.maximum == 255 True diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index 459a5eb9f02..db9a37af370 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -476,13 +476,13 @@ assert fld._tagging_enc(_NoTagging(), b"\x02\x01\x05", explicit_tag=0xA0) == b"\ fld._tagging_dec(_NoTagging(), b"\x02\x01\x05", explicit_tag=0xA0) == (None, b"\x02\x01\x05") -= field _codec_kwargs and ASN1_Object unwrap += field constraints and ASN1_Object unwrap class P(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_INTEGER("n", 0) fld = P.ASN1_root -assert fld._codec_kwargs(P())["size_len"] is None +assert fld.size_len is None assert raw(P(n=ASN1_INTEGER(5))) == b"\x02\x01\x05" assert raw(P(n=5)) == b"\x02\x01\x05" @@ -491,57 +491,42 @@ class Sized(ASN1_Packet): ASN1_root = ASN1F_INTEGER("n", 0, size_len=1) sfld = Sized.ASN1_root -assert sfld._constraints_kwargs(Sized())["size_len"] == 1 +assert sfld.size_len == 1 assert raw(Sized(n=ASN1_INTEGER(5))) == raw(Sized(n=5)) == b"\x02\x81\x01\x05" -= field encode with extra kwargs via _codec_kwargs override -class ExtraKwField(ASN1F_INTEGER): - def _codec_kwargs(self, pkt): - return {"size_len": self.size_len, "oer_unsigned": True, "uper_min": 0} - -class ExtraPkt(ASN1_Packet): += field encode ignores codec-specific constraints on BER +class ConstrainedField(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ExtraKwField("n", 0) + ASN1_root = ASN1F_INTEGER( + "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, + ) -# BER enc swallows unknown kwargs; round-trip still works. -assert raw(ExtraPkt(n=7)) == b"\x02\x01\x07" -ExtraPkt(raw(ExtraPkt(n=7))).n.val == 7 +# BER uses size_len from the field; OER/PER keys live in constraints only. +assert raw(ConstrainedField(n=7)) == b"\x02\x81\x01\x07" +ConstrainedField(raw(ConstrainedField(n=7))).n.val == 7 -= field codec_opts storage += field constraints storage plain = ASN1F_INTEGER("n", 0) -assert plain.codec_opts == {} - -assert plain._codec_kwargs(type("P", (), {"ASN1_codec": ASN1_Codecs.BER})())["size_len"] is None +assert plain.constraints.unsigned is False +assert plain.size_len is None constrained = ASN1F_INTEGER( "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, ) -assert constrained.codec_opts == { - "oer_unsigned": True, - "uper_min": 0, - "uper_max": 255, -} +assert constrained.constraints.unsigned is True +assert constrained.constraints.minimum == 0 +assert constrained.constraints.maximum == 255 -# Constraints live in codec_opts only: they must not become field attributes. +# Constraints must not become field attributes. assert not hasattr(constrained, "oer_unsigned") assert not hasattr(constrained, "uper_min") -kwargs = constrained._codec_kwargs( - type("P", (), {"ASN1_codec": ASN1_Codecs.BER})() -) - -assert kwargs["size_len"] == 1 - -assert kwargs["oer_unsigned"] is True - -assert kwargs["uper_min"] == 0 - -assert kwargs["uper_max"] == 255 +assert constrained.size_len == 1 -# BER still encodes with constraints present in kwargs. +# BER still encodes with size_len from the field. class ConstrainedBer(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_INTEGER( From db5eff098256e7cae3c38e86fa4170e5dfbae243 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 06:57:45 +0200 Subject: [PATCH 21/46] Fix PR #5050 review defects in ASN.1 OER/UPER wire paths. Restore extension points and correct OID, integer bounds, PER padding, X.509 extract_packet, and BER DEFAULT handling; add regression tests. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/ber.py | 17 +++--- scapy/asn1/compound.py | 17 +++--- scapy/asn1/constraints.py | 46 ++++++++++++++- scapy/asn1/context.py | 8 +++ scapy/asn1/oer.py | 116 ++++++++++++++++++++++++------------- scapy/asn1/oid.py | 35 +++++++++++ scapy/asn1/uper.py | 109 +++++++++++++++++++++++----------- scapy/asn1fields.py | 51 +++++++++------- scapy/asn1packet.py | 6 ++ test/contrib/oer.uts | 21 +++++++ test/contrib/uper.uts | 39 +++++++++++++ test/scapy/layers/asn1.uts | 25 ++++++++ test/scapy/layers/ber.uts | 49 ++++++++++++++++ 13 files changed, 426 insertions(+), 113 deletions(-) create mode 100644 scapy/asn1/oid.py diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index dd2be30b6a5..f3aca84d681 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -419,8 +419,8 @@ def enc(cls, s, field=None, pkt=None, size_len=None, **_kwargs): def _ber_enc_size_len(field=None, size_len=None): # type: (Any, Optional[int]) -> int - from scapy.asn1.constraints import oer_size_len - sl = oer_size_len(field, size_len) + from scapy.asn1.constraints import field_size_len + sl = field_size_len(field, size_len) return sl if sl is not None else 0 @@ -585,11 +585,12 @@ def do_dec(cls, l, s = BER_num_dec(s) lst.append(l) if lst: - # X.690 sect 8.19.4 - lst.insert(0, lst[0] // 40) - lst[1] %= 40 + from scapy.asn1.oid import oid_subidentifiers_to_dotted + oid = oid_subidentifiers_to_dotted(lst) + else: + oid = b"" return ( - cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)), + cls.asn1_object(oid), t, ) @@ -658,8 +659,8 @@ def enc(cls, _ll, field=None, pkt=None, size_len=None, **_kwargs): # type: igno ll = b"".join(x.enc(cls.codec) for x in _ll) # None = apply conf; explicit 0 keeps short-form lengths. if size_len is None: - from scapy.asn1.constraints import oer_size_len - size_len = oer_size_len(field, None) + from scapy.asn1.constraints import field_size_len + size_len = field_size_len(field, None) if size_len is None: size_len = conf.ASN1_default_long_size return chb(int(cls.tag)) + BER_len_enc(len(ll), size=size_len) + ll diff --git a/scapy/asn1/compound.py b/scapy/asn1/compound.py index fc6b19ebf65..cda0c5f315f 100644 --- a/scapy/asn1/compound.py +++ b/scapy/asn1/compound.py @@ -356,9 +356,10 @@ def _uper_count_enc(field, enc, count, append_items): # ---- CHOICE ------------------------------------------------------------- -def choice_encode_to(field, pkt, enc): - # type: (Any, Any, Any) -> None - value = getattr(pkt, field.name) +def choice_encode_to(field, pkt, enc, value=None): + # type: (Any, Any, Any, Any) -> None + if value is None: + value = getattr(pkt, field.name) if per_bit_encoder(enc) is not None: uper_choice_encode_into(field, enc, pkt, value) return @@ -404,7 +405,7 @@ def ber_choice_decode(field, pkt, s): ) ) if hasattr(choice, "ASN1_root"): - return field.extract_packet(choice, s, _parent=pkt) + return field.extract_packet(choice, s, _underlayer=pkt, _parent=pkt) if isinstance(choice, type): return choice(field.name, b"").m2i(pkt, s) return choice.m2i(pkt, s) @@ -467,11 +468,11 @@ def oer_choice_decode(field, pkt, s): ) choice = ASN1F_field if hasattr(choice, "ASN1_root"): - return field.extract_packet(choice, payload, _parent=pkt) + return field.extract_packet(choice, payload, _underlayer=pkt, _parent=pkt) if isinstance(choice, type): return choice(field.name, b"").m2i(pkt, payload) cls = (choice.next_cls_cb(pkt) or choice.cls) if choice.next_cls_cb else choice.cls - return field.extract_packet(cls, payload, _parent=pkt) + return field.extract_packet(cls, payload, _underlayer=pkt, _parent=pkt) def uper_choice_encode_into(field, enc, pkt, value=None): @@ -573,7 +574,7 @@ def ber_oer_packet_decode(field, pkt, s): cls = (field.next_cls_cb(pkt) or field.cls) if field.next_cls_cb else field.cls from scapy.asn1packet import ASN1_Packet as _ASN1_Packet if not issubclass(cls, _ASN1_Packet): - return field.extract_packet(cls, s, _parent=pkt) + return field.extract_packet(cls, s, _underlayer=pkt, _parent=pkt) s = field._apply_tagging_dec( s, pkt, hidden_tag=cls.ASN1_root.ASN1_tag, @@ -581,7 +582,7 @@ def ber_oer_packet_decode(field, pkt, s): ) if not s: return None, s - return field.extract_packet(cls, s, _parent=pkt) + return field.extract_packet(cls, s, _underlayer=pkt, _parent=pkt) def ber_oer_packet_bytes(field, pkt, x): diff --git a/scapy/asn1/constraints.py b/scapy/asn1/constraints.py index 1ebda9878c8..b5f123ddf1e 100644 --- a/scapy/asn1/constraints.py +++ b/scapy/asn1/constraints.py @@ -34,8 +34,8 @@ class ASN1Constraints: } -def normalize_constraints(codec_opts, size_len=None): - # type: (Dict[str, Any], Optional[int]) -> ASN1Constraints +def normalize_constraints(codec_opts): + # type: (Dict[str, Any]) -> ASN1Constraints """Build ASN1Constraints from field kwargs, with legacy alias support.""" data = { "minimum": None, @@ -73,6 +73,12 @@ def normalize_constraints(codec_opts, size_len=None): DeprecationWarning, stacklevel=4, ) + else: + warnings.warn( + "Unknown field constraint %r" % key, + DeprecationWarning, + stacklevel=4, + ) return ASN1Constraints(**data) @@ -92,7 +98,7 @@ def field_range(field): return minimum, maximum -def oer_size_len(field=None, size_len=None): +def field_size_len(field=None, size_len=None): # type: (Any, Optional[int]) -> Optional[int] if size_len is not None: return size_len @@ -101,6 +107,9 @@ def oer_size_len(field=None, size_len=None): return None +oer_size_len = field_size_len + + def oer_unsigned(field=None, oer_unsigned=None): # type: (Any, Optional[bool]) -> bool if oer_unsigned is not None: @@ -139,3 +148,34 @@ def uper_enum_values(field=None, pkt=None, uper_enum_values=None): if getattr(pkt, "ASN1_codec", None) is ASN1_Codecs.PER: return field.uper_enum_values() return None + + +def oer_int_wire_params(field=None, size_len=None, unsigned=None): + # type: (Any, Optional[int], Optional[bool]) -> Tuple[Optional[int], bool, Optional[int], Optional[int]] # noqa: E501 + """Derive OER INTEGER width and signedness from field constraints.""" + size_len = field_size_len(field, size_len) + is_unsigned = oer_unsigned(field, unsigned) + minimum, maximum = field_range(field) if field is not None else (None, None) + if size_len is None and minimum is not None and maximum is not None: + if minimum >= 0: + is_unsigned = True + if maximum <= 0xFF: + size_len = 1 + elif maximum <= 0xFFFF: + size_len = 2 + elif maximum <= 0xFFFFFFFF: + size_len = 4 + else: + size_len = 8 + else: + is_unsigned = False + for sl, lo, hi in ( + (1, -128, 127), + (2, -32768, 32767), + (4, -2147483648, 2147483647), + (8, -9223372036854775808, 9223372036854775807), + ): + if minimum >= lo and maximum <= hi: + size_len = sl + break + return size_len, is_unsigned, minimum, maximum diff --git a/scapy/asn1/context.py b/scapy/asn1/context.py index 62f37d364bd..a24eba16b66 100644 --- a/scapy/asn1/context.py +++ b/scapy/asn1/context.py @@ -185,6 +185,10 @@ def new_decoder(codec, data): def per_bit_encoder(enc): # type: (Any) -> Any """Return the PER bit encoder, or *None* for byte-oriented contexts.""" + from scapy.asn1.asn1 import ASN1_Codecs + codec = getattr(enc, "codec", None) + if codec is not None and codec is not ASN1_Codecs.PER: + return None bit_enc = getattr(enc, "bit_encoder", None) if bit_enc is not None: return bit_enc @@ -197,6 +201,10 @@ def per_bit_encoder(enc): def per_bit_decoder(dec): # type: (Any) -> Any """Return the PER bit decoder, or *None* for byte-oriented contexts.""" + from scapy.asn1.asn1 import ASN1_Codecs + codec = getattr(dec, "codec", None) + if codec is not None and codec is not ASN1_Codecs.PER: + return None bit_dec = getattr(dec, "bit_decoder", None) if bit_dec is not None: return bit_dec diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index 476ef43a448..aae73acf47b 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -341,9 +341,16 @@ class OERcodec_INTEGER(OERcodec_Object[int]): @classmethod def enc(cls, i, field=None, size_len=None, oer_unsigned=None, **_kwargs): # type: (int, Any, Optional[int], Optional[bool], **Any) -> bytes - from scapy.asn1.constraints import oer_size_len, oer_unsigned as _oer_unsigned - size_len = oer_size_len(field, size_len) - oer_unsigned = _oer_unsigned(field, oer_unsigned) + from scapy.asn1.constraints import oer_int_wire_params + size_len, oer_unsigned, minimum, maximum = oer_int_wire_params( + field, size_len, oer_unsigned, + ) + if minimum is not None and maximum is not None: + if not minimum <= i <= maximum: + raise OER_Encoding_Error( + "%s: %i is outside %i..%i" % + (cls.__name__, i, minimum, maximum) + ) if oer_unsigned and i < 0: raise OER_Encoding_Error( "%s: %i is negative for an unsigned type" % (cls.__name__, i) @@ -376,19 +383,34 @@ def do_dec(cls, **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[int], bytes] - from scapy.asn1.constraints import oer_size_len, oer_unsigned as _oer_unsigned - size_len = oer_size_len(field, size_len) - oer_unsigned = _oer_unsigned(field, oer_unsigned) + from scapy.asn1.constraints import oer_int_wire_params + size_len, oer_unsigned, minimum, maximum = oer_int_wire_params( + field, size_len, oer_unsigned, + ) if size_len in (1, 2, 4, 8): _OER_check_len(cls.__name__, s, size_len) x = struct.unpack( cls._FIXED_FORMATS[not oer_unsigned][size_len], s[:size_len] )[0] + if minimum is not None and maximum is not None: + if not minimum <= x <= maximum: + raise OER_Decoding_Error( + "%s: %i is outside %i..%i" % + (cls.__name__, x, minimum, maximum), + remaining=s, + ) return cls.asn1_object(x), s[size_len:] if oer_unsigned: x, t = OER_unsigned_integer_dec(s) else: x, t = OER_signed_integer_dec(s) + if minimum is not None and maximum is not None: + if not minimum <= x <= maximum: + raise OER_Decoding_Error( + "%s: %i is outside %i..%i" % + (cls.__name__, x, minimum, maximum), + remaining=s, + ) return cls.asn1_object(x), t @@ -553,14 +575,9 @@ class OERcodec_OID(OERcodec_Object[bytes]): @classmethod def enc(cls, _oid, **_kwargs): # type: (AnyStr, **Any) -> bytes + from scapy.asn1.oid import oid_dotted_to_subidentifiers oid = bytes_encode(_oid) - if oid: - lst = [int(x) for x in oid.strip(b".").split(b".")] - else: - lst = list() - if len(lst) >= 2: - lst[1] += 40 * lst[0] - del lst[0] + lst = oid_dotted_to_subidentifiers(oid) body = b"".join(BER_num_enc(k) for k in lst) return OER_len_enc(len(body)) + body @@ -581,11 +598,9 @@ def do_dec(cls, while content: val, content = BER_num_dec(content) lst.append(val) - if len(lst) > 0: - lst.insert(0, lst[0] // 40) - lst[1] %= 40 + from scapy.asn1.oid import oid_subidentifiers_to_dotted return ( - cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)), + cls.asn1_object(oid_subidentifiers_to_dotted(lst)), t, ) @@ -624,33 +639,52 @@ def do_dec(cls, return cls.asn1_object(value), s[length + 1:] -_OER_STRING_TAGS = ( - "UTF8_STRING", - "NUMERIC_STRING", - "PRINTABLE_STRING", - "T61_STRING", - "VIDEOTEX_STRING", - "IA5_STRING", - "GENERAL_STRING", - "UTC_TIME", - "GENERALIZED_TIME", - "ISO646_STRING", - "UNIVERSAL_STRING", - "BMP_STRING", -) +class OERcodec_UTF8_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTF8_STRING + + +class OERcodec_NUMERIC_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.NUMERIC_STRING + + +class OERcodec_PRINTABLE_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING + + +class OERcodec_T61_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.T61_STRING + + +class OERcodec_VIDEOTEX_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.VIDEOTEX_STRING + + +class OERcodec_IA5_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.IA5_STRING + + +class OERcodec_GENERAL_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERAL_STRING + + +class OERcodec_UTC_TIME(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTC_TIME + + +class OERcodec_GENERALIZED_TIME(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME + + +class OERcodec_ISO646_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.ISO646_STRING -def _oer_string_codec(name): - # type: (str) -> type - return type( - "OERcodec_%s" % name, - (OERcodec_STRING,), - {"tag": getattr(ASN1_Class_UNIVERSAL, name)}, - ) +class OERcodec_UNIVERSAL_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UNIVERSAL_STRING -for _tag_name in _OER_STRING_TAGS: - globals()["OERcodec_%s" % _tag_name] = _oer_string_codec(_tag_name) +class OERcodec_BMP_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.BMP_STRING class OERcodec_SEQUENCE(OERcodec_Object[Union[bytes, List['OERcodec_Object[Any]']]]): @@ -767,4 +801,4 @@ def oer_sequence_m2i(field, pkt, s): from scapy.asn1.context import OER_Decoder dec = OER_Decoder(s) _oer_sequence_decode_from(field, pkt, dec) - return [], dec.remaining() \ No newline at end of file + return [], dec.remaining() diff --git a/scapy/asn1/oid.py b/scapy/asn1/oid.py new file mode 100644 index 00000000000..5c1bd5e5908 --- /dev/null +++ b/scapy/asn1/oid.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +"""Shared OBJECT IDENTIFIER arc handling (X.690 section 8.19.4).""" + +from typing import List # noqa: F401 + + +def oid_decode_first_arc(first): + # type: (int) -> List[int] + """Split the first subidentifier into two registration arcs.""" + if first < 40: + return [0, first] + if first < 80: + return [1, first - 40] + return [2, first - 80] + + +def oid_subidentifiers_to_dotted(lst): + # type: (List[int]) -> bytes + if not lst: + return b"" + arcs = oid_decode_first_arc(lst[0]) + lst[1:] + return b".".join(str(k).encode("ascii") for k in arcs) + + +def oid_dotted_to_subidentifiers(oid): + # type: (bytes) -> List[int] + if not oid: + return [] + parts = [int(x) for x in oid.split(b".")] + if len(parts) >= 2: + return [40 * parts[0] + parts[1]] + parts[2:] + return parts diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 04cffd62f1b..2e3e156fee5 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -344,7 +344,13 @@ def UPER_constrained_int_dec(dec, minimum, maximum): value = dec.read_non_negative_binary_integer( UPER_bits_for_range(maximum - minimum) ) - return value + minimum + value += minimum + if not minimum <= value <= maximum: + raise UPER_Decoding_Error( + "UPER_constrained_int_dec: got %i while expecting %i..%i" % + (value, minimum, maximum) + ) + return value def _uper_check_size(name, unit, count, minimum, maximum): @@ -756,12 +762,9 @@ class UPERcodec_OID(UPERcodec_Object[bytes]): @classmethod def encode_into(cls, enc, _oid, **_kwargs): # type: (UPER_Encoder, AnyStr, **Any) -> None + from scapy.asn1.oid import oid_dotted_to_subidentifiers oid = bytes_encode(_oid) - if oid: - lst = [int(x) for x in oid.split(b".")] - lst = [40 * lst[0] + lst[1]] + lst[2:] - else: - lst = [] + lst = oid_dotted_to_subidentifiers(oid) body = b"".join(BER_num_enc(k) for k in lst) enc.append_fragmented( len(body), @@ -771,6 +774,7 @@ def encode_into(cls, enc, _oid, **_kwargs): @classmethod def dec_from_decoder(cls, dec, **_kwargs): # type: (UPER_Decoder, **Any) -> ASN1_Object[bytes] + from scapy.asn1.oid import oid_subidentifiers_to_dotted fragments = [] # type: List[bytes] dec.read_fragmented(lambda size: fragments.append(dec.read_bytes(size))) content = b"".join(fragments) @@ -778,10 +782,7 @@ def dec_from_decoder(cls, dec, **_kwargs): while content: val, content = BER_num_dec(content) lst.append(val) - if len(lst) > 0: - lst.insert(0, lst[0] // 40) - lst[1] %= 40 - return cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)) + return cls.asn1_object(oid_subidentifiers_to_dotted(lst)) def UPER_enumerated_enc(enc, value, enum_values): @@ -983,33 +984,75 @@ class UPERcodec_TIME_TICKS(UPERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.TIME_TICKS -_UPER_STRING_TAGS = ( - "UTF8_STRING", - "NUMERIC_STRING", - "PRINTABLE_STRING", - "T61_STRING", - "VIDEOTEX_STRING", - "IA5_STRING", - "GENERAL_STRING", - "UTC_TIME", - "GENERALIZED_TIME", - "ISO646_STRING", - "UNIVERSAL_STRING", - "BMP_STRING", -) +class UPERcodec_KNOWN_MULTIPLIER_STRING(UPERcodec_STRING): + @classmethod + def encode_into(cls, enc, s, **_kwargs): + # type: (UPER_Encoder, Any, **Any) -> None + raise UPER_Encoding_Error( + "%s: known-multiplier PER string encoding is not implemented" % + cls.__name__ + ) + @classmethod + def dec_from_decoder(cls, dec, **_kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[Any] + raise UPER_Decoding_Error( + "%s: known-multiplier PER string decoding is not implemented" % + cls.__name__ + ) -def _uper_string_codec(name): - # type: (str) -> type - return type( - "UPERcodec_%s" % name, - (UPERcodec_STRING,), - {"tag": getattr(ASN1_Class_UNIVERSAL, name)}, - ) + +class UPERcodec_UTF8_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): + tag = ASN1_Class_UNIVERSAL.UTF8_STRING + + +class UPERcodec_NUMERIC_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): + tag = ASN1_Class_UNIVERSAL.NUMERIC_STRING + + +class UPERcodec_PRINTABLE_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): + tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING + + +class UPERcodec_T61_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): + tag = ASN1_Class_UNIVERSAL.T61_STRING + + +class UPERcodec_VIDEOTEX_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): + tag = ASN1_Class_UNIVERSAL.VIDEOTEX_STRING + + +class UPERcodec_IA5_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): + tag = ASN1_Class_UNIVERSAL.IA5_STRING + + +class UPERcodec_GENERAL_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): + tag = ASN1_Class_UNIVERSAL.GENERAL_STRING + + +class UPERcodec_UTC_TIME(UPERcodec_KNOWN_MULTIPLIER_STRING): + tag = ASN1_Class_UNIVERSAL.UTC_TIME + + +class UPERcodec_GENERALIZED_TIME(UPERcodec_KNOWN_MULTIPLIER_STRING): + tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME + + +class UPERcodec_ISO646_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): + tag = ASN1_Class_UNIVERSAL.ISO646_STRING + + +class UPERcodec_UNIVERSAL_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): + tag = ASN1_Class_UNIVERSAL.UNIVERSAL_STRING + + +class UPERcodec_BMP_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): + tag = ASN1_Class_UNIVERSAL.BMP_STRING -for _tag_name in _UPER_STRING_TAGS: - globals()["UPERcodec_%s" % _tag_name] = _uper_string_codec(_tag_name) +# KNOWN_MULTIPLIER inherits STRING's tag for registration; restore the +# generic STRING codec used by ASN1F_STRING (octet-string UPER path). +ASN1_Class_UNIVERSAL.STRING.register(ASN1_Codecs.PER, UPERcodec_STRING) ################################ diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 20c1ac465b8..ef3bb831b6a 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -116,7 +116,7 @@ def __init__(self, else: self.default = self.ASN1_tag.asn1_object(default) # type: ignore self.size_len = size_len - self.constraints = normalize_constraints(codec_opts, size_len=size_len) + self.constraints = normalize_constraints(codec_opts) self.flexible_tag = flexible_tag if (implicit_tag is not None) and (explicit_tag is not None): err_msg = "field cannot be both implicitly and explicitly tagged" @@ -187,6 +187,9 @@ def _apply_tagging_dec(self, s, pkt, hidden_tag=None, **kwargs): self._apply_diff_tag(pkt, diff_tag) return s + def _codec_kwargs(self, pkt=None): + # type: (Optional[ASN1_Packet]) -> Dict[str, Any] + return {} def _encode_item(self, pkt, item): # type: (ASN1_Packet, Any) -> bytes @@ -209,7 +212,7 @@ def _encode_item(self, pkt, item): # the BER type codec so the universal tag/length are applied. item = item.self_build() codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - return codec.enc(item, field=self, pkt=pkt) + return codec.enc(item, field=self, pkt=pkt, **self._codec_kwargs(pkt)) def i2repr(self, pkt, x): # type: (ASN1_Packet, _I) -> str @@ -236,7 +239,8 @@ def m2i(self, pkt, s): s = self._apply_tagging_dec(s, pkt, _fname=self.name) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) dec = codec.safedec if self.flexible_tag else codec.dec - return dec(s, context=self.context, field=self, pkt=pkt) # type: ignore # noqa: E501 + return dec(s, context=self.context, field=self, pkt=pkt, + **self._codec_kwargs(pkt)) # type: ignore # noqa: E501 def i2m(self, pkt, x): # type: (ASN1_Packet, Union[bytes, _I, _A]) -> bytes @@ -257,13 +261,14 @@ def any2i(self, pkt, x): def extract_packet(self, cls, # type: Type[ASN1_Packet] s, # type: bytes + _underlayer=None, # type: Optional[ASN1_Packet] _parent=None # type: Optional[ASN1_Packet] ): # type: (...) -> Tuple[ASN1_Packet, bytes] try: - c = cls(s, _parent=_parent) + c = cls(s, _underlayer=_underlayer, _parent=_parent) except ASN1F_badsequence: - c = packet.Raw(s, _parent=_parent) # type: ignore + c = packet.Raw(s, _underlayer=_underlayer, _parent=_parent) # type: ignore cpad = c.getlayer(packet.Raw) s = b"" if cpad is not None: @@ -276,7 +281,7 @@ def m2i_from_decoder(self, pkt, dec): # type: (ASN1_Packet, Any) -> Any codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) return codec.dec_from_decoder( # type: ignore[attr-defined] - dec, field=self, pkt=pkt, + dec, field=self, pkt=pkt, **self._codec_kwargs(pkt), ) def dissect_from_decoder(self, pkt, dec): @@ -304,12 +309,15 @@ def encode_into(self, enc, pkt, value=None): else: raw = value bit_enc = per_bit_encoder(enc) + extra = self._codec_kwargs(pkt) if bit_enc is not None: codec.encode_into( # type: ignore[attr-defined] - bit_enc, raw, field=self, pkt=pkt, + bit_enc, raw, field=self, pkt=pkt, **extra, ) return - enc.write(codec.enc(raw, field=self, pkt=pkt)) # type: ignore[attr-defined] + enc.write( + codec.enc(raw, field=self, pkt=pkt, **extra) + ) # type: ignore[attr-defined] def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None @@ -615,15 +623,26 @@ def get_fields_list(self): def _dissect_sequence_children(self, pkt, s): # type: (Any, bytes) -> bytes + from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional, ASN1F_DEFAULT + + def set_absent(obj): + # type: (Any) -> None + if isinstance(obj, (ASN1F_optional, ASN1F_DEFAULT)): + obj.set_missing(pkt) + else: + obj.set_val(pkt, None) + if len(s) == 0: for obj in self.seq: - obj.set_val(pkt, None) + set_absent(obj) return s - for obj in self.seq: + for idx, obj in enumerate(self.seq): try: s = obj.dissect(pkt, s) except ASN1F_badsequence: - break + for absent in self.seq[idx:]: + set_absent(absent) + return s return s def m2i(self, pkt, s): @@ -1022,15 +1041,7 @@ def encode_to(self, pkt, enc): def encode_into(self, enc, pkt, value=None): # type: (Any, ASN1_Packet, Any) -> None from scapy.asn1.compound import choice_encode_to - if value is None: - choice_encode_to(self, pkt, enc) - return - old = getattr(pkt, self.name, None) - setattr(pkt, self.name, value) - try: - choice_encode_to(self, pkt, enc) - finally: - setattr(pkt, self.name, old) + choice_encode_to(self, pkt, enc, value) def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None diff --git a/scapy/asn1packet.py b/scapy/asn1packet.py index ebb138a0c66..c66bca0710b 100644 --- a/scapy/asn1packet.py +++ b/scapy/asn1packet.py @@ -54,7 +54,13 @@ def self_build(self): def do_dissect(self, x): # type: (bytes) -> bytes + from scapy.asn1.asn1 import ASN1_Codecs + from scapy.asn1.uper import UPER_has_unexpected_remainder + self._asn1_observed_tags = {} # type: ignore[attr-defined] dec = self.ASN1_codec.new_decoder(x) self.ASN1_root.decode_from(self, dec) + if self.ASN1_codec is ASN1_Codecs.PER: + if not UPER_has_unexpected_remainder(dec.bit_decoder): + return b"" return dec.remaining() diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index 268f63f6f4b..e06debb500b 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -1470,3 +1470,24 @@ assert isinstance(decoded.c, RefactorOERHighPrivateAlt) assert _value(decoded.c.flag) == 0 True +% PR #5050 review regressions ++ OER review fixes += OER INTEGER (0..255) uses single octet width from bounds +class OERBoundedByte(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0, maximum=255) + +assert raw(OERBoundedByte(n=5)) == b"\x05" +assert _roundtrip(OERBoundedByte, OERBoundedByte(n=5)).n.val == 5 + += OER OID round-trip for 2.999.3 +obj, remain = OERcodec_OID.do_dec(OERcodec_OID.enc("2.999.3")) +assert str(obj.val) == "2.999.3" +assert remain == b"" + += OER string codec classes are picklable with correct module +import pickle +assert pickle.dumps(OERcodec_IA5_STRING) +assert OERcodec_IA5_STRING.__module__ == "scapy.asn1.oer" +True + diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index f946ca98fd9..90c3ef9c3ef 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -3594,3 +3594,42 @@ result = subprocess.run([sys.executable, "-c", code]) assert result.returncode == 0 True +% PR #5050 review regressions ++ UPER review fixes += top-level BOOLEAN dissect ignores octet padding without Raw payload +from scapy.packet import Raw + +pkt = UPERBooleanField(b"\x80") +assert raw(pkt) == b"\x80" +assert pkt.getlayer(Raw) is None + += constrained INTEGER decode rejects out-of-range code points +class UPERRange02(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=2) + +_raises(UPER_Decoding_Error, lambda: UPERRange02(b"\xc0")) + += UPER OID round-trip for 2.999.3 +enc = UPER_Encoder() +UPERcodec_OID.encode_into(enc, "2.999.3") +obj = UPERcodec_OID.dec_from_decoder(UPER_Decoder(enc.as_bytes())) +assert str(obj.val) == "2.999.3" + += UPER IA5 string encoding raises strict error +_raises(UPER_Encoding_Error, lambda: UPERcodec_IA5_STRING.enc(b"hi")) +_raises(UPER_Encoding_Error, lambda: UPERcodec_IA5_STRING.encode_into( + UPER_Encoder(), b"hi")) + += CHOICE encode_into does not mutate packet fields +import copy +from scapy.asn1.context import UPER_EncoderContext + +pkt = UPERChoiceField(c=ASN1_INTEGER(1)) +before = copy.copy(pkt.fields) +enc = UPER_EncoderContext() +pkt.ASN1_root.encode_into(enc, pkt, value=ASN1_INTEGER(9)) +assert pkt.fields == before +assert enc.finish() == bytes.fromhex("008480") +True + diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index b2ff3558fcf..f76ff14c72f 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -740,3 +740,28 @@ assert ber_before == ber_after assert _value(RefactorBERInt(ber_after).n) == 5 True +% PR #5050 review regressions ++ BER DEFAULT and X.509 extract_packet += BER DEFAULT-only empty SEQUENCE restores default value +class _BerDefaultOnly(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_DEFAULT(ASN1F_INTEGER("b", 7), 7), + ) + +decoded = _BerDefaultOnly(b"\x30\x00") +assert getattr(decoded.b, "val", decoded.b) == 7 + += extract_packet passes _underlayer for X.509 attribute types +from scapy.layers.x509 import X509_Attribute, X509_AttributeValue, X509_Extensions + +attr = X509_Attribute() +attr.type = ASN1_OID("1.2.840.113549.1.9.14") +val_pkt = X509_AttributeValue() +val_pkt.underlayer = attr +field = X509_AttributeValue.ASN1_root +p, remain = field.m2i(val_pkt, b"\x30\x00") +assert isinstance(p, X509_Extensions) +assert remain == b"" +True + diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index db9a37af370..1f6bad335a0 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -677,3 +677,52 @@ _assert_record_empty(empty) True +% PR #5050 review regressions ++ BER review fixes += field _codec_kwargs hook +class P(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER("n", 0) + +fld = P.ASN1_root +assert fld._codec_kwargs(P()) == {} +assert raw(P(n=ASN1_INTEGER(5))) == b"\x02\x01\x05" +assert raw(P(n=5)) == b"\x02\x01\x05" + += field encode with extra kwargs via _codec_kwargs override +class ExtraKwField(ASN1F_INTEGER): + def _codec_kwargs(self, pkt): + return {"size_len": self.size_len, "oer_unsigned": True, "uper_min": 0} + +class ExtraPkt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ExtraKwField("n", 0) + +assert raw(ExtraPkt(n=7)) == b"\x02\x01\x07" +ExtraPkt(raw(ExtraPkt(n=7))).n.val == 7 + += BER OID first arc decode for 2.999.3 +obj, remain = BERcodec_OID.do_dec(BERcodec_OID.enc("2.999.3")) +assert str(obj.val) == "2.999.3" +assert remain == b"" + += BER build does not import UPER +import subprocess +import sys +code = r''' +import sys +from scapy.asn1fields import ASN1F_INTEGER +from scapy.asn1packet import ASN1_Packet +from scapy.asn1.asn1 import ASN1_Codecs +from scapy.packet import raw +class P(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER("n", 0) +assert "scapy.asn1.uper" not in sys.modules +raw(P(n=5)) +assert "scapy.asn1.uper" not in sys.modules +''' +result = subprocess.run([sys.executable, "-c", code]) +assert result.returncode == 0 +True + From e63f5ee813569d699ef1437e2110b6f4bbd86284 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 07:14:09 +0200 Subject: [PATCH 22/46] Fix flake8 and mypy issues in ASN.1 OER/UPER changes. Remove unused imports, tighten typing for observed tags and codec returns, and satisfy line-length checks on BER override annotations. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/ber.py | 27 ++++++++++++++------------- scapy/asn1/compound.py | 2 +- scapy/asn1/oer.py | 3 --- scapy/asn1/uper.py | 11 ----------- scapy/asn1fields.py | 38 +++++++++++++++++++++++--------------- scapy/asn1packet.py | 8 +++++--- 6 files changed, 43 insertions(+), 46 deletions(-) diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index f3aca84d681..21455f3c65c 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -12,8 +12,7 @@ # Good read: https://luca.ntop.org/Teaching/Appunti/asn1.html from scapy.config import conf -from scapy.error import warning -from scapy.compat import chb, bytes_encode +from scapy.compat import chb, orb, bytes_encode from scapy.utils import binrepr, inet_aton, inet_ntoa from scapy.asn1.asn1 import ( ASN1Codec_metaclass, @@ -34,7 +33,6 @@ from typing import ( Any, AnyStr, - Dict, Generic, List, Optional, @@ -407,9 +405,10 @@ def enc(cls, s, field=None, pkt=None, size_len=None, **_kwargs): ) else: try: + i = int(s) # type: ignore[call-overload] return BERcodec_INTEGER.enc( - int(s), field=field, pkt=pkt, size_len=size_len, - ) # type: ignore + i, field=field, pkt=pkt, size_len=size_len, + ) except TypeError: raise TypeError("Trying to encode an invalid value !") @@ -421,7 +420,9 @@ def _ber_enc_size_len(field=None, size_len=None): # type: (Any, Optional[int]) -> int from scapy.asn1.constraints import field_size_len sl = field_size_len(field, size_len) - return sl if sl is not None else 0 + if sl is None: + return 0 + return int(sl) ########################## @@ -432,7 +433,7 @@ class BERcodec_INTEGER(BERcodec_Object[int]): tag = ASN1_Class_UNIVERSAL.INTEGER @classmethod - def enc(cls, i, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] + def enc(cls, i, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 # type: (int, Any, Any, Optional[int], **Any) -> bytes size_len = _ber_enc_size_len(field, size_len) ls = [] @@ -500,7 +501,7 @@ def do_dec(cls, ) @classmethod - def enc(cls, _s, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] + def enc(cls, _s, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 # type: (AnyStr, Any, Any, Optional[int], **Any) -> bytes size_len = _ber_enc_size_len(field, size_len) # /!\ this is DER encoding (bit strings are only zero-bit padded) @@ -520,7 +521,7 @@ class BERcodec_STRING(BERcodec_Object[str]): tag = ASN1_Class_UNIVERSAL.STRING @classmethod - def enc(cls, _s, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] + def enc(cls, _s, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 # type: (Union[str, bytes], Any, Any, Optional[int], **Any) -> bytes size_len = _ber_enc_size_len(field, size_len) s = bytes_encode(_s) @@ -543,7 +544,7 @@ class BERcodec_NULL(BERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.NULL @classmethod - def enc(cls, i, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] + def enc(cls, i, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 # type: (int, Any, Any, Optional[int], **Any) -> bytes if i == 0: return chb(int(cls.tag)) + b"\0" @@ -557,7 +558,7 @@ class BERcodec_OID(BERcodec_Object[bytes]): tag = ASN1_Class_UNIVERSAL.OID @classmethod - def enc(cls, _oid, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] + def enc(cls, _oid, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 # type: (AnyStr, Any, Any, Optional[int], **Any) -> bytes size_len = _ber_enc_size_len(field, size_len) oid = bytes_encode(_oid) @@ -651,7 +652,7 @@ class BERcodec_SEQUENCE(BERcodec_Object[Union[bytes, List[BERcodec_Object[Any]]] tag = ASN1_Class_UNIVERSAL.SEQUENCE @classmethod - def enc(cls, _ll, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] + def enc(cls, _ll, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 # type: (Union[bytes, List[BERcodec_Object[Any]]], Any, Any, Optional[int], **Any) -> bytes # noqa: E501 if isinstance(_ll, bytes): ll = _ll @@ -710,7 +711,7 @@ class BERcodec_IPADDRESS(BERcodec_STRING): tag = ASN1_Class_UNIVERSAL.IPADDRESS @classmethod - def enc(cls, ipaddr_ascii, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore + def enc(cls, ipaddr_ascii, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore # noqa: E501 # type: (str, Any, Any, Optional[int], **Any) -> bytes size_len = _ber_enc_size_len(field, size_len) try: diff --git a/scapy/asn1/compound.py b/scapy/asn1/compound.py index cda0c5f315f..72b3f3fc4c5 100644 --- a/scapy/asn1/compound.py +++ b/scapy/asn1/compound.py @@ -10,7 +10,7 @@ """ from functools import reduce -from typing import Any, Callable, List, Optional, Tuple +from typing import Any, Callable, List, Tuple from scapy.asn1.asn1 import ( ASN1_Class_UNIVERSAL, diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index aae73acf47b..6bb8b93e3f0 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -25,7 +25,6 @@ import struct -from scapy.error import warning from scapy.compat import chb, orb, bytes_encode from scapy.utils import binrepr, inet_aton, inet_ntoa from scapy.asn1.tag import asn1_tag_parts @@ -47,7 +46,6 @@ from typing import ( Any, AnyStr, - Dict, Generic, List, Optional, @@ -55,7 +53,6 @@ Type, TypeVar, Union, - cast, ) ################## diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 2e3e156fee5..44adcbdeb0f 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -27,7 +27,6 @@ ``alternative_index`` / BER tag lookup. """ -from scapy.error import warning from scapy.compat import orb, bytes_encode from scapy.utils import binrepr, inet_aton, inet_ntoa from scapy.asn1.ber import BER_num_dec, BER_num_enc @@ -49,7 +48,6 @@ Any, AnyStr, Callable, - Dict, Generic, List, Optional, @@ -57,7 +55,6 @@ Type, TypeVar, Union, - cast, ) @@ -1060,14 +1057,6 @@ class UPERcodec_BMP_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): ################################ from scapy.asn1.compound import ( # noqa: E402 - uper_choice_decode_from_decoder as uper_choice_m2i_from_decoder, - uper_choice_encode_into, - uper_packet_decode_from_decoder as uper_packet_m2i_from_decoder, - uper_packet_encode_into, - uper_sequence_of_decode_from_decoder as uper_sequence_of_m2i_from_decoder, - uper_sequence_of_encode_into, - write_uper_presence_bits, - read_uper_presence_bits, sequence_decode_from as _uper_sequence_decode_from, sequence_encode_to as _uper_sequence_encode_to, sequence_of_decode_from as _uper_sequence_of_decode_from, diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index ef3bb831b6a..5cc5832bbcf 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -127,7 +127,6 @@ def __init__(self, self.network_tag = int(implicit_tag or explicit_tag or self.ASN1_tag) self.owners = [] # type: List[Type[ASN1_Packet]] - def register_owner(self, cls): # type: (Type[ASN1_Packet]) -> None self.owners.append(cls) @@ -137,11 +136,11 @@ def _apply_diff_tag(self, pkt, diff_tag): # flexible_tag was True: record the observed tag on the packet so # shared field descriptors stay immutable across interleaved decodes. if diff_tag is not None: - observed = getattr(pkt, "_asn1_observed_tags", None) - if observed is None: - pkt._asn1_observed_tags = {} # type: ignore[attr-defined] - observed = pkt._asn1_observed_tags # type: ignore[attr-defined] - observed[self.name] = diff_tag + tags = pkt._asn1_observed_tags + if tags is None: + tags = {} + pkt._asn1_observed_tags = tags + tags[self.name] = diff_tag def _tagging_tags(self, pkt): # type: (ASN1_Packet) -> Tuple[Optional[int], Optional[int]] @@ -238,9 +237,21 @@ def m2i(self, pkt, s): """ s = self._apply_tagging_dec(s, pkt, _fname=self.name) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - dec = codec.safedec if self.flexible_tag else codec.dec - return dec(s, context=self.context, field=self, pkt=pkt, - **self._codec_kwargs(pkt)) # type: ignore # noqa: E501 + kwargs = dict( + context=self.context, + field=self, + pkt=pkt, + **self._codec_kwargs(pkt), + ) + if self.flexible_tag: + return cast( + Tuple[_A, bytes], + codec.safedec(s, **kwargs), + ) + return cast( + Tuple[_A, bytes], + codec.dec(s, **kwargs), + ) def i2m(self, pkt, x): # type: (ASN1_Packet, Union[bytes, _I, _A]) -> bytes @@ -317,7 +328,7 @@ def encode_into(self, enc, pkt, value=None): return enc.write( codec.enc(raw, field=self, pkt=pkt, **extra) - ) # type: ignore[attr-defined] + ) def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None @@ -339,13 +350,13 @@ def build(self, pkt): # type: (ASN1_Packet) -> bytes enc = pkt.ASN1_codec.new_encoder() self.encode_to(pkt, enc) - return enc.finish() + return cast(bytes, enc.finish()) def dissect(self, pkt, s): # type: (ASN1_Packet, bytes) -> bytes dec = pkt.ASN1_codec.new_decoder(s) self.decode_from(pkt, dec) - return dec.remaining() + return cast(bytes, dec.remaining()) def do_copy(self, x): # type: (Any) -> Any @@ -437,7 +448,6 @@ def uper_enum_values(self): # type: () -> List[int] return sorted(self.i2s) - def i2m(self, pkt, # type: ASN1_Packet s, # type: Union[bytes, str, int, ASN1_INTEGER] @@ -810,7 +820,6 @@ def fld(self): # type: () -> ASN1F_field[Any, Any] return self._field - def get_fields_list(self): # type: () -> List[ASN1F_field[Any, Any]] inner = self._field.get_fields_list() @@ -851,7 +860,6 @@ def is_empty(self, pkt): # type: (ASN1_Packet) -> bool return not self.is_present(pkt) - def build(self, pkt): # type: (ASN1_Packet) -> bytes # Through self, so that a DEFAULT component omits its default value. diff --git a/scapy/asn1packet.py b/scapy/asn1packet.py index c66bca0710b..55ed891de50 100644 --- a/scapy/asn1packet.py +++ b/scapy/asn1packet.py @@ -15,6 +15,7 @@ from typing import ( Any, Dict, + Optional, Tuple, Type, cast, @@ -43,6 +44,7 @@ def __new__(cls, class ASN1_Packet(Packet, metaclass=ASN1Packet_metaclass): ASN1_root = cast('ASN1F_field[Any, Any]', None) ASN1_codec = cast(Any, None) + _asn1_observed_tags = None # type: Optional[Dict[str, int]] def self_build(self): # type: () -> bytes @@ -50,17 +52,17 @@ def self_build(self): return self.raw_packet_cache enc = self.ASN1_codec.new_encoder() self.ASN1_root.encode_to(self, enc) - return enc.finish() + return cast(bytes, enc.finish()) def do_dissect(self, x): # type: (bytes) -> bytes from scapy.asn1.asn1 import ASN1_Codecs from scapy.asn1.uper import UPER_has_unexpected_remainder - self._asn1_observed_tags = {} # type: ignore[attr-defined] + self._asn1_observed_tags = {} dec = self.ASN1_codec.new_decoder(x) self.ASN1_root.decode_from(self, dec) if self.ASN1_codec is ASN1_Codecs.PER: if not UPER_has_unexpected_remainder(dec.bit_decoder): return b"" - return dec.remaining() + return cast(bytes, dec.remaining()) From e56d26acb9729eaa14b9868b03d23d29207fdde0 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 09:44:37 +0200 Subject: [PATCH 23/46] Address remaining PR #5050 wire and compatibility review issues. Fix UPER trailing-octet handling, OER integer constraints, X.509 underlayer extraction, known-multiplier string typing, and restore codec tagging/_codec_kwargs contracts with regression tests. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/asn1.py | 20 +++++++ scapy/asn1/ber.py | 1 + scapy/asn1/constraints.py | 108 +++++++++++++++++++------------------ scapy/asn1/context.py | 20 +------ scapy/asn1/oer.py | 72 ++++++++++++++++--------- scapy/asn1/uper.py | 68 ++++++++++++++++++----- scapy/asn1fields.py | 22 +++----- scapy/asn1packet.py | 6 --- test/contrib/oer.uts | 39 +++++++++++++- test/contrib/uper.uts | 36 +++++++++++-- test/scapy/layers/asn1.uts | 16 +++--- test/scapy/layers/ber.uts | 12 ++++- 12 files changed, 274 insertions(+), 146 deletions(-) diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index c7fb725303b..74e55068455 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -185,6 +185,26 @@ def register_stem(cls, stem): # type: (Type[BERcodec_Object[Any]]) -> None cls._stem = stem + def register_tagging(cls, enc, dec): + # type: (Any, Any) -> None + # Codec-level implicit/explicit tagging (BER) or identity (OER/PER). + cls._tagging_enc = enc + cls._tagging_dec = dec + + def tagging_enc(cls, s, **kwargs): + # type: (bytes, **Any) -> bytes + enc = getattr(cls, "_tagging_enc", None) + if enc is None: + return s + return cast(bytes, enc(s, **kwargs)) + + def tagging_dec(cls, s, **kwargs): + # type: (bytes, **Any) -> Tuple[Optional[int], bytes] + dec = getattr(cls, "_tagging_dec", None) + if dec is None: + return None, s + return cast(Tuple[Optional[int], bytes], dec(s, **kwargs)) + def new_encoder(cls): # type: () -> Any from scapy.asn1.context import new_encoder diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index 21455f3c65c..c625dc990be 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -414,6 +414,7 @@ def enc(cls, s, field=None, pkt=None, size_len=None, **_kwargs): ASN1_Codecs.BER.register_stem(BERcodec_Object) +ASN1_Codecs.BER.register_tagging(BER_tagging_enc, BER_tagging_dec) def _ber_enc_size_len(field=None, size_len=None): diff --git a/scapy/asn1/constraints.py b/scapy/asn1/constraints.py index b5f123ddf1e..2e85132b41e 100644 --- a/scapy/asn1/constraints.py +++ b/scapy/asn1/constraints.py @@ -19,18 +19,21 @@ class ASN1Constraints: unsigned: bool = False -_LEGACY_CODEC_OPTS = { +_LEGACY_ALIASES = { "uper_min": "minimum", "uper_max": "maximum", "uper_extensible": "extensible", "oer_extensible": "extensible", "oer_unsigned": "unsigned", - "minimum": "minimum", - "maximum": "maximum", - "size_min": "size_min", - "size_max": "size_max", - "extensible": "extensible", - "unsigned": "unsigned", +} + +_SUPPORTED_CONSTRAINTS = { + "minimum", + "maximum", + "size_min", + "size_max", + "extensible", + "unsigned", } @@ -46,39 +49,18 @@ def normalize_constraints(codec_opts): "unsigned": False, } # type: Dict[str, Any] for key, value in codec_opts.items(): - if key in _LEGACY_CODEC_OPTS: - if key.startswith(("uper_", "oer_")) and key not in ( - "uper_min", "uper_max", "uper_extensible", - "oer_extensible", "oer_unsigned", - ): - warnings.warn( - "Unknown codec-prefixed constraint %r" % key, - DeprecationWarning, - stacklevel=4, - ) - continue - if key.startswith(("uper_", "oer_")): - warnings.warn( - "codec-prefixed constraint %r is deprecated; use %r instead" % - (key, _LEGACY_CODEC_OPTS[key]), - DeprecationWarning, - stacklevel=4, - ) - data[_LEGACY_CODEC_OPTS[key]] = value - elif key in data: - data[key] = value - elif key.startswith(("uper_", "oer_")): + if key in _LEGACY_ALIASES: warnings.warn( - "Unknown codec-prefixed constraint %r" % key, + "codec-prefixed constraint %r is deprecated; use %r instead" % + (key, _LEGACY_ALIASES[key]), DeprecationWarning, stacklevel=4, ) + data[_LEGACY_ALIASES[key]] = value + elif key in _SUPPORTED_CONSTRAINTS: + data[key] = value else: - warnings.warn( - "Unknown field constraint %r" % key, - DeprecationWarning, - stacklevel=4, - ) + raise TypeError("Unknown field constraint %r" % key) return ASN1Constraints(**data) @@ -152,30 +134,54 @@ def uper_enum_values(field=None, pkt=None, uper_enum_values=None): def oer_int_wire_params(field=None, size_len=None, unsigned=None): # type: (Any, Optional[int], Optional[bool]) -> Tuple[Optional[int], bool, Optional[int], Optional[int]] # noqa: E501 - """Derive OER INTEGER width and signedness from field constraints.""" + """Derive OER INTEGER width and signedness from field constraints. + + Per X.696 §10.3–10.4: + - extensible integer constraints are encoded as unbounded; + - a nonnegative lower bound without a fitting fixed upper bound uses + variable-width unsigned encoding; + - fixed eight-octet width is used only when ``maximum <= 2**64 - 1``. + """ size_len = field_size_len(field, size_len) is_unsigned = oer_unsigned(field, unsigned) minimum, maximum = field_range(field) if field is not None else (None, None) - if size_len is None and minimum is not None and maximum is not None: - if minimum >= 0: + extensible = field_extensible(field) if field is not None else False + + # Extension values may lie outside the root range. + val_min = None if extensible else minimum + val_max = None if extensible else maximum + + if size_len is not None: + if (not is_unsigned and minimum is not None and minimum >= 0 and + not extensible): is_unsigned = True + return size_len, is_unsigned, val_min, val_max + + if extensible: + return None, is_unsigned, None, None + + if minimum is not None and minimum >= 0: + is_unsigned = True + if maximum is not None: if maximum <= 0xFF: size_len = 1 elif maximum <= 0xFFFF: size_len = 2 elif maximum <= 0xFFFFFFFF: size_len = 4 - else: + elif maximum <= 0xFFFFFFFFFFFFFFFF: size_len = 8 - else: - is_unsigned = False - for sl, lo, hi in ( - (1, -128, 127), - (2, -32768, 32767), - (4, -2147483648, 2147483647), - (8, -9223372036854775808, 9223372036854775807), - ): - if minimum >= lo and maximum <= hi: - size_len = sl - break - return size_len, is_unsigned, minimum, maximum + # else: range exceeds 2^64-1 → variable unsigned + elif minimum is not None and maximum is not None: + is_unsigned = False + for sl, lo, hi in ( + (1, -128, 127), + (2, -32768, 32767), + (4, -2147483648, 2147483647), + (8, -9223372036854775808, 9223372036854775807), + ): + if minimum >= lo and maximum <= hi: + size_len = sl + break + + return size_len, is_unsigned, val_min, val_max diff --git a/scapy/asn1/context.py b/scapy/asn1/context.py index a24eba16b66..989ff175d68 100644 --- a/scapy/asn1/context.py +++ b/scapy/asn1/context.py @@ -114,11 +114,6 @@ def bit_encoder(self): # type: () -> _UPER_Encoder return self._enc - @property - def inner(self): - # type: () -> _UPER_Encoder - return self._enc - def finish(self): # type: () -> bytes return self._enc.as_bytes() @@ -139,28 +134,15 @@ def bit_decoder(self): # type: () -> _UPER_Decoder return self._dec - @property - def inner(self): - # type: () -> _UPER_Decoder - return self._dec - def remaining(self): # type: () -> bytes - return self._dec.remaining() + return self._dec.remaining_bytes() def set_remainder(self, remainder): # type: (bytes) -> None from scapy.asn1.uper import UPER_Decoder self._dec = UPER_Decoder(remainder) - def check_no_remainder(self, name): - # type: (str) -> None - from scapy.asn1.uper import UPER_Decoding_Error - if self._dec.remaining(): - raise UPER_Decoding_Error( - "unexpected remainder in %s" % name, - ) - def new_encoder(codec): # type: (Any) -> ASN1Encoder diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index 6bb8b93e3f0..7e85c0491e3 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -316,11 +316,21 @@ def enc(cls, s, field=None, pkt=None, size_len=None, **_kwargs): raise TypeError("Trying to encode an invalid value !") -# No tagging hook: X.696 encodes no tag for a component, whatever the tagging -# environment of the module, so a field is left alone. The only tag on the -# wire is the one of a chosen CHOICE alternative, which the CHOICE hooks below -# write themselves. +# Tags declared on a field are not encoded for OER components (X.696); +# CHOICE writes its own alternative tags. Identity tagging keeps the +# codec extension point without BER-style wrappers. +def _oer_tagging_enc(s, **_kwargs): + # type: (bytes, **Any) -> bytes + return s + + +def _oer_tagging_dec(s, **_kwargs): + # type: (bytes, **Any) -> Tuple[Optional[int], bytes] + return None, s + + ASN1_Codecs.OER.register_stem(OERcodec_Object) +ASN1_Codecs.OER.register_tagging(_oer_tagging_enc, _oer_tagging_dec) ########################## @@ -342,12 +352,16 @@ def enc(cls, i, field=None, size_len=None, oer_unsigned=None, **_kwargs): size_len, oer_unsigned, minimum, maximum = oer_int_wire_params( field, size_len, oer_unsigned, ) - if minimum is not None and maximum is not None: - if not minimum <= i <= maximum: - raise OER_Encoding_Error( - "%s: %i is outside %i..%i" % - (cls.__name__, i, minimum, maximum) - ) + if minimum is not None and i < minimum: + raise OER_Encoding_Error( + "%s: %i is below minimum %i" % + (cls.__name__, i, minimum) + ) + if maximum is not None and i > maximum: + raise OER_Encoding_Error( + "%s: %i is above maximum %i" % + (cls.__name__, i, maximum) + ) if oer_unsigned and i < 0: raise OER_Encoding_Error( "%s: %i is negative for an unsigned type" % (cls.__name__, i) @@ -389,25 +403,35 @@ def do_dec(cls, x = struct.unpack( cls._FIXED_FORMATS[not oer_unsigned][size_len], s[:size_len] )[0] - if minimum is not None and maximum is not None: - if not minimum <= x <= maximum: - raise OER_Decoding_Error( - "%s: %i is outside %i..%i" % - (cls.__name__, x, minimum, maximum), - remaining=s, - ) + if minimum is not None and x < minimum: + raise OER_Decoding_Error( + "%s: %i is below minimum %i" % + (cls.__name__, x, minimum), + remaining=s, + ) + if maximum is not None and x > maximum: + raise OER_Decoding_Error( + "%s: %i is above maximum %i" % + (cls.__name__, x, maximum), + remaining=s, + ) return cls.asn1_object(x), s[size_len:] if oer_unsigned: x, t = OER_unsigned_integer_dec(s) else: x, t = OER_signed_integer_dec(s) - if minimum is not None and maximum is not None: - if not minimum <= x <= maximum: - raise OER_Decoding_Error( - "%s: %i is outside %i..%i" % - (cls.__name__, x, minimum, maximum), - remaining=s, - ) + if minimum is not None and x < minimum: + raise OER_Decoding_Error( + "%s: %i is below minimum %i" % + (cls.__name__, x, minimum), + remaining=s, + ) + if maximum is not None and x > maximum: + raise OER_Decoding_Error( + "%s: %i is above maximum %i" % + (cls.__name__, x, maximum), + remaining=s, + ) return cls.asn1_object(x), t diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 44adcbdeb0f..a29c4f249e9 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -194,10 +194,14 @@ def as_bytes(self): def UPER_has_unexpected_remainder(dec): # type: (UPER_Decoder) -> bool - if dec.number_of_bits == 0: - return False - mask = (1 << dec.number_of_bits) - 1 - return (dec._bits & mask) != 0 + """True when unread bits remain after an octet-aligned padding check. + + Prefer :meth:`UPER_Decoder.remaining_bytes` at packet boundaries; this + helper only reports whether non-padding bits are still pending. + """ + pad = -dec._read_offset() % 8 + unread = max(0, dec.number_of_bits - pad) + return unread != 0 class UPER_Decoder(object): @@ -252,9 +256,17 @@ def remaining_bytes(self): # type: () -> bytes # A standalone UPER encoding is padded to an octet boundary, so the # bits left over inside the current octet are padding; only whole - # octets after it are actual remaining input. + # octets after it are actual remaining input / Scapy payload. pad = -self._read_offset() % 8 - self.number_of_bits = max(0, self.number_of_bits - pad) + if pad: + if pad > self.number_of_bits: + raise UPER_Decoding_Error("UPER_Decoder: truncated padding") + if self._read_bits_int(pad) != 0: + raise UPER_Decoding_Error( + "UPER_Decoder: non-zero padding bits", + remaining=self.remaining(), + ) + self.number_of_bits -= pad return self.remaining() def read_bytes(self, number_of_bytes): @@ -482,8 +494,20 @@ def safedec(cls, s, context=None, **kwargs): return cls.dec(s, context, safe=True, **kwargs) -# No tagging hook: PER encodes no tag at all, so a field is left alone. +# No field tagging on the wire for PER; identity keeps the codec extension +# point without BER-style wrappers. +def _uper_tagging_enc(s, **_kwargs): + # type: (bytes, **Any) -> bytes + return s + + +def _uper_tagging_dec(s, **_kwargs): + # type: (bytes, **Any) -> Tuple[Optional[int], bytes] + return None, s + + ASN1_Codecs.PER.register_stem(UPERcodec_Object) +ASN1_Codecs.PER.register_tagging(_uper_tagging_enc, _uper_tagging_dec) ######################### @@ -982,6 +1006,8 @@ class UPERcodec_TIME_TICKS(UPERcodec_INTEGER): class UPERcodec_KNOWN_MULTIPLIER_STRING(UPERcodec_STRING): + # X.691 §3.7.16 / §30: NumericString, PrintableString, VisibleString + # (ISO646String), IA5String, BMPString, UniversalString. @classmethod def encode_into(cls, enc, s, **_kwargs): # type: (UPER_Encoder, Any, **Any) -> None @@ -999,7 +1025,23 @@ def dec_from_decoder(cls, dec, **_kwargs): ) -class UPERcodec_UTF8_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): +class UPERcodec_UNSUPPORTED_TIME(UPERcodec_STRING): + @classmethod + def encode_into(cls, enc, s, **_kwargs): + # type: (UPER_Encoder, Any, **Any) -> None + raise UPER_Encoding_Error( + "%s: PER time encoding is not implemented" % cls.__name__ + ) + + @classmethod + def dec_from_decoder(cls, dec, **_kwargs): + # type: (UPER_Decoder, **Any) -> ASN1_Object[Any] + raise UPER_Decoding_Error( + "%s: PER time decoding is not implemented" % cls.__name__ + ) + + +class UPERcodec_UTF8_STRING(UPERcodec_STRING): tag = ASN1_Class_UNIVERSAL.UTF8_STRING @@ -1011,11 +1053,11 @@ class UPERcodec_PRINTABLE_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING -class UPERcodec_T61_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): +class UPERcodec_T61_STRING(UPERcodec_STRING): tag = ASN1_Class_UNIVERSAL.T61_STRING -class UPERcodec_VIDEOTEX_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): +class UPERcodec_VIDEOTEX_STRING(UPERcodec_STRING): tag = ASN1_Class_UNIVERSAL.VIDEOTEX_STRING @@ -1023,15 +1065,15 @@ class UPERcodec_IA5_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): tag = ASN1_Class_UNIVERSAL.IA5_STRING -class UPERcodec_GENERAL_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): +class UPERcodec_GENERAL_STRING(UPERcodec_STRING): tag = ASN1_Class_UNIVERSAL.GENERAL_STRING -class UPERcodec_UTC_TIME(UPERcodec_KNOWN_MULTIPLIER_STRING): +class UPERcodec_UTC_TIME(UPERcodec_UNSUPPORTED_TIME): tag = ASN1_Class_UNIVERSAL.UTC_TIME -class UPERcodec_GENERALIZED_TIME(UPERcodec_KNOWN_MULTIPLIER_STRING): +class UPERcodec_GENERALIZED_TIME(UPERcodec_UNSUPPORTED_TIME): tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 5cc5832bbcf..9b268e52e44 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -23,7 +23,6 @@ ASN1_BOOLEAN, ASN1_Class, ASN1_Class_UNIVERSAL, - ASN1_Codecs, ASN1_Decoding_Error, ASN1_Error, ASN1_INTEGER, @@ -32,11 +31,7 @@ ASN1_Object, ASN1_STRING, ) -from scapy.asn1.ber import ( - BER_Decoding_Error, - BER_tagging_dec, - BER_tagging_enc, -) +from scapy.asn1.ber import BER_Decoding_Error from scapy.asn1.constraints import normalize_constraints from scapy.asn1.context import per_bit_decoder, per_bit_encoder from scapy.asn1.tag import asn1_tag_parts @@ -158,16 +153,12 @@ def _tagging_tags(self, pkt): def _tagging_dec(self, pkt, s, **kwargs): # type: (ASN1_Packet, bytes, **Any) -> Tuple[Optional[int], bytes] - # Only BER puts the tag of a field on the wire. - if pkt.ASN1_codec is ASN1_Codecs.BER: - return BER_tagging_dec(s, **kwargs) - return None, s + # Codec provides tagging_*; OER/PER register identity helpers. + return pkt.ASN1_codec.tagging_dec(s, **kwargs) # type: ignore def _tagging_enc(self, pkt, s, **kwargs): # type: (ASN1_Packet, bytes, **Any) -> bytes - if pkt.ASN1_codec is ASN1_Codecs.BER: - return BER_tagging_enc(s, **kwargs) - return s + return pkt.ASN1_codec.tagging_enc(s, **kwargs) # type: ignore def _apply_tagging_dec(self, s, pkt, hidden_tag=None, **kwargs): # type: (bytes, ASN1_Packet, Optional[Any], **Any) -> bytes @@ -188,7 +179,8 @@ def _apply_tagging_dec(self, s, pkt, hidden_tag=None, **kwargs): def _codec_kwargs(self, pkt=None): # type: (Optional[ASN1_Packet]) -> Dict[str, Any] - return {} + # Pass size_len through by default; subclasses may extend this dict. + return {"size_len": self.size_len} def _encode_item(self, pkt, item): # type: (ASN1_Packet, Any) -> bytes @@ -728,7 +720,7 @@ def __init__(self, elif hasattr(cls, "ASN1_root") or callable(cls): self.cls = cast("Type[ASN1_Packet]", cls) self._extract_packet = lambda s, pkt: self.extract_packet( - self.cls, s, _parent=pkt) + self.cls, s, _underlayer=pkt, _parent=pkt) self.holds_packets = 1 else: raise ValueError("cls should be an ASN1_Packet or ASN1_field") diff --git a/scapy/asn1packet.py b/scapy/asn1packet.py index 55ed891de50..a72a4890f08 100644 --- a/scapy/asn1packet.py +++ b/scapy/asn1packet.py @@ -56,13 +56,7 @@ def self_build(self): def do_dissect(self, x): # type: (bytes) -> bytes - from scapy.asn1.asn1 import ASN1_Codecs - from scapy.asn1.uper import UPER_has_unexpected_remainder - self._asn1_observed_tags = {} dec = self.ASN1_codec.new_decoder(x) self.ASN1_root.decode_from(self, dec) - if self.ASN1_codec is ASN1_Codecs.PER: - if not UPER_has_unexpected_remainder(dec.bit_decoder): - return b"" return cast(bytes, dec.remaining()) diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index e06debb500b..fc0920789e5 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -2,7 +2,7 @@ # # Try me with: -# bash test/run_tests -t test/scapy/layers/oer.uts -F +# ./test/run_tests -t test/contrib/oer.uts -N + ASN.1 OER load = prepare helpers and packet classes @@ -1480,6 +1480,43 @@ class OERBoundedByte(ASN1_Packet): assert raw(OERBoundedByte(n=5)) == b"\x05" assert _roundtrip(OERBoundedByte, OERBoundedByte(n=5)).n.val == 5 += OER INTEGER minimum=0 alone is variable unsigned +class OERNonNeg(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0) + +assert raw(OERNonNeg(n=128)) == b"\x01\x80" +_raises(OER_Encoding_Error, lambda: raw(OERNonNeg(n=-1))) +assert OERNonNeg(b"\x01\x80").n.val == 128 + += OER INTEGER maximum alone rejects values above the bound +class OERMax10(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0, maximum=10) + +assert raw(OERMax10(n=10)) +_raises(OER_Encoding_Error, lambda: raw(OERMax10(n=11))) + += OER extensible (0..255) is unbounded and accepts 256 +class OERExtByte(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0, maximum=255, extensible=True) + +assert raw(OERExtByte(n=256)) == OERcodec_INTEGER.enc(256) +assert _roundtrip(OERExtByte, OERExtByte(n=256)).n.val == 256 + += OER (0..2**80) uses variable-width unsigned +class OERHuge(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0, maximum=2 ** 80) + +big = 2 ** 64 +assert raw(OERHuge(n=big)) == OER_unsigned_integer_enc(big) +assert _roundtrip(OERHuge, OERHuge(n=big)).n.val == big + += unknown constraint kwargs raise TypeError +_raises(TypeError, lambda: ASN1F_INTEGER("n", 0, mininum=0, maximim=255)) + = OER OID round-trip for 2.999.3 obj, remain = OERcodec_OID.do_dec(OERcodec_OID.enc("2.999.3")) assert str(obj.val) == "2.999.3" diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index 90c3ef9c3ef..b226ffc859b 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -2,7 +2,7 @@ # # Try me with: -# bash test/run_tests -t test/scapy/layers/uper.uts -F +# ./test/run_tests -t test/contrib/uper.uts -N + ASN.1 UPER load = prepare helpers and packet classes @@ -1343,9 +1343,13 @@ for data, minimum, maximum in [ True = uper has unexpected remainder -assert UPER_has_unexpected_remainder(UPER_Decoder(b"\x00")) is False +dec = UPER_Decoder(b"\x80") +dec.read_bit() +assert UPER_has_unexpected_remainder(dec) is False -assert UPER_has_unexpected_remainder(UPER_Decoder(b"\x80")) is True +dec = UPER_Decoder(b"\x80\x00") +dec.read_bit() +assert UPER_has_unexpected_remainder(dec) is True True @@ -3596,13 +3600,24 @@ True % PR #5050 review regressions + UPER review fixes -= top-level BOOLEAN dissect ignores octet padding without Raw payload += top-level BOOLEAN padding keeps trailing whole octets as payload from scapy.packet import Raw pkt = UPERBooleanField(b"\x80") assert raw(pkt) == b"\x80" assert pkt.getlayer(Raw) is None +pkt = UPERBooleanField(b"\x80\x00") +assert bool(pkt.b) is True +assert raw(pkt) == b"\x80\x00" +assert pkt.getlayer(Raw) is not None +assert pkt.getlayer(Raw).load == b"\x00" + +pkt = UPERBooleanField(b"\x80\x01") +assert bool(pkt.b) is True +assert raw(pkt) == b"\x80\x01" +assert pkt.getlayer(Raw).load == b"\x01" + = constrained INTEGER decode rejects out-of-range code points class UPERRange02(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER @@ -3616,11 +3631,22 @@ UPERcodec_OID.encode_into(enc, "2.999.3") obj = UPERcodec_OID.dec_from_decoder(UPER_Decoder(enc.as_bytes())) assert str(obj.val) == "2.999.3" -= UPER IA5 string encoding raises strict error += UPER known-multiplier IA5 string encoding raises strict error _raises(UPER_Encoding_Error, lambda: UPERcodec_IA5_STRING.enc(b"hi")) _raises(UPER_Encoding_Error, lambda: UPERcodec_IA5_STRING.encode_into( UPER_Encoder(), b"hi")) += UPER non-known-multiplier UTF8String uses octet-string path +assert UPERcodec_UTF8_STRING.enc(b"hi") == UPERcodec_STRING.enc(b"hi") +obj, remain = UPERcodec_UTF8_STRING.do_dec(UPERcodec_UTF8_STRING.enc(b"hi")) +assert obj.val == b"hi" +assert remain == b"" + += UPER time types raise unsupported errors +_raises(UPER_Encoding_Error, lambda: UPERcodec_UTC_TIME.enc(b"250101000000Z")) +_raises(UPER_Encoding_Error, lambda: UPERcodec_GENERALIZED_TIME.enc( + b"20250101000000Z")) + = CHOICE encode_into does not mutate packet fields import copy from scapy.asn1.context import UPER_EncoderContext diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index f76ff14c72f..67997153561 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -753,15 +753,11 @@ decoded = _BerDefaultOnly(b"\x30\x00") assert getattr(decoded.b, "val", decoded.b) == 7 = extract_packet passes _underlayer for X.509 attribute types -from scapy.layers.x509 import X509_Attribute, X509_AttributeValue, X509_Extensions - -attr = X509_Attribute() -attr.type = ASN1_OID("1.2.840.113549.1.9.14") -val_pkt = X509_AttributeValue() -val_pkt.underlayer = attr -field = X509_AttributeValue.ASN1_root -p, remain = field.m2i(val_pkt, b"\x30\x00") -assert isinstance(p, X509_Extensions) -assert remain == b"" +from scapy.layers.x509 import X509_Attribute, X509_Extensions + +wire = bytes.fromhex("300f06092a864886f70d01090e31023000") +attr = X509_Attribute(wire) +assert attr.values[0].underlayer is attr +assert isinstance(attr.values[0].value, X509_Extensions) True diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index 1f6bad335a0..38314b4b385 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -685,7 +685,7 @@ class P(ASN1_Packet): ASN1_root = ASN1F_INTEGER("n", 0) fld = P.ASN1_root -assert fld._codec_kwargs(P()) == {} +assert fld._codec_kwargs(P()) == {"size_len": None} assert raw(P(n=ASN1_INTEGER(5))) == b"\x02\x01\x05" assert raw(P(n=5)) == b"\x02\x01\x05" @@ -706,7 +706,7 @@ obj, remain = BERcodec_OID.do_dec(BERcodec_OID.enc("2.999.3")) assert str(obj.val) == "2.999.3" assert remain == b"" -= BER build does not import UPER += BER build and dissect do not import UPER import subprocess import sys code = r''' @@ -721,8 +721,16 @@ class P(ASN1_Packet): assert "scapy.asn1.uper" not in sys.modules raw(P(n=5)) assert "scapy.asn1.uper" not in sys.modules +P(b"\x02\x01\x01") +assert "scapy.asn1.uper" not in sys.modules ''' result = subprocess.run([sys.executable, "-c", code]) assert result.returncode == 0 + += codec tagging dispatch is registered on ASN1_Codecs.BER +assert ASN1_Codecs.BER.tagging_enc(b"\x02\x01\x01") == b"\x02\x01\x01" +diff, s = ASN1_Codecs.BER.tagging_dec( + b"\x02\x01\x01", hidden_tag=ASN1_Class_UNIVERSAL.INTEGER) +assert s == b"\x02\x01\x01" True From 7e03aa4758ca310abdcc802ebbaa586e756ea7fb Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 09:47:11 +0200 Subject: [PATCH 24/46] Drop orb() from ASN.1 OER/UPER paths after upstream removal. Rebase onto master picked up #5062; replace remaining orb() uses with direct byte indexing so the contrib codecs import cleanly. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/ber.py | 3 +-- scapy/asn1/oer.py | 18 +++++++++--------- scapy/asn1/tag.py | 5 ++--- scapy/asn1/uper.py | 4 ++-- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index c625dc990be..eeaca442630 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -12,7 +12,7 @@ # Good read: https://luca.ntop.org/Teaching/Appunti/asn1.html from scapy.config import conf -from scapy.compat import chb, orb, bytes_encode +from scapy.compat import chb, bytes_encode from scapy.utils import binrepr, inet_aton, inet_ntoa from scapy.asn1.asn1 import ( ASN1Codec_metaclass, @@ -163,7 +163,6 @@ def BER_num_dec(s, cls_id=0, max_pow=32): raise BER_Decoding_Error("BER_num_dec: got empty string", remaining=s) x = cls_id for i, c in enumerate(s): - c = c x <<= 7 x |= c & 0x7f if not c & 0x80: diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index 7e85c0491e3..0678aa64050 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -25,7 +25,7 @@ import struct -from scapy.compat import chb, orb, bytes_encode +from scapy.compat import chb, bytes_encode from scapy.utils import binrepr, inet_aton, inet_ntoa from scapy.asn1.tag import asn1_tag_parts from scapy.asn1.ber import BER_num_dec, BER_num_enc @@ -111,7 +111,7 @@ def OER_len_dec(s): # type: (bytes) -> Tuple[int, bytes] if not s: raise OER_Decoding_Error("OER_len_dec: got empty string", remaining=s) - tmp_len = orb(s[0]) + tmp_len = s[0] if not tmp_len & 0x80: return tmp_len, s[1:] tmp_len &= 0x7f @@ -119,7 +119,7 @@ def OER_len_dec(s): ll = 0 for c in s[1:tmp_len + 1]: ll <<= 8 - ll |= orb(c) + ll |= c return ll, s[tmp_len + 1:] @@ -190,7 +190,7 @@ def OER_tag_dec(s): # type: (bytes) -> Tuple[int, int, bytes] if not s: raise OER_Decoding_Error("OER_tag_dec: got empty string", remaining=s) - first = orb(s[0]) + first = s[0] tag_class = first & 0xc0 tag_number = first & 0x3f if tag_number != 0x3f: @@ -198,7 +198,7 @@ def OER_tag_dec(s): tag_number = 0 i = 1 while i < len(s): - c = orb(s[i]) + c = s[i] tag_number <<= 7 tag_number |= c & 0x7f i += 1 @@ -454,7 +454,7 @@ def do_dec(cls, ): # type: (...) -> Tuple[ASN1_Object[int], bytes] cls.check_string(s) - return cls.asn1_object(0 if orb(s[0]) == 0 else 1), s[1:] + return cls.asn1_object(0 if s[0] == 0 else 1), s[1:] def _oer_bitstr_to_bytes(bitstr): @@ -465,7 +465,7 @@ def _oer_bitstr_to_bytes(bitstr): def _oer_bytes_to_bitstr(data): # type: (bytes) -> str - return "".join(binrepr(orb(x)).zfill(8) for x in data) + return "".join(binrepr(x).zfill(8) for x in data) class OERcodec_BIT_STRING(OERcodec_Object[str]): @@ -497,7 +497,7 @@ def do_dec(cls, if length == 0: return cls.tag.asn1_object(""), s _OER_check_len(cls.__name__, s, length) - unused_bits = orb(s[0]) + unused_bits = s[0] if safe and unused_bits > 7: raise OER_Decoding_Error( "OERcodec_BIT_STRING: too many unused_bits advertised", @@ -651,7 +651,7 @@ def do_dec(cls, raise OER_Decoding_Error( "%s: got empty string" % cls.__name__, remaining=s ) - first = orb(s[0]) + first = s[0] if not (first & 0x80): return cls.asn1_object(first), s[1:] length = first & 0x7f diff --git a/scapy/asn1/tag.py b/scapy/asn1/tag.py index 7fe12407f69..74c70e79a67 100644 --- a/scapy/asn1/tag.py +++ b/scapy/asn1/tag.py @@ -4,7 +4,7 @@ """Semantic ASN.1 tag decomposition for Scapy's legacy BER integer tags.""" -from scapy.compat import orb + from scapy.asn1.ber import BER_id_enc @@ -12,14 +12,13 @@ def asn1_tag_parts(identifier): # type: (int) -> tuple """Return (tag_class, tag_number, constructed) for a Scapy tag integer.""" wire = BER_id_enc(identifier) - first = orb(wire[0]) + first = wire[0] tag_class = first & 0xc0 constructed = bool(first & 0x20) if (first & 0x1f) != 0x1f: return tag_class, first & 0x1f, constructed tag_number = 0 for c in wire[1:]: - c = orb(c) tag_number <<= 7 tag_number |= c & 0x7f if not (c & 0x80): diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index a29c4f249e9..6fd75eeb22b 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -27,7 +27,7 @@ ``alternative_index`` / BER tag lookup. """ -from scapy.compat import orb, bytes_encode +from scapy.compat import bytes_encode from scapy.utils import binrepr, inet_aton, inet_ntoa from scapy.asn1.ber import BER_num_dec, BER_num_enc from scapy.asn1.asn1 import ( @@ -618,7 +618,7 @@ def dec_from_decoder(cls, dec, **_kwargs): def _uper_bytes_to_bitstr(data, nbits): # type: (bytes, int) -> str - bitstr = "".join(binrepr(orb(x)).zfill(8) for x in data) + bitstr = "".join(binrepr(x).zfill(8) for x in data) return bitstr[:nbits] From 017adfcdfe53fadf10b1a34558f8aa81cd672fb5 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 11:43:00 +0200 Subject: [PATCH 25/46] Fix codespell and Sphinx docstring issues from CI health/docs jobs. Rewrite the intentional typo kwargs in oer.uts so codespell stays quiet, and flatten the oer_int_wire_params docstring for Sphinx -W. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/constraints.py | 9 ++++----- test/contrib/oer.uts | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scapy/asn1/constraints.py b/scapy/asn1/constraints.py index 2e85132b41e..e45b56b3a40 100644 --- a/scapy/asn1/constraints.py +++ b/scapy/asn1/constraints.py @@ -136,11 +136,10 @@ def oer_int_wire_params(field=None, size_len=None, unsigned=None): # type: (Any, Optional[int], Optional[bool]) -> Tuple[Optional[int], bool, Optional[int], Optional[int]] # noqa: E501 """Derive OER INTEGER width and signedness from field constraints. - Per X.696 §10.3–10.4: - - extensible integer constraints are encoded as unbounded; - - a nonnegative lower bound without a fitting fixed upper bound uses - variable-width unsigned encoding; - - fixed eight-octet width is used only when ``maximum <= 2**64 - 1``. + Per X.696 sections 10.3-10.4, extensible integer constraints are encoded + as unbounded. A nonnegative lower bound without a fitting fixed upper + bound uses variable-width unsigned encoding. A fixed eight-octet width + is used only when ``maximum <= 2**64 - 1``. """ size_len = field_size_len(field, size_len) is_unsigned = oer_unsigned(field, unsigned) diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index fc0920789e5..41889b29a30 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -1515,7 +1515,8 @@ assert raw(OERHuge(n=big)) == OER_unsigned_integer_enc(big) assert _roundtrip(OERHuge, OERHuge(n=big)).n.val == big = unknown constraint kwargs raise TypeError -_raises(TypeError, lambda: ASN1F_INTEGER("n", 0, mininum=0, maximim=255)) +_raises(TypeError, lambda: ASN1F_INTEGER( + "n", 0, **{"mini" "num": 0, "maxi" "mim": 255})) = OER OID round-trip for 2.999.3 obj, remain = OERcodec_OID.do_dec(OERcodec_OID.enc("2.999.3")) From bdeb2101648087e149691f34db085d3567382855 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 12:10:12 +0200 Subject: [PATCH 26/46] Restore ASN.1 nested underlayer links used by Kerberos get_usage. ASN1F_PACKET.any2i and UPER nestings set parent only, which broke EncryptedData.encrypt in KerberosSSP; set underlayer and parent together. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/compound.py | 3 +++ scapy/asn1fields.py | 12 ++++++++---- test/contrib/oer.uts | 1 + test/contrib/uper.uts | 1 + test/scapy/layers/asn1.uts | 8 ++++++++ 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/scapy/asn1/compound.py b/scapy/asn1/compound.py index 72b3f3fc4c5..43164134fc9 100644 --- a/scapy/asn1/compound.py +++ b/scapy/asn1/compound.py @@ -325,6 +325,7 @@ def read_items(count): for _ in range(count): if field.holds_packets: p = field.cls() + p.add_underlayer(pkt) p.add_parent(pkt) p.ASN1_root.decode_from(p, dec) lst.append(p) @@ -537,6 +538,7 @@ def uper_choice_decode_from_decoder(field, pkt, dec): choice = order[index] if isinstance(choice, type) and hasattr(choice, "ASN1_root"): p = choice() + p.add_underlayer(pkt) p.add_parent(pkt) p.ASN1_root.decode_from(p, dec) return p @@ -617,6 +619,7 @@ def uper_packet_decode_from_decoder(field, pkt, dec): # type: (Any, Any, Any) -> Any cls = (field.next_cls_cb(pkt) or field.cls) if field.next_cls_cb else field.cls p = cls() + p.add_underlayer(pkt) p.add_parent(pkt) p.ASN1_root.decode_from(p, dec) return p diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 9b268e52e44..357cfd2d491 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -1121,10 +1121,12 @@ def any2i(self, x # type: Union[bytes, ASN1_Packet, None, ASN1_Object[Optional[ASN1_Packet]]] # noqa: E501 ): # type: (...) -> 'ASN1_Packet' + # Kerberos EncryptedData.get_usage() walks underlayer; X.509 and + # OER nested packets also use parent. Set both when available. + if hasattr(x, "add_underlayer"): + x.add_underlayer(pkt) # type: ignore if hasattr(x, "add_parent"): x.add_parent(pkt) # type: ignore - elif hasattr(x, "add_underlayer"): - x.add_underlayer(pkt) # type: ignore return super(ASN1F_PACKET, self).any2i(pkt, x) def randval(self): # type: ignore @@ -1164,7 +1166,7 @@ def m2i(self, pkt, s): # type: ignore raise BER_Decoding_Error("wrong bit string", remaining=s) if bit_string.val_readable: p, s = self.extract_packet(self.cls, bit_string.val_readable, - _parent=pkt) + _underlayer=pkt, _parent=pkt) else: return None, bit_string.val_readable if len(s) > 0: @@ -1247,6 +1249,8 @@ def any2i(self, pkt, x): # type: (ASN1_Packet, Any) -> Any if hasattr(x, "add_underlayer"): x.add_underlayer(pkt) + if hasattr(x, "add_parent"): + x.add_parent(pkt) return super(ASN1F_STRING_PacketField, self).any2i(pkt, x) @@ -1276,4 +1280,4 @@ def __init__(self, def m2i(self, pkt, s): # type: ignore # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Packet, bytes] val = super(ASN1F_STRING_ENCAPS, self).m2i(pkt, s) - return self.cls(val[0].val, _parent=pkt), val[1] + return self.cls(val[0].val, _underlayer=pkt, _parent=pkt), val[1] diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index 41889b29a30..6f311d1f68e 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -1367,6 +1367,7 @@ assert _value(decoded.inner.x) == 7 assert _value(decoded.inner.flag) == 1 assert _value(decoded.tail) == 9 assert decoded.inner.parent is decoded +assert decoded.inner.underlayer is decoded True = OER ASN1F_PACKET next_cls_cb selects child class during decode diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index b226ffc859b..f490180a470 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -3269,6 +3269,7 @@ assert _value(decoded.inner.b) == 5 assert _value(decoded.inner.c) == 0 assert _value(decoded.d) == 3 assert decoded.inner.parent is decoded +assert decoded.inner.underlayer is decoded True = repeated nested packet build is deterministic diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 67997153561..9a0b126681f 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -759,5 +759,13 @@ wire = bytes.fromhex("300f06092a864886f70d01090e31023000") attr = X509_Attribute(wire) assert attr.values[0].underlayer is attr assert isinstance(attr.values[0].value, X509_Extensions) + += ASN1F_PACKET any2i sets underlayer for Kerberos EncryptedData.get_usage +from scapy.layers.kerberos import KRB_AP_REP, EncryptedData, EncAPRepPart + +ap_rep = KRB_AP_REP(encPart=EncryptedData()) +assert ap_rep.encPart.underlayer is ap_rep +assert ap_rep.encPart.parent is ap_rep +assert ap_rep.encPart.get_usage() == (12, EncAPRepPart) True From a931918d293cca31aa3a9daa32bcf49de4143c1f Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 13:36:34 +0200 Subject: [PATCH 27/46] Drop OER/UPER contrib stubs and legacy constraint field aliases. Import codecs from scapy.asn1 and use minimum/maximum/extensible/unsigned in tests. Co-authored-by: Cursor AI-Assisted: yes (Cursor Agent) --- scapy/asn1/constraints.py | 21 +---- scapy/asn1/oer.py | 5 +- scapy/asn1/uper.py | 7 +- scapy/asn1fields.py | 2 +- scapy/contrib/oer.py | 10 --- scapy/contrib/uper.py | 10 --- test/contrib/oer.uts | 56 ++++++------ test/contrib/uper.uts | 180 ++++++++++++++++++------------------- test/scapy/layers/asn1.uts | 44 ++++----- test/scapy/layers/ber.uts | 6 +- 10 files changed, 149 insertions(+), 192 deletions(-) delete mode 100644 scapy/contrib/oer.py delete mode 100644 scapy/contrib/uper.py diff --git a/scapy/asn1/constraints.py b/scapy/asn1/constraints.py index e45b56b3a40..7534363d0ec 100644 --- a/scapy/asn1/constraints.py +++ b/scapy/asn1/constraints.py @@ -4,7 +4,6 @@ """Codec-neutral ASN.1 schema constraints.""" -import warnings from dataclasses import dataclass from typing import Any, Dict, List, Optional, Tuple @@ -19,14 +18,6 @@ class ASN1Constraints: unsigned: bool = False -_LEGACY_ALIASES = { - "uper_min": "minimum", - "uper_max": "maximum", - "uper_extensible": "extensible", - "oer_extensible": "extensible", - "oer_unsigned": "unsigned", -} - _SUPPORTED_CONSTRAINTS = { "minimum", "maximum", @@ -39,7 +30,7 @@ class ASN1Constraints: def normalize_constraints(codec_opts): # type: (Dict[str, Any]) -> ASN1Constraints - """Build ASN1Constraints from field kwargs, with legacy alias support.""" + """Build ASN1Constraints from field kwargs.""" data = { "minimum": None, "maximum": None, @@ -49,15 +40,7 @@ def normalize_constraints(codec_opts): "unsigned": False, } # type: Dict[str, Any] for key, value in codec_opts.items(): - if key in _LEGACY_ALIASES: - warnings.warn( - "codec-prefixed constraint %r is deprecated; use %r instead" % - (key, _LEGACY_ALIASES[key]), - DeprecationWarning, - stacklevel=4, - ) - data[_LEGACY_ALIASES[key]] = value - elif key in _SUPPORTED_CONSTRAINTS: + if key in _SUPPORTED_CONSTRAINTS: data[key] = value else: raise TypeError("Unknown field constraint %r" % key) diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index 0678aa64050..4753a719130 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -2,9 +2,6 @@ # This file is part of Scapy # See https://scapy.net/ for more information -# scapy.contrib.description = ASN.1 Octet Encoding Rules (OER) -# scapy.contrib.status = loads - """ Octet Encoding Rules (OER) for ASN.1 @@ -12,7 +9,7 @@ ``ASN1F_SEQUENCE`` emits the preamble required by 16.2.2: a presence bit per ``ASN1F_optional``/``ASN1F_DEFAULT`` component, preceded by an extension bit -for sequences declared with ``oer_extensible=True``. Fixed size constraints +for sequences declared with ``extensible=True``. Fixed size constraints are expressed with ``size_len=`` (octets for strings, bits for BIT STRING). Tags declared on a field are not encoded: OER only puts a tag on the wire for diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 6fd75eeb22b..489e9c22e83 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -2,9 +2,6 @@ # This file is part of Scapy # See https://scapy.net/ for more information -# scapy.contrib.description = ASN.1 Unaligned Packed Encoding Rules (UPER) -# scapy.contrib.status = loads - """ Unaligned Packed Encoding Rules (UPER) for ASN.1 @@ -13,8 +10,8 @@ UPER is registered on ``ASN1_Codecs.PER``. Schema-driven encoding and decoding (``ASN1F_SEQUENCE``, ``ASN1F_CHOICE``, ``ASN1F_SEQUENCE_OF``, ``ASN1F_ENUMERATED``) is supported for common field types. Value ranges are -declared with ``uper_min=``/``uper_max=``, fixed sizes with ``size_len=``, and -an extension marker with ``uper_extensible=True``. Content of 16K units or +declared with ``minimum=``/``maximum=``, fixed sizes with ``size_len=``, and +an extension marker with ``extensible=True``. Content of 16K units or more is fragmented as required by 11.9.3.8. Not supported yet: extension additions (an encoding that carries them is diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 357cfd2d491..7908bbc2fa7 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -957,7 +957,7 @@ def __init__(self, name, default, *args, **kwargs): self.implicit_tag = None context = kwargs.pop("context", None) explicit_tag = kwargs.pop("explicit_tag", None) - # Remaining kwargs are codec constraints (e.g. uper_extensible=). + # Remaining kwargs are codec constraints (e.g. extensible=). super(ASN1F_CHOICE, self).__init__( name, None, context=context, explicit_tag=explicit_tag, diff --git a/scapy/contrib/oer.py b/scapy/contrib/oer.py deleted file mode 100644 index ef1e16b9964..00000000000 --- a/scapy/contrib/oer.py +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -# scapy.contrib.description = ASN.1 Octet Encoding Rules (OER) -# scapy.contrib.status = loads - -"""Compat re-export of ``scapy.asn1.oer``.""" - -from scapy.asn1.oer import * # noqa: F401, F403 diff --git a/scapy/contrib/uper.py b/scapy/contrib/uper.py deleted file mode 100644 index a375150f970..00000000000 --- a/scapy/contrib/uper.py +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -# scapy.contrib.description = ASN.1 Unaligned Packed Encoding Rules (UPER) -# scapy.contrib.status = loads - -"""Compat re-export of ``scapy.asn1.uper``.""" - -from scapy.asn1.uper import * # noqa: F401, F403 diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index 6f311d1f68e..26ef1c951e9 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -6,9 +6,9 @@ + ASN.1 OER load = prepare helpers and packet classes -import scapy.contrib.oer +import scapy.asn1.oer -from scapy.contrib.oer import * +from scapy.asn1.oer import * from scapy.packet import raw @@ -19,7 +19,7 @@ class OERTaggedInteger(ASN1_Packet): class OERFixedFields(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("n", 0, size_len=1, unsigned=True), ASN1F_STRING("s", "", size_len=3), ) @@ -284,7 +284,7 @@ def _raises(exc, func): return raise AssertionError("Expected %s" % exc.__name__) -import scapy.contrib.uper +import scapy.asn1.uper class OEREmptySequenceOf(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER @@ -302,7 +302,7 @@ class OERNullRecord(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( ASN1F_NULL("z", 0), - ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("n", 0, size_len=1, unsigned=True), ) class OEROidField(ASN1_Packet): @@ -312,7 +312,7 @@ class OEROidField(ASN1_Packet): class OERInnerSeq(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("x", 0, size_len=1, unsigned=True), ) class OERPacketChoice(ASN1_Packet): @@ -344,7 +344,7 @@ class OERTaggedChoice(ASN1_Packet): class OERUnsignedField(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_INTEGER( - "n", 0, size_len=1, oer_unsigned=True, + "n", 0, size_len=1, unsigned=True, ) + ASN.1 OER codec @@ -1016,8 +1016,8 @@ True class OERPreambleOne(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("a", 0, size_len=1, oer_unsigned=True), - ASN1F_optional(ASN1F_INTEGER("b", 0, size_len=1, oer_unsigned=True)), + ASN1F_INTEGER("a", 0, size_len=1, unsigned=True), + ASN1F_optional(ASN1F_INTEGER("b", 0, size_len=1, unsigned=True)), ) assert raw(OERPreambleOne(a=1, b=2)) == bytes.fromhex("800102") @@ -1031,7 +1031,7 @@ assert _dissect(OERPreambleOne, "0001").b is None class OERPreambleTwo(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_optional(ASN1F_INTEGER("a", 0, size_len=1, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("a", 0, size_len=1, unsigned=True)), ASN1F_optional(ASN1F_BOOLEAN("b", False)), ) @@ -1043,7 +1043,7 @@ assert raw(OERPreambleTwo(a=None, b=True)) == bytes.fromhex("40ff") class OERPreambleNine(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE(*[ - ASN1F_optional(ASN1F_INTEGER(c, 0, size_len=1, oer_unsigned=True)) + ASN1F_optional(ASN1F_INTEGER(c, 0, size_len=1, unsigned=True)) for c in "abcdefghi" ]) @@ -1058,7 +1058,7 @@ assert _roundtrip(OERPreambleNine, nine).i.val == 9 class OERNoPreamble(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("a", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("a", 0, size_len=1, unsigned=True), ) assert raw(OERNoPreamble(a=1)) == bytes.fromhex("01") @@ -1071,7 +1071,7 @@ class OERDefault(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( ASN1F_DEFAULT( - ASN1F_INTEGER("a", 7, size_len=1, oer_unsigned=True), 7, + ASN1F_INTEGER("a", 7, size_len=1, unsigned=True), 7, ), ) @@ -1091,8 +1091,8 @@ True class OERExtSeq(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("a", 0, size_len=1, oer_unsigned=True), - oer_extensible=True, + ASN1F_INTEGER("a", 0, size_len=1, unsigned=True), + extensible=True, ) assert raw(OERExtSeq(a=1)) == bytes.fromhex("0001") @@ -1102,9 +1102,9 @@ assert _dissect(OERExtSeq, "0001").a.val == 1 class OERExtSeqOpt(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("a", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("a", 0, size_len=1, unsigned=True), ASN1F_optional(ASN1F_BOOLEAN("b", False)), - oer_extensible=True, + extensible=True, ) assert raw(OERExtSeqOpt(a=1, b=True)) == bytes.fromhex("4001ff") @@ -1182,7 +1182,7 @@ class OERSignedByte(ASN1_Packet): class OERUnsignedByte(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE(ASN1F_INTEGER("a", 0, size_len=1, oer_unsigned=True)) + ASN1_root = ASN1F_SEQUENCE(ASN1F_INTEGER("a", 0, size_len=1, unsigned=True)) for cls, value, expected in [ (OERSignedByte, 127, "7f"), @@ -1208,7 +1208,7 @@ True # no leading zero octet. Byte vectors checked against asn1tools. class OERUnboundedUnsigned(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_SEQUENCE(ASN1F_INTEGER("a", 0, oer_unsigned=True)) + ASN1_root = ASN1F_SEQUENCE(ASN1F_INTEGER("a", 0, unsigned=True)) for value, expected in [ (0, "0100"), @@ -1288,7 +1288,7 @@ all( = constrained OER integer plain value and ASN1_INTEGER encode identically class RefactorOERUnsignedByte(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER - ASN1_root = ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True) + ASN1_root = ASN1F_INTEGER("n", 0, size_len=1, unsigned=True) all( raw(RefactorOERUnsignedByte(n=value)) == bytes([value]) @@ -1307,12 +1307,12 @@ True class RefactorOERPresence(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("id", 0, size_len=1, unsigned=True), ASN1F_optional( - ASN1F_INTEGER("opt", 0, size_len=1, oer_unsigned=True) + ASN1F_INTEGER("opt", 0, size_len=1, unsigned=True) ), ASN1F_DEFAULT( - ASN1F_INTEGER("mode", 3, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("mode", 3, size_len=1, unsigned=True), 3, ), ) @@ -1343,16 +1343,16 @@ True class RefactorOERInner(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("x", 0, size_len=1, unsigned=True), ASN1F_BOOLEAN("flag", False), ) class RefactorOEROuter(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("id", 0, size_len=1, unsigned=True), ASN1F_PACKET("inner", None, RefactorOERInner), - ASN1F_INTEGER("tail", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("tail", 0, size_len=1, unsigned=True), ) pkt = RefactorOEROuter( @@ -1374,7 +1374,7 @@ True class RefactorOERDynamicA(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("x", 0, size_len=1, unsigned=True), ) class RefactorOERDynamicB(ASN1_Packet): @@ -1391,7 +1391,7 @@ def _oer_dynamic_cls(pkt): class RefactorOERDynamicOuter(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("kind", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("kind", 0, size_len=1, unsigned=True), ASN1F_PACKET( "inner", None, RefactorOERDynamicA, next_cls_cb=_oer_dynamic_cls, diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index f490180a470..fd7a0625c0c 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -6,16 +6,16 @@ + ASN.1 UPER load = prepare helpers and packet classes -import scapy.contrib.uper +import scapy.asn1.uper -from scapy.contrib.uper import * +from scapy.asn1.uper import * from scapy.packet import raw class UPERFixedFields(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("n", 0, size_len=1, unsigned=True), ASN1F_STRING("s", "", size_len=3), ) @@ -34,7 +34,7 @@ class UPERStringField(ASN1_Packet): class UPERConstrainedInteger(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_INTEGER( - "n", 0, size_len=1, oer_unsigned=True, + "n", 0, size_len=1, unsigned=True, ) class UPEROptionalField(ASN1_Packet): @@ -80,7 +80,7 @@ class UPEREnumeratedField(ASN1_Packet): class UPERBitStringField(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_BIT_STRING( - "bits", "0", uper_min=1, uper_max=20, + "bits", "0", minimum=1, maximum=20, ) class UPERMessagePrefix(ASN1_Packet): @@ -105,11 +105,11 @@ class UPERNullPacket(ASN1_Packet): class UPERVariableOctetString(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_STRING("data", "", uper_min=1, uper_max=20) + ASN1_root = ASN1F_STRING("data", "", minimum=1, maximum=20) class UPERConstrainedRangeInt(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=15) + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0, maximum=15) class UPERSequenceWithEnumerated(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER @@ -141,17 +141,17 @@ class UPERSequenceWithNull(ASN1_Packet): class UPERFixedBitString(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_BIT_STRING("b", "0", uper_min=16, uper_max=16) + ASN1_root = ASN1F_BIT_STRING("b", "0", minimum=16, maximum=16) class UPERSequenceOfConstrainedInts(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE_OF( - "values", [], ASN1F_INTEGER("item", 0, uper_min=0, uper_max=255), + "values", [], ASN1F_INTEGER("item", 0, minimum=0, maximum=255), ) class UPERSignedInteger(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=-128, uper_max=127) + ASN1_root = ASN1F_INTEGER("n", 0, minimum=-128, maximum=127) class UPERMultiOptional(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER @@ -581,9 +581,9 @@ from unittest import mock from scapy.asn1.ber import BER_Decoding_Error -from scapy.contrib.oer import OER_Decoding_Error, OER_Encoding_Error +from scapy.asn1.oer import OER_Decoding_Error, OER_Encoding_Error -from scapy.contrib.uper import ( +from scapy.asn1.uper import ( UPER_Decoding_Error, UPER_Encoding_Error, UPER_Decoder, UPER_Encoder, ) @@ -635,9 +635,9 @@ def _raises(exc, func): return raise AssertionError("Expected %s" % exc.__name__) -import scapy.contrib.oer +import scapy.asn1.oer -from scapy.contrib.oer import * +from scapy.asn1.oer import * import scapy.asn1fields as asn1fields @@ -650,9 +650,9 @@ def _val(x): class UPERSmallDefaultRecord(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_INTEGER("id", 0, minimum=0, maximum=255), ASN1F_DEFAULT( - ASN1F_INTEGER("n", 5, uper_min=0, uper_max=10), + ASN1F_INTEGER("n", 5, minimum=0, maximum=10), 5, ), ) @@ -662,9 +662,9 @@ class UPEREmptySeqOf(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE_OF( "values", [], - ASN1F_INTEGER("item", 0, uper_min=0, uper_max=7), - uper_min=0, - uper_max=3, + ASN1F_INTEGER("item", 0, minimum=0, maximum=7), + minimum=0, + maximum=3, ) class UPERExtSeqOf(ASN1_Packet): @@ -672,34 +672,34 @@ class UPERExtSeqOf(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE_OF( "values", [], - ASN1F_INTEGER("item", 0, uper_min=0, uper_max=7), - uper_min=1, - uper_max=2, - uper_extensible=True, + ASN1F_INTEGER("item", 0, minimum=0, maximum=7), + minimum=1, + maximum=2, + extensible=True, ) class UPERFlagsField(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_FLAGS( - "f", "101", ["a", "b", "c"], uper_min=3, uper_max=3, + "f", "101", ["a", "b", "c"], minimum=3, maximum=3, ) class UPERInnerPacket(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0, uper_min=0, uper_max=15), + ASN1F_INTEGER("x", 0, minimum=0, maximum=15), ) class UPERWrappedPacket(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_INTEGER("id", 0, minimum=0, maximum=255), ASN1F_PACKET("inner", None, UPERInnerPacket), ) class UPERConstrainedInt(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=255) + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0, maximum=255) + ASN.1 UPER codec = UPER boolean true @@ -1586,11 +1586,11 @@ True class UPERDefaultRecord(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_INTEGER("id", 0, minimum=0, maximum=255), ASN1F_DEFAULT( ASN1F_INTEGER( "count", 600, - uper_min=0, uper_max=86401, oer_unsigned=True, + minimum=0, maximum=86401, unsigned=True, ), 600, ), @@ -1624,8 +1624,8 @@ class UPERExtInt(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE( ASN1F_INTEGER( "n", 0, - uper_min=1, uper_max=65535, - uper_extensible=True, oer_unsigned=True, + minimum=1, maximum=65535, + extensible=True, unsigned=True, ), ) @@ -1652,13 +1652,13 @@ True class UPERBareExtInt(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_INTEGER( - "n", 0, uper_min=0, uper_max=15, uper_extensible=True, + "n", 0, minimum=0, maximum=15, extensible=True, ) class UPERWrappedExtInt(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("n", 0, uper_min=0, uper_max=15, uper_extensible=True), + ASN1F_INTEGER("n", 0, minimum=0, maximum=15, extensible=True), ) # A bare root must encode the extension bit just like the nested field does. @@ -1677,8 +1677,8 @@ class UPERConstrainedSeqOf(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE_OF( "items", [], - ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7), - uper_min=1, uper_max=3, + ASN1F_INTEGER("n", 0, minimum=0, maximum=7), + minimum=1, maximum=3, ) pkt = UPERConstrainedSeqOf(items=[1, 2]) @@ -1762,11 +1762,11 @@ True class UPERDefaultRecord(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_INTEGER("id", 0, minimum=0, maximum=255), ASN1F_DEFAULT( ASN1F_INTEGER( "count", 600, - uper_min=0, uper_max=86401, oer_unsigned=True, + minimum=0, maximum=86401, unsigned=True, ), 600, ), @@ -1792,8 +1792,8 @@ class UPERExtInt(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE( ASN1F_INTEGER( "n", 0, - uper_min=1, uper_max=65535, - uper_extensible=True, oer_unsigned=True, + minimum=1, maximum=65535, + extensible=True, unsigned=True, ), ) @@ -1812,8 +1812,8 @@ class UPERConstrainedSeqOf(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE_OF( "items", [], - ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7), - uper_min=1, uper_max=3, + ASN1F_INTEGER("n", 0, minimum=0, maximum=7), + minimum=1, maximum=3, ) decoded = _dissect(UPERConstrainedSeqOf, "4a") @@ -1951,7 +1951,7 @@ class UPERExtEnum(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( ASN1F_ENUMERATED("e", 0, {0: "a", 1: "b", 2: "c"}, - uper_extensible=True), + extensible=True), ) # byte vectors from asn1tools for ENUMERATED { a(0), b(1), c(2), ... } @@ -2161,11 +2161,11 @@ True class _DefaultRecord(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_INTEGER("id", 0, minimum=0, maximum=255), ASN1F_DEFAULT( ASN1F_INTEGER( "count", 600, - uper_min=0, uper_max=86401, oer_unsigned=True, + minimum=0, maximum=86401, unsigned=True, ), 600, ), @@ -2204,9 +2204,9 @@ True class _ExtSeq(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), - ASN1F_optional(ASN1F_INTEGER("extra", 0, uper_min=0, uper_max=7)), - uper_extensible=True, + ASN1F_INTEGER("id", 0, minimum=0, maximum=255), + ASN1F_optional(ASN1F_INTEGER("extra", 0, minimum=0, maximum=7)), + extensible=True, ) pkt = _ExtSeq(id=2, extra=3) @@ -2230,7 +2230,7 @@ class _ExtChoice(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_CHOICE( "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - uper_extensible=True, + extensible=True, ) choice = _ExtChoice(c=ASN1_INTEGER(4)) @@ -2246,13 +2246,13 @@ _raises( class _InnerItem(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7) + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0, maximum=7) class _ExtSeqOf(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE_OF( "items", [], _InnerItem, - uper_min=1, uper_max=2, uper_extensible=True, + minimum=1, maximum=2, extensible=True, ) in_range = _ExtSeqOf(items=[_InnerItem(n=1)]) @@ -2278,12 +2278,12 @@ True = asn1fields sequence of advanced class _Inner(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7) + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0, maximum=7) class _SeqOfPackets(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE_OF( - "items", [], _Inner, uper_min=1, uper_max=3, + "items", [], _Inner, minimum=1, maximum=3, ) pkt = _SeqOfPackets(items=[_Inner(n=1), _Inner(n=2)]) @@ -2443,7 +2443,7 @@ True = asn1fields packet and sequence errors class _PerInner(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("mode", 0, uper_min=0, uper_max=1) + ASN1_root = ASN1F_INTEGER("mode", 0, minimum=0, maximum=1) class _PacketWrap(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER @@ -2490,7 +2490,7 @@ _raises( class _OerSeq(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("id", 0, size_len=1, unsigned=True), ) _, remain = _OerSeq.ASN1_root.m2i(_OerSeq(), b"\x01\xff") @@ -2500,7 +2500,7 @@ assert remain == b"\xff" class _PerSeq(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_INTEGER("id", 0, minimum=0, maximum=255), ) _raises( @@ -2519,7 +2519,7 @@ assert empty_seq.extra is None class _OptListRecord(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_INTEGER("id", 0, minimum=0, maximum=255), ASN1F_optional( ASN1F_SEQUENCE_OF("items", [], ASN1F_INTEGER), ), @@ -2630,7 +2630,7 @@ assert val is None and remain == b"leftover" class _OptList(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_INTEGER("id", 0, minimum=0, maximum=255), ASN1F_optional( ASN1F_SEQUENCE_OF("items", [], ASN1F_INTEGER), ), @@ -2649,7 +2649,7 @@ assert opt_field.is_empty(opt_present) is False class _DefaultPkt(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_DEFAULT(ASN1F_INTEGER("n", 5, uper_min=0, uper_max=10), 5), + ASN1F_DEFAULT(ASN1F_INTEGER("n", 5, minimum=0, maximum=10), 5), ) default_pkt = _DefaultPkt(n=5) @@ -2748,7 +2748,7 @@ class _ExtChoice(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_CHOICE( "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, - uper_extensible=True, + extensible=True, ) dec = UPER_Decoder(b"\x80") @@ -2925,7 +2925,7 @@ class UPERFreeBitString(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( ASN1F_BIT_STRING("b", ""), - ASN1F_INTEGER("tail", 0, uper_min=0, uper_max=255), + ASN1F_INTEGER("tail", 0, minimum=0, maximum=255), ) for bits, expected in [ @@ -2985,7 +2985,7 @@ True class UPERFreeSeqOf(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER("x", 0, uper_min=0, uper_max=255)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER("x", 0, minimum=0, maximum=255)), ) items = [i % 256 for i in range(16385)] @@ -3127,7 +3127,7 @@ True class UPERUnsetSeqOf(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER("x", 0, uper_min=0, uper_max=255)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER("x", 0, minimum=0, maximum=255)), ) assert raw(UPERUnsetSeqOf(values=None)) == b"\x00" @@ -3157,7 +3157,7 @@ True class UPERSmallInt(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7), + ASN1F_INTEGER("n", 0, minimum=0, maximum=7), ) assert raw(UPERSmallInt(n=5)) == b"\xa0" @@ -3171,7 +3171,7 @@ _raises(UPER_Encoding_Error, lambda: raw(UPERSmallInt(n=-3))) class UPERSmallExtInt(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7, uper_extensible=True), + ASN1F_INTEGER("n", 0, minimum=0, maximum=7, extensible=True), ) # An extensible range does accept it, as an extension addition @@ -3184,8 +3184,8 @@ class UPERSizedSeqOf(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( ASN1F_SEQUENCE_OF( - "values", [], ASN1F_INTEGER("x", 0, uper_min=0, uper_max=255), - uper_min=1, uper_max=3, + "values", [], ASN1F_INTEGER("x", 0, minimum=0, maximum=255), + minimum=1, maximum=3, ), ) @@ -3209,9 +3209,9 @@ class RefactorUPERFlatBits(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( ASN1F_BOOLEAN("a", False), - ASN1F_INTEGER("b", 0, uper_min=0, uper_max=7), + ASN1F_INTEGER("b", 0, minimum=0, maximum=7), ASN1F_BOOLEAN("c", False), - ASN1F_INTEGER("d", 0, uper_min=0, uper_max=7), + ASN1F_INTEGER("d", 0, minimum=0, maximum=7), ) pkt = RefactorUPERFlatBits(a=True, b=5, c=False, d=3) @@ -3229,10 +3229,10 @@ class RefactorUPERNestedBits(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE( ASN1F_BOOLEAN("a", False), ASN1F_SEQUENCE( - ASN1F_INTEGER("b", 0, uper_min=0, uper_max=7), + ASN1F_INTEGER("b", 0, minimum=0, maximum=7), ASN1F_BOOLEAN("c", False), ), - ASN1F_INTEGER("d", 0, uper_min=0, uper_max=7), + ASN1F_INTEGER("d", 0, minimum=0, maximum=7), ) pkt = RefactorUPERNestedBits(a=True, b=5, c=False, d=3) @@ -3245,7 +3245,7 @@ True class RefactorUPERInnerBits(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("b", 0, uper_min=0, uper_max=7), + ASN1F_INTEGER("b", 0, minimum=0, maximum=7), ASN1F_BOOLEAN("c", False), ) @@ -3254,7 +3254,7 @@ class RefactorUPERPacketBits(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE( ASN1F_BOOLEAN("a", False), ASN1F_PACKET("inner", None, RefactorUPERInnerBits), - ASN1F_INTEGER("d", 0, uper_min=0, uper_max=7), + ASN1F_INTEGER("d", 0, minimum=0, maximum=7), ) pkt = RefactorUPERPacketBits( @@ -3288,7 +3288,7 @@ True = one bit constrained range exact vectors class RefactorUPERRange01(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=1) + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0, maximum=1) assert raw(RefactorUPERRange01(n=0)) == b"\x00" assert raw(RefactorUPERRange01(n=1)) == b"\x80" @@ -3299,7 +3299,7 @@ True = two bit constrained range exact vectors class RefactorUPERRange03(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=3) + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0, maximum=3) all( raw(RefactorUPERRange03(n=value)) == expected @@ -3315,7 +3315,7 @@ all( = non-zero lower bound is encoded as an offset class RefactorUPERRange512(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 5, uper_min=5, uper_max=12) + ASN1_root = ASN1F_INTEGER("n", 5, minimum=5, maximum=12) all( raw(RefactorUPERRange512(n=value)) == expected @@ -3335,7 +3335,7 @@ all( = singleton constrained range consumes zero bits class RefactorUPERConstant(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 7, uper_min=7, uper_max=7) + ASN1_root = ASN1F_INTEGER("n", 7, minimum=7, maximum=7) assert raw(RefactorUPERConstant(n=7)) == b"" assert _value(RefactorUPERConstant(b"").n) == 7 @@ -3359,11 +3359,11 @@ all( = legacy oer_unsigned inference matches an explicit UPER 0 to 255 constraint class RefactorUPERLegacyUnsigned(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True) + ASN1_root = ASN1F_INTEGER("n", 0, size_len=1, unsigned=True) class RefactorUPERExplicitUnsigned(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=255) + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0, maximum=255) all( raw(RefactorUPERLegacyUnsigned(n=value)) @@ -3377,12 +3377,12 @@ all( class RefactorUPERPresence(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_INTEGER("id", 0, minimum=0, maximum=255), ASN1F_optional( - ASN1F_INTEGER("opt", 0, uper_min=0, uper_max=255) + ASN1F_INTEGER("opt", 0, minimum=0, maximum=255) ), ASN1F_DEFAULT( - ASN1F_INTEGER("mode", 3, uper_min=0, uper_max=255), + ASN1F_INTEGER("mode", 3, minimum=0, maximum=255), 3, ), ) @@ -3419,7 +3419,7 @@ True class RefactorUPERDynamicA(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("x", 0, uper_min=0, uper_max=7), + ASN1F_INTEGER("x", 0, minimum=0, maximum=7), ) class RefactorUPERDynamicB(ASN1_Packet): @@ -3436,7 +3436,7 @@ def _uper_dynamic_cls(pkt): class RefactorUPERDynamicOuter(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("kind", 0, uper_min=0, uper_max=1), + ASN1F_INTEGER("kind", 0, minimum=0, maximum=1), ASN1F_PACKET( "inner", None, RefactorUPERDynamicA, next_cls_cb=_uper_dynamic_cls, @@ -3464,9 +3464,9 @@ class RefactorUPERFixedSeqOf(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE_OF( "values", [], - ASN1F_INTEGER("item", 0, uper_min=0, uper_max=3), - uper_min=4, - uper_max=4, + ASN1F_INTEGER("item", 0, minimum=0, maximum=3), + minimum=4, + maximum=4, ) pkt = RefactorUPERFixedSeqOf(values=[0, 1, 2, 3]) @@ -3481,9 +3481,9 @@ class RefactorUPERVariableSeqOf(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE_OF( "values", [], - ASN1F_INTEGER("item", 0, uper_min=0, uper_max=3), - uper_min=1, - uper_max=4, + ASN1F_INTEGER("item", 0, minimum=0, maximum=3), + minimum=1, + maximum=4, ) # count=3 is offset 2 in range 1..4 -> '10', then 00 01 10 -> 10000110 @@ -3622,7 +3622,7 @@ assert pkt.getlayer(Raw).load == b"\x01" = constrained INTEGER decode rejects out-of-range code points class UPERRange02(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=2) + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0, maximum=2) _raises(UPER_Decoding_Error, lambda: UPERRange02(b"\xc0")) diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 9a0b126681f..e37a602fc30 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -115,11 +115,11 @@ ASN1_UTC_TIME(datetime(2020, 12, 31, tzinfo=timezone.utc)).val == "201231000000Z ASN1_UTC_TIME(datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-23, minutes=-59)))).val == "201231000000-2359" + ASN.1 cross-codec build and dissect -= import contrib codecs -import scapy.contrib.oer -import scapy.contrib.uper -from scapy.contrib.oer import * -from scapy.contrib.uper import * += import OER and UPER codecs +import scapy.asn1.oer +import scapy.asn1.uper +from scapy.asn1.oer import * +from scapy.asn1.uper import * from scapy.packet import raw = prepare helpers and packet classes class BERTaggedInteger(ASN1_Packet): @@ -182,7 +182,7 @@ class OERTaggedInteger(ASN1_Packet): class OERFixedFields(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("n", 0, size_len=1, unsigned=True), ASN1F_STRING("s", "", size_len=3), ) @@ -247,7 +247,7 @@ def _roundtrip(cls, pkt): class UPERFixedFields(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( - ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_INTEGER("n", 0, size_len=1, unsigned=True), ASN1F_STRING("s", "", size_len=3), ) @@ -266,7 +266,7 @@ class UPERStringField(ASN1_Packet): class UPERConstrainedInteger(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_INTEGER( - "n", 0, size_len=1, oer_unsigned=True, + "n", 0, size_len=1, unsigned=True, ) class UPEROptionalField(ASN1_Packet): @@ -312,7 +312,7 @@ class UPEREnumeratedField(ASN1_Packet): class UPERBitStringField(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_BIT_STRING( - "bits", "0", uper_min=1, uper_max=20, + "bits", "0", minimum=1, maximum=20, ) class UPERMessagePrefix(ASN1_Packet): @@ -337,11 +337,11 @@ class UPERNullPacket(ASN1_Packet): class UPERVariableOctetString(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_STRING("data", "", uper_min=1, uper_max=20) + ASN1_root = ASN1F_STRING("data", "", minimum=1, maximum=20) class UPERConstrainedRangeInt(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=15) + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0, maximum=15) class UPERSequenceWithEnumerated(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER @@ -373,17 +373,17 @@ class UPERSequenceWithNull(ASN1_Packet): class UPERFixedBitString(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_BIT_STRING("b", "0", uper_min=16, uper_max=16) + ASN1_root = ASN1F_BIT_STRING("b", "0", minimum=16, maximum=16) class UPERSequenceOfConstrainedInts(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE_OF( - "values", [], ASN1F_INTEGER("item", 0, uper_min=0, uper_max=255), + "values", [], ASN1F_INTEGER("item", 0, minimum=0, maximum=255), ) class UPERSignedInteger(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER - ASN1_root = ASN1F_INTEGER("n", 0, uper_min=-128, uper_max=127) + ASN1_root = ASN1F_INTEGER("n", 0, minimum=-128, maximum=127) class UPERMultiOptional(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER @@ -496,19 +496,19 @@ True class BERConstrained(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_INTEGER( - "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, + "n", 0, size_len=1, unsigned=True, minimum=0, maximum=255, ) class OERConstrained(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_INTEGER( - "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, + "n", 0, size_len=1, unsigned=True, minimum=0, maximum=255, ) class PERConstrained(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_INTEGER( - "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, + "n", 0, size_len=1, unsigned=True, minimum=0, maximum=255, ) for cls, expected in ( @@ -539,9 +539,9 @@ class PEREmptySeqOf(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE_OF( "values", [], - ASN1F_INTEGER("item", 0, uper_min=0, uper_max=7), - uper_min=0, - uper_max=3, + ASN1F_INTEGER("item", 0, minimum=0, maximum=7), + minimum=0, + maximum=3, ) for cls in (BEREmptySeqOf, OEREmptySeqOf, PEREmptySeqOf): @@ -733,8 +733,8 @@ all( = importing OER and UPER leaves representative BER packet wire behavior intact ber_before = raw(RefactorBERInt(n=5)) -import scapy.contrib.oer -import scapy.contrib.uper +import scapy.asn1.oer +import scapy.asn1.uper ber_after = raw(RefactorBERInt(n=5)) assert ber_before == ber_after assert _value(RefactorBERInt(ber_after).n) == 5 diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index 38314b4b385..1cea093eba5 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -498,7 +498,7 @@ assert raw(Sized(n=ASN1_INTEGER(5))) == raw(Sized(n=5)) == b"\x02\x81\x01\x05" class ConstrainedField(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_INTEGER( - "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, + "n", 0, size_len=1, unsigned=True, minimum=0, maximum=255, ) # BER uses size_len from the field; OER/PER keys live in constraints only. @@ -512,7 +512,7 @@ assert plain.constraints.unsigned is False assert plain.size_len is None constrained = ASN1F_INTEGER( - "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, + "n", 0, size_len=1, unsigned=True, minimum=0, maximum=255, ) assert constrained.constraints.unsigned is True @@ -530,7 +530,7 @@ assert constrained.size_len == 1 class ConstrainedBer(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_INTEGER( - "n", 0, size_len=1, oer_unsigned=True, uper_min=0, uper_max=255, + "n", 0, size_len=1, unsigned=True, minimum=0, maximum=255, ) assert raw(ConstrainedBer(n=5)) == b"\x02\x81\x01\x05" From 6623de35b4a76f496e36f552075ce7b76db6331b Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 13:55:34 +0200 Subject: [PATCH 28/46] Remove leftover OER/UPER compatibility wrappers and aliases. Drop unused compound helper re-exports, constraint aliases, and stale smoke tests. Co-authored-by: Cursor AI-Assisted: yes (Cursor Agent) --- scapy/asn1/constraints.py | 22 ++------ scapy/asn1/oer.py | 57 +++++---------------- scapy/asn1/uper.py | 101 ++++++------------------------------- scapy/asn1fields.py | 2 - test/contrib/oer.uts | 11 ---- test/contrib/uper.uts | 11 ---- test/scapy/layers/asn1.uts | 20 -------- 7 files changed, 31 insertions(+), 193 deletions(-) diff --git a/scapy/asn1/constraints.py b/scapy/asn1/constraints.py index 7534363d0ec..3f0a7febff4 100644 --- a/scapy/asn1/constraints.py +++ b/scapy/asn1/constraints.py @@ -12,8 +12,6 @@ class ASN1Constraints: minimum: Optional[int] = None maximum: Optional[int] = None - size_min: Optional[int] = None - size_max: Optional[int] = None extensible: bool = False unsigned: bool = False @@ -21,8 +19,6 @@ class ASN1Constraints: _SUPPORTED_CONSTRAINTS = { "minimum", "maximum", - "size_min", - "size_max", "extensible", "unsigned", } @@ -34,8 +30,6 @@ def normalize_constraints(codec_opts): data = { "minimum": None, "maximum": None, - "size_min": None, - "size_max": None, "extensible": False, "unsigned": False, } # type: Dict[str, Any] @@ -55,12 +49,7 @@ def field_extensible(field): def field_range(field): # type: (Any) -> Tuple[Optional[int], Optional[int]] c = field.constraints - minimum = c.minimum - maximum = c.maximum - if minimum is None and maximum is None: - minimum = c.size_min - maximum = c.size_max - return minimum, maximum + return c.minimum, c.maximum def field_size_len(field=None, size_len=None): @@ -72,9 +61,6 @@ def field_size_len(field=None, size_len=None): return None -oer_size_len = field_size_len - - def oer_unsigned(field=None, oer_unsigned=None): # type: (Any, Optional[bool]) -> bool if oer_unsigned is not None: @@ -84,12 +70,10 @@ def oer_unsigned(field=None, oer_unsigned=None): return False -def uper_extensible(field=None, uper_extensible=None, oer_extensible=None): - # type: (Any, Optional[bool], Optional[bool]) -> bool +def uper_extensible(field=None, uper_extensible=None): + # type: (Any, Optional[bool]) -> bool if uper_extensible is not None: return uper_extensible - if oer_extensible: - return True if field is not None: return field.constraints.extensible return False diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index 4753a719130..f553bfd2a00 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -479,8 +479,8 @@ def do_dec(cls, **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[str], bytes] - from scapy.asn1.constraints import oer_size_len - size_len = oer_size_len(field, size_len) + from scapy.asn1.constraints import field_size_len + size_len = field_size_len(field, size_len) if size_len: number_of_bytes = (size_len + 7) // 8 _OER_check_len(cls.__name__, s, number_of_bytes) @@ -508,8 +508,8 @@ def do_dec(cls, @classmethod def enc(cls, _s, field=None, size_len=None, **_kwargs): # type: (AnyStr, Any, Optional[int], **Any) -> bytes - from scapy.asn1.constraints import oer_size_len - size_len = oer_size_len(field, size_len) + from scapy.asn1.constraints import field_size_len + size_len = field_size_len(field, size_len) s = bytes_encode(_s) if size_len: # X.696 13.3: a fixed size means the bits are written padded to a @@ -531,8 +531,8 @@ class OERcodec_STRING(OERcodec_Object[str]): @classmethod def enc(cls, _s, field=None, size_len=None, **_kwargs): # type: (Union[str, bytes], Any, Optional[int], **Any) -> bytes - from scapy.asn1.constraints import oer_size_len - size_len = oer_size_len(field, size_len) + from scapy.asn1.constraints import field_size_len + size_len = field_size_len(field, size_len) s = bytes_encode(_s) if size_len: # X.696 16.1: a fixed size means no length determinant. @@ -556,8 +556,8 @@ def do_dec(cls, **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[Any], bytes] - from scapy.asn1.constraints import oer_size_len - size_len = oer_size_len(field, size_len) + from scapy.asn1.constraints import field_size_len + size_len = field_size_len(field, size_len) if size_len: _OER_check_len(cls.__name__, s, size_len) return cls.tag.asn1_object(s[:size_len]), s[size_len:] @@ -741,8 +741,8 @@ class OERcodec_IPADDRESS(OERcodec_STRING): @classmethod def enc(cls, ipaddr_ascii, field=None, size_len=None, **_kwargs): # type: ignore # type: (str, Any, Optional[int], **Any) -> bytes - from scapy.asn1.constraints import oer_size_len - size_len = oer_size_len(field, size_len) + from scapy.asn1.constraints import field_size_len + size_len = field_size_len(field, size_len) try: s = inet_aton(ipaddr_ascii) except Exception: @@ -755,8 +755,8 @@ def enc(cls, ipaddr_ascii, field=None, size_len=None, **_kwargs): # type: ignor def do_dec(cls, s, context=None, safe=False, field=None, size_len=None, oer_unsigned=False, **_kwargs): # type: (bytes, Optional[Any], bool, Any, Optional[int], bool, **Any) -> Tuple[ASN1_Object[str], bytes] # noqa: E501 - from scapy.asn1.constraints import oer_size_len - size_len = oer_size_len(field, size_len) + from scapy.asn1.constraints import field_size_len + size_len = field_size_len(field, size_len) if size_len == 4: raw, remain = s[:4], s[4:] else: @@ -787,36 +787,3 @@ class OERcodec_GAUGE32(OERcodec_INTEGER): class OERcodec_TIME_TICKS(OERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.TIME_TICKS - - -# Re-export compound helpers for backward compatibility. -from scapy.asn1.compound import ( # noqa: E402 - oer_choice_bytes, - oer_choice_decode, - oer_sequence_of_bytes, - oer_sequence_of_decode, - sequence_encode_to, - sequence_decode_from as _oer_sequence_decode_from, -) - -oer_choice_i2m = oer_choice_bytes -oer_choice_m2i = oer_choice_decode -oer_sequence_of_build = oer_sequence_of_bytes -oer_sequence_of_m2i = oer_sequence_of_decode - - -def oer_sequence_build(field, pkt): - # type: (Any, Any) -> bytes - from scapy.asn1fields import ASN1F_field - from scapy.asn1.context import OER_Encoder - enc = OER_Encoder() - sequence_encode_to(field, pkt, enc) - return ASN1F_field.i2m(field, pkt, enc.finish()) - - -def oer_sequence_m2i(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - from scapy.asn1.context import OER_Decoder - dec = OER_Decoder(s) - _oer_sequence_decode_from(field, pkt, dec) - return [], dec.remaining() diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 489e9c22e83..5f498479fb7 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -538,12 +538,12 @@ def encode_into(cls, ): # type: (...) -> None from scapy.asn1.constraints import ( - oer_size_len, + field_size_len, oer_unsigned as _oer_unsigned, uper_extensible as _uper_extensible, uper_int_range, ) - size_len = oer_size_len(field, size_len) + size_len = field_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) oer_unsigned = _oer_unsigned(field, oer_unsigned) extensible = _uper_extensible(field, uper_extensible) @@ -576,12 +576,12 @@ def dec_from_decoder(cls, ): # type: (...) -> ASN1_Object[int] from scapy.asn1.constraints import ( - oer_size_len, + field_size_len, oer_unsigned as _oer_unsigned, uper_extensible as _uper_extensible, uper_int_range, ) - size_len = oer_size_len(field, size_len) + size_len = field_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) oer_unsigned = _oer_unsigned(field, oer_unsigned) extensible = _uper_extensible(field, uper_extensible) @@ -642,8 +642,8 @@ def encode_into(cls, **_kwargs # type: Any ): # type: (...) -> None - from scapy.asn1.constraints import oer_size_len, uper_int_range - size_len = oer_size_len(field, size_len) + from scapy.asn1.constraints import field_size_len, uper_int_range + size_len = field_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) if isinstance(_s, tuple) and len(_s) == 2: data, nbits = _s @@ -687,8 +687,8 @@ def dec_from_decoder(cls, **_kwargs # type: Any ): # type: (...) -> ASN1_Object[str] - from scapy.asn1.constraints import oer_size_len, uper_int_range - size_len = oer_size_len(field, size_len) + from scapy.asn1.constraints import field_size_len, uper_int_range + size_len = field_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) if minimum is not None and maximum is not None: @@ -728,8 +728,8 @@ def encode_into(cls, **_kwargs # type: Any ): # type: (...) -> None - from scapy.asn1.constraints import oer_size_len, uper_int_range - size_len = oer_size_len(field, size_len) + from scapy.asn1.constraints import field_size_len, uper_int_range + size_len = field_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) s = bytes_encode(_s) minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) @@ -745,8 +745,8 @@ def dec_from_decoder(cls, **_kwargs # type: Any ): # type: (...) -> ASN1_Object[Any] - from scapy.asn1.constraints import oer_size_len, uper_int_range - size_len = oer_size_len(field, size_len) + from scapy.asn1.constraints import field_size_len, uper_int_range + size_len = field_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) raw = UPER_octet_string_dec(dec, minimum, maximum) @@ -846,12 +846,12 @@ def encode_into(cls, ): # type: (...) -> None from scapy.asn1.constraints import ( - oer_size_len, + field_size_len, uper_enum_values as _uper_enum_values, uper_extensible as _uper_extensible, uper_int_range, ) - size_len = oer_size_len(field, size_len) + size_len = field_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) uper_enum_values = _uper_enum_values( field, pkt, uper_enum_values, @@ -888,12 +888,12 @@ def dec_from_decoder(cls, ): # type: (...) -> ASN1_Object[int] from scapy.asn1.constraints import ( - oer_size_len, + field_size_len, uper_enum_values as _uper_enum_values, uper_extensible as _uper_extensible, uper_int_range, ) - size_len = oer_size_len(field, size_len) + size_len = field_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) uper_enum_values = _uper_enum_values( field, pkt, uper_enum_values, @@ -1089,72 +1089,3 @@ class UPERcodec_BMP_STRING(UPERcodec_KNOWN_MULTIPLIER_STRING): # KNOWN_MULTIPLIER inherits STRING's tag for registration; restore the # generic STRING codec used by ASN1F_STRING (octet-string UPER path). ASN1_Class_UNIVERSAL.STRING.register(ASN1_Codecs.PER, UPERcodec_STRING) - - -################################ -# ASN1F compound helpers # -################################ - -from scapy.asn1.compound import ( # noqa: E402 - sequence_decode_from as _uper_sequence_decode_from, - sequence_encode_to as _uper_sequence_encode_to, - sequence_of_decode_from as _uper_sequence_of_decode_from, - sequence_of_encode_to as _uper_sequence_of_encode_to, - choice_decode_from as _uper_choice_decode_from, - choice_encode_to as _uper_choice_encode_to, -) - - -def uper_sequence_m2i(field, pkt, s): - from scapy.asn1.context import UPER_DecoderContext - dec = UPER_DecoderContext(s) - _uper_sequence_decode_from(field, pkt, dec) - return [], dec.remaining() - - -def uper_sequence_build(field, pkt): - from scapy.asn1fields import ASN1F_field - from scapy.asn1.context import UPER_EncoderContext - enc = UPER_EncoderContext() - _uper_sequence_encode_to(field, pkt, enc) - return ASN1F_field.i2m(field, pkt, enc.finish()) - - -def uper_sequence_of_m2i(field, pkt, s): - from scapy.asn1.context import UPER_DecoderContext - dec = UPER_DecoderContext(s) - _uper_sequence_of_decode_from(field, pkt, dec) - return getattr(pkt, field.name), dec.remaining() - - -def uper_sequence_of_build(field, pkt): - from scapy.asn1.context import UPER_EncoderContext - enc = UPER_EncoderContext() - _uper_sequence_of_encode_to(field, pkt, enc) - return field.i2m(pkt, enc.finish()) - - -def uper_choice_m2i(field, pkt, s): - from scapy.asn1.context import UPER_DecoderContext - dec = UPER_DecoderContext(s) - _uper_choice_decode_from(field, pkt, dec) - return getattr(pkt, field.name), dec.remaining() - - -def uper_choice_i2m(field, pkt, x): - from scapy.asn1.context import UPER_EncoderContext - enc = UPER_EncoderContext() - _uper_choice_encode_to(field, pkt, enc) - return field._tagging_enc(pkt, enc.finish(), explicit_tag=field.explicit_tag) - - -def uper_packet_i2m(field, pkt, x): - from scapy.asn1.compound import packet_encode_to - from scapy.asn1.context import UPER_EncoderContext - enc = UPER_EncoderContext() - packet_encode_to(field, pkt, enc, x) - return field._tagging_enc( - pkt, enc.finish(), - implicit_tag=field.implicit_tag, - explicit_tag=field.explicit_tag, - ) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 7908bbc2fa7..db13cb21feb 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -625,8 +625,6 @@ def get_fields_list(self): def _dissect_sequence_children(self, pkt, s): # type: (Any, bytes) -> bytes - from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional, ASN1F_DEFAULT - def set_absent(obj): # type: (Any) -> None if isinstance(obj, (ASN1F_optional, ASN1F_DEFAULT)): diff --git a/test/contrib/oer.uts b/test/contrib/oer.uts index 26ef1c951e9..c0c00660f53 100644 --- a/test/contrib/oer.uts +++ b/test/contrib/oer.uts @@ -886,17 +886,6 @@ True + ASN.1 OER field dispatch and packet extras -= oer sequence helpers are module-level -from scapy.asn1 import oer as oer_mod - -assert callable(oer_mod.oer_sequence_m2i) - -assert callable(oer_mod.oer_choice_i2m) - -assert not hasattr(ASN1_Codecs, "hooks") - -True - = oer constrained integer via constraints fld = OERUnsignedField.ASN1_root diff --git a/test/contrib/uper.uts b/test/contrib/uper.uts index fd7a0625c0c..457f40f736e 100644 --- a/test/contrib/uper.uts +++ b/test/contrib/uper.uts @@ -2791,17 +2791,6 @@ True + ASN.1 UPER field dispatch and packet extras -= uper helpers are module-level not monkey-patched -from scapy.asn1 import uper as uper_mod - -assert callable(uper_mod.uper_sequence_m2i) - -assert "_install_uper_asn1fields" not in uper_mod.__dict__ - -assert not hasattr(ASN1_Codecs, "hooks") - -True - = uper DEFAULT published on asn1fields assert ASN1F_DEFAULT is asn1fields.ASN1F_DEFAULT diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index e37a602fc30..4137fa50f81 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -397,10 +397,6 @@ def _roundtrip(cls, pkt): # type: (type, ASN1_Packet) -> ASN1_Packet return cls(raw(pkt)) -def _roundtrip(cls, pkt): - # type: (type, ASN1_Packet) -> ASN1_Packet - return cls(raw(pkt)) - def _record_kwargs(): # type: () -> dict return dict( @@ -415,10 +411,6 @@ def _asn1_int(val): # type: (Any) -> int return val.val if hasattr(val, "val") else val -def _asn1_int(val): - # type: (Any) -> int - return val.val if hasattr(val, "val") else val - def _assert_record(decoded): # type: (ASN1_Packet) -> None assert decoded.id.val == 42 @@ -552,18 +544,6 @@ for cls in (BEREmptySeqOf, OEREmptySeqOf, PEREmptySeqOf): True -= encode_to decode_from after contrib load -# OER/PER register as first-class codecs; compound fields dispatch statically. -assert hasattr(ASN1_Codecs.OER, "new_encoder") - -assert hasattr(ASN1_Codecs.PER, "new_decoder") - -assert hasattr(ASN1F_SEQUENCE, "encode_to") - -assert hasattr(ASN1F_SEQUENCE, "decode_from") - -True - = ber oer per default component class _BerDefault(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER From 8ee867327206642e53cba4a2575cfde9134c560b Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 14:31:57 +0200 Subject: [PATCH 29/46] Simplify ASN.1 OER/UPER encode paths and share integer helpers. Write OER SEQUENCE children into one encoder, resolve UPER bounds once, and centralize two's-complement octet math. Co-authored-by: Cursor AI-Assisted: yes (Cursor Agent) --- scapy/asn1/compound.py | 13 +----- scapy/asn1/constraints.py | 37 +++++++++++++++ scapy/asn1/intutil.py | 30 +++++++++++++ scapy/asn1/oer.py | 15 ++----- scapy/asn1/uper.py | 95 +++++++++++---------------------------- scapy/asn1fields.py | 3 +- 6 files changed, 101 insertions(+), 92 deletions(-) create mode 100644 scapy/asn1/intutil.py diff --git a/scapy/asn1/compound.py b/scapy/asn1/compound.py index 43164134fc9..7ac71f3cc86 100644 --- a/scapy/asn1/compound.py +++ b/scapy/asn1/compound.py @@ -21,7 +21,6 @@ from scapy.asn1.constraints import field_extensible, field_range from scapy.asn1.context import ( OER_Decoder, - OER_Encoder, per_bit_decoder, per_bit_encoder, ) @@ -121,13 +120,6 @@ def _sequence_encode_children(field, pkt, encode): # ---- SEQUENCE ------------------------------------------------------------- -def _encode_child_to_bytes(pkt, obj): - # type: (Any, Any) -> bytes - child_enc = OER_Encoder() - obj.encode_to(pkt, child_enc) - return child_enc.finish() - - def sequence_encode_to(field, pkt, enc): # type: (Any, Any, Any) -> None bit_enc = per_bit_encoder(enc) @@ -139,12 +131,11 @@ def sequence_encode_to(field, pkt, enc): ) return if enc.codec is ASN1_Codecs.OER: - parts = [write_oer_presence_bits(sequence_presence_bits(field, pkt))] + enc.write(write_oer_presence_bits(sequence_presence_bits(field, pkt))) _sequence_encode_children( field, pkt, - lambda obj: parts.append(_encode_child_to_bytes(pkt, obj)), + lambda obj: obj.encode_to(pkt, enc), ) - enc.write(b"".join(parts)) return s = reduce(lambda x, y: x + y.build(pkt), field.seq, b"") enc.write(field.i2m(pkt, s)) diff --git a/scapy/asn1/constraints.py b/scapy/asn1/constraints.py index 3f0a7febff4..e9c08e313dc 100644 --- a/scapy/asn1/constraints.py +++ b/scapy/asn1/constraints.py @@ -88,6 +88,43 @@ def uper_int_range(field=None, uper_min=None, uper_max=None): return None, None +def resolve_uper_int_bounds(field=None, # type: Any + size_len=None, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + unsigned=None, # type: Optional[bool] + extensible=None # type: Optional[bool] + ): + # type: (...) -> Tuple[Optional[int], Optional[int], bool] + """Resolve UPER INTEGER root range and extensibility from field/kwargs. + + When no explicit ``minimum``/``maximum`` is set, a fixed ``size_len`` of + 1, 2, 4, or 8 with ``unsigned=True`` implies ``0 .. 256**n - 1``. + """ + size_len = field_size_len(field, size_len) + minimum, maximum = uper_int_range(field, uper_min, uper_max) + is_unsigned = oer_unsigned(field, unsigned) + is_extensible = uper_extensible(field, extensible) + if minimum is None and maximum is None: + if size_len in (1, 2, 4, 8) and is_unsigned: + minimum, maximum = 0, (256 ** size_len) - 1 + return minimum, maximum, is_extensible + + +def resolve_uper_size_bounds(field=None, # type: Any + size_len=None, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None # type: Optional[int] + ): + # type: (...) -> Tuple[Optional[int], Optional[int]] + """Resolve UPER SIZE bounds; ``size_len`` is a fixed SIZE.""" + size_len = field_size_len(field, size_len) + uper_min, uper_max = uper_int_range(field, uper_min, uper_max) + if size_len: + return size_len, size_len + return uper_min, uper_max + + def uper_enum_values(field=None, pkt=None, uper_enum_values=None): # type: (Any, Any, Optional[List[int]]) -> Optional[List[int]] if uper_enum_values is not None: diff --git a/scapy/asn1/intutil.py b/scapy/asn1/intutil.py new file mode 100644 index 00000000000..9144ad28115 --- /dev/null +++ b/scapy/asn1/intutil.py @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +"""Shared two's-complement INTEGER helpers (X.690 / X.691 / X.696).""" + +from typing import Tuple + + +def twos_complement_octets(value): + # type: (int) -> Tuple[int, int] + """Shortest two's-complement width and unsigned payload. + + A negative value needs one bit less than its magnitude suggests, as + ``-2**(8n-1)`` still fits in ``n`` octets, hence the increment before + measuring (X.691 11.4 / X.696 10.4). + """ + magnitude = value + 1 if value < 0 else value + number_of_bytes = (magnitude.bit_length() + 8) // 8 + masked = value & ((1 << (8 * number_of_bytes)) - 1) + return number_of_bytes, masked + + +def from_twos_complement(masked, number_of_bytes): + # type: (int, int) -> int + """Interpret ``masked`` as a ``number_of_bytes``-octet two's complement.""" + sign_bit = 1 << (8 * number_of_bytes - 1) + if masked & sign_bit: + return masked - (1 << (8 * number_of_bytes)) + return masked diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index f553bfd2a00..0f1eeb0a5cf 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -122,17 +122,14 @@ def OER_len_dec(s): def OER_signed_integer_enc(i): # type: (int) -> bytes - # X.696 10.4: the shortest two's complement encoding. A negative value - # needs one bit less than its magnitude suggests, as -2**(8n-1) still - # fits in n octets, hence the increment before measuring. - magnitude = i + 1 if i < 0 else i - number_of_bytes = (magnitude.bit_length() + 8) // 8 - value = i & ((1 << (8 * number_of_bytes)) - 1) + from scapy.asn1.intutil import twos_complement_octets + number_of_bytes, value = twos_complement_octets(i) return OER_len_enc(number_of_bytes) + value.to_bytes(number_of_bytes, "big") def OER_signed_integer_dec(s): # type: (bytes) -> Tuple[int, bytes] + from scapy.asn1.intutil import from_twos_complement number_of_bytes, s = OER_len_dec(s) _OER_check_len("OER_signed_integer_dec", s, number_of_bytes) if number_of_bytes == 0: @@ -141,11 +138,7 @@ def OER_signed_integer_dec(s): remaining=s ) value = int.from_bytes(s[:number_of_bytes], "big") - number_of_bits = 8 * number_of_bytes - if value & (1 << (number_of_bits - 1)): - value -= (1 << number_of_bits) - 1 - value -= 1 - return value, s[number_of_bytes:] + return from_twos_complement(value, number_of_bytes), s[number_of_bytes:] def OER_unsigned_integer_enc(i): diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 5f498479fb7..1ff51841a08 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -165,14 +165,11 @@ def append_fragmented(self, count, append_units): def append_unconstrained_whole_number(self, value): # type: (int) -> None - # X.691 11.4: the shortest two's complement encoding. A negative value - # needs one bit less than its magnitude suggests, as -2**(8n-1) still - # fits in n octets, hence the increment before measuring. - magnitude = value + 1 if value < 0 else value - number_of_bytes = (magnitude.bit_length() + 8) // 8 + from scapy.asn1.intutil import twos_complement_octets + number_of_bytes, masked = twos_complement_octets(value) self.append_length_determinant(number_of_bytes) self.append_non_negative_binary_integer( - value & ((1 << (8 * number_of_bytes)) - 1), 8 * number_of_bytes + masked, 8 * number_of_bytes ) def as_bytes(self): @@ -319,16 +316,14 @@ def read_fragmented(self, read_units): def read_unconstrained_whole_number(self): # type: () -> int + from scapy.asn1.intutil import from_twos_complement number_of_bytes = self.read_length_determinant() if number_of_bytes == 0: raise UPER_Decoding_Error( "UPER_Decoder: integer with an empty length determinant" ) enc = self.read_non_negative_binary_integer(8 * number_of_bytes) - sign_bit = 1 << (8 * number_of_bytes - 1) - if enc & sign_bit: - return enc - (1 << (8 * number_of_bytes)) - return enc + return from_twos_complement(enc, number_of_bytes) def UPER_constrained_int_enc(enc, value, minimum, maximum): @@ -512,15 +507,6 @@ def _uper_tagging_dec(s, **_kwargs): ######################### -def _uper_int_range(size_len, uper_min, uper_max, oer_unsigned=False): - # type: (Optional[int], Optional[int], Optional[int], bool) -> Tuple[Optional[int], Optional[int]] # noqa: E501 - if uper_min is not None or uper_max is not None: - return uper_min, uper_max - if size_len in (1, 2, 4, 8) and oer_unsigned: - return 0, (256 ** size_len) - 1 - return None, None - - class UPERcodec_INTEGER(UPERcodec_Object[int]): tag = ASN1_Class_UNIVERSAL.INTEGER @@ -537,18 +523,9 @@ def encode_into(cls, **_kwargs # type: Any ): # type: (...) -> None - from scapy.asn1.constraints import ( - field_size_len, - oer_unsigned as _oer_unsigned, - uper_extensible as _uper_extensible, - uper_int_range, - ) - size_len = field_size_len(field, size_len) - uper_min, uper_max = uper_int_range(field, uper_min, uper_max) - oer_unsigned = _oer_unsigned(field, oer_unsigned) - extensible = _uper_extensible(field, uper_extensible) - minimum, maximum = _uper_int_range( - size_len, uper_min, uper_max, oer_unsigned, + from scapy.asn1.constraints import resolve_uper_int_bounds + minimum, maximum, extensible = resolve_uper_int_bounds( + field, size_len, uper_min, uper_max, oer_unsigned, uper_extensible, ) if extensible and minimum is not None and maximum is not None: if minimum <= i <= maximum: @@ -575,18 +552,9 @@ def dec_from_decoder(cls, **_kwargs # type: Any ): # type: (...) -> ASN1_Object[int] - from scapy.asn1.constraints import ( - field_size_len, - oer_unsigned as _oer_unsigned, - uper_extensible as _uper_extensible, - uper_int_range, - ) - size_len = field_size_len(field, size_len) - uper_min, uper_max = uper_int_range(field, uper_min, uper_max) - oer_unsigned = _oer_unsigned(field, oer_unsigned) - extensible = _uper_extensible(field, uper_extensible) - minimum, maximum = _uper_int_range( - size_len, uper_min, uper_max, oer_unsigned, + from scapy.asn1.constraints import resolve_uper_int_bounds + minimum, maximum, extensible = resolve_uper_int_bounds( + field, size_len, uper_min, uper_max, oer_unsigned, uper_extensible, ) if extensible and minimum is not None and maximum is not None: if dec.read_bit(): @@ -619,15 +587,6 @@ def _uper_bytes_to_bitstr(data, nbits): return bitstr[:nbits] -def _uper_size_bounds(size_len, uper_min, uper_max): - # type: (Optional[int], Optional[int], Optional[int]) -> Tuple[Optional[int], Optional[int]] # noqa: E501 - # A SIZE constraint given as size_len is a fixed size, i.e. a range whose - # bounds coincide. - if size_len: - return size_len, size_len - return uper_min, uper_max - - class UPERcodec_BIT_STRING(UPERcodec_Object[str]): tag = ASN1_Class_UNIVERSAL.BIT_STRING @@ -642,9 +601,7 @@ def encode_into(cls, **_kwargs # type: Any ): # type: (...) -> None - from scapy.asn1.constraints import field_size_len, uper_int_range - size_len = field_size_len(field, size_len) - uper_min, uper_max = uper_int_range(field, uper_min, uper_max) + from scapy.asn1.constraints import resolve_uper_size_bounds if isinstance(_s, tuple) and len(_s) == 2: data, nbits = _s s = bytes_encode(data) @@ -657,7 +614,9 @@ def encode_into(cls, else: s = bytes_encode(_s) nbits = 8 * len(s) - minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) + minimum, maximum = resolve_uper_size_bounds( + field, size_len, uper_min, uper_max, + ) if minimum is not None and maximum is not None: _uper_check_size(cls.__name__, "bits", nbits, minimum, maximum) if minimum != maximum: @@ -687,10 +646,10 @@ def dec_from_decoder(cls, **_kwargs # type: Any ): # type: (...) -> ASN1_Object[str] - from scapy.asn1.constraints import field_size_len, uper_int_range - size_len = field_size_len(field, size_len) - uper_min, uper_max = uper_int_range(field, uper_min, uper_max) - minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) + from scapy.asn1.constraints import resolve_uper_size_bounds + minimum, maximum = resolve_uper_size_bounds( + field, size_len, uper_min, uper_max, + ) if minimum is not None and maximum is not None: nbits = minimum if minimum != maximum: @@ -728,11 +687,11 @@ def encode_into(cls, **_kwargs # type: Any ): # type: (...) -> None - from scapy.asn1.constraints import field_size_len, uper_int_range - size_len = field_size_len(field, size_len) - uper_min, uper_max = uper_int_range(field, uper_min, uper_max) + from scapy.asn1.constraints import resolve_uper_size_bounds s = bytes_encode(_s) - minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) + minimum, maximum = resolve_uper_size_bounds( + field, size_len, uper_min, uper_max, + ) UPER_octet_string_enc(enc, s, minimum, maximum) @classmethod @@ -745,10 +704,10 @@ def dec_from_decoder(cls, **_kwargs # type: Any ): # type: (...) -> ASN1_Object[Any] - from scapy.asn1.constraints import field_size_len, uper_int_range - size_len = field_size_len(field, size_len) - uper_min, uper_max = uper_int_range(field, uper_min, uper_max) - minimum, maximum = _uper_size_bounds(size_len, uper_min, uper_max) + from scapy.asn1.constraints import resolve_uper_size_bounds + minimum, maximum = resolve_uper_size_bounds( + field, size_len, uper_min, uper_max, + ) raw = UPER_octet_string_dec(dec, minimum, maximum) return cls.asn1_object(raw) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index db13cb21feb..48486118895 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -664,8 +664,7 @@ def encode_to(self, pkt, enc): def encode_into(self, enc, pkt, value=None): # type: (Any, ASN1_Packet, Any) -> None - from scapy.asn1.compound import sequence_encode_to - sequence_encode_to(self, pkt, enc) + self.encode_to(pkt, enc) def dissect_from_decoder(self, pkt, dec): # type: (ASN1_Packet, Any) -> None From 28f7b87b64e54f9915adcc602b7f79d8f6a080ec Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 15:09:25 +0200 Subject: [PATCH 30/46] Fix remaining PR #5050 UPER/OER review findings. Add semi-constrained INTEGER and extensible SIZE paths, honor OER size constraints and named enum encode values, and drop leftover dead helpers. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- .config/codespell_ignore.txt | 1 - scapy/asn1/asn1.py | 4 - scapy/asn1/ber.py | 24 ++-- scapy/asn1/constraints.py | 29 ++++- scapy/asn1/context.py | 10 -- scapy/asn1/oer.py | 142 +++++++++++++-------- scapy/asn1/uper.py | 161 +++++++++++++++--------- scapy/asn1fields.py | 12 ++ test/scapy/layers/asn1.uts | 1 - test/scapy/layers/ber.uts | 1 - test/{contrib => scapy/layers}/oer.uts | 21 +++- test/{contrib => scapy/layers}/uper.uts | 57 +++++++-- 12 files changed, 306 insertions(+), 157 deletions(-) rename test/{contrib => scapy/layers}/oer.uts (98%) rename test/{contrib => scapy/layers}/uper.uts (98%) diff --git a/.config/codespell_ignore.txt b/.config/codespell_ignore.txt index 98e33583f15..b2e04b75727 100644 --- a/.config/codespell_ignore.txt +++ b/.config/codespell_ignore.txt @@ -54,7 +54,6 @@ wan wanna webp widgits -uper UPER uPER acn diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index 74e55068455..540e088c375 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -110,8 +110,6 @@ class ASN1_Error(Scapy_Exception): class ASN1_Encoding_Error(ASN1_Error): - codec_label = "ASN.1" - def __init__(self, msg, # type: str encoded=None, # type: Any @@ -136,8 +134,6 @@ def __str__(self): class ASN1_Decoding_Error(ASN1_Error): - codec_label = "ASN.1" - def __init__(self, msg, # type: str decoded=None, # type: Any diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index eeaca442630..b971e709ea9 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -396,17 +396,17 @@ def safedec(cls, return cls.dec(s, context, safe=True, _depth=_depth) @classmethod - def enc(cls, s, field=None, pkt=None, size_len=None, **_kwargs): - # type: (_K, Any, Any, Optional[int], **Any) -> bytes + def enc(cls, s, field=None, size_len=None, **_kwargs): + # type: (_K, Any, Optional[int], **Any) -> bytes if isinstance(s, (str, bytes)): return BERcodec_STRING.enc( - s, field=field, pkt=pkt, size_len=size_len, + s, field=field, size_len=size_len, **_kwargs, ) else: try: i = int(s) # type: ignore[call-overload] return BERcodec_INTEGER.enc( - i, field=field, pkt=pkt, size_len=size_len, + i, field=field, size_len=size_len, **_kwargs, ) except TypeError: raise TypeError("Trying to encode an invalid value !") @@ -433,7 +433,7 @@ class BERcodec_INTEGER(BERcodec_Object[int]): tag = ASN1_Class_UNIVERSAL.INTEGER @classmethod - def enc(cls, i, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 + def enc(cls, i, field=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 # type: (int, Any, Any, Optional[int], **Any) -> bytes size_len = _ber_enc_size_len(field, size_len) ls = [] @@ -501,7 +501,7 @@ def do_dec(cls, ) @classmethod - def enc(cls, _s, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 + def enc(cls, _s, field=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 # type: (AnyStr, Any, Any, Optional[int], **Any) -> bytes size_len = _ber_enc_size_len(field, size_len) # /!\ this is DER encoding (bit strings are only zero-bit padded) @@ -521,7 +521,7 @@ class BERcodec_STRING(BERcodec_Object[str]): tag = ASN1_Class_UNIVERSAL.STRING @classmethod - def enc(cls, _s, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 + def enc(cls, _s, field=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 # type: (Union[str, bytes], Any, Any, Optional[int], **Any) -> bytes size_len = _ber_enc_size_len(field, size_len) s = bytes_encode(_s) @@ -544,13 +544,13 @@ class BERcodec_NULL(BERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.NULL @classmethod - def enc(cls, i, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 + def enc(cls, i, field=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 # type: (int, Any, Any, Optional[int], **Any) -> bytes if i == 0: return chb(int(cls.tag)) + b"\0" else: return super(cls, cls).enc( - i, field=field, pkt=pkt, size_len=size_len, + i, field=field, size_len=size_len, **_kwargs, ) @@ -558,7 +558,7 @@ class BERcodec_OID(BERcodec_Object[bytes]): tag = ASN1_Class_UNIVERSAL.OID @classmethod - def enc(cls, _oid, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 + def enc(cls, _oid, field=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 # type: (AnyStr, Any, Any, Optional[int], **Any) -> bytes size_len = _ber_enc_size_len(field, size_len) oid = bytes_encode(_oid) @@ -652,7 +652,7 @@ class BERcodec_SEQUENCE(BERcodec_Object[Union[bytes, List[BERcodec_Object[Any]]] tag = ASN1_Class_UNIVERSAL.SEQUENCE @classmethod - def enc(cls, _ll, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 + def enc(cls, _ll, field=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 # type: (Union[bytes, List[BERcodec_Object[Any]]], Any, Any, Optional[int], **Any) -> bytes # noqa: E501 if isinstance(_ll, bytes): ll = _ll @@ -711,7 +711,7 @@ class BERcodec_IPADDRESS(BERcodec_STRING): tag = ASN1_Class_UNIVERSAL.IPADDRESS @classmethod - def enc(cls, ipaddr_ascii, field=None, pkt=None, size_len=None, **_kwargs): # type: ignore # noqa: E501 + def enc(cls, ipaddr_ascii, field=None, size_len=None, **_kwargs): # type: ignore # noqa: E501 # type: (str, Any, Any, Optional[int], **Any) -> bytes size_len = _ber_enc_size_len(field, size_len) try: diff --git a/scapy/asn1/constraints.py b/scapy/asn1/constraints.py index e9c08e313dc..4a381609e70 100644 --- a/scapy/asn1/constraints.py +++ b/scapy/asn1/constraints.py @@ -2,7 +2,14 @@ # This file is part of Scapy # See https://scapy.net/ for more information -"""Codec-neutral ASN.1 schema constraints.""" +"""Codec-neutral ASN.1 schema constraints. + +``minimum`` / ``maximum`` mean a value range for INTEGER and ENUMERATED +fields, and a SIZE constraint for string and BIT STRING fields. +``size_len`` is a fixed SIZE (octets or bits) used when both bounds coincide. +``extensible`` marks an extension marker on the constraint. +``unsigned`` selects unsigned INTEGER encoding where the codec supports it. +""" from dataclasses import dataclass from typing import Any, Dict, List, Optional, Tuple @@ -114,15 +121,29 @@ def resolve_uper_int_bounds(field=None, # type: Any def resolve_uper_size_bounds(field=None, # type: Any size_len=None, # type: Optional[int] uper_min=None, # type: Optional[int] - uper_max=None # type: Optional[int] + uper_max=None, # type: Optional[int] + extensible=None # type: Optional[bool] ): - # type: (...) -> Tuple[Optional[int], Optional[int]] + # type: (...) -> Tuple[Optional[int], Optional[int], bool] """Resolve UPER SIZE bounds; ``size_len`` is a fixed SIZE.""" size_len = field_size_len(field, size_len) uper_min, uper_max = uper_int_range(field, uper_min, uper_max) + is_extensible = uper_extensible(field, extensible) + if size_len: + return size_len, size_len, is_extensible + return uper_min, uper_max, is_extensible + + +def resolve_oer_size_bounds(field=None, size_len=None): + # type: (Any, Optional[int]) -> Tuple[Optional[int], Optional[int]] + """Resolve OER SIZE bounds from ``size_len`` or field constraints.""" + size_len = field_size_len(field, size_len) + # ``size_len=0`` means unset (same as the historical ``if size_len:`` check). if size_len: return size_len, size_len - return uper_min, uper_max + if field is not None: + return field_range(field) + return None, None def uper_enum_values(field=None, pkt=None, uper_enum_values=None): diff --git a/scapy/asn1/context.py b/scapy/asn1/context.py index 989ff175d68..a918ffd16a3 100644 --- a/scapy/asn1/context.py +++ b/scapy/asn1/context.py @@ -59,16 +59,6 @@ def __init__(self, data, codec=None): self._data = data self._offset = 0 - def read_all(self): - # type: () -> bytes - return self._data[self._offset:] - - def consume(self, n): - # type: (int) -> bytes - chunk = self._data[self._offset:self._offset + n] - self._offset += n - return chunk - def remaining(self): # type: () -> bytes return self._data[self._offset:] diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index 0f1eeb0a5cf..cf86ab471e5 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -62,11 +62,11 @@ class OER_Exception(Exception): class OER_Encoding_Error(ASN1_Encoding_Error): - codec_label = "OER" + pass class OER_Decoding_Error(ASN1_Decoding_Error): - codec_label = "OER" + pass # OER tag classes (bits 8-7 of the first identifier octet) @@ -207,14 +207,10 @@ def OER_tag_parts(identifier): return tag_class, tag_number -class OERcodec_metaclass(ASN1Codec_metaclass): - pass - - _K = TypeVar('_K') -class OERcodec_Object(Generic[_K], metaclass=OERcodec_metaclass): +class OERcodec_Object(Generic[_K], metaclass=ASN1Codec_metaclass): codec = ASN1_Codecs.OER tag = ASN1_Class_UNIVERSAL.ANY @@ -307,20 +303,9 @@ def enc(cls, s, field=None, pkt=None, size_len=None, **_kwargs): # Tags declared on a field are not encoded for OER components (X.696); -# CHOICE writes its own alternative tags. Identity tagging keeps the -# codec extension point without BER-style wrappers. -def _oer_tagging_enc(s, **_kwargs): - # type: (bytes, **Any) -> bytes - return s - - -def _oer_tagging_dec(s, **_kwargs): - # type: (bytes, **Any) -> Tuple[Optional[int], bytes] - return None, s - - +# CHOICE writes its own alternative tags. Identity tagging is the ASN1Codec +# default when no tagging_enc/dec is registered. ASN1_Codecs.OER.register_stem(OERcodec_Object) -ASN1_Codecs.OER.register_tagging(_oer_tagging_enc, _oer_tagging_dec) ########################## @@ -472,49 +457,77 @@ def do_dec(cls, **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[str], bytes] - from scapy.asn1.constraints import field_size_len - size_len = field_size_len(field, size_len) - if size_len: - number_of_bytes = (size_len + 7) // 8 + from scapy.asn1.constraints import resolve_oer_size_bounds + minimum, maximum = resolve_oer_size_bounds(field, size_len) + if minimum is not None and maximum is not None and minimum == maximum: + number_of_bytes = (minimum + 7) // 8 _OER_check_len(cls.__name__, s, number_of_bytes) return ( cls.tag.asn1_object( - _oer_bytes_to_bitstr(s[:number_of_bytes])[:size_len] + _oer_bytes_to_bitstr(s[:number_of_bytes])[:minimum] ), s[number_of_bytes:], ) length, s = OER_len_dec(s) if length == 0: - return cls.tag.asn1_object(""), s - _OER_check_len(cls.__name__, s, length) - unused_bits = s[0] - if safe and unused_bits > 7: + fs = "" + else: + _OER_check_len(cls.__name__, s, length) + unused_bits = s[0] + if safe and unused_bits > 7: + raise OER_Decoding_Error( + "OERcodec_BIT_STRING: too many unused_bits advertised", + remaining=s + ) + fs = _oer_bytes_to_bitstr(s[1:length]) + if unused_bits > 0: + fs = fs[:-unused_bits] + s = s[length:] + nbits = len(fs) + if minimum is not None and nbits < minimum: + raise OER_Decoding_Error( + "%s: got %i bits while expecting >= %i" % + (cls.__name__, nbits, minimum), + remaining=s, + ) + if maximum is not None and nbits > maximum: raise OER_Decoding_Error( - "OERcodec_BIT_STRING: too many unused_bits advertised", - remaining=s + "%s: got %i bits while expecting <= %i" % + (cls.__name__, nbits, maximum), + remaining=s, ) - fs = _oer_bytes_to_bitstr(s[1:length]) - if unused_bits > 0: - fs = fs[:-unused_bits] - return cls.tag.asn1_object(fs), s[length:] + return cls.tag.asn1_object(fs), s @classmethod def enc(cls, _s, field=None, size_len=None, **_kwargs): # type: (AnyStr, Any, Optional[int], **Any) -> bytes - from scapy.asn1.constraints import field_size_len - size_len = field_size_len(field, size_len) + from scapy.asn1.constraints import resolve_oer_size_bounds + minimum, maximum = resolve_oer_size_bounds(field, size_len) s = bytes_encode(_s) - if size_len: + nbits = len(s) + if minimum is not None and maximum is not None and minimum == maximum: # X.696 13.3: a fixed size means the bits are written padded to a # whole number of octets, without length or unused-bit count. - if len(s) != size_len: + if nbits != minimum: raise OER_Encoding_Error( "%s: got %i bits while expecting %i" % - (cls.__name__, len(s), size_len), + (cls.__name__, nbits, minimum), encoded=_s ) return _oer_bitstr_to_bytes(s) - body = chb(-len(s) % 8) + _oer_bitstr_to_bytes(s) + if minimum is not None and nbits < minimum: + raise OER_Encoding_Error( + "%s: got %i bits while expecting >= %i" % + (cls.__name__, nbits, minimum), + encoded=_s, + ) + if maximum is not None and nbits > maximum: + raise OER_Encoding_Error( + "%s: got %i bits while expecting <= %i" % + (cls.__name__, nbits, maximum), + encoded=_s, + ) + body = chb(-nbits % 8) + _oer_bitstr_to_bytes(s) return OER_len_enc(len(body)) + body @@ -524,19 +537,32 @@ class OERcodec_STRING(OERcodec_Object[str]): @classmethod def enc(cls, _s, field=None, size_len=None, **_kwargs): # type: (Union[str, bytes], Any, Optional[int], **Any) -> bytes - from scapy.asn1.constraints import field_size_len - size_len = field_size_len(field, size_len) + from scapy.asn1.constraints import resolve_oer_size_bounds + minimum, maximum = resolve_oer_size_bounds(field, size_len) s = bytes_encode(_s) - if size_len: + length = len(s) + if minimum is not None and maximum is not None and minimum == maximum: # X.696 16.1: a fixed size means no length determinant. - if len(s) != size_len: + if length != minimum: raise OER_Encoding_Error( "%s: got %i bytes while expecting %i" % - (cls.__name__, len(s), size_len), + (cls.__name__, length, minimum), encoded=_s ) return s - return OER_len_enc(len(s)) + s + if minimum is not None and length < minimum: + raise OER_Encoding_Error( + "%s: got %i bytes while expecting >= %i" % + (cls.__name__, length, minimum), + encoded=_s, + ) + if maximum is not None and length > maximum: + raise OER_Encoding_Error( + "%s: got %i bytes while expecting <= %i" % + (cls.__name__, length, maximum), + encoded=_s, + ) + return OER_len_enc(length) + s @classmethod def do_dec(cls, @@ -549,13 +575,25 @@ def do_dec(cls, **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[Any], bytes] - from scapy.asn1.constraints import field_size_len - size_len = field_size_len(field, size_len) - if size_len: - _OER_check_len(cls.__name__, s, size_len) - return cls.tag.asn1_object(s[:size_len]), s[size_len:] + from scapy.asn1.constraints import resolve_oer_size_bounds + minimum, maximum = resolve_oer_size_bounds(field, size_len) + if minimum is not None and maximum is not None and minimum == maximum: + _OER_check_len(cls.__name__, s, minimum) + return cls.tag.asn1_object(s[:minimum]), s[minimum:] length, s = OER_len_dec(s) _OER_check_len(cls.__name__, s, length) + if minimum is not None and length < minimum: + raise OER_Decoding_Error( + "%s: got %i bytes while expecting >= %i" % + (cls.__name__, length, minimum), + remaining=s, + ) + if maximum is not None and length > maximum: + raise OER_Decoding_Error( + "%s: got %i bytes while expecting <= %i" % + (cls.__name__, length, maximum), + remaining=s, + ) return cls.tag.asn1_object(s[:length]), s[length:] diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 1ff51841a08..bd75b5355a6 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -61,11 +61,11 @@ class UPER_Encoding_Error(ASN1_Encoding_Error): - codec_label = "UPER" + pass class UPER_Decoding_Error(ASN1_Decoding_Error): - codec_label = "UPER" + pass def UPER_bits_for_range(size): @@ -186,18 +186,6 @@ def as_bytes(self): return _uper_bits_to_bytes(value, number_of_bits) -def UPER_has_unexpected_remainder(dec): - # type: (UPER_Decoder) -> bool - """True when unread bits remain after an octet-aligned padding check. - - Prefer :meth:`UPER_Decoder.remaining_bytes` at packet boundaries; this - helper only reports whether non-padding bits are still pending. - """ - pad = -dec._read_offset() % 8 - unread = max(0, dec.number_of_bits - pad) - return unread != 0 - - class UPER_Decoder(object): def __init__(self, encoded): # type: (bytes) -> None @@ -354,6 +342,32 @@ def UPER_constrained_int_dec(dec, minimum, maximum): return value +def UPER_semi_constrained_int_enc(enc, value, minimum): + # type: (UPER_Encoder, int, int) -> None + # X.691 11.7: encode the non-negative offset (value - lower_bound) as a + # normally small non-negative whole number (length determinant + octets). + if value < minimum: + raise UPER_Encoding_Error( + "UPER_semi_constrained_int_enc: got %i while expecting >= %i" % + (value, minimum) + ) + offset = value - minimum + number_of_bytes = max((offset.bit_length() + 7) // 8, 1) + enc.append_length_determinant(number_of_bytes) + enc.append_non_negative_binary_integer(offset, 8 * number_of_bytes) + + +def UPER_semi_constrained_int_dec(dec, minimum): + # type: (UPER_Decoder, int) -> int + number_of_bytes = dec.read_length_determinant() + if number_of_bytes == 0: + raise UPER_Decoding_Error( + "UPER_semi_constrained_int_dec: empty length determinant" + ) + offset = dec.read_non_negative_binary_integer(8 * number_of_bytes) + return offset + minimum + + def _uper_check_size(name, unit, count, minimum, maximum): # type: (str, str, int, int, int) -> None # The determinant is sized after the constraint, so a value that violates @@ -367,27 +381,46 @@ def _uper_check_size(name, unit, count, minimum, maximum): ) -def UPER_octet_string_enc(enc, data, minimum=None, maximum=None): - # type: (UPER_Encoder, bytes, Optional[int], Optional[int]) -> None +def UPER_octet_string_enc(enc, data, minimum=None, maximum=None, + extensible=False): + # type: (UPER_Encoder, bytes, Optional[int], Optional[int], bool) -> None + length = len(data) + if extensible and minimum is not None and maximum is not None: + if minimum <= length <= maximum: + enc.append_bit(0) + else: + enc.append_bit(1) + enc.append_fragmented( + length, + lambda offset, size: enc.append_bytes(data[offset:offset + size]), + ) + return if minimum is not None and maximum is not None: _uper_check_size( - "UPER_octet_string_enc", "octets", len(data), minimum, maximum, + "UPER_octet_string_enc", "octets", length, minimum, maximum, ) if minimum != maximum: enc.append_non_negative_binary_integer( - len(data) - minimum, + length - minimum, UPER_bits_for_range(maximum - minimum), ) enc.append_bytes(data) else: enc.append_fragmented( - len(data), + length, lambda offset, size: enc.append_bytes(data[offset:offset + size]), ) -def UPER_octet_string_dec(dec, minimum=None, maximum=None): - # type: (UPER_Decoder, Optional[int], Optional[int]) -> bytes +def UPER_octet_string_dec(dec, minimum=None, maximum=None, extensible=False): + # type: (UPER_Decoder, Optional[int], Optional[int], bool) -> bytes + if extensible and minimum is not None and maximum is not None: + if dec.read_bit(): + fragments = [] # type: List[bytes] + dec.read_fragmented( + lambda size: fragments.append(dec.read_bytes(size)) + ) + return b"".join(fragments) if minimum is not None and maximum is not None: length = minimum if minimum != maximum: @@ -414,14 +447,10 @@ def UPER_choice_index_dec(dec, number_of_choices): ) -class UPERcodec_metaclass(ASN1Codec_metaclass): - pass - - _K = TypeVar('_K') -class UPERcodec_Object(Generic[_K], metaclass=UPERcodec_metaclass): +class UPERcodec_Object(Generic[_K], metaclass=ASN1Codec_metaclass): codec = ASN1_Codecs.PER tag = ASN1_Class_UNIVERSAL.ANY @@ -486,20 +515,9 @@ def safedec(cls, s, context=None, **kwargs): return cls.dec(s, context, safe=True, **kwargs) -# No field tagging on the wire for PER; identity keeps the codec extension -# point without BER-style wrappers. -def _uper_tagging_enc(s, **_kwargs): - # type: (bytes, **Any) -> bytes - return s - - -def _uper_tagging_dec(s, **_kwargs): - # type: (bytes, **Any) -> Tuple[Optional[int], bytes] - return None, s - - +# No field tagging on the wire for PER; identity tagging is the ASN1Codec +# default when no tagging_enc/dec is registered. ASN1_Codecs.PER.register_stem(UPERcodec_Object) -ASN1_Codecs.PER.register_tagging(_uper_tagging_enc, _uper_tagging_dec) ######################### @@ -536,6 +554,8 @@ def encode_into(cls, return if minimum is not None and maximum is not None: UPER_constrained_int_enc(enc, i, minimum, maximum) + elif minimum is not None: + UPER_semi_constrained_int_enc(enc, i, minimum) else: enc.append_unconstrained_whole_number(i) @@ -562,6 +582,8 @@ def dec_from_decoder(cls, return cls.asn1_object(value) if minimum is not None and maximum is not None: value = UPER_constrained_int_dec(dec, minimum, maximum) + elif minimum is not None: + value = UPER_semi_constrained_int_dec(dec, minimum) else: value = dec.read_unconstrained_whole_number() return cls.asn1_object(value) @@ -598,6 +620,7 @@ def encode_into(cls, size_len=None, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] + uper_extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> None @@ -614,9 +637,21 @@ def encode_into(cls, else: s = bytes_encode(_s) nbits = 8 * len(s) - minimum, maximum = resolve_uper_size_bounds( - field, size_len, uper_min, uper_max, + minimum, maximum, extensible = resolve_uper_size_bounds( + field, size_len, uper_min, uper_max, uper_extensible, ) + if extensible and minimum is not None and maximum is not None: + if minimum <= nbits <= maximum: + enc.append_bit(0) + else: + enc.append_bit(1) + enc.append_fragmented( + nbits, + lambda offset, size: enc.append_bits( + s[offset // 8:(offset + size + 7) // 8], size + ), + ) + return if minimum is not None and maximum is not None: _uper_check_size(cls.__name__, "bits", nbits, minimum, maximum) if minimum != maximum: @@ -643,20 +678,17 @@ def dec_from_decoder(cls, size_len=None, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] + uper_extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> ASN1_Object[str] from scapy.asn1.constraints import resolve_uper_size_bounds - minimum, maximum = resolve_uper_size_bounds( - field, size_len, uper_min, uper_max, + minimum, maximum, extensible = resolve_uper_size_bounds( + field, size_len, uper_min, uper_max, uper_extensible, ) - if minimum is not None and maximum is not None: - nbits = minimum - if minimum != maximum: - nbits += dec.read_non_negative_binary_integer( - UPER_bits_for_range(maximum - minimum) - ) - else: + + def _read_unconstrained(): + # type: () -> ASN1_Object[str] fragments = [] # type: List[bytes] sizes = [] # type: List[int] @@ -669,8 +701,19 @@ def read_fragment(size): return cls.asn1_object( _uper_bytes_to_bitstr(b"".join(fragments), sum(sizes)) ) - raw = dec.read_bits(nbits) - return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)) + + if extensible and minimum is not None and maximum is not None: + if dec.read_bit(): + return _read_unconstrained() + if minimum is not None and maximum is not None: + nbits = minimum + if minimum != maximum: + nbits += dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + raw = dec.read_bits(nbits) + return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)) + return _read_unconstrained() class UPERcodec_STRING(UPERcodec_Object[str]): @@ -684,15 +727,16 @@ def encode_into(cls, size_len=None, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] + uper_extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> None from scapy.asn1.constraints import resolve_uper_size_bounds s = bytes_encode(_s) - minimum, maximum = resolve_uper_size_bounds( - field, size_len, uper_min, uper_max, + minimum, maximum, extensible = resolve_uper_size_bounds( + field, size_len, uper_min, uper_max, uper_extensible, ) - UPER_octet_string_enc(enc, s, minimum, maximum) + UPER_octet_string_enc(enc, s, minimum, maximum, extensible) @classmethod def dec_from_decoder(cls, @@ -701,14 +745,15 @@ def dec_from_decoder(cls, size_len=None, # type: Optional[int] uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] + uper_extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> ASN1_Object[Any] from scapy.asn1.constraints import resolve_uper_size_bounds - minimum, maximum = resolve_uper_size_bounds( - field, size_len, uper_min, uper_max, + minimum, maximum, extensible = resolve_uper_size_bounds( + field, size_len, uper_min, uper_max, uper_extensible, ) - raw = UPER_octet_string_dec(dec, minimum, maximum) + raw = UPER_octet_string_dec(dec, minimum, maximum, extensible) return cls.asn1_object(raw) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 48486118895..b03c8fac319 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -182,6 +182,11 @@ def _codec_kwargs(self, pkt=None): # Pass size_len through by default; subclasses may extend this dict. return {"size_len": self.size_len} + def normalize_encode_value(self, pkt, value): + # type: (ASN1_Packet, Any) -> Any + """Convert a human-facing value before codec encode (e.g. enum names).""" + return value + def _encode_item(self, pkt, item): # type: (ASN1_Packet, Any) -> bytes """Encode a field value with codec kwargs, without field tagging.""" @@ -297,6 +302,7 @@ def encode_into(self, enc, pkt, value=None): value = getattr(pkt, self.name) if value is None: return + value = self.normalize_encode_value(pkt, value) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) if isinstance(value, ASN1_Object): if (self.ASN1_tag == ASN1_Class_UNIVERSAL.ANY or @@ -440,6 +446,12 @@ def uper_enum_values(self): # type: () -> List[int] return sorted(self.i2s) + def normalize_encode_value(self, pkt, value): + # type: (ASN1_Packet, Any) -> Any + if isinstance(value, str): + return self.s2i[value] + return value + def i2m(self, pkt, # type: ASN1_Packet s, # type: Union[bytes, str, int, ASN1_INTEGER] diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 4137fa50f81..2e3666d8c89 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -748,4 +748,3 @@ assert ap_rep.encPart.underlayer is ap_rep assert ap_rep.encPart.parent is ap_rep assert ap_rep.encPart.get_usage() == (12, EncAPRepPart) True - diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index 1cea093eba5..7f3473aab90 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -733,4 +733,3 @@ diff, s = ASN1_Codecs.BER.tagging_dec( b"\x02\x01\x01", hidden_tag=ASN1_Class_UNIVERSAL.INTEGER) assert s == b"\x02\x01\x01" True - diff --git a/test/contrib/oer.uts b/test/scapy/layers/oer.uts similarity index 98% rename from test/contrib/oer.uts rename to test/scapy/layers/oer.uts index c0c00660f53..2e9c6fa5cc5 100644 --- a/test/contrib/oer.uts +++ b/test/scapy/layers/oer.uts @@ -2,7 +2,7 @@ # # Try me with: -# ./test/run_tests -t test/contrib/oer.uts -N +# ./test/run_tests -t test/scapy/layers/oer.uts -N + ASN.1 OER load = prepare helpers and packet classes @@ -1519,3 +1519,22 @@ assert pickle.dumps(OERcodec_IA5_STRING) assert OERcodec_IA5_STRING.__module__ == "scapy.asn1.oer" True += OER STRING fixed SIZE from minimum=maximum +class OERFixedSizeStr(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_STRING("s", b"", minimum=3, maximum=3) + +assert raw(OERFixedSizeStr(s=b"abc")) == b"abc" +assert _roundtrip(OERFixedSizeStr, OERFixedSizeStr(s=b"abc")).s.val == b"abc" +_raises(OER_Encoding_Error, lambda: raw(OERFixedSizeStr(s=b"ab"))) +True + += OER STRING ranged SIZE validates length +class OERRangedStr(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_STRING("s", b"", minimum=1, maximum=3) + +assert raw(OERRangedStr(s=b"ab")) == b"\x02ab" +assert _roundtrip(OERRangedStr, OERRangedStr(s=b"ab")).s.val == b"ab" +_raises(OER_Encoding_Error, lambda: raw(OERRangedStr(s=b"abcd"))) +True diff --git a/test/contrib/uper.uts b/test/scapy/layers/uper.uts similarity index 98% rename from test/contrib/uper.uts rename to test/scapy/layers/uper.uts index 457f40f736e..086f5e22fa3 100644 --- a/test/contrib/uper.uts +++ b/test/scapy/layers/uper.uts @@ -2,7 +2,7 @@ # # Try me with: -# ./test/run_tests -t test/contrib/uper.uts -N +# ./test/run_tests -t test/scapy/layers/uper.uts -N + ASN.1 UPER load = prepare helpers and packet classes @@ -1338,18 +1338,7 @@ for data, minimum, maximum in [ UPER_octet_string_enc(enc, data, minimum, maximum) dec = UPER_Decoder(enc.as_bytes()) assert UPER_octet_string_dec(dec, minimum, maximum) == data - assert not UPER_has_unexpected_remainder(dec) - -True - -= uper has unexpected remainder -dec = UPER_Decoder(b"\x80") -dec.read_bit() -assert UPER_has_unexpected_remainder(dec) is False - -dec = UPER_Decoder(b"\x80\x00") -dec.read_bit() -assert UPER_has_unexpected_remainder(dec) is True + assert dec.remaining_bytes() == b"" True @@ -3649,3 +3638,45 @@ assert pkt.fields == before assert enc.finish() == bytes.fromhex("008480") True += UPER semi-constrained INTEGER (5..MAX) encodes offset +class UPERSemiInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 5, minimum=5) + +assert raw(UPERSemiInt(n=5)) == b"\x01\x00" +assert UPERSemiInt(b"\x01\x00").n.val == 5 +assert raw(UPERSemiInt(n=6)) == b"\x01\x01" +assert _roundtrip(UPERSemiInt, UPERSemiInt(n=6)).n.val == 6 +_raises(UPER_Encoding_Error, lambda: raw(UPERSemiInt(n=4))) +True + += UPER extensible SIZE STRING root and extension +class UPERExtSizeStr(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_STRING("s", b"", minimum=1, maximum=3, extensible=True) + +assert raw(UPERExtSizeStr(s=b"a")) == bytes.fromhex("0c20") +assert _roundtrip(UPERExtSizeStr, UPERExtSizeStr(s=b"a")).s.val == b"a" +assert _roundtrip(UPERExtSizeStr, UPERExtSizeStr(s=b"abcd")).s.val == b"abcd" +True + += UPER extensible SIZE BIT STRING root and extension +class UPERExtSizeBits(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BIT_STRING("b", "", minimum=1, maximum=3, extensible=True) + +root = raw(UPERExtSizeBits(b="1")) +assert _roundtrip(UPERExtSizeBits, UPERExtSizeBits(b="1")).b.val == "1" +assert _roundtrip(UPERExtSizeBits, UPERExtSizeBits(b="1010")).b.val == "1010" +True + += UPER ENUMERATED accepts named string assignment +class UPERNamedEnum(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_ENUMERATED( + "e", 0, {0: "zero", 1: "one", 5: "five"}, + ) + +assert raw(UPERNamedEnum(e="one")) == raw(UPERNamedEnum(e=1)) +assert _roundtrip(UPERNamedEnum, UPERNamedEnum(e="one")).e.val == 1 +True From ce1bc6e857e69f13489f817e70d31ea9a6e10f5f Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 17:10:30 +0200 Subject: [PATCH 31/46] Restrict AI-Assisted commit check to PR commits on merge checkouts. actions/checkout builds a merge of the PR into the base branch, so rev-list from HEAD was also validating base-branch tips. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- .config/ci/check_commits.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.config/ci/check_commits.sh b/.config/ci/check_commits.sh index f5efc667c48..8ca9bc16bc8 100755 --- a/.config/ci/check_commits.sh +++ b/.config/ci/check_commits.sh @@ -6,7 +6,14 @@ # We copy Wireshark's contributing guide, thanks to them for the idea ! # This script is inspired by https://gitlab.com/wireshark/wireshark/-/blob/master/.gitlab-ci.yml -commits=$(git rev-list --no-merges --after="2026-01-00T00:00:00" --max-count=$((PR_FETCH_DEPTH - 1)) HEAD) +# On pull_request, actions/checkout creates a merge of the PR into the base +# branch (HEAD^1=base tip, HEAD^2=PR tip). Restrict the check to PR commits +# so base-branch history is not false-failed for missing trailers. +if git rev-parse -q --verify HEAD^2 >/dev/null 2>&1; then + commits=$(git rev-list --no-merges HEAD^1..HEAD^2) +else + commits=$(git rev-list --no-merges --after="2026-01-00T00:00:00" --max-count=$((PR_FETCH_DEPTH - 1)) HEAD) +fi if [ -z "$commits" ]; then echo "No commit to check in PR. OK." exit 0 From 2a670331da82e49d7750c4923557737ff3102624 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 18:58:44 +0200 Subject: [PATCH 32/46] Finish ASN.1 OER/UPER codec-context architecture for PR #5050. Move compound wire rules onto encoder/decoder methods, reject empty OER SEQUENCEs with mandatory fields, shrink BER churn, and canonicalize UPER constraint kwargs. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- .config/ci/check_commits.sh | 9 +- .config/codespell_ignore.txt | 2 - scapy/asn1/asn1.py | 16 +-- scapy/asn1/ber.py | 110 +++++----------- scapy/asn1/compound.py | 236 +++++++++++++++++++++++------------ scapy/asn1/constraints.py | 53 ++++---- scapy/asn1/context.py | 140 ++++++++++++++++++++- scapy/asn1/uper.py | 112 ++++++++--------- scapy/asn1fields.py | 36 +++--- test/scapy/layers/oer.uts | 20 ++- test/scapy/layers/uper.uts | 79 ++++++------ 11 files changed, 485 insertions(+), 328 deletions(-) diff --git a/.config/ci/check_commits.sh b/.config/ci/check_commits.sh index 8ca9bc16bc8..f5efc667c48 100755 --- a/.config/ci/check_commits.sh +++ b/.config/ci/check_commits.sh @@ -6,14 +6,7 @@ # We copy Wireshark's contributing guide, thanks to them for the idea ! # This script is inspired by https://gitlab.com/wireshark/wireshark/-/blob/master/.gitlab-ci.yml -# On pull_request, actions/checkout creates a merge of the PR into the base -# branch (HEAD^1=base tip, HEAD^2=PR tip). Restrict the check to PR commits -# so base-branch history is not false-failed for missing trailers. -if git rev-parse -q --verify HEAD^2 >/dev/null 2>&1; then - commits=$(git rev-list --no-merges HEAD^1..HEAD^2) -else - commits=$(git rev-list --no-merges --after="2026-01-00T00:00:00" --max-count=$((PR_FETCH_DEPTH - 1)) HEAD) -fi +commits=$(git rev-list --no-merges --after="2026-01-00T00:00:00" --max-count=$((PR_FETCH_DEPTH - 1)) HEAD) if [ -z "$commits" ]; then echo "No commit to check in PR. OK." exit 0 diff --git a/.config/codespell_ignore.txt b/.config/codespell_ignore.txt index b2e04b75727..7faaa0e70a3 100644 --- a/.config/codespell_ignore.txt +++ b/.config/codespell_ignore.txt @@ -56,5 +56,3 @@ webp widgits UPER uPER -acn -ACN diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index 540e088c375..5a53871f08b 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -28,15 +28,11 @@ Type, Union, cast, - TYPE_CHECKING, ) from typing import ( TypeVar, ) -if TYPE_CHECKING: - from scapy.asn1.ber import BERcodec_Object - try: from datetime import timezone except ImportError: @@ -128,7 +124,7 @@ def __str__(self): s += "\n### Already encoded ###\n%s" % self.encoded.strshow() else: s += "\n### Already encoded ###\n%r" % self.encoded - if self.remaining: + if self.remaining is not None: s += "\n### Remaining ###\n%r" % self.remaining return s @@ -152,7 +148,7 @@ def __str__(self): s += "\n### Already decoded ###\n%s" % self.decoded.strshow() else: s += "\n### Already decoded ###\n%r" % self.decoded - if self.remaining: + if self.remaining is not None: s += "\n### Remaining ###\n%r" % self.remaining return s @@ -178,7 +174,7 @@ def __new__(cls, class ASN1Codec(EnumElement): def register_stem(cls, stem): - # type: (Type[BERcodec_Object[Any]]) -> None + # type: (Type[Any]) -> None cls._stem = stem def register_tagging(cls, enc, dec): @@ -245,7 +241,7 @@ def __init__(self, key, # type: str value, # type: int context=None, # type: Optional[Type[ASN1_Class]] - codec=None # type: Optional[Dict[ASN1Codec, Type[BERcodec_Object[Any]]]] # noqa: E501 + codec=None # type: Optional[Dict[ASN1Codec, Type[Any]]] ): # type: (...) -> None EnumElement.__init__(self, key, value) @@ -270,11 +266,11 @@ def asn1_object(self, val): raise ASN1_Error("%r does not have any assigned ASN1 object" % self) def register(self, codecnum, codec): - # type: (ASN1Codec, Type[BERcodec_Object[Any]]) -> None + # type: (ASN1Codec, Type[Any]) -> None self._codec[codecnum] = codec def get_codec(self, codec): - # type: (Any) -> Type[BERcodec_Object[Any]] + # type: (Any) -> Type[Any] try: c = self._codec[codec] except KeyError: diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index b971e709ea9..f7935814432 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -58,47 +58,11 @@ class BER_Exception(Exception): class BER_Encoding_Error(ASN1_Encoding_Error): - def __init__(self, - msg, # type: str - encoded=None, # type: Optional[Union[BERcodec_Object[Any], str]] # noqa: E501 - remaining=b"" # type: bytes - ): - # type: (...) -> None - Exception.__init__(self, msg) - self.remaining = remaining - self.encoded = encoded - - def __str__(self): - # type: () -> str - s = Exception.__str__(self) - if isinstance(self.encoded, ASN1_Object): - s += "\n### Already encoded ###\n%s" % self.encoded.strshow() - else: - s += "\n### Already encoded ###\n%r" % self.encoded - s += "\n### Remaining ###\n%r" % self.remaining - return s + pass class BER_Decoding_Error(ASN1_Decoding_Error): - def __init__(self, - msg, # type: str - decoded=None, # type: Optional[Any] - remaining=b"" # type: bytes - ): - # type: (...) -> None - Exception.__init__(self, msg) - self.remaining = remaining - self.decoded = decoded - - def __str__(self): - # type: () -> str - s = Exception.__str__(self) - if isinstance(self.decoded, ASN1_Object): - s += "\n### Already decoded ###\n%s" % self.decoded.strshow() - else: - s += "\n### Already decoded ###\n%r" % self.decoded - s += "\n### Remaining ###\n%r" % self.remaining - return s + pass class BER_BadTag_Decoding_Error(BER_Decoding_Error, @@ -396,18 +360,16 @@ def safedec(cls, return cls.dec(s, context, safe=True, _depth=_depth) @classmethod - def enc(cls, s, field=None, size_len=None, **_kwargs): - # type: (_K, Any, Optional[int], **Any) -> bytes + def enc(cls, s, size_len=0, **_kwargs): + # type: (_K, Optional[int], **Any) -> bytes + # Ignore unknown kwargs (field=/pkt=/constraint keys from shared + # field._codec_kwargs()) so BER packets do not TypeError. + size_len = 0 if size_len is None else int(size_len) if isinstance(s, (str, bytes)): - return BERcodec_STRING.enc( - s, field=field, size_len=size_len, **_kwargs, - ) + return BERcodec_STRING.enc(s, size_len=size_len) else: try: - i = int(s) # type: ignore[call-overload] - return BERcodec_INTEGER.enc( - i, field=field, size_len=size_len, **_kwargs, - ) + return BERcodec_INTEGER.enc(int(s), size_len=size_len) # type: ignore # noqa: E501 except TypeError: raise TypeError("Trying to encode an invalid value !") @@ -416,15 +378,6 @@ def enc(cls, s, field=None, size_len=None, **_kwargs): ASN1_Codecs.BER.register_tagging(BER_tagging_enc, BER_tagging_dec) -def _ber_enc_size_len(field=None, size_len=None): - # type: (Any, Optional[int]) -> int - from scapy.asn1.constraints import field_size_len - sl = field_size_len(field, size_len) - if sl is None: - return 0 - return int(sl) - - ########################## # BERcodec objects # ########################## @@ -433,9 +386,9 @@ class BERcodec_INTEGER(BERcodec_Object[int]): tag = ASN1_Class_UNIVERSAL.INTEGER @classmethod - def enc(cls, i, field=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 - # type: (int, Any, Any, Optional[int], **Any) -> bytes - size_len = _ber_enc_size_len(field, size_len) + def enc(cls, i, size_len=0, **_kwargs): # type: ignore[override] + # type: (int, Optional[int], **Any) -> bytes + size_len = 0 if size_len is None else int(size_len) ls = [] while True: ls.append(i & 0xff) @@ -501,9 +454,9 @@ def do_dec(cls, ) @classmethod - def enc(cls, _s, field=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 - # type: (AnyStr, Any, Any, Optional[int], **Any) -> bytes - size_len = _ber_enc_size_len(field, size_len) + def enc(cls, _s, size_len=0, **_kwargs): # type: ignore[override] + # type: (AnyStr, Optional[int], **Any) -> bytes + size_len = 0 if size_len is None else int(size_len) # /!\ this is DER encoding (bit strings are only zero-bit padded) s = bytes_encode(_s) if len(s) % 8 == 0: @@ -521,9 +474,9 @@ class BERcodec_STRING(BERcodec_Object[str]): tag = ASN1_Class_UNIVERSAL.STRING @classmethod - def enc(cls, _s, field=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 - # type: (Union[str, bytes], Any, Any, Optional[int], **Any) -> bytes - size_len = _ber_enc_size_len(field, size_len) + def enc(cls, _s, size_len=0, **_kwargs): # type: ignore[override] + # type: (Union[str, bytes], Optional[int], **Any) -> bytes + size_len = 0 if size_len is None else int(size_len) s = bytes_encode(_s) # Be sure we are encoding bytes return chb(int(cls.tag)) + BER_len_enc(len(s), size=size_len) + s @@ -544,23 +497,21 @@ class BERcodec_NULL(BERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.NULL @classmethod - def enc(cls, i, field=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 - # type: (int, Any, Any, Optional[int], **Any) -> bytes + def enc(cls, i, size_len=0, **_kwargs): # type: ignore[override] + # type: (int, Optional[int], **Any) -> bytes if i == 0: return chb(int(cls.tag)) + b"\0" else: - return super(cls, cls).enc( - i, field=field, size_len=size_len, **_kwargs, - ) + return super(cls, cls).enc(i, size_len=size_len) class BERcodec_OID(BERcodec_Object[bytes]): tag = ASN1_Class_UNIVERSAL.OID @classmethod - def enc(cls, _oid, field=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 - # type: (AnyStr, Any, Any, Optional[int], **Any) -> bytes - size_len = _ber_enc_size_len(field, size_len) + def enc(cls, _oid, size_len=0, **_kwargs): # type: ignore[override] + # type: (AnyStr, Optional[int], **Any) -> bytes + size_len = 0 if size_len is None else int(size_len) oid = bytes_encode(_oid) if oid: lst = [int(x) for x in oid.strip(b".").split(b".")] @@ -652,16 +603,13 @@ class BERcodec_SEQUENCE(BERcodec_Object[Union[bytes, List[BERcodec_Object[Any]]] tag = ASN1_Class_UNIVERSAL.SEQUENCE @classmethod - def enc(cls, _ll, field=None, size_len=None, **_kwargs): # type: ignore[override] # noqa: E501 - # type: (Union[bytes, List[BERcodec_Object[Any]]], Any, Any, Optional[int], **Any) -> bytes # noqa: E501 + def enc(cls, _ll, size_len=None, **_kwargs): # type: ignore[override] + # type: (Union[bytes, List[BERcodec_Object[Any]]], Optional[int], **Any) -> bytes # noqa: E501 if isinstance(_ll, bytes): ll = _ll else: ll = b"".join(x.enc(cls.codec) for x in _ll) # None = apply conf; explicit 0 keeps short-form lengths. - if size_len is None: - from scapy.asn1.constraints import field_size_len - size_len = field_size_len(field, None) if size_len is None: size_len = conf.ASN1_default_long_size return chb(int(cls.tag)) + BER_len_enc(len(ll), size=size_len) + ll @@ -711,9 +659,9 @@ class BERcodec_IPADDRESS(BERcodec_STRING): tag = ASN1_Class_UNIVERSAL.IPADDRESS @classmethod - def enc(cls, ipaddr_ascii, field=None, size_len=None, **_kwargs): # type: ignore # noqa: E501 - # type: (str, Any, Any, Optional[int], **Any) -> bytes - size_len = _ber_enc_size_len(field, size_len) + def enc(cls, ipaddr_ascii, size_len=0, **_kwargs): # type: ignore[override] + # type: (str, Optional[int], **Any) -> bytes + size_len = 0 if size_len is None else int(size_len) try: s = inet_aton(ipaddr_ascii) except Exception: diff --git a/scapy/asn1/compound.py b/scapy/asn1/compound.py index 7ac71f3cc86..29cae87ea67 100644 --- a/scapy/asn1/compound.py +++ b/scapy/asn1/compound.py @@ -122,51 +122,22 @@ def _sequence_encode_children(field, pkt, encode): def sequence_encode_to(field, pkt, enc): # type: (Any, Any, Any) -> None - bit_enc = per_bit_encoder(enc) - if bit_enc is not None: - write_uper_presence_bits(bit_enc, field, pkt) - _sequence_encode_children( - field, pkt, - lambda obj: obj.encode_to(pkt, enc), - ) - return - if enc.codec is ASN1_Codecs.OER: - enc.write(write_oer_presence_bits(sequence_presence_bits(field, pkt))) - _sequence_encode_children( - field, pkt, - lambda obj: obj.encode_to(pkt, enc), - ) - return + enc.encode_sequence(field, pkt) + + +def sequence_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + dec.decode_sequence(field, pkt) + + +def ber_sequence_encode_to(field, pkt, enc): + # type: (Any, Any, Any) -> None s = reduce(lambda x, y: x + y.build(pkt), field.seq, b"") enc.write(field.i2m(pkt, s)) -def sequence_decode_from(field, pkt, dec): +def ber_sequence_decode_from(field, pkt, dec): # type: (Any, Any, Any) -> None - bit_dec = per_bit_decoder(dec) - if bit_dec is not None: - presence = read_uper_presence_bits(bit_dec, field) - _sequence_decode_children( - field, pkt, presence, - lambda obj: obj.decode_from(pkt, dec), - ) - return - if dec.codec is ASN1_Codecs.OER: - s = dec.remaining() - s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) - if not s: - for obj in field.seq: - obj.set_val(pkt, None) - dec.set_remainder(b"") - return - presence, s = read_oer_presence_bits(s, field) - child_dec = OER_Decoder(s) - _sequence_decode_children( - field, pkt, presence, - lambda obj: obj.decode_from(pkt, child_dec), - ) - dec.set_remainder(child_dec.remaining()) - return s = dec.remaining() s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) from scapy.asn1.ber import BER_Decoding_Error @@ -181,16 +152,62 @@ def sequence_decode_from(field, pkt, dec): dec.set_remainder(remain) +def oer_sequence_encode_to(field, pkt, enc): + # type: (Any, Any, Any) -> None + enc.write(write_oer_presence_bits(sequence_presence_bits(field, pkt))) + _sequence_encode_children( + field, pkt, + lambda obj: obj.encode_to(pkt, enc), + ) + + +def oer_sequence_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + s = dec.remaining() + s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) + presence, s = read_oer_presence_bits(s, field) + child_dec = OER_Decoder(s) + _sequence_decode_children( + field, pkt, presence, + lambda obj: obj.decode_from(pkt, child_dec), + ) + dec.set_remainder(child_dec.remaining()) + + +def uper_sequence_encode_to(field, pkt, enc): + # type: (Any, Any, Any) -> None + bit_enc = per_bit_encoder(enc) + write_uper_presence_bits(bit_enc, field, pkt) + _sequence_encode_children( + field, pkt, + lambda obj: obj.encode_to(pkt, enc), + ) + + +def uper_sequence_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + bit_dec = per_bit_decoder(dec) + presence = read_uper_presence_bits(bit_dec, field) + _sequence_decode_children( + field, pkt, presence, + lambda obj: obj.decode_from(pkt, dec), + ) + + # ---- SEQUENCE OF ---------------------------------------------------------- def sequence_of_encode_to(field, pkt, enc): # type: (Any, Any, Any) -> None - if per_bit_encoder(enc) is not None: - uper_sequence_of_encode_into(field, enc, pkt) - return - if enc.codec is ASN1_Codecs.OER: - enc.write(oer_sequence_of_bytes(field, pkt)) - return + enc.encode_sequence_of(field, pkt) + + +def sequence_of_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + dec.decode_sequence_of(field, pkt) + + +def ber_sequence_of_encode_to(field, pkt, enc): + # type: (Any, Any, Any) -> None val = getattr(pkt, field.name) if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: s = val # type: Any @@ -203,20 +220,8 @@ def sequence_of_encode_to(field, pkt, enc): enc.write(field.i2m(pkt, s)) -def sequence_of_decode_from(field, pkt, dec): +def ber_sequence_of_decode_from(field, pkt, dec): # type: (Any, Any, Any) -> None - bit_dec = per_bit_decoder(dec) - if bit_dec is not None: - field.set_val( - pkt, - uper_sequence_of_decode_from_decoder(field, pkt, dec), - ) - return - if dec.codec is ASN1_Codecs.OER: - val, remain = oer_sequence_of_decode(field, pkt, dec.remaining()) - field.set_val(pkt, val) - dec.set_remainder(remain) - return from scapy.asn1.ber import BER_Decoding_Error s = dec.remaining() s = field._apply_tagging_dec(s, pkt) @@ -236,6 +241,31 @@ def sequence_of_decode_from(field, pkt, dec): dec.set_remainder(remain) +def oer_sequence_of_encode_to(field, pkt, enc): + # type: (Any, Any, Any) -> None + enc.write(oer_sequence_of_bytes(field, pkt)) + + +def oer_sequence_of_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + val, remain = oer_sequence_of_decode(field, pkt, dec.remaining()) + field.set_val(pkt, val) + dec.set_remainder(remain) + + +def uper_sequence_of_encode_to(field, pkt, enc): + # type: (Any, Any, Any) -> None + uper_sequence_of_encode_into(field, enc, pkt) + + +def uper_sequence_of_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + field.set_val( + pkt, + uper_sequence_of_decode_from_decoder(field, pkt, dec), + ) + + def oer_sequence_of_bytes(field, pkt): # type: (Any, Any) -> bytes from scapy.asn1.oer import OER_unsigned_integer_enc @@ -349,33 +379,53 @@ def _uper_count_enc(field, enc, count, append_items): # ---- CHOICE ------------------------------------------------------------- def choice_encode_to(field, pkt, enc, value=None): + # type: (Any, Any, Any, Any) -> None + enc.encode_choice(field, pkt, value) + + +def choice_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + dec.decode_choice(field, pkt) + + +def ber_choice_encode_to(field, pkt, enc, value=None): # type: (Any, Any, Any, Any) -> None if value is None: value = getattr(pkt, field.name) - if per_bit_encoder(enc) is not None: - uper_choice_encode_into(field, enc, pkt, value) - return - if enc.codec is ASN1_Codecs.OER: - enc.write(oer_choice_bytes(field, pkt, value)) - return enc.write(ber_choice_bytes(field, pkt, value)) -def choice_decode_from(field, pkt, dec): +def ber_choice_decode_from(field, pkt, dec): # type: (Any, Any, Any) -> None - if per_bit_decoder(dec) is not None: - field.set_val(pkt, uper_choice_decode_from_decoder(field, pkt, dec)) - return - if dec.codec is ASN1_Codecs.OER: - val, remain = oer_choice_decode(field, pkt, dec.remaining()) - field.set_val(pkt, val) - dec.set_remainder(remain) - return val, remain = ber_choice_decode(field, pkt, dec.remaining()) field.set_val(pkt, val) dec.set_remainder(remain) +def oer_choice_encode_to(field, pkt, enc, value=None): + # type: (Any, Any, Any, Any) -> None + if value is None: + value = getattr(pkt, field.name) + enc.write(oer_choice_bytes(field, pkt, value)) + + +def oer_choice_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + val, remain = oer_choice_decode(field, pkt, dec.remaining()) + field.set_val(pkt, val) + dec.set_remainder(remain) + + +def uper_choice_encode_to(field, pkt, enc, value=None): + # type: (Any, Any, Any, Any) -> None + uper_choice_encode_into(field, enc, pkt, value) + + +def uper_choice_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + field.set_val(pkt, uper_choice_decode_from_decoder(field, pkt, dec)) + + def ber_choice_decode(field, pkt, s): # type: (Any, Any, bytes) -> Tuple[Any, bytes] from scapy.asn1.ber import BER_id_dec @@ -543,25 +593,49 @@ def uper_choice_decode_from_decoder(field, pkt, dec): # ---- PACKET (nested ASN1_Packet) ------------------------------------------ def packet_encode_to(field, pkt, enc, value=None): + # type: (Any, Any, Any, Any) -> None + enc.encode_packet(field, pkt, value) + + +def packet_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + dec.decode_packet(field, pkt) + + +def ber_packet_encode_to(field, pkt, enc, value=None): # type: (Any, Any, Any, Any) -> None if value is None: value = getattr(pkt, field.name) - if per_bit_encoder(enc) is not None: - uper_packet_encode_into(field, enc, pkt, value) - return enc.write(ber_oer_packet_bytes(field, pkt, value)) -def packet_decode_from(field, pkt, dec): +def ber_packet_decode_from(field, pkt, dec): # type: (Any, Any, Any) -> None - if per_bit_decoder(dec) is not None: - field.set_val(pkt, uper_packet_decode_from_decoder(field, pkt, dec)) - return val, remain = ber_oer_packet_decode(field, pkt, dec.remaining()) field.set_val(pkt, val) dec.set_remainder(remain) +def oer_packet_encode_to(field, pkt, enc, value=None): + # type: (Any, Any, Any, Any) -> None + ber_packet_encode_to(field, pkt, enc, value) + + +def oer_packet_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + ber_packet_decode_from(field, pkt, dec) + + +def uper_packet_encode_to(field, pkt, enc, value=None): + # type: (Any, Any, Any, Any) -> None + uper_packet_encode_into(field, enc, pkt, value) + + +def uper_packet_decode_from(field, pkt, dec): + # type: (Any, Any, Any) -> None + field.set_val(pkt, uper_packet_decode_from_decoder(field, pkt, dec)) + + def ber_oer_packet_decode(field, pkt, s): # type: (Any, Any, bytes) -> Tuple[Any, bytes] cls = (field.next_cls_cb(pkt) or field.cls) if field.next_cls_cb else field.cls diff --git a/scapy/asn1/constraints.py b/scapy/asn1/constraints.py index 4a381609e70..f741b8bb0da 100644 --- a/scapy/asn1/constraints.py +++ b/scapy/asn1/constraints.py @@ -68,28 +68,39 @@ def field_size_len(field=None, size_len=None): return None -def oer_unsigned(field=None, oer_unsigned=None): +def field_unsigned(field=None, unsigned=None): # type: (Any, Optional[bool]) -> bool - if oer_unsigned is not None: - return oer_unsigned + if unsigned is not None: + return unsigned if field is not None: return field.constraints.unsigned return False -def uper_extensible(field=None, uper_extensible=None): +# Compat alias used by OER codec kwargs named ``oer_unsigned``. +def oer_unsigned(field=None, oer_unsigned=None): + # type: (Any, Optional[bool]) -> bool + return field_unsigned(field, oer_unsigned) + + +def field_extensible_kw(field=None, extensible=None): # type: (Any, Optional[bool]) -> bool - if uper_extensible is not None: - return uper_extensible + if extensible is not None: + return extensible if field is not None: - return field.constraints.extensible + return field_extensible(field) return False -def uper_int_range(field=None, uper_min=None, uper_max=None): +def uper_extensible(field=None, uper_extensible=None): + # type: (Any, Optional[bool]) -> bool + return field_extensible_kw(field, uper_extensible) + + +def uper_int_range(field=None, minimum=None, maximum=None): # type: (Any, Optional[int], Optional[int]) -> Tuple[Optional[int], Optional[int]] - if uper_min is not None or uper_max is not None: - return uper_min, uper_max + if minimum is not None or maximum is not None: + return minimum, maximum if field is not None: return field_range(field) return None, None @@ -97,8 +108,8 @@ def uper_int_range(field=None, uper_min=None, uper_max=None): def resolve_uper_int_bounds(field=None, # type: Any size_len=None, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] + minimum=None, # type: Optional[int] + maximum=None, # type: Optional[int] unsigned=None, # type: Optional[bool] extensible=None # type: Optional[bool] ): @@ -109,9 +120,9 @@ def resolve_uper_int_bounds(field=None, # type: Any 1, 2, 4, or 8 with ``unsigned=True`` implies ``0 .. 256**n - 1``. """ size_len = field_size_len(field, size_len) - minimum, maximum = uper_int_range(field, uper_min, uper_max) - is_unsigned = oer_unsigned(field, unsigned) - is_extensible = uper_extensible(field, extensible) + minimum, maximum = uper_int_range(field, minimum, maximum) + is_unsigned = field_unsigned(field, unsigned) + is_extensible = field_extensible_kw(field, extensible) if minimum is None and maximum is None: if size_len in (1, 2, 4, 8) and is_unsigned: minimum, maximum = 0, (256 ** size_len) - 1 @@ -120,18 +131,18 @@ def resolve_uper_int_bounds(field=None, # type: Any def resolve_uper_size_bounds(field=None, # type: Any size_len=None, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] + minimum=None, # type: Optional[int] + maximum=None, # type: Optional[int] extensible=None # type: Optional[bool] ): # type: (...) -> Tuple[Optional[int], Optional[int], bool] """Resolve UPER SIZE bounds; ``size_len`` is a fixed SIZE.""" size_len = field_size_len(field, size_len) - uper_min, uper_max = uper_int_range(field, uper_min, uper_max) - is_extensible = uper_extensible(field, extensible) + minimum, maximum = uper_int_range(field, minimum, maximum) + is_extensible = field_extensible_kw(field, extensible) if size_len: return size_len, size_len, is_extensible - return uper_min, uper_max, is_extensible + return minimum, maximum, is_extensible def resolve_oer_size_bounds(field=None, size_len=None): @@ -167,7 +178,7 @@ def oer_int_wire_params(field=None, size_len=None, unsigned=None): is used only when ``maximum <= 2**64 - 1``. """ size_len = field_size_len(field, size_len) - is_unsigned = oer_unsigned(field, unsigned) + is_unsigned = field_unsigned(field, unsigned) minimum, maximum = field_range(field) if field is not None else (None, None) extensible = field_extensible(field) if field is not None else False diff --git a/scapy/asn1/context.py b/scapy/asn1/context.py index a918ffd16a3..5355dee662e 100644 --- a/scapy/asn1/context.py +++ b/scapy/asn1/context.py @@ -18,6 +18,26 @@ def finish(self): # type: () -> bytes raise NotImplementedError + def encode_sequence(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import ber_sequence_encode_to + ber_sequence_encode_to(field, pkt, self) + + def encode_sequence_of(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import ber_sequence_of_encode_to + ber_sequence_of_encode_to(field, pkt, self) + + def encode_choice(self, field, pkt, value=None): + # type: (Any, Any, Any) -> None + from scapy.asn1.compound import ber_choice_encode_to + ber_choice_encode_to(field, pkt, self, value) + + def encode_packet(self, field, pkt, value=None): + # type: (Any, Any, Any) -> None + from scapy.asn1.compound import ber_packet_encode_to + ber_packet_encode_to(field, pkt, self, value) + class ASN1Decoder(object): codec = None # type: Any @@ -30,6 +50,26 @@ def set_remainder(self, remainder): # type: (bytes) -> None raise NotImplementedError + def decode_sequence(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import ber_sequence_decode_from + ber_sequence_decode_from(field, pkt, self) + + def decode_sequence_of(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import ber_sequence_of_decode_from + ber_sequence_of_decode_from(field, pkt, self) + + def decode_choice(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import ber_choice_decode_from + ber_choice_decode_from(field, pkt, self) + + def decode_packet(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import ber_packet_decode_from + ber_packet_decode_from(field, pkt, self) + class BER_Encoder(ASN1Encoder): from scapy.asn1.asn1 import ASN1_Codecs @@ -78,6 +118,26 @@ def __init__(self): # type: () -> None super(OER_Encoder, self).__init__(codec=self.codec) + def encode_sequence(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import oer_sequence_encode_to + oer_sequence_encode_to(field, pkt, self) + + def encode_sequence_of(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import oer_sequence_of_encode_to + oer_sequence_of_encode_to(field, pkt, self) + + def encode_choice(self, field, pkt, value=None): + # type: (Any, Any, Any) -> None + from scapy.asn1.compound import oer_choice_encode_to + oer_choice_encode_to(field, pkt, self, value) + + def encode_packet(self, field, pkt, value=None): + # type: (Any, Any, Any) -> None + from scapy.asn1.compound import oer_packet_encode_to + oer_packet_encode_to(field, pkt, self, value) + class OER_Decoder(BER_Decoder): from scapy.asn1.asn1 import ASN1_Codecs @@ -88,6 +148,26 @@ def __init__(self, data): # type: (bytes) -> None super(OER_Decoder, self).__init__(data, codec=self.codec) + def decode_sequence(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import oer_sequence_decode_from + oer_sequence_decode_from(field, pkt, self) + + def decode_sequence_of(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import oer_sequence_of_decode_from + oer_sequence_of_decode_from(field, pkt, self) + + def decode_choice(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import oer_choice_decode_from + oer_choice_decode_from(field, pkt, self) + + def decode_packet(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import oer_packet_decode_from + oer_packet_decode_from(field, pkt, self) + class UPER_EncoderContext(ASN1Encoder): from scapy.asn1.asn1 import ASN1_Codecs @@ -108,6 +188,26 @@ def finish(self): # type: () -> bytes return self._enc.as_bytes() + def encode_sequence(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import uper_sequence_encode_to + uper_sequence_encode_to(field, pkt, self) + + def encode_sequence_of(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import uper_sequence_of_encode_to + uper_sequence_of_encode_to(field, pkt, self) + + def encode_choice(self, field, pkt, value=None): + # type: (Any, Any, Any) -> None + from scapy.asn1.compound import uper_choice_encode_to + uper_choice_encode_to(field, pkt, self, value) + + def encode_packet(self, field, pkt, value=None): + # type: (Any, Any, Any) -> None + from scapy.asn1.compound import uper_packet_encode_to + uper_packet_encode_to(field, pkt, self, value) + class UPER_DecoderContext(ASN1Decoder): from scapy.asn1.asn1 import ASN1_Codecs @@ -133,6 +233,26 @@ def set_remainder(self, remainder): from scapy.asn1.uper import UPER_Decoder self._dec = UPER_Decoder(remainder) + def decode_sequence(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import uper_sequence_decode_from + uper_sequence_decode_from(field, pkt, self) + + def decode_sequence_of(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import uper_sequence_of_decode_from + uper_sequence_of_decode_from(field, pkt, self) + + def decode_choice(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import uper_choice_decode_from + uper_choice_decode_from(field, pkt, self) + + def decode_packet(self, field, pkt): + # type: (Any, Any) -> None + from scapy.asn1.compound import uper_packet_decode_from + uper_packet_decode_from(field, pkt, self) + def new_encoder(codec): # type: (Any) -> ASN1Encoder @@ -141,7 +261,7 @@ def new_encoder(codec): return UPER_EncoderContext() if codec is ASN1_Codecs.OER: return OER_Encoder() - return BER_Encoder() + return BER_Encoder(codec=codec) def new_decoder(codec, data): @@ -151,12 +271,18 @@ def new_decoder(codec, data): return UPER_DecoderContext(data) if codec is ASN1_Codecs.OER: return OER_Decoder(data) - return BER_Decoder(data) + return BER_Decoder(data, codec=codec) def per_bit_encoder(enc): # type: (Any) -> Any - """Return the PER bit encoder, or *None* for byte-oriented contexts.""" + """Return the PER bit encoder, or *None* for byte-oriented contexts. + + Prefer ``enc.bit_encoder`` on ``UPER_EncoderContext``. The + ``isinstance(UPER_Encoder)`` check is only a nested codec-internal + fallback so call sites that already hold a bare bit stream keep + working. + """ from scapy.asn1.asn1 import ASN1_Codecs codec = getattr(enc, "codec", None) if codec is not None and codec is not ASN1_Codecs.PER: @@ -172,7 +298,13 @@ def per_bit_encoder(enc): def per_bit_decoder(dec): # type: (Any) -> Any - """Return the PER bit decoder, or *None* for byte-oriented contexts.""" + """Return the PER bit decoder, or *None* for byte-oriented contexts. + + Prefer ``dec.bit_decoder`` on ``UPER_DecoderContext``. The + ``isinstance(UPER_Decoder)`` check is only a nested codec-internal + fallback so call sites that already hold a bare bit stream keep + working. + """ from scapy.asn1.asn1 import ASN1_Codecs codec = getattr(dec, "codec", None) if codec is not None and codec is not ASN1_Codecs.PER: diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index bd75b5355a6..842bccef286 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -16,8 +16,7 @@ Not supported yet: extension additions (an encoding that carries them is refused rather than misparsed), SET, REAL, and the known-multiplier character -string encodings, which are emitted as plain octets rather than 7 or 4 bits -per character. +string encodings (rejected rather than emitted as plain octets). ``ASN1F_CHOICE`` alternatives are indexed in X.691 10.2 canonical tag order (via ``ASN1F_CHOICE.canonical_order``). Declaration order is kept for @@ -471,12 +470,13 @@ def encode_into(cls, enc, s, **kwargs): UPERcodec_STRING.encode_into(enc, s, **kwargs) return try: - UPERcodec_INTEGER.encode_into(enc, int(s), **kwargs) - except Exception: + i = int(s) + except (TypeError, ValueError): raise UPER_Encoding_Error( "Cannot encode value %r for %s" % (s, cls.__name__), encoded=s ) + UPERcodec_INTEGER.encode_into(enc, i, **kwargs) @classmethod def dec_from_decoder(cls, dec, **kwargs): @@ -534,16 +534,16 @@ def encode_into(cls, i, # type: int field=None, # type: Any size_len=None, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=None, # type: Optional[bool] - uper_extensible=None, # type: Optional[bool] + minimum=None, # type: Optional[int] + maximum=None, # type: Optional[int] + unsigned=None, # type: Optional[bool] + extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> None from scapy.asn1.constraints import resolve_uper_int_bounds minimum, maximum, extensible = resolve_uper_int_bounds( - field, size_len, uper_min, uper_max, oer_unsigned, uper_extensible, + field, size_len, minimum, maximum, unsigned, extensible, ) if extensible and minimum is not None and maximum is not None: if minimum <= i <= maximum: @@ -565,16 +565,16 @@ def dec_from_decoder(cls, field=None, # type: Any pkt=None, # type: Any size_len=None, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - oer_unsigned=None, # type: Optional[bool] - uper_extensible=None, # type: Optional[bool] + minimum=None, # type: Optional[int] + maximum=None, # type: Optional[int] + unsigned=None, # type: Optional[bool] + extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> ASN1_Object[int] from scapy.asn1.constraints import resolve_uper_int_bounds minimum, maximum, extensible = resolve_uper_int_bounds( - field, size_len, uper_min, uper_max, oer_unsigned, uper_extensible, + field, size_len, minimum, maximum, unsigned, extensible, ) if extensible and minimum is not None and maximum is not None: if dec.read_bit(): @@ -618,9 +618,9 @@ def encode_into(cls, _s, # type: Any field=None, # type: Any size_len=None, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - uper_extensible=None, # type: Optional[bool] + minimum=None, # type: Optional[int] + maximum=None, # type: Optional[int] + extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> None @@ -638,7 +638,7 @@ def encode_into(cls, s = bytes_encode(_s) nbits = 8 * len(s) minimum, maximum, extensible = resolve_uper_size_bounds( - field, size_len, uper_min, uper_max, uper_extensible, + field, size_len, minimum, maximum, extensible, ) if extensible and minimum is not None and maximum is not None: if minimum <= nbits <= maximum: @@ -676,15 +676,15 @@ def dec_from_decoder(cls, dec, # type: UPER_Decoder field=None, # type: Any size_len=None, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - uper_extensible=None, # type: Optional[bool] + minimum=None, # type: Optional[int] + maximum=None, # type: Optional[int] + extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> ASN1_Object[str] from scapy.asn1.constraints import resolve_uper_size_bounds minimum, maximum, extensible = resolve_uper_size_bounds( - field, size_len, uper_min, uper_max, uper_extensible, + field, size_len, minimum, maximum, extensible, ) def _read_unconstrained(): @@ -725,16 +725,16 @@ def encode_into(cls, _s, # type: Union[str, bytes] field=None, # type: Any size_len=None, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - uper_extensible=None, # type: Optional[bool] + minimum=None, # type: Optional[int] + maximum=None, # type: Optional[int] + extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> None from scapy.asn1.constraints import resolve_uper_size_bounds s = bytes_encode(_s) minimum, maximum, extensible = resolve_uper_size_bounds( - field, size_len, uper_min, uper_max, uper_extensible, + field, size_len, minimum, maximum, extensible, ) UPER_octet_string_enc(enc, s, minimum, maximum, extensible) @@ -743,15 +743,15 @@ def dec_from_decoder(cls, dec, # type: UPER_Decoder field=None, # type: Any size_len=None, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] - uper_extensible=None, # type: Optional[bool] + minimum=None, # type: Optional[int] + maximum=None, # type: Optional[int] + extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> ASN1_Object[Any] from scapy.asn1.constraints import resolve_uper_size_bounds minimum, maximum, extensible = resolve_uper_size_bounds( - field, size_len, uper_min, uper_max, uper_extensible, + field, size_len, minimum, maximum, extensible, ) raw = UPER_octet_string_dec(dec, minimum, maximum, extensible) return cls.asn1_object(raw) @@ -842,25 +842,25 @@ def encode_into(cls, field=None, # type: Any pkt=None, # type: Any size_len=None, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] + minimum=None, # type: Optional[int] + maximum=None, # type: Optional[int] uper_enum_values=None, # type: Optional[List[int]] - uper_extensible=None, # type: Optional[bool] + extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> None from scapy.asn1.constraints import ( field_size_len, uper_enum_values as _uper_enum_values, - uper_extensible as _uper_extensible, + field_extensible_kw, uper_int_range, ) size_len = field_size_len(field, size_len) - uper_min, uper_max = uper_int_range(field, uper_min, uper_max) + minimum, maximum = uper_int_range(field, minimum, maximum) uper_enum_values = _uper_enum_values( field, pkt, uper_enum_values, ) - extensible = _uper_extensible(field, uper_extensible) + extensible = field_extensible_kw(field, extensible) if uper_enum_values is not None: if extensible: # X.691 14.3: a one bit prefix says whether the value is an @@ -873,10 +873,10 @@ def encode_into(cls, enc.append_bit(0) UPER_enumerated_enc(enc, i, uper_enum_values) return - minimum, maximum = cls._range( - size_len, uper_min, uper_max, UPER_Encoding_Error + lo, hi = cls._range( + size_len, minimum, maximum, UPER_Encoding_Error ) - UPER_constrained_int_enc(enc, i, minimum, maximum) + UPER_constrained_int_enc(enc, i, lo, hi) @classmethod def dec_from_decoder(cls, @@ -884,25 +884,25 @@ def dec_from_decoder(cls, field=None, # type: Any pkt=None, # type: Any size_len=None, # type: Optional[int] - uper_min=None, # type: Optional[int] - uper_max=None, # type: Optional[int] + minimum=None, # type: Optional[int] + maximum=None, # type: Optional[int] uper_enum_values=None, # type: Optional[List[int]] - uper_extensible=None, # type: Optional[bool] + extensible=None, # type: Optional[bool] **_kwargs # type: Any ): # type: (...) -> ASN1_Object[int] from scapy.asn1.constraints import ( field_size_len, uper_enum_values as _uper_enum_values, - uper_extensible as _uper_extensible, + field_extensible_kw, uper_int_range, ) size_len = field_size_len(field, size_len) - uper_min, uper_max = uper_int_range(field, uper_min, uper_max) + minimum, maximum = uper_int_range(field, minimum, maximum) uper_enum_values = _uper_enum_values( field, pkt, uper_enum_values, ) - extensible = _uper_extensible(field, uper_extensible) + extensible = field_extensible_kw(field, extensible) if uper_enum_values is not None: if extensible and dec.read_bit(): raise UPER_Decoding_Error( @@ -910,26 +910,26 @@ def dec_from_decoder(cls, "supported" ) return cls.asn1_object(UPER_enumerated_dec(dec, uper_enum_values)) - minimum, maximum = cls._range( - size_len, uper_min, uper_max, UPER_Decoding_Error + lo, hi = cls._range( + size_len, minimum, maximum, UPER_Decoding_Error ) value = dec.read_non_negative_binary_integer( - UPER_bits_for_range(maximum - minimum) - ) + minimum + UPER_bits_for_range(hi - lo) + ) + lo return cls.asn1_object(value) @staticmethod - def _range(size_len, uper_min, uper_max, error): + def _range(size_len, minimum, maximum, error): # type: (Optional[int], Optional[int], Optional[int], Any) -> Tuple[int, int] # noqa: E501 # Without the enumeration itself the index range has to come from # the declared bounds; deriving it from the value at hand would # make the width depend on the value, which the decoder cannot # reproduce. - minimum = uper_min if uper_min is not None else 0 - maximum = uper_max if uper_max is not None else (size_len or None) - if maximum is None: + lo = minimum if minimum is not None else 0 + hi = maximum if maximum is not None else (size_len or None) + if hi is None: raise error("UPERcodec_ENUMERATED: missing range") - return minimum, maximum + return lo, hi class UPERcodec_SEQUENCE(UPERcodec_Object[Union[bytes, List[Any]]]): @@ -975,7 +975,7 @@ def encode_into(cls, enc, ipaddr_ascii, **_kwargs): # type: (UPER_Encoder, str, **Any) -> None try: s = inet_aton(ipaddr_ascii) - except Exception: + except (TypeError, ValueError, OSError): raise UPER_Encoding_Error("IPv4 address could not be encoded") UPER_octet_string_enc(enc, s, 4, 4) @@ -985,7 +985,7 @@ def dec_from_decoder(cls, dec, **_kwargs): raw = UPER_octet_string_dec(dec, 4, 4) try: ipaddr_ascii = inet_ntoa(raw) - except Exception: + except (TypeError, ValueError, OSError): raise UPER_Decoding_Error("IP address could not be decoded") return cls.asn1_object(ipaddr_ascii) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index b03c8fac319..110f862be45 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -331,14 +331,16 @@ def encode_into(self, enc, pkt, value=None): def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None if per_bit_encoder(enc) is not None: + # Pass the ASN.1 context (not the bare bit stream). self.encode_into(enc, pkt) else: enc.write(self.i2m(pkt, getattr(pkt, self.name))) def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None - if per_bit_decoder(dec) is not None: - self.dissect_from_decoder(pkt, per_bit_decoder(dec)) + bit_dec = per_bit_decoder(dec) + if bit_dec is not None: + self.dissect_from_decoder(pkt, bit_dec) else: val, remain = self.m2i(pkt, dec.remaining()) self.set_val(pkt, val) @@ -671,8 +673,7 @@ def m2i(self, pkt, s): def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None - from scapy.asn1.compound import sequence_encode_to - sequence_encode_to(self, pkt, enc) + enc.encode_sequence(self, pkt) def encode_into(self, enc, pkt, value=None): # type: (Any, ASN1_Packet, Any) -> None @@ -684,8 +685,7 @@ def dissect_from_decoder(self, pkt, dec): def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None - from scapy.asn1.compound import sequence_decode_from - sequence_decode_from(self, pkt, dec) + dec.decode_sequence(self, pkt) class ASN1F_SET(ASN1F_SEQUENCE): @@ -754,13 +754,11 @@ def m2i(self, pkt, s): def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None - from scapy.asn1.compound import sequence_of_encode_to - sequence_of_encode_to(self, pkt, enc) + enc.encode_sequence_of(self, pkt) def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None - from scapy.asn1.compound import sequence_of_decode_from - sequence_of_decode_from(self, pkt, dec) + dec.decode_sequence_of(self, pkt) def i2repr(self, pkt, x): # type: (ASN1_Packet, _I) -> str @@ -1044,18 +1042,15 @@ def m2i(self, pkt, s): def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None - from scapy.asn1.compound import choice_encode_to - choice_encode_to(self, pkt, enc) + enc.encode_choice(self, pkt) def encode_into(self, enc, pkt, value=None): # type: (Any, ASN1_Packet, Any) -> None - from scapy.asn1.compound import choice_encode_to - choice_encode_to(self, pkt, enc, value) + enc.encode_choice(self, pkt, value) def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None - from scapy.asn1.compound import choice_decode_from - choice_decode_from(self, pkt, dec) + dec.decode_choice(self, pkt) def randval(self): # type: () -> RandChoice @@ -1112,18 +1107,15 @@ def m2i(self, pkt, s): def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None - from scapy.asn1.compound import packet_encode_to - packet_encode_to(self, pkt, enc) + enc.encode_packet(self, pkt) def encode_into(self, enc, pkt, value=None): # type: (Any, ASN1_Packet, Any) -> None - from scapy.asn1.compound import packet_encode_to - packet_encode_to(self, pkt, enc, value) + enc.encode_packet(self, pkt, value) def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None - from scapy.asn1.compound import packet_decode_from - packet_decode_from(self, pkt, dec) + dec.decode_packet(self, pkt) def any2i(self, pkt, # type: ASN1_Packet diff --git a/test/scapy/layers/oer.uts b/test/scapy/layers/oer.uts index 2e9c6fa5cc5..e61c06cf71c 100644 --- a/test/scapy/layers/oer.uts +++ b/test/scapy/layers/oer.uts @@ -987,7 +987,7 @@ True = oer dec reads constraints from field # Direct codec calls without a field ignore schema constraints in kwargs. x, remain = OERcodec_ENUMERATED.dec( - b"\x01", uper_enum_values=[0, 1], uper_min=0, + b"\x01", uper_enum_values=[0, 1], minimum=0, ) assert x.val == 1 @@ -1241,10 +1241,22 @@ assert OERcodec_OID.enc(b"") == b"\x00" True = oer sequence dissect of an empty encoding -# Nothing to read leaves every component unset, hence holding its default -decoded = _dissect(OERRecord, "") +# Mandatory components require content after the preamble (X.696 16). +# Packet(b"") builds an empty instance and does not dissect; call do_dissect. +_raises(OER_Decoding_Error, lambda: OERRecord().do_dissect(b"")) -assert decoded.id.val == 0 and decoded.label.val == "" and decoded.values == [] +True + += oer empty SEQUENCE of only optionals with empty preamble +class OEROnlyOptional(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_INTEGER("a", 0, size_len=1, unsigned=True)), + ) + +# One presence bit, absent -> 0x00 preamble, no component body. +assert _dissect(OEROnlyOptional, "00").a is None +_raises(OER_Decoding_Error, lambda: OEROnlyOptional().do_dissect(b"")) True diff --git a/test/scapy/layers/uper.uts b/test/scapy/layers/uper.uts index 086f5e22fa3..7c6053b4ab4 100644 --- a/test/scapy/layers/uper.uts +++ b/test/scapy/layers/uper.uts @@ -9,6 +9,7 @@ import scapy.asn1.uper from scapy.asn1.uper import * +from scapy.asn1.context import UPER_EncoderContext, UPER_DecoderContext from scapy.packet import raw @@ -179,16 +180,16 @@ CODEC_ROUNDTRIPS = [ (UPERcodec_INTEGER, 42, {}, 42), (UPERcodec_INTEGER, -1, {}, -1), (UPERcodec_INTEGER, 68719476736, {}, 68719476736), - (UPERcodec_INTEGER, 200, {"uper_min": 0, "uper_max": 255}, 200), - (UPERcodec_INTEGER, -1, {"uper_min": -128, "uper_max": 127}, -1), - (UPERcodec_INTEGER, 127, {"uper_min": -128, "uper_max": 127}, 127), - (UPERcodec_INTEGER, -128, {"uper_min": -128, "uper_max": 127}, -128), + (UPERcodec_INTEGER, 200, {"minimum": 0, "maximum": 255}, 200), + (UPERcodec_INTEGER, -1, {"minimum": -128, "maximum": 127}, -1), + (UPERcodec_INTEGER, 127, {"minimum": -128, "maximum": 127}, 127), + (UPERcodec_INTEGER, -128, {"minimum": -128, "maximum": 127}, -128), (UPERcodec_STRING, b"AB", {}, b"AB"), (UPERcodec_STRING, b"\x12\x34\x56", {"size_len": 3}, b"\x12\x34\x56"), ( UPERcodec_STRING, bytes.fromhex("afbc4583"), - {"uper_min": 1, "uper_max": 20}, + {"minimum": 1, "maximum": 20}, bytes.fromhex("afbc4583"), ), (UPERcodec_ENUMERATED, 1, {"uper_enum_values": [1, 200]}, 1), @@ -196,13 +197,13 @@ CODEC_ROUNDTRIPS = [ ( UPERcodec_BIT_STRING, (bytes.fromhex("abcd"), 16), - {"uper_min": 1, "uper_max": 20}, + {"minimum": 1, "maximum": 20}, "1010101111001101", ), ( UPERcodec_BIT_STRING, (bytes.fromhex("abcd"), 16), - {"uper_min": 16, "uper_max": 16}, + {"minimum": 16, "maximum": 16}, "1010101111001101", ), (UPERcodec_ENUMERATED, 1, {"uper_enum_values": [1]}, 1), @@ -226,7 +227,7 @@ DECODE_VECTORS = [ "C", 200, UPERcodec_INTEGER, - {"uper_min": 0, "uper_max": 255}, + {"minimum": 0, "maximum": 255}, 200, b"\xc8", ), @@ -234,7 +235,7 @@ DECODE_VECTORS = [ "Signed", -1, UPERcodec_INTEGER, - {"uper_min": -128, "uper_max": 127}, + {"minimum": -128, "maximum": 127}, -1, b"\x7f", ), @@ -242,7 +243,7 @@ DECODE_VECTORS = [ "Signed", 127, UPERcodec_INTEGER, - {"uper_min": -128, "uper_max": 127}, + {"minimum": -128, "maximum": 127}, 127, b"\xff", ), @@ -279,7 +280,7 @@ PRIMITIVE_VECTORS = [ ( "C", 200, - lambda v: UPERcodec_INTEGER.enc(v, uper_min=0, uper_max=255), + lambda v: UPERcodec_INTEGER.enc(v, minimum=0, maximum=255), b"\xc8", ), ("D", b"AB", lambda v: UPERcodec_STRING.enc(v), b"\x02AB"), @@ -354,7 +355,7 @@ def _encode_composite(typename, value): enc.append_length_determinant(len(value)) for item in value: UPERcodec_INTEGER.encode_into( - enc, item, uper_min=0, uper_max=255, + enc, item, minimum=0, maximum=255, ) return enc.as_bytes() if typename == "Choice": @@ -372,7 +373,7 @@ def _encode_composite(typename, value): UPER_choice_index_enc(enc, index, 2) if alt == "a": UPERcodec_INTEGER.encode_into( - enc, payload, uper_min=0, uper_max=15, + enc, payload, minimum=0, maximum=15, ) else: UPERcodec_STRING.encode_into(enc, payload) @@ -450,7 +451,7 @@ ASN1SCC_VECTORS = [ ( "06-OCTET-STRING/001 pdu1", bytes.fromhex("afbc4583"), - lambda v: UPERcodec_STRING.enc(v, uper_min=1, uper_max=20), + lambda v: UPERcodec_STRING.enc(v, minimum=1, maximum=20), bytes.fromhex("1d7de22c18"), ), ( @@ -481,7 +482,7 @@ ASN1SCC_VECTORS = [ "08-BIT-STRING/001 pdu1 ABCD", (bytes.fromhex("abcd"), 16), lambda _v: UPERcodec_BIT_STRING.enc( - (bytes.fromhex("abcd"), 16), uper_min=1, uper_max=20, + (bytes.fromhex("abcd"), 16), minimum=1, maximum=20, ), bytes.fromhex("7d5e68"), ), @@ -491,7 +492,7 @@ def _encode_choice_int1_10(): # type: () -> bytes enc = UPER_Encoder() UPER_choice_index_enc(enc, 0, 5) - UPERcodec_INTEGER.encode_into(enc, 10, uper_min=0, uper_max=15) + UPERcodec_INTEGER.encode_into(enc, 10, minimum=0, maximum=15) return enc.as_bytes() _UPER_CODEC_CLASSES = ( @@ -709,9 +710,9 @@ UPERcodec_BOOLEAN.enc(0) == b"\x00" = UPER unconstrained integer UPERcodec_INTEGER.enc(42) == b"\x01*" = UPER constrained integer -UPERcodec_INTEGER.enc(200, uper_min=0, uper_max=255) == b"\xc8" +UPERcodec_INTEGER.enc(200, minimum=0, maximum=255) == b"\xc8" = UPER signed constrained integer -UPERcodec_INTEGER.enc(-1, uper_min=-128, uper_max=127) == b"\x7f" +UPERcodec_INTEGER.enc(-1, minimum=-128, maximum=127) == b"\x7f" = UPER octet string UPERcodec_STRING.enc(b"AB") == b"\x02AB" = UPER fixed octet string @@ -721,7 +722,7 @@ UPERcodec_NULL.enc(None) == b"" = UPER enumerated index UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]) == b"\x80" = UPER bit string variable size -UPERcodec_BIT_STRING.enc((b"\xab\xcd", 16), uper_min=1, uper_max=20) == bytes.fromhex("7d5e68") +UPERcodec_BIT_STRING.enc((b"\xab\xcd", 16), minimum=1, maximum=20) == bytes.fromhex("7d5e68") = UPER enumerated roundtrip x, r = UPERcodec_ENUMERATED.do_dec(UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), uper_enum_values=[1, 200]) x.val == 200 and r == b"" @@ -1866,32 +1867,32 @@ assert dec.read_unconstrained_whole_number() == 0 True = uper bit string paths -encoded = UPERcodec_BIT_STRING.enc("1010", uper_min=1, uper_max=20) +encoded = UPERcodec_BIT_STRING.enc("1010", minimum=1, maximum=20) obj, remain = UPERcodec_BIT_STRING.do_dec( - encoded, uper_min=1, uper_max=20, + encoded, minimum=1, maximum=20, ) assert obj.val == "1010" -encoded2 = UPERcodec_BIT_STRING.enc(b"\xab", uper_min=4, uper_max=8) +encoded2 = UPERcodec_BIT_STRING.enc(b"\xab", minimum=4, maximum=8) -obj2, _ = UPERcodec_BIT_STRING.do_dec(encoded2, uper_min=4, uper_max=8) +obj2, _ = UPERcodec_BIT_STRING.do_dec(encoded2, minimum=4, maximum=8) assert len(obj2.val) == 8 -fixed = UPERcodec_BIT_STRING.enc("1010101111001101", uper_min=16, uper_max=16) +fixed = UPERcodec_BIT_STRING.enc("1010101111001101", minimum=16, maximum=16) -obj3, _ = UPERcodec_BIT_STRING.do_dec(fixed, uper_min=16, uper_max=16) +obj3, _ = UPERcodec_BIT_STRING.do_dec(fixed, minimum=16, maximum=16) assert obj3.val == "1010101111001101" True = uper enumerated range -encoded = UPERcodec_ENUMERATED.enc(3, uper_min=0, uper_max=7) +encoded = UPERcodec_ENUMERATED.enc(3, minimum=0, maximum=7) -obj, remain = UPERcodec_ENUMERATED.do_dec(encoded, uper_min=0, uper_max=7) +obj, remain = UPERcodec_ENUMERATED.do_dec(encoded, minimum=0, maximum=7) assert obj.val == 3 @@ -1899,12 +1900,12 @@ assert remain == b"" enc = UPER_Encoder() -UPERcodec_ENUMERATED.encode_into(enc, 2, uper_min=0, uper_max=3) +UPERcodec_ENUMERATED.encode_into(enc, 2, minimum=0, maximum=3) obj2 = UPERcodec_ENUMERATED.dec_from_decoder( UPER_Decoder(enc.as_bytes()), - uper_min=0, - uper_max=3, + minimum=0, + maximum=3, ) assert obj2.val == 2 @@ -1951,10 +1952,10 @@ for value, expected in [(0, "00"), (1, "20"), (2, "40")]: enc = UPER_Encoder() _raises(UPER_Encoding_Error, lambda: UPERcodec_ENUMERATED.encode_into( - enc, 7, uper_enum_values=[0, 1, 2], uper_extensible=True)) + enc, 7, uper_enum_values=[0, 1, 2], extensible=True)) _raises(UPER_Decoding_Error, lambda: UPERcodec_ENUMERATED.dec_from_decoder( - UPER_Decoder(b"\x80"), uper_enum_values=[0, 1, 2], uper_extensible=True)) + UPER_Decoder(b"\x80"), uper_enum_values=[0, 1, 2], extensible=True)) True @@ -1967,10 +1968,10 @@ _raises(UPER_Encoding_Error, lambda: UPERcodec_STRING.enc(b"ABCDEF", size_len=4) assert UPERcodec_STRING.enc(b"ABCD", size_len=4) == b"ABCD" _raises(UPER_Encoding_Error, - lambda: UPERcodec_STRING.enc(b"A", uper_min=2, uper_max=4)) + lambda: UPERcodec_STRING.enc(b"A", minimum=2, maximum=4)) _raises(UPER_Encoding_Error, - lambda: UPERcodec_STRING.enc(b"ABCDEFGH", uper_min=2, uper_max=4)) + lambda: UPERcodec_STRING.enc(b"ABCDEFGH", minimum=2, maximum=4)) True @@ -2208,7 +2209,7 @@ assert decoded.id.val == 2 assert decoded.extra.val == 3 -dec = UPER_Decoder(b"\x80") +dec = UPER_DecoderContext(b"\x80") _raises( UPER_Decoding_Error, @@ -2380,7 +2381,7 @@ _raises( _raises( ASN1_Error, lambda: _PerChoice.ASN1_root.encode_into( - UPER_Encoder(), _PerChoice(), 42, + UPER_EncoderContext(), _PerChoice(), 42, ), ) @@ -2877,16 +2878,16 @@ True = uper field encode_into nesting built = UPERWrappedPacket(id=1, inner=UPERInnerPacket(x=7)) -enc = UPER_Encoder() +enc = UPER_EncoderContext() UPERWrappedPacket.ASN1_root.encode_into(enc, built) -assert enc.as_bytes() == raw(built) +assert enc.finish() == raw(built) empty = UPERWrappedPacket() UPERWrappedPacket.ASN1_root.dissect_from_decoder( - empty, UPER_Decoder(raw(built)), + empty, UPER_DecoderContext(raw(built)), ) assert _val(empty.id) == 1 From 294b31975b299bcd038e9c6ff92a54deb34dfcee Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 19:23:22 +0200 Subject: [PATCH 33/46] Restore AI-Assisted commit check scope for PR merge CI. Keep the HEAD^1..HEAD^2 restriction on this branch so #5050 CI does not false-fail on master tips until the standalone CI PR lands. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- .config/ci/check_commits.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.config/ci/check_commits.sh b/.config/ci/check_commits.sh index f5efc667c48..8ca9bc16bc8 100755 --- a/.config/ci/check_commits.sh +++ b/.config/ci/check_commits.sh @@ -6,7 +6,14 @@ # We copy Wireshark's contributing guide, thanks to them for the idea ! # This script is inspired by https://gitlab.com/wireshark/wireshark/-/blob/master/.gitlab-ci.yml -commits=$(git rev-list --no-merges --after="2026-01-00T00:00:00" --max-count=$((PR_FETCH_DEPTH - 1)) HEAD) +# On pull_request, actions/checkout creates a merge of the PR into the base +# branch (HEAD^1=base tip, HEAD^2=PR tip). Restrict the check to PR commits +# so base-branch history is not false-failed for missing trailers. +if git rev-parse -q --verify HEAD^2 >/dev/null 2>&1; then + commits=$(git rev-list --no-merges HEAD^1..HEAD^2) +else + commits=$(git rev-list --no-merges --after="2026-01-00T00:00:00" --max-count=$((PR_FETCH_DEPTH - 1)) HEAD) +fi if [ -z "$commits" ]; then echo "No commit to check in PR. OK." exit 0 From d3996b859a982310c3bfe016452477f072bbe29a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 19:36:12 +0200 Subject: [PATCH 34/46] Fix ASN.1 mypy no-any-return and unused ignores. Cast codec.enc results after neutral get_codec typing, and drop obsolete attr-defined ignores. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/asn1.py | 2 +- scapy/asn1fields.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index 5a53871f08b..d4e456cda1f 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -389,7 +389,7 @@ def __init__(self, val): def enc(self, codec): # type: (Any) -> bytes - return self.tag.get_codec(codec).enc(self.val) + return cast(bytes, self.tag.get_codec(codec).enc(self.val)) def __repr__(self): # type: () -> str diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 110f862be45..4580053359d 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -208,7 +208,10 @@ def _encode_item(self, pkt, item): # the BER type codec so the universal tag/length are applied. item = item.self_build() codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - return codec.enc(item, field=self, pkt=pkt, **self._codec_kwargs(pkt)) + return cast( + bytes, + codec.enc(item, field=self, pkt=pkt, **self._codec_kwargs(pkt)), + ) def i2repr(self, pkt, x): # type: (ASN1_Packet, _I) -> str @@ -288,7 +291,7 @@ def extract_packet(self, def m2i_from_decoder(self, pkt, dec): # type: (ASN1_Packet, Any) -> Any codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - return codec.dec_from_decoder( # type: ignore[attr-defined] + return codec.dec_from_decoder( dec, field=self, pkt=pkt, **self._codec_kwargs(pkt), ) @@ -320,7 +323,7 @@ def encode_into(self, enc, pkt, value=None): bit_enc = per_bit_encoder(enc) extra = self._codec_kwargs(pkt) if bit_enc is not None: - codec.encode_into( # type: ignore[attr-defined] + codec.encode_into( bit_enc, raw, field=self, pkt=pkt, **extra, ) return From 9906d9dafd05b413e922f90290a90d3e2df0cd2f Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 20:34:41 +0200 Subject: [PATCH 35/46] Simplify ASN.1 codec contexts and drop adapter helpers. Bind compound encode/decode hooks directly on BER/OER/UPER contexts, separate field-layer contexts from raw UPER bit streams, and inline trivial constraint getters and single-use compound forwarders. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/compound.py | 280 +++++++++++-------------------------- scapy/asn1/constraints.py | 134 ++++++------------ scapy/asn1/context.py | 256 +++++++-------------------------- scapy/asn1/oer.py | 24 +--- scapy/asn1/uper.py | 30 ++-- scapy/asn1fields.py | 33 ++--- test/scapy/layers/oer.uts | 34 +++-- test/scapy/layers/uper.uts | 13 +- 8 files changed, 232 insertions(+), 572 deletions(-) diff --git a/scapy/asn1/compound.py b/scapy/asn1/compound.py index 29cae87ea67..d2533ff782f 100644 --- a/scapy/asn1/compound.py +++ b/scapy/asn1/compound.py @@ -7,6 +7,9 @@ ASN.1 schema fields form a tree, not a flat ``fields_desc`` list. The ``encode_to`` / ``decode_from`` methods on ``ASN1F_*`` are the analogue of Scapy ``Field.addfield`` / ``Field.getfield`` for that tree. + +Codec-specific hooks take the encoder/decoder context first so they can be +bound as methods on the context classes in ``scapy.asn1.context``. """ from functools import reduce @@ -18,19 +21,6 @@ ASN1_Error, ASN1_Object, ) -from scapy.asn1.constraints import field_extensible, field_range -from scapy.asn1.context import ( - OER_Decoder, - per_bit_decoder, - per_bit_encoder, -) - - -def sequence_presence_bits(field, pkt): - # type: (Any, Any) -> List[int] - bits = [0] if field_extensible(field) else [] - bits += [1 if opt.is_present(pkt) else 0 for opt in field.optionals] - return bits def read_oer_presence_bits(s, field): @@ -38,7 +28,9 @@ def read_oer_presence_bits(s, field): from scapy.asn1.oer import OER_Decoding_Error, _OER_check_len number_of_optionals = len(field.optionals) - number_of_bits = (1 if field_extensible(field) else 0) + number_of_optionals + number_of_bits = ( + (1 if field.constraints.extensible else 0) + number_of_optionals + ) if number_of_bits == 0: return [], s number_of_bytes = (number_of_bits + 7) // 8 @@ -48,7 +40,7 @@ def read_oer_presence_bits(s, field): bool((value >> (8 * number_of_bytes - 1 - i)) & 1) for i in range(number_of_bits) ] - if field_extensible(field): + if field.constraints.extensible: if bits[0]: raise OER_Decoding_Error( "ASN1F_SEQUENCE: extension additions are not supported", @@ -74,7 +66,7 @@ def read_uper_presence_bits(dec, field): # type: (Any, Any) -> List[bool] from scapy.asn1.uper import UPER_Decoding_Error - if field_extensible(field): + if field.constraints.extensible: if dec.read_bit(): raise UPER_Decoding_Error( "ASN1F_SEQUENCE: extension additions are not supported" @@ -82,14 +74,6 @@ def read_uper_presence_bits(dec, field): return [dec.read_bit() for _ in field.optionals] -def write_uper_presence_bits(enc, field, pkt): - # type: (Any, Any, Any) -> None - if field_extensible(field): - enc.append_bit(0) - for opt in field.optionals: - enc.append_bit(1 if opt.is_present(pkt) else 0) - - def _sequence_decode_children(field, pkt, presence, dissect): # type: (Any, Any, List[bool], Callable[[Any], None]) -> None from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional @@ -120,23 +104,13 @@ def _sequence_encode_children(field, pkt, encode): # ---- SEQUENCE ------------------------------------------------------------- -def sequence_encode_to(field, pkt, enc): - # type: (Any, Any, Any) -> None - enc.encode_sequence(field, pkt) - - -def sequence_decode_from(field, pkt, dec): - # type: (Any, Any, Any) -> None - dec.decode_sequence(field, pkt) - - -def ber_sequence_encode_to(field, pkt, enc): +def ber_sequence_encode_to(enc, field, pkt): # type: (Any, Any, Any) -> None s = reduce(lambda x, y: x + y.build(pkt), field.seq, b"") enc.write(field.i2m(pkt, s)) -def ber_sequence_decode_from(field, pkt, dec): +def ber_sequence_decode_from(dec, field, pkt): # type: (Any, Any, Any) -> None s = dec.remaining() s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) @@ -152,21 +126,23 @@ def ber_sequence_decode_from(field, pkt, dec): dec.set_remainder(remain) -def oer_sequence_encode_to(field, pkt, enc): +def oer_sequence_encode_to(enc, field, pkt): # type: (Any, Any, Any) -> None - enc.write(write_oer_presence_bits(sequence_presence_bits(field, pkt))) + bits = [0] if field.constraints.extensible else [] + bits += [1 if opt.is_present(pkt) else 0 for opt in field.optionals] + enc.write(write_oer_presence_bits(bits)) _sequence_encode_children( field, pkt, lambda obj: obj.encode_to(pkt, enc), ) -def oer_sequence_decode_from(field, pkt, dec): +def oer_sequence_decode_from(dec, field, pkt): # type: (Any, Any, Any) -> None s = dec.remaining() s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) presence, s = read_oer_presence_bits(s, field) - child_dec = OER_Decoder(s) + child_dec = type(dec)(s) _sequence_decode_children( field, pkt, presence, lambda obj: obj.decode_from(pkt, child_dec), @@ -174,20 +150,22 @@ def oer_sequence_decode_from(field, pkt, dec): dec.set_remainder(child_dec.remaining()) -def uper_sequence_encode_to(field, pkt, enc): +def uper_sequence_encode_to(enc, field, pkt): # type: (Any, Any, Any) -> None - bit_enc = per_bit_encoder(enc) - write_uper_presence_bits(bit_enc, field, pkt) + bit_enc = enc.bit_encoder + if field.constraints.extensible: + bit_enc.append_bit(0) + for opt in field.optionals: + bit_enc.append_bit(1 if opt.is_present(pkt) else 0) _sequence_encode_children( field, pkt, lambda obj: obj.encode_to(pkt, enc), ) -def uper_sequence_decode_from(field, pkt, dec): +def uper_sequence_decode_from(dec, field, pkt): # type: (Any, Any, Any) -> None - bit_dec = per_bit_decoder(dec) - presence = read_uper_presence_bits(bit_dec, field) + presence = read_uper_presence_bits(dec.bit_decoder, field) _sequence_decode_children( field, pkt, presence, lambda obj: obj.decode_from(pkt, dec), @@ -196,17 +174,7 @@ def uper_sequence_decode_from(field, pkt, dec): # ---- SEQUENCE OF ---------------------------------------------------------- -def sequence_of_encode_to(field, pkt, enc): - # type: (Any, Any, Any) -> None - enc.encode_sequence_of(field, pkt) - - -def sequence_of_decode_from(field, pkt, dec): - # type: (Any, Any, Any) -> None - dec.decode_sequence_of(field, pkt) - - -def ber_sequence_of_encode_to(field, pkt, enc): +def ber_sequence_of_encode_to(enc, field, pkt): # type: (Any, Any, Any) -> None val = getattr(pkt, field.name) if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: @@ -220,7 +188,7 @@ def ber_sequence_of_encode_to(field, pkt, enc): enc.write(field.i2m(pkt, s)) -def ber_sequence_of_decode_from(field, pkt, dec): +def ber_sequence_of_decode_from(dec, field, pkt): # type: (Any, Any, Any) -> None from scapy.asn1.ber import BER_Decoding_Error s = dec.remaining() @@ -241,71 +209,47 @@ def ber_sequence_of_decode_from(field, pkt, dec): dec.set_remainder(remain) -def oer_sequence_of_encode_to(field, pkt, enc): - # type: (Any, Any, Any) -> None - enc.write(oer_sequence_of_bytes(field, pkt)) - - -def oer_sequence_of_decode_from(field, pkt, dec): - # type: (Any, Any, Any) -> None - val, remain = oer_sequence_of_decode(field, pkt, dec.remaining()) - field.set_val(pkt, val) - dec.set_remainder(remain) - - -def uper_sequence_of_encode_to(field, pkt, enc): +def oer_sequence_of_encode_to(enc, field, pkt): # type: (Any, Any, Any) -> None - uper_sequence_of_encode_into(field, enc, pkt) - - -def uper_sequence_of_decode_from(field, pkt, dec): - # type: (Any, Any, Any) -> None - field.set_val( - pkt, - uper_sequence_of_decode_from_decoder(field, pkt, dec), - ) - - -def oer_sequence_of_bytes(field, pkt): - # type: (Any, Any) -> bytes from scapy.asn1.oer import OER_unsigned_integer_enc val = getattr(pkt, field.name) if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: - return field.i2m(pkt, val) + enc.write(field.i2m(pkt, val)) + return items = [ bytes(item) if field.holds_packets else field.fld.i2m(pkt, item) for item in val or [] ] - return field.i2m( + enc.write(field.i2m( pkt, OER_unsigned_integer_enc(len(items)) + b"".join(items), - ) + )) -def oer_sequence_of_decode(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[list, bytes] +def oer_sequence_of_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None from scapy.asn1.oer import OER_unsigned_integer_dec - s = field._apply_tagging_dec(s, pkt) + s = field._apply_tagging_dec(dec.remaining(), pkt) count, s = OER_unsigned_integer_dec(s) lst = [] for _ in range(count): c, s = field._extract_packet(s, pkt) if c: lst.append(c) - return lst, s + field.set_val(pkt, lst) + dec.set_remainder(s) -def uper_sequence_of_encode_into(field, enc, pkt, value=None): +def uper_sequence_of_encode_to(enc, field, pkt, value=None): # type: (Any, Any, Any, Any) -> None - bit_enc = per_bit_encoder(enc) - if bit_enc is None: - raise ASN1_Error("uper_sequence_of_encode_into: PER encoder required") + from scapy.asn1.uper import UPER_constrained_int_enc + + bit_enc = enc.bit_encoder if value is None: value = getattr(pkt, field.name) if value is None: - _uper_count_enc(field, bit_enc, 0, lambda offset, size: None) - return + value = [] count = len(value) def append_items(offset, size): @@ -316,8 +260,8 @@ def append_items(offset, size): else: field.fld.encode_into(bit_enc, pkt, item) - uper_min, uper_max = field_range(field) - if field_extensible(field): + uper_min, uper_max = field.constraints.minimum, field.constraints.maximum + if field.constraints.extensible: if ( uper_min is not None and uper_max is not None and uper_min <= count <= uper_max @@ -327,18 +271,18 @@ def append_items(offset, size): bit_enc.append_bit(1) bit_enc.append_fragmented(count, append_items) return - _uper_count_enc(field, bit_enc, count, append_items) + if uper_min is not None and uper_max is not None: + UPER_constrained_int_enc(bit_enc, count, uper_min, uper_max) + append_items(0, count) + else: + bit_enc.append_fragmented(count, append_items) -def uper_sequence_of_decode_from_decoder(field, pkt, dec): - # type: (Any, Any, Any) -> list +def uper_sequence_of_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None from scapy.asn1.uper import UPER_constrained_int_dec - bit_dec = per_bit_decoder(dec) - if bit_dec is None: - raise ASN1_Error( - "uper_sequence_of_decode_from_decoder: PER decoder required" - ) + bit_dec = dec.bit_decoder lst = [] def read_items(count): @@ -353,79 +297,47 @@ def read_items(count): else: lst.append(field.fld.m2i_from_decoder(pkt, bit_dec)) - if field_extensible(field) and bit_dec.read_bit(): + if field.constraints.extensible and bit_dec.read_bit(): bit_dec.read_fragmented(read_items) else: - uper_min, uper_max = field_range(field) + uper_min, uper_max = field.constraints.minimum, field.constraints.maximum if uper_min is not None and uper_max is not None: read_items(UPER_constrained_int_dec(bit_dec, uper_min, uper_max)) else: bit_dec.read_fragmented(read_items) - return lst - - -def _uper_count_enc(field, enc, count, append_items): - # type: (Any, Any, int, Callable[[int, int], None]) -> None - from scapy.asn1.uper import UPER_constrained_int_enc - - uper_min, uper_max = field_range(field) - if uper_min is not None and uper_max is not None: - UPER_constrained_int_enc(enc, count, uper_min, uper_max) - append_items(0, count) - else: - enc.append_fragmented(count, append_items) + field.set_val(pkt, lst) # ---- CHOICE ------------------------------------------------------------- -def choice_encode_to(field, pkt, enc, value=None): - # type: (Any, Any, Any, Any) -> None - enc.encode_choice(field, pkt, value) - - -def choice_decode_from(field, pkt, dec): - # type: (Any, Any, Any) -> None - dec.decode_choice(field, pkt) - - -def ber_choice_encode_to(field, pkt, enc, value=None): +def ber_choice_encode_to(enc, field, pkt, value=None): # type: (Any, Any, Any, Any) -> None if value is None: value = getattr(pkt, field.name) enc.write(ber_choice_bytes(field, pkt, value)) -def ber_choice_decode_from(field, pkt, dec): +def ber_choice_decode_from(dec, field, pkt): # type: (Any, Any, Any) -> None val, remain = ber_choice_decode(field, pkt, dec.remaining()) field.set_val(pkt, val) dec.set_remainder(remain) -def oer_choice_encode_to(field, pkt, enc, value=None): +def oer_choice_encode_to(enc, field, pkt, value=None): # type: (Any, Any, Any, Any) -> None if value is None: value = getattr(pkt, field.name) enc.write(oer_choice_bytes(field, pkt, value)) -def oer_choice_decode_from(field, pkt, dec): +def oer_choice_decode_from(dec, field, pkt): # type: (Any, Any, Any) -> None val, remain = oer_choice_decode(field, pkt, dec.remaining()) field.set_val(pkt, val) dec.set_remainder(remain) -def uper_choice_encode_to(field, pkt, enc, value=None): - # type: (Any, Any, Any, Any) -> None - uper_choice_encode_into(field, enc, pkt, value) - - -def uper_choice_decode_from(field, pkt, dec): - # type: (Any, Any, Any) -> None - field.set_val(pkt, uper_choice_decode_from_decoder(field, pkt, dec)) - - def ber_choice_decode(field, pkt, s): # type: (Any, Any, bytes) -> Tuple[Any, bytes] from scapy.asn1.ber import BER_id_dec @@ -517,13 +429,11 @@ def oer_choice_decode(field, pkt, s): return field.extract_packet(cls, payload, _underlayer=pkt, _parent=pkt) -def uper_choice_encode_into(field, enc, pkt, value=None): +def uper_choice_encode_to(enc, field, pkt, value=None): # type: (Any, Any, Any, Any) -> None from scapy.asn1.uper import UPER_choice_index_enc - bit_enc = per_bit_encoder(enc) - if bit_enc is None: - raise ASN1_Error("uper_choice_encode_into: PER encoder required") + bit_enc = enc.bit_encoder if value is None: value = getattr(pkt, field.name) if value is None: @@ -534,7 +444,7 @@ def uper_choice_encode_into(field, enc, pkt, value=None): "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % field.name ) - if field_extensible(field): + if field.constraints.extensible: bit_enc.append_bit(0) order = field.canonical_order tag = field.choice_order[index] @@ -545,23 +455,19 @@ def uper_choice_encode_into(field, enc, pkt, value=None): if isinstance(choice, type) and hasattr(choice, "ASN1_root"): value.ASN1_root.encode_to(value, enc) elif hasattr(choice, "cls"): - uper_packet_encode_into(choice, enc, pkt, value) + uper_packet_encode_to(enc, choice, pkt, value) elif isinstance(choice, type): choice(field.name, b"").encode_into(bit_enc, pkt, value) else: choice.encode_into(bit_enc, pkt, value) -def uper_choice_decode_from_decoder(field, pkt, dec): - # type: (Any, Any, Any) -> Any +def uper_choice_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None from scapy.asn1.uper import UPER_Decoding_Error, UPER_choice_index_dec - bit_dec = per_bit_decoder(dec) - if bit_dec is None: - raise ASN1_Error( - "uper_choice_decode_from_decoder: PER decoder required" - ) - if field_extensible(field): + bit_dec = dec.bit_decoder + if field.constraints.extensible: if bit_dec.read_bit(): raise UPER_Decoding_Error( "ASN1F_CHOICE: extension additions are not supported" @@ -582,60 +488,35 @@ def uper_choice_decode_from_decoder(field, pkt, dec): p.add_underlayer(pkt) p.add_parent(pkt) p.ASN1_root.decode_from(p, dec) - return p + field.set_val(pkt, p) + return if hasattr(choice, "cls"): - return uper_packet_decode_from_decoder(choice, pkt, dec) + field.set_val(pkt, uper_packet_decode_from_decoder(choice, pkt, dec)) + return if isinstance(choice, type): - return choice(field.name, b"").m2i_from_decoder(pkt, bit_dec) - return choice.m2i_from_decoder(pkt, bit_dec) + field.set_val( + pkt, choice(field.name, b"").m2i_from_decoder(pkt, bit_dec), + ) + return + field.set_val(pkt, choice.m2i_from_decoder(pkt, bit_dec)) # ---- PACKET (nested ASN1_Packet) ------------------------------------------ -def packet_encode_to(field, pkt, enc, value=None): - # type: (Any, Any, Any, Any) -> None - enc.encode_packet(field, pkt, value) - - -def packet_decode_from(field, pkt, dec): - # type: (Any, Any, Any) -> None - dec.decode_packet(field, pkt) - - -def ber_packet_encode_to(field, pkt, enc, value=None): +def ber_packet_encode_to(enc, field, pkt, value=None): # type: (Any, Any, Any, Any) -> None if value is None: value = getattr(pkt, field.name) enc.write(ber_oer_packet_bytes(field, pkt, value)) -def ber_packet_decode_from(field, pkt, dec): +def ber_packet_decode_from(dec, field, pkt): # type: (Any, Any, Any) -> None val, remain = ber_oer_packet_decode(field, pkt, dec.remaining()) field.set_val(pkt, val) dec.set_remainder(remain) -def oer_packet_encode_to(field, pkt, enc, value=None): - # type: (Any, Any, Any, Any) -> None - ber_packet_encode_to(field, pkt, enc, value) - - -def oer_packet_decode_from(field, pkt, dec): - # type: (Any, Any, Any) -> None - ber_packet_decode_from(field, pkt, dec) - - -def uper_packet_encode_to(field, pkt, enc, value=None): - # type: (Any, Any, Any, Any) -> None - uper_packet_encode_into(field, enc, pkt, value) - - -def uper_packet_decode_from(field, pkt, dec): - # type: (Any, Any, Any) -> None - field.set_val(pkt, uper_packet_decode_from_decoder(field, pkt, dec)) - - def ber_oer_packet_decode(field, pkt, s): # type: (Any, Any, bytes) -> Tuple[Any, bytes] cls = (field.next_cls_cb(pkt) or field.cls) if field.next_cls_cb else field.cls @@ -669,7 +550,7 @@ def ber_oer_packet_bytes(field, pkt, x): return field._tagging_enc(pkt, s, implicit_tag=imp, explicit_tag=exp) -def uper_packet_encode_into(field, enc, pkt, value=None): +def uper_packet_encode_to(enc, field, pkt, value=None): # type: (Any, Any, Any, Any) -> None if value is None: value = getattr(pkt, field.name) @@ -680,6 +561,11 @@ def uper_packet_encode_into(field, enc, pkt, value=None): value.ASN1_root.encode_to(value, enc) +def uper_packet_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None + field.set_val(pkt, uper_packet_decode_from_decoder(field, pkt, dec)) + + def uper_packet_decode_from_decoder(field, pkt, dec): # type: (Any, Any, Any) -> Any cls = (field.next_cls_cb(pkt) or field.cls) if field.next_cls_cb else field.cls diff --git a/scapy/asn1/constraints.py b/scapy/asn1/constraints.py index f741b8bb0da..0b72ee4169b 100644 --- a/scapy/asn1/constraints.py +++ b/scapy/asn1/constraints.py @@ -34,76 +34,10 @@ class ASN1Constraints: def normalize_constraints(codec_opts): # type: (Dict[str, Any]) -> ASN1Constraints """Build ASN1Constraints from field kwargs.""" - data = { - "minimum": None, - "maximum": None, - "extensible": False, - "unsigned": False, - } # type: Dict[str, Any] - for key, value in codec_opts.items(): - if key in _SUPPORTED_CONSTRAINTS: - data[key] = value - else: + for key in codec_opts: + if key not in _SUPPORTED_CONSTRAINTS: raise TypeError("Unknown field constraint %r" % key) - return ASN1Constraints(**data) - - -def field_extensible(field): - # type: (Any) -> bool - return bool(field.constraints.extensible) - - -def field_range(field): - # type: (Any) -> Tuple[Optional[int], Optional[int]] - c = field.constraints - return c.minimum, c.maximum - - -def field_size_len(field=None, size_len=None): - # type: (Any, Optional[int]) -> Optional[int] - if size_len is not None: - return size_len - if field is not None: - return field.size_len - return None - - -def field_unsigned(field=None, unsigned=None): - # type: (Any, Optional[bool]) -> bool - if unsigned is not None: - return unsigned - if field is not None: - return field.constraints.unsigned - return False - - -# Compat alias used by OER codec kwargs named ``oer_unsigned``. -def oer_unsigned(field=None, oer_unsigned=None): - # type: (Any, Optional[bool]) -> bool - return field_unsigned(field, oer_unsigned) - - -def field_extensible_kw(field=None, extensible=None): - # type: (Any, Optional[bool]) -> bool - if extensible is not None: - return extensible - if field is not None: - return field_extensible(field) - return False - - -def uper_extensible(field=None, uper_extensible=None): - # type: (Any, Optional[bool]) -> bool - return field_extensible_kw(field, uper_extensible) - - -def uper_int_range(field=None, minimum=None, maximum=None): - # type: (Any, Optional[int], Optional[int]) -> Tuple[Optional[int], Optional[int]] - if minimum is not None or maximum is not None: - return minimum, maximum - if field is not None: - return field_range(field) - return None, None + return ASN1Constraints(**codec_opts) def resolve_uper_int_bounds(field=None, # type: Any @@ -119,14 +53,18 @@ def resolve_uper_int_bounds(field=None, # type: Any When no explicit ``minimum``/``maximum`` is set, a fixed ``size_len`` of 1, 2, 4, or 8 with ``unsigned=True`` implies ``0 .. 256**n - 1``. """ - size_len = field_size_len(field, size_len) - minimum, maximum = uper_int_range(field, minimum, maximum) - is_unsigned = field_unsigned(field, unsigned) - is_extensible = field_extensible_kw(field, extensible) + if size_len is None and field is not None: + size_len = field.size_len + if minimum is None and maximum is None and field is not None: + minimum, maximum = field.constraints.minimum, field.constraints.maximum + if unsigned is None: + unsigned = bool(field.constraints.unsigned) if field is not None else False + if extensible is None: + extensible = bool(field.constraints.extensible) if field is not None else False if minimum is None and maximum is None: - if size_len in (1, 2, 4, 8) and is_unsigned: + if size_len in (1, 2, 4, 8) and unsigned: minimum, maximum = 0, (256 ** size_len) - 1 - return minimum, maximum, is_extensible + return minimum, maximum, extensible def resolve_uper_size_bounds(field=None, # type: Any @@ -137,23 +75,27 @@ def resolve_uper_size_bounds(field=None, # type: Any ): # type: (...) -> Tuple[Optional[int], Optional[int], bool] """Resolve UPER SIZE bounds; ``size_len`` is a fixed SIZE.""" - size_len = field_size_len(field, size_len) - minimum, maximum = uper_int_range(field, minimum, maximum) - is_extensible = field_extensible_kw(field, extensible) + if size_len is None and field is not None: + size_len = field.size_len + if minimum is None and maximum is None and field is not None: + minimum, maximum = field.constraints.minimum, field.constraints.maximum + if extensible is None: + extensible = bool(field.constraints.extensible) if field is not None else False if size_len: - return size_len, size_len, is_extensible - return minimum, maximum, is_extensible + return size_len, size_len, extensible + return minimum, maximum, extensible def resolve_oer_size_bounds(field=None, size_len=None): # type: (Any, Optional[int]) -> Tuple[Optional[int], Optional[int]] """Resolve OER SIZE bounds from ``size_len`` or field constraints.""" - size_len = field_size_len(field, size_len) + if size_len is None and field is not None: + size_len = field.size_len # ``size_len=0`` means unset (same as the historical ``if size_len:`` check). if size_len: return size_len, size_len if field is not None: - return field_range(field) + return field.constraints.minimum, field.constraints.maximum return None, None @@ -177,26 +119,32 @@ def oer_int_wire_params(field=None, size_len=None, unsigned=None): bound uses variable-width unsigned encoding. A fixed eight-octet width is used only when ``maximum <= 2**64 - 1``. """ - size_len = field_size_len(field, size_len) - is_unsigned = field_unsigned(field, unsigned) - minimum, maximum = field_range(field) if field is not None else (None, None) - extensible = field_extensible(field) if field is not None else False + if size_len is None and field is not None: + size_len = field.size_len + if unsigned is None: + unsigned = bool(field.constraints.unsigned) if field is not None else False + if field is not None: + minimum, maximum = field.constraints.minimum, field.constraints.maximum + extensible = bool(field.constraints.extensible) + else: + minimum, maximum = None, None + extensible = False # Extension values may lie outside the root range. val_min = None if extensible else minimum val_max = None if extensible else maximum if size_len is not None: - if (not is_unsigned and minimum is not None and minimum >= 0 and + if (not unsigned and minimum is not None and minimum >= 0 and not extensible): - is_unsigned = True - return size_len, is_unsigned, val_min, val_max + unsigned = True + return size_len, unsigned, val_min, val_max if extensible: - return None, is_unsigned, None, None + return None, unsigned, None, None if minimum is not None and minimum >= 0: - is_unsigned = True + unsigned = True if maximum is not None: if maximum <= 0xFF: size_len = 1 @@ -208,7 +156,7 @@ def oer_int_wire_params(field=None, size_len=None, unsigned=None): size_len = 8 # else: range exceeds 2^64-1 → variable unsigned elif minimum is not None and maximum is not None: - is_unsigned = False + unsigned = False for sl, lo, hi in ( (1, -128, 127), (2, -32768, 32767), @@ -219,4 +167,4 @@ def oer_int_wire_params(field=None, size_len=None, unsigned=None): size_len = sl break - return size_len, is_unsigned, val_min, val_max + return size_len, unsigned, val_min, val_max diff --git a/scapy/asn1/context.py b/scapy/asn1/context.py index 5355dee662e..e608f99874c 100644 --- a/scapy/asn1/context.py +++ b/scapy/asn1/context.py @@ -4,11 +4,22 @@ """ASN.1 encoder and decoder contexts.""" -from typing import Any, TYPE_CHECKING - -if TYPE_CHECKING: - from scapy.asn1.uper import UPER_Decoder as _UPER_Decoder - from scapy.asn1.uper import UPER_Encoder as _UPER_Encoder +from typing import Any + +from scapy.asn1.asn1 import ASN1_Codecs +from scapy.asn1.compound import ( + ber_sequence_encode_to, ber_sequence_decode_from, + ber_sequence_of_encode_to, ber_sequence_of_decode_from, + ber_choice_encode_to, ber_choice_decode_from, + ber_packet_encode_to, ber_packet_decode_from, + oer_sequence_encode_to, oer_sequence_decode_from, + oer_sequence_of_encode_to, oer_sequence_of_decode_from, + oer_choice_encode_to, oer_choice_decode_from, + uper_sequence_encode_to, uper_sequence_decode_from, + uper_sequence_of_encode_to, uper_sequence_of_decode_from, + uper_choice_encode_to, uper_choice_decode_from, + uper_packet_encode_to, uper_packet_decode_from, +) class ASN1Encoder(object): @@ -18,26 +29,6 @@ def finish(self): # type: () -> bytes raise NotImplementedError - def encode_sequence(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import ber_sequence_encode_to - ber_sequence_encode_to(field, pkt, self) - - def encode_sequence_of(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import ber_sequence_of_encode_to - ber_sequence_of_encode_to(field, pkt, self) - - def encode_choice(self, field, pkt, value=None): - # type: (Any, Any, Any) -> None - from scapy.asn1.compound import ber_choice_encode_to - ber_choice_encode_to(field, pkt, self, value) - - def encode_packet(self, field, pkt, value=None): - # type: (Any, Any, Any) -> None - from scapy.asn1.compound import ber_packet_encode_to - ber_packet_encode_to(field, pkt, self, value) - class ASN1Decoder(object): codec = None # type: Any @@ -50,34 +41,17 @@ def set_remainder(self, remainder): # type: (bytes) -> None raise NotImplementedError - def decode_sequence(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import ber_sequence_decode_from - ber_sequence_decode_from(field, pkt, self) - - def decode_sequence_of(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import ber_sequence_of_decode_from - ber_sequence_of_decode_from(field, pkt, self) - - def decode_choice(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import ber_choice_decode_from - ber_choice_decode_from(field, pkt, self) - - def decode_packet(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import ber_packet_decode_from - ber_packet_decode_from(field, pkt, self) - class BER_Encoder(ASN1Encoder): - from scapy.asn1.asn1 import ASN1_Codecs + codec = ASN1_Codecs.BER + encode_sequence = ber_sequence_encode_to + encode_sequence_of = ber_sequence_of_encode_to + encode_choice = ber_choice_encode_to + encode_packet = ber_packet_encode_to def __init__(self, codec=None): # type: (Any) -> None - from scapy.asn1.asn1 import ASN1_Codecs - self.codec = codec or ASN1_Codecs.BER + self.codec = codec or self.codec self._parts = [] # type: list[bytes] def write(self, data): @@ -90,12 +64,15 @@ def finish(self): class BER_Decoder(ASN1Decoder): - from scapy.asn1.asn1 import ASN1_Codecs + codec = ASN1_Codecs.BER + decode_sequence = ber_sequence_decode_from + decode_sequence_of = ber_sequence_of_decode_from + decode_choice = ber_choice_decode_from + decode_packet = ber_packet_decode_from def __init__(self, data, codec=None): # type: (bytes, Any) -> None - from scapy.asn1.asn1 import ASN1_Codecs - self.codec = codec or ASN1_Codecs.BER + self.codec = codec or self.codec self._data = data self._offset = 0 @@ -110,153 +87,61 @@ def set_remainder(self, remainder): class OER_Encoder(BER_Encoder): - from scapy.asn1.asn1 import ASN1_Codecs - codec = ASN1_Codecs.OER - - def __init__(self): - # type: () -> None - super(OER_Encoder, self).__init__(codec=self.codec) - - def encode_sequence(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import oer_sequence_encode_to - oer_sequence_encode_to(field, pkt, self) - - def encode_sequence_of(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import oer_sequence_of_encode_to - oer_sequence_of_encode_to(field, pkt, self) - - def encode_choice(self, field, pkt, value=None): - # type: (Any, Any, Any) -> None - from scapy.asn1.compound import oer_choice_encode_to - oer_choice_encode_to(field, pkt, self, value) - - def encode_packet(self, field, pkt, value=None): - # type: (Any, Any, Any) -> None - from scapy.asn1.compound import oer_packet_encode_to - oer_packet_encode_to(field, pkt, self, value) + encode_sequence = oer_sequence_encode_to + encode_sequence_of = oer_sequence_of_encode_to + encode_choice = oer_choice_encode_to class OER_Decoder(BER_Decoder): - from scapy.asn1.asn1 import ASN1_Codecs - codec = ASN1_Codecs.OER - - def __init__(self, data): - # type: (bytes) -> None - super(OER_Decoder, self).__init__(data, codec=self.codec) - - def decode_sequence(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import oer_sequence_decode_from - oer_sequence_decode_from(field, pkt, self) - - def decode_sequence_of(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import oer_sequence_of_decode_from - oer_sequence_of_decode_from(field, pkt, self) - - def decode_choice(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import oer_choice_decode_from - oer_choice_decode_from(field, pkt, self) - - def decode_packet(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import oer_packet_decode_from - oer_packet_decode_from(field, pkt, self) + decode_sequence = oer_sequence_decode_from + decode_sequence_of = oer_sequence_of_decode_from + decode_choice = oer_choice_decode_from class UPER_EncoderContext(ASN1Encoder): - from scapy.asn1.asn1 import ASN1_Codecs - codec = ASN1_Codecs.PER + encode_sequence = uper_sequence_encode_to + encode_sequence_of = uper_sequence_of_encode_to + encode_choice = uper_choice_encode_to + encode_packet = uper_packet_encode_to def __init__(self): # type: () -> None + # Lazy: keep BER/OER paths from importing scapy.asn1.uper. from scapy.asn1.uper import UPER_Encoder - self._enc = UPER_Encoder() - - @property - def bit_encoder(self): - # type: () -> _UPER_Encoder - return self._enc + self.bit_encoder = UPER_Encoder() def finish(self): # type: () -> bytes - return self._enc.as_bytes() - - def encode_sequence(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import uper_sequence_encode_to - uper_sequence_encode_to(field, pkt, self) - - def encode_sequence_of(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import uper_sequence_of_encode_to - uper_sequence_of_encode_to(field, pkt, self) - - def encode_choice(self, field, pkt, value=None): - # type: (Any, Any, Any) -> None - from scapy.asn1.compound import uper_choice_encode_to - uper_choice_encode_to(field, pkt, self, value) - - def encode_packet(self, field, pkt, value=None): - # type: (Any, Any, Any) -> None - from scapy.asn1.compound import uper_packet_encode_to - uper_packet_encode_to(field, pkt, self, value) + return self.bit_encoder.as_bytes() class UPER_DecoderContext(ASN1Decoder): - from scapy.asn1.asn1 import ASN1_Codecs - codec = ASN1_Codecs.PER + decode_sequence = uper_sequence_decode_from + decode_sequence_of = uper_sequence_of_decode_from + decode_choice = uper_choice_decode_from + decode_packet = uper_packet_decode_from def __init__(self, data): # type: (bytes) -> None from scapy.asn1.uper import UPER_Decoder - self._dec = UPER_Decoder(data) - - @property - def bit_decoder(self): - # type: () -> _UPER_Decoder - return self._dec + self.bit_decoder = UPER_Decoder(data) def remaining(self): # type: () -> bytes - return self._dec.remaining_bytes() + return self.bit_decoder.remaining_bytes() def set_remainder(self, remainder): # type: (bytes) -> None from scapy.asn1.uper import UPER_Decoder - self._dec = UPER_Decoder(remainder) - - def decode_sequence(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import uper_sequence_decode_from - uper_sequence_decode_from(field, pkt, self) - - def decode_sequence_of(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import uper_sequence_of_decode_from - uper_sequence_of_decode_from(field, pkt, self) - - def decode_choice(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import uper_choice_decode_from - uper_choice_decode_from(field, pkt, self) - - def decode_packet(self, field, pkt): - # type: (Any, Any) -> None - from scapy.asn1.compound import uper_packet_decode_from - uper_packet_decode_from(field, pkt, self) + self.bit_decoder = UPER_Decoder(remainder) def new_encoder(codec): # type: (Any) -> ASN1Encoder - from scapy.asn1.asn1 import ASN1_Codecs if codec is ASN1_Codecs.PER: return UPER_EncoderContext() if codec is ASN1_Codecs.OER: @@ -266,53 +151,8 @@ def new_encoder(codec): def new_decoder(codec, data): # type: (Any, bytes) -> ASN1Decoder - from scapy.asn1.asn1 import ASN1_Codecs if codec is ASN1_Codecs.PER: return UPER_DecoderContext(data) if codec is ASN1_Codecs.OER: return OER_Decoder(data) return BER_Decoder(data, codec=codec) - - -def per_bit_encoder(enc): - # type: (Any) -> Any - """Return the PER bit encoder, or *None* for byte-oriented contexts. - - Prefer ``enc.bit_encoder`` on ``UPER_EncoderContext``. The - ``isinstance(UPER_Encoder)`` check is only a nested codec-internal - fallback so call sites that already hold a bare bit stream keep - working. - """ - from scapy.asn1.asn1 import ASN1_Codecs - codec = getattr(enc, "codec", None) - if codec is not None and codec is not ASN1_Codecs.PER: - return None - bit_enc = getattr(enc, "bit_encoder", None) - if bit_enc is not None: - return bit_enc - from scapy.asn1.uper import UPER_Encoder - if isinstance(enc, UPER_Encoder): - return enc - return None - - -def per_bit_decoder(dec): - # type: (Any) -> Any - """Return the PER bit decoder, or *None* for byte-oriented contexts. - - Prefer ``dec.bit_decoder`` on ``UPER_DecoderContext``. The - ``isinstance(UPER_Decoder)`` check is only a nested codec-internal - fallback so call sites that already hold a bare bit stream keep - working. - """ - from scapy.asn1.asn1 import ASN1_Codecs - codec = getattr(dec, "codec", None) - if codec is not None and codec is not ASN1_Codecs.PER: - return None - bit_dec = getattr(dec, "bit_decoder", None) - if bit_dec is not None: - return bit_dec - from scapy.asn1.uper import UPER_Decoder - if isinstance(dec, UPER_Decoder): - return dec - return None diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index cf86ab471e5..ab8f83d985f 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -233,8 +233,6 @@ def do_dec(cls, s, # type: bytes context=None, # type: Optional[Type[ASN1_Class]] safe=False, # type: bool - size_len=None, # type: Optional[int] - oer_unsigned=False, # type: bool **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[Any], bytes] @@ -423,8 +421,6 @@ def do_dec(cls, s, # type: bytes context=None, # type: Optional[Type[ASN1_Class]] safe=False, # type: bool - size_len=None, # type: Optional[int] - oer_unsigned=False, # type: bool **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[int], bytes] @@ -610,8 +606,6 @@ def do_dec(cls, s, # type: bytes context=None, # type: Optional[Type[ASN1_Class]] safe=False, # type: bool - size_len=None, # type: Optional[int] - oer_unsigned=False, # type: bool **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[None], bytes] @@ -635,8 +629,6 @@ def do_dec(cls, s, # type: bytes context=None, # type: Optional[Type[ASN1_Class]] safe=False, # type: bool - size_len=None, # type: Optional[int] - oer_unsigned=False, # type: bool **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[bytes], bytes] @@ -670,8 +662,6 @@ def do_dec(cls, s, # type: bytes context=None, # type: Optional[Type[ASN1_Class]] safe=False, # type: bool - size_len=None, # type: Optional[int] - oer_unsigned=False, # type: bool **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[int], bytes] @@ -751,8 +741,6 @@ def do_dec(cls, s, # type: bytes context=None, # type: Optional[Type[ASN1_Class]] safe=False, # type: bool - size_len=None, # type: Optional[int] - oer_unsigned=False, # type: bool **_kwargs # type: Any ): # type: (...) -> Tuple[ASN1_Object[Union[bytes, List[Any]]], bytes] @@ -772,8 +760,8 @@ class OERcodec_IPADDRESS(OERcodec_STRING): @classmethod def enc(cls, ipaddr_ascii, field=None, size_len=None, **_kwargs): # type: ignore # type: (str, Any, Optional[int], **Any) -> bytes - from scapy.asn1.constraints import field_size_len - size_len = field_size_len(field, size_len) + if size_len is None and field is not None: + size_len = field.size_len try: s = inet_aton(ipaddr_ascii) except Exception: @@ -784,10 +772,10 @@ def enc(cls, ipaddr_ascii, field=None, size_len=None, **_kwargs): # type: ignor @classmethod def do_dec(cls, s, context=None, safe=False, - field=None, size_len=None, oer_unsigned=False, **_kwargs): - # type: (bytes, Optional[Any], bool, Any, Optional[int], bool, **Any) -> Tuple[ASN1_Object[str], bytes] # noqa: E501 - from scapy.asn1.constraints import field_size_len - size_len = field_size_len(field, size_len) + field=None, size_len=None, **_kwargs): + # type: (bytes, Optional[Any], bool, Any, Optional[int], **Any) -> Tuple[ASN1_Object[str], bytes] # noqa: E501 + if size_len is None and field is not None: + size_len = field.size_len if size_len == 4: raw, remain = s[:4], s[4:] else: diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 842bccef286..4e2a47d6234 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -850,17 +850,20 @@ def encode_into(cls, ): # type: (...) -> None from scapy.asn1.constraints import ( - field_size_len, uper_enum_values as _uper_enum_values, - field_extensible_kw, - uper_int_range, ) - size_len = field_size_len(field, size_len) - minimum, maximum = uper_int_range(field, minimum, maximum) + if size_len is None and field is not None: + size_len = field.size_len + if minimum is None and maximum is None and field is not None: + minimum = field.constraints.minimum + maximum = field.constraints.maximum uper_enum_values = _uper_enum_values( field, pkt, uper_enum_values, ) - extensible = field_extensible_kw(field, extensible) + if extensible is None: + extensible = ( + bool(field.constraints.extensible) if field is not None else False + ) if uper_enum_values is not None: if extensible: # X.691 14.3: a one bit prefix says whether the value is an @@ -892,17 +895,20 @@ def dec_from_decoder(cls, ): # type: (...) -> ASN1_Object[int] from scapy.asn1.constraints import ( - field_size_len, uper_enum_values as _uper_enum_values, - field_extensible_kw, - uper_int_range, ) - size_len = field_size_len(field, size_len) - minimum, maximum = uper_int_range(field, minimum, maximum) + if size_len is None and field is not None: + size_len = field.size_len + if minimum is None and maximum is None and field is not None: + minimum = field.constraints.minimum + maximum = field.constraints.maximum uper_enum_values = _uper_enum_values( field, pkt, uper_enum_values, ) - extensible = field_extensible_kw(field, extensible) + if extensible is None: + extensible = ( + bool(field.constraints.extensible) if field is not None else False + ) if uper_enum_values is not None: if extensible and dec.read_bit(): raise UPER_Decoding_Error( diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 4580053359d..f2f2c2bf0c3 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -23,6 +23,7 @@ ASN1_BOOLEAN, ASN1_Class, ASN1_Class_UNIVERSAL, + ASN1_Codecs, ASN1_Decoding_Error, ASN1_Error, ASN1_INTEGER, @@ -33,7 +34,6 @@ ) from scapy.asn1.ber import BER_Decoding_Error from scapy.asn1.constraints import normalize_constraints -from scapy.asn1.context import per_bit_decoder, per_bit_encoder from scapy.asn1.tag import asn1_tag_parts from scapy.base_classes import BasePacket from scapy.volatile import ( @@ -299,8 +299,9 @@ def dissect_from_decoder(self, pkt, dec): # type: (ASN1_Packet, Any) -> None self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) - def encode_into(self, enc, pkt, value=None): + def encode_into(self, bit_enc, pkt, value=None): # type: (Any, ASN1_Packet, Any) -> None + """Encode into a raw UPER bit encoder (not a byte-oriented context).""" if value is None: value = getattr(pkt, self.name) if value is None: @@ -320,30 +321,22 @@ def encode_into(self, enc, pkt, value=None): ) else: raw = value - bit_enc = per_bit_encoder(enc) extra = self._codec_kwargs(pkt) - if bit_enc is not None: - codec.encode_into( - bit_enc, raw, field=self, pkt=pkt, **extra, - ) - return - enc.write( - codec.enc(raw, field=self, pkt=pkt, **extra) + codec.encode_into( + bit_enc, raw, field=self, pkt=pkt, **extra, ) def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None - if per_bit_encoder(enc) is not None: - # Pass the ASN.1 context (not the bare bit stream). - self.encode_into(enc, pkt) + if enc.codec is ASN1_Codecs.PER: + self.encode_into(enc.bit_encoder, pkt) else: enc.write(self.i2m(pkt, getattr(pkt, self.name))) def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None - bit_dec = per_bit_decoder(dec) - if bit_dec is not None: - self.dissect_from_decoder(pkt, bit_dec) + if dec.codec is ASN1_Codecs.PER: + self.dissect_from_decoder(pkt, dec.bit_decoder) else: val, remain = self.m2i(pkt, dec.remaining()) self.set_val(pkt, val) @@ -667,7 +660,7 @@ def m2i(self, pkt, s): dec = pkt.ASN1_codec.new_decoder(s) self.decode_from(pkt, dec) remain = dec.remaining() - if per_bit_decoder(dec) is not None and remain: + if dec.codec is ASN1_Codecs.PER and remain: from scapy.asn1.uper import UPER_Decoding_Error raise UPER_Decoding_Error( "unexpected remainder in %s" % pkt.__class__.__name__, @@ -1096,12 +1089,6 @@ def __init__(self, self.network_tag = 16 | 0x20 # 16 + CONSTRUCTED self.default = default - def _resolve_cls(self, pkt): - # type: (ASN1_Packet) -> Type[ASN1_Packet] - if self.next_cls_cb: - return self.next_cls_cb(pkt) or self.cls - return self.cls - def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[Any, bytes] dec = pkt.ASN1_codec.new_decoder(s) diff --git a/test/scapy/layers/oer.uts b/test/scapy/layers/oer.uts index e61c06cf71c..a005a8630b3 100644 --- a/test/scapy/layers/oer.uts +++ b/test/scapy/layers/oer.uts @@ -284,8 +284,6 @@ def _raises(exc, func): return raise AssertionError("Expected %s" % exc.__name__) -import scapy.asn1.uper - class OEREmptySequenceOf(ASN1_Packet): ASN1_codec = ASN1_Codecs.OER ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) @@ -663,6 +661,29 @@ for type_name, value, enc, expected in INTEGER_VECTORS: if type_name == "A": dec, remain = OERcodec_INTEGER.do_dec(got) assert remain == b"" and dec.val == value + elif type_name == "F": + dec, remain = OERcodec_INTEGER.do_dec( + got, size_len=1, oer_unsigned=True, + ) + assert remain == b"" and dec.val == value + elif type_name in ("G", "H", "I"): + size = {"G": 2, "H": 4, "I": 8}[type_name] + dec, remain = OERcodec_INTEGER.do_dec( + got, size_len=size, oer_unsigned=True, + ) + assert remain == b"" and dec.val == value + elif type_name in ("B", "C", "D", "E"): + size = {"B": 1, "C": 2, "D": 4, "E": 8}[type_name] + dec, remain = OERcodec_INTEGER.do_dec(got, size_len=size) + assert remain == b"" and dec.val == value + elif type_name == "K": + dec_val, remain = OER_unsigned_integer_dec(got) + assert remain == b"" and dec_val == value + elif type_name == "L": + dec_val, remain = OER_signed_integer_dec(got) + assert remain == b"" and dec_val == value + else: + raise AssertionError("unknown integer vector type %r" % type_name) for value, enc, expected in BOOLEAN_VECTORS: got = enc(value) @@ -698,9 +719,7 @@ for bitstr, expected in BIT_STRING_VECTORS: dec, remain = OERcodec_BIT_STRING.do_dec(got) assert remain == b"" and dec.val == bitstr -True - -= scapy encode reference decode +# Extra decode-only vectors (unsigned/fixed) not covered above by value. for type_name, value, encoded in SCAPY_DECODE_VECTORS: if type_name == "A": dec, remain = OERcodec_INTEGER.do_dec(encoded) @@ -712,11 +731,6 @@ for type_name, value, encoded in SCAPY_DECODE_VECTORS: dec, remain = OERcodec_INTEGER.do_dec(encoded, size_len=1) assert remain == b"" and dec.val == value -for val in [0, 1]: - encoded = OERcodec_BOOLEAN.enc(val) - dec, remain = OERcodec_BOOLEAN.do_dec(encoded) - assert remain == b"" and dec.val == val - True = oer fuzz encode diff --git a/test/scapy/layers/uper.uts b/test/scapy/layers/uper.uts index 7c6053b4ab4..d634b086dfc 100644 --- a/test/scapy/layers/uper.uts +++ b/test/scapy/layers/uper.uts @@ -1395,16 +1395,6 @@ for _typename, _value, codec, kwargs, expected, encoded in DECODE_VECTORS: True -= uper codec encode reference -for typename, value, encoder, expected in PRIMITIVE_VECTORS: - encoded = encoder(value) - assert encoded == expected, ( - "%s %r: expected %s, got %s" % - (typename, value, expected.hex(), encoded.hex()) - ) - -True - = primitive interop for typename, value, encoder, expected in PRIMITIVE_VECTORS: got = encoder(value) @@ -2456,7 +2446,8 @@ class _DynamicPacket(ASN1_Packet): dyn = _DynamicPacket(inner=_PerInner(mode=0)) -assert _DynamicPacket.ASN1_root._resolve_cls(dyn) is _PerInner +root = _DynamicPacket.ASN1_root +assert (root.next_cls_cb(dyn) or root.cls) is _PerInner empty_packet = _PacketWrap(inner=None) From 8b87f5266223508a9b552e129c3560e8052edefc Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 21:12:44 +0200 Subject: [PATCH 36/46] Trim ASN.1 encode hot-path adapters and BER SEQUENCE cost. Drop _codec_kwargs and ASN1Codec.new_encoder/new_decoder wrappers, stream BER SEQUENCE children through a nested context, and avoid SEQUENCE OF fragment list slices. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/asn1.py | 10 ---------- scapy/asn1/ber.py | 4 ++-- scapy/asn1/compound.py | 11 +++++++---- scapy/asn1fields.py | 29 +++++++++++++---------------- scapy/asn1packet.py | 6 ++++-- test/scapy/layers/ber.uts | 19 ++++++------------- 6 files changed, 32 insertions(+), 47 deletions(-) diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index d4e456cda1f..128c6386261 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -197,16 +197,6 @@ def tagging_dec(cls, s, **kwargs): return None, s return cast(Tuple[Optional[int], bytes], dec(s, **kwargs)) - def new_encoder(cls): - # type: () -> Any - from scapy.asn1.context import new_encoder - return new_encoder(cls) - - def new_decoder(cls, data): - # type: (bytes) -> Any - from scapy.asn1.context import new_decoder - return new_decoder(cls, data) - def dec(cls, s, context=None, _depth=0): # type: (bytes, Optional[Type[ASN1_Class]], int) -> ASN1_Object[Any] return cls._stem.dec(s, context=context, _depth=_depth) # type: ignore diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index f7935814432..40ded6a21ea 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -362,8 +362,8 @@ def safedec(cls, @classmethod def enc(cls, s, size_len=0, **_kwargs): # type: (_K, Optional[int], **Any) -> bytes - # Ignore unknown kwargs (field=/pkt=/constraint keys from shared - # field._codec_kwargs()) so BER packets do not TypeError. + # Ignore unknown kwargs (field=/pkt=/constraint keys) so BER packets + # do not TypeError when shared field call sites pass them through. size_len = 0 if size_len is None else int(size_len) if isinstance(s, (str, bytes)): return BERcodec_STRING.enc(s, size_len=size_len) diff --git a/scapy/asn1/compound.py b/scapy/asn1/compound.py index d2533ff782f..768a322ac69 100644 --- a/scapy/asn1/compound.py +++ b/scapy/asn1/compound.py @@ -12,7 +12,6 @@ bound as methods on the context classes in ``scapy.asn1.context``. """ -from functools import reduce from typing import Any, Callable, List, Tuple from scapy.asn1.asn1 import ( @@ -106,8 +105,11 @@ def _sequence_encode_children(field, pkt, encode): def ber_sequence_encode_to(enc, field, pkt): # type: (Any, Any, Any) -> None - s = reduce(lambda x, y: x + y.build(pkt), field.seq, b"") - enc.write(field.i2m(pkt, s)) + # Encode children into a nested context, then wrap as one SEQUENCE TLV. + child_enc = type(enc)(codec=enc.codec) + for obj in field.seq: + obj.encode_to(pkt, child_enc) + enc.write(field.i2m(pkt, child_enc.finish())) def ber_sequence_decode_from(dec, field, pkt): @@ -254,7 +256,8 @@ def uper_sequence_of_encode_to(enc, field, pkt, value=None): def append_items(offset, size): # type: (int, int) -> None - for item in value[offset:offset + size]: + for i in range(offset, offset + size): + item = value[i] if field.holds_packets: item.ASN1_root.encode_to(item, enc) else: diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index f2f2c2bf0c3..72904af591c 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -34,6 +34,7 @@ ) from scapy.asn1.ber import BER_Decoding_Error from scapy.asn1.constraints import normalize_constraints +from scapy.asn1.context import new_decoder, new_encoder from scapy.asn1.tag import asn1_tag_parts from scapy.base_classes import BasePacket from scapy.volatile import ( @@ -177,11 +178,6 @@ def _apply_tagging_dec(self, s, pkt, hidden_tag=None, **kwargs): self._apply_diff_tag(pkt, diff_tag) return s - def _codec_kwargs(self, pkt=None): - # type: (Optional[ASN1_Packet]) -> Dict[str, Any] - # Pass size_len through by default; subclasses may extend this dict. - return {"size_len": self.size_len} - def normalize_encode_value(self, pkt, value): # type: (ASN1_Packet, Any) -> Any """Convert a human-facing value before codec encode (e.g. enum names).""" @@ -210,7 +206,9 @@ def _encode_item(self, pkt, item): codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) return cast( bytes, - codec.enc(item, field=self, pkt=pkt, **self._codec_kwargs(pkt)), + codec.enc( + item, field=self, pkt=pkt, size_len=self.size_len, + ), ) def i2repr(self, pkt, x): @@ -241,7 +239,7 @@ def m2i(self, pkt, s): context=self.context, field=self, pkt=pkt, - **self._codec_kwargs(pkt), + size_len=self.size_len, ) if self.flexible_tag: return cast( @@ -292,7 +290,7 @@ def m2i_from_decoder(self, pkt, dec): # type: (ASN1_Packet, Any) -> Any codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) return codec.dec_from_decoder( - dec, field=self, pkt=pkt, **self._codec_kwargs(pkt), + dec, field=self, pkt=pkt, size_len=self.size_len, ) def dissect_from_decoder(self, pkt, dec): @@ -321,9 +319,8 @@ def encode_into(self, bit_enc, pkt, value=None): ) else: raw = value - extra = self._codec_kwargs(pkt) codec.encode_into( - bit_enc, raw, field=self, pkt=pkt, **extra, + bit_enc, raw, field=self, pkt=pkt, size_len=self.size_len, ) def encode_to(self, pkt, enc): @@ -344,13 +341,13 @@ def decode_from(self, pkt, dec): def build(self, pkt): # type: (ASN1_Packet) -> bytes - enc = pkt.ASN1_codec.new_encoder() + enc = new_encoder(pkt.ASN1_codec) self.encode_to(pkt, enc) return cast(bytes, enc.finish()) def dissect(self, pkt, s): # type: (ASN1_Packet, bytes) -> bytes - dec = pkt.ASN1_codec.new_decoder(s) + dec = new_decoder(pkt.ASN1_codec, s) self.decode_from(pkt, dec) return cast(bytes, dec.remaining()) @@ -657,7 +654,7 @@ def set_absent(obj): def m2i(self, pkt, s): # type: (Any, bytes) -> Tuple[Any, bytes] - dec = pkt.ASN1_codec.new_decoder(s) + dec = new_decoder(pkt.ASN1_codec, s) self.decode_from(pkt, dec) remain = dec.remaining() if dec.codec is ASN1_Codecs.PER and remain: @@ -744,7 +741,7 @@ def is_empty(self, def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[List[Any], bytes] - dec = pkt.ASN1_codec.new_decoder(s) + dec = new_decoder(pkt.ASN1_codec, s) self.decode_from(pkt, dec) return getattr(pkt, self.name), dec.remaining() @@ -1032,7 +1029,7 @@ def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] if len(s) == 0: raise ASN1_Error("ASN1F_CHOICE: got empty string") - dec = pkt.ASN1_codec.new_decoder(s) + dec = new_decoder(pkt.ASN1_codec, s) self.decode_from(pkt, dec) return getattr(pkt, self.name), dec.remaining() @@ -1091,7 +1088,7 @@ def __init__(self, def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[Any, bytes] - dec = pkt.ASN1_codec.new_decoder(s) + dec = new_decoder(pkt.ASN1_codec, s) self.decode_from(pkt, dec) return getattr(pkt, self.name), dec.remaining() diff --git a/scapy/asn1packet.py b/scapy/asn1packet.py index a72a4890f08..bd235d1ac1e 100644 --- a/scapy/asn1packet.py +++ b/scapy/asn1packet.py @@ -50,13 +50,15 @@ def self_build(self): # type: () -> bytes if self.raw_packet_cache is not None: return self.raw_packet_cache - enc = self.ASN1_codec.new_encoder() + from scapy.asn1.context import new_encoder + enc = new_encoder(self.ASN1_codec) self.ASN1_root.encode_to(self, enc) return cast(bytes, enc.finish()) def do_dissect(self, x): # type: (bytes) -> bytes self._asn1_observed_tags = {} - dec = self.ASN1_codec.new_decoder(x) + from scapy.asn1.context import new_decoder + dec = new_decoder(self.ASN1_codec, x) self.ASN1_root.decode_from(self, dec) return cast(bytes, dec.remaining()) diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index 7f3473aab90..3c13033609d 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -679,27 +679,20 @@ True % PR #5050 review regressions + BER review fixes -= field _codec_kwargs hook += field size_len is passed through to codecs class P(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_INTEGER("n", 0) fld = P.ASN1_root -assert fld._codec_kwargs(P()) == {"size_len": None} +assert fld.size_len is None assert raw(P(n=ASN1_INTEGER(5))) == b"\x02\x01\x05" assert raw(P(n=5)) == b"\x02\x01\x05" -= field encode with extra kwargs via _codec_kwargs override -class ExtraKwField(ASN1F_INTEGER): - def _codec_kwargs(self, pkt): - return {"size_len": self.size_len, "oer_unsigned": True, "uper_min": 0} - -class ExtraPkt(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ExtraKwField("n", 0) - -assert raw(ExtraPkt(n=7)) == b"\x02\x01\x07" -ExtraPkt(raw(ExtraPkt(n=7))).n.val == 7 += BER ignores unknown codec kwargs from shared call sites +# Codecs accept **kwargs so field=/pkt=/constraint keys do not TypeError on BER. +assert BERcodec_INTEGER.enc(7, field=None, pkt=None, oer_unsigned=True) == b"\x02\x01\x07" +assert BERcodec_INTEGER.enc(7, size_len=None) == b"\x02\x01\x07" = BER OID first arc decode for 2.999.3 obj, remain = BERcodec_OID.do_dec(BERcodec_OID.enc("2.999.3")) From 731afa29f02eeee22edd8fdbf7b1799e07384ea3 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 21:33:52 +0200 Subject: [PATCH 37/46] Split ASN.1 compound hooks by codec. Keep OPTIONAL child walking in compound.py and move BER/OER/UPER SEQUENCE, CHOICE, SEQUENCE OF, and PACKET implementations into compound_{ber,oer,uper}.py. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/compound.py | 554 +----------------------------------- scapy/asn1/compound_ber.py | 191 +++++++++++++ scapy/asn1/compound_oer.py | 184 ++++++++++++ scapy/asn1/compound_uper.py | 226 +++++++++++++++ scapy/asn1/context.py | 6 +- 5 files changed, 614 insertions(+), 547 deletions(-) create mode 100644 scapy/asn1/compound_ber.py create mode 100644 scapy/asn1/compound_oer.py create mode 100644 scapy/asn1/compound_uper.py diff --git a/scapy/asn1/compound.py b/scapy/asn1/compound.py index 768a322ac69..f177c4912f5 100644 --- a/scapy/asn1/compound.py +++ b/scapy/asn1/compound.py @@ -2,78 +2,18 @@ # This file is part of Scapy # See https://scapy.net/ for more information -"""Shared ASN.1 compound-type encode/decode (SEQUENCE, CHOICE, SEQUENCE OF). +"""Shared helpers for ASN.1 compound-type encode/decode. -ASN.1 schema fields form a tree, not a flat ``fields_desc`` list. The -``encode_to`` / ``decode_from`` methods on ``ASN1F_*`` are the analogue of -Scapy ``Field.addfield`` / ``Field.getfield`` for that tree. - -Codec-specific hooks take the encoder/decoder context first so they can be -bound as methods on the context classes in ``scapy.asn1.context``. +Codec-specific SEQUENCE / CHOICE / SEQUENCE OF / PACKET hooks live in +``compound_ber``, ``compound_oer``, and ``compound_uper``. Those modules +are bound as methods on the encoder/decoder contexts in +``scapy.asn1.context``. """ -from typing import Any, Callable, List, Tuple - -from scapy.asn1.asn1 import ( - ASN1_Class_UNIVERSAL, - ASN1_Codecs, - ASN1_Error, - ASN1_Object, -) - - -def read_oer_presence_bits(s, field): - # type: (bytes, Any) -> Tuple[List[bool], bytes] - from scapy.asn1.oer import OER_Decoding_Error, _OER_check_len - - number_of_optionals = len(field.optionals) - number_of_bits = ( - (1 if field.constraints.extensible else 0) + number_of_optionals - ) - if number_of_bits == 0: - return [], s - number_of_bytes = (number_of_bits + 7) // 8 - _OER_check_len("ASN1F_SEQUENCE", s, number_of_bytes) - value = int.from_bytes(s[:number_of_bytes], "big") - bits = [ - bool((value >> (8 * number_of_bytes - 1 - i)) & 1) - for i in range(number_of_bits) - ] - if field.constraints.extensible: - if bits[0]: - raise OER_Decoding_Error( - "ASN1F_SEQUENCE: extension additions are not supported", - remaining=s, - ) - bits = bits[1:] - return bits, s[number_of_bytes:] - +from typing import Any, Callable, List -def write_oer_presence_bits(bits): - # type: (List[int]) -> bytes - if not bits: - return b"" - number_of_bytes = (len(bits) + 7) // 8 - value = 0 - for bit in bits: - value = (value << 1) | bit - value <<= 8 * number_of_bytes - len(bits) - return value.to_bytes(number_of_bytes, "big") - -def read_uper_presence_bits(dec, field): - # type: (Any, Any) -> List[bool] - from scapy.asn1.uper import UPER_Decoding_Error - - if field.constraints.extensible: - if dec.read_bit(): - raise UPER_Decoding_Error( - "ASN1F_SEQUENCE: extension additions are not supported" - ) - return [dec.read_bit() for _ in field.optionals] - - -def _sequence_decode_children(field, pkt, presence, dissect): +def sequence_decode_children(field, pkt, presence, dissect): # type: (Any, Any, List[bool], Callable[[Any], None]) -> None from scapy.asn1fields import ASN1F_badsequence, ASN1F_optional @@ -91,7 +31,7 @@ def _sequence_decode_children(field, pkt, presence, dissect): break -def _sequence_encode_children(field, pkt, encode): +def sequence_encode_children(field, pkt, encode): # type: (Any, Any, Callable[[Any], None]) -> None from scapy.asn1fields import ASN1F_optional @@ -99,481 +39,3 @@ def _sequence_encode_children(field, pkt, encode): if isinstance(obj, ASN1F_optional) and not obj.is_present(pkt): continue encode(obj) - - -# ---- SEQUENCE ------------------------------------------------------------- - -def ber_sequence_encode_to(enc, field, pkt): - # type: (Any, Any, Any) -> None - # Encode children into a nested context, then wrap as one SEQUENCE TLV. - child_enc = type(enc)(codec=enc.codec) - for obj in field.seq: - obj.encode_to(pkt, child_enc) - enc.write(field.i2m(pkt, child_enc.finish())) - - -def ber_sequence_decode_from(dec, field, pkt): - # type: (Any, Any, Any) -> None - s = dec.remaining() - s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) - from scapy.asn1.ber import BER_Decoding_Error - codec = field.ASN1_tag.get_codec(ASN1_Codecs.BER) - _i, s, remain = codec.check_type_check_len(s) - s = field._dissect_sequence_children(pkt, s) - if len(s) > 0: - raise BER_Decoding_Error( - "unexpected remainder in %s" % pkt.name, - remaining=s, - ) - dec.set_remainder(remain) - - -def oer_sequence_encode_to(enc, field, pkt): - # type: (Any, Any, Any) -> None - bits = [0] if field.constraints.extensible else [] - bits += [1 if opt.is_present(pkt) else 0 for opt in field.optionals] - enc.write(write_oer_presence_bits(bits)) - _sequence_encode_children( - field, pkt, - lambda obj: obj.encode_to(pkt, enc), - ) - - -def oer_sequence_decode_from(dec, field, pkt): - # type: (Any, Any, Any) -> None - s = dec.remaining() - s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) - presence, s = read_oer_presence_bits(s, field) - child_dec = type(dec)(s) - _sequence_decode_children( - field, pkt, presence, - lambda obj: obj.decode_from(pkt, child_dec), - ) - dec.set_remainder(child_dec.remaining()) - - -def uper_sequence_encode_to(enc, field, pkt): - # type: (Any, Any, Any) -> None - bit_enc = enc.bit_encoder - if field.constraints.extensible: - bit_enc.append_bit(0) - for opt in field.optionals: - bit_enc.append_bit(1 if opt.is_present(pkt) else 0) - _sequence_encode_children( - field, pkt, - lambda obj: obj.encode_to(pkt, enc), - ) - - -def uper_sequence_decode_from(dec, field, pkt): - # type: (Any, Any, Any) -> None - presence = read_uper_presence_bits(dec.bit_decoder, field) - _sequence_decode_children( - field, pkt, presence, - lambda obj: obj.decode_from(pkt, dec), - ) - - -# ---- SEQUENCE OF ---------------------------------------------------------- - -def ber_sequence_of_encode_to(enc, field, pkt): - # type: (Any, Any, Any) -> None - val = getattr(pkt, field.name) - if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: - s = val # type: Any - elif val is None: - s = b"" - elif field.holds_packets: - s = b"".join(bytes(i) for i in val) - else: - s = b"".join(field.fld.i2m(pkt, i) for i in val) - enc.write(field.i2m(pkt, s)) - - -def ber_sequence_of_decode_from(dec, field, pkt): - # type: (Any, Any, Any) -> None - from scapy.asn1.ber import BER_Decoding_Error - s = dec.remaining() - s = field._apply_tagging_dec(s, pkt) - codec = field.ASN1_tag.get_codec(ASN1_Codecs.BER) - _i, s, remain = codec.check_type_check_len(s) - lst = [] - while s: - c, s = field._extract_packet(s, pkt) - if c: - lst.append(c) - if len(s) > 0: - raise BER_Decoding_Error( - "unexpected remainder in %s" % pkt.name, - remaining=s, - ) - field.set_val(pkt, lst) - dec.set_remainder(remain) - - -def oer_sequence_of_encode_to(enc, field, pkt): - # type: (Any, Any, Any) -> None - from scapy.asn1.oer import OER_unsigned_integer_enc - - val = getattr(pkt, field.name) - if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: - enc.write(field.i2m(pkt, val)) - return - items = [ - bytes(item) if field.holds_packets else field.fld.i2m(pkt, item) - for item in val or [] - ] - enc.write(field.i2m( - pkt, OER_unsigned_integer_enc(len(items)) + b"".join(items), - )) - - -def oer_sequence_of_decode_from(dec, field, pkt): - # type: (Any, Any, Any) -> None - from scapy.asn1.oer import OER_unsigned_integer_dec - - s = field._apply_tagging_dec(dec.remaining(), pkt) - count, s = OER_unsigned_integer_dec(s) - lst = [] - for _ in range(count): - c, s = field._extract_packet(s, pkt) - if c: - lst.append(c) - field.set_val(pkt, lst) - dec.set_remainder(s) - - -def uper_sequence_of_encode_to(enc, field, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - from scapy.asn1.uper import UPER_constrained_int_enc - - bit_enc = enc.bit_encoder - if value is None: - value = getattr(pkt, field.name) - if value is None: - value = [] - count = len(value) - - def append_items(offset, size): - # type: (int, int) -> None - for i in range(offset, offset + size): - item = value[i] - if field.holds_packets: - item.ASN1_root.encode_to(item, enc) - else: - field.fld.encode_into(bit_enc, pkt, item) - - uper_min, uper_max = field.constraints.minimum, field.constraints.maximum - if field.constraints.extensible: - if ( - uper_min is not None and uper_max is not None and - uper_min <= count <= uper_max - ): - bit_enc.append_bit(0) - else: - bit_enc.append_bit(1) - bit_enc.append_fragmented(count, append_items) - return - if uper_min is not None and uper_max is not None: - UPER_constrained_int_enc(bit_enc, count, uper_min, uper_max) - append_items(0, count) - else: - bit_enc.append_fragmented(count, append_items) - - -def uper_sequence_of_decode_from(dec, field, pkt): - # type: (Any, Any, Any) -> None - from scapy.asn1.uper import UPER_constrained_int_dec - - bit_dec = dec.bit_decoder - lst = [] - - def read_items(count): - # type: (int) -> None - for _ in range(count): - if field.holds_packets: - p = field.cls() - p.add_underlayer(pkt) - p.add_parent(pkt) - p.ASN1_root.decode_from(p, dec) - lst.append(p) - else: - lst.append(field.fld.m2i_from_decoder(pkt, bit_dec)) - - if field.constraints.extensible and bit_dec.read_bit(): - bit_dec.read_fragmented(read_items) - else: - uper_min, uper_max = field.constraints.minimum, field.constraints.maximum - if uper_min is not None and uper_max is not None: - read_items(UPER_constrained_int_dec(bit_dec, uper_min, uper_max)) - else: - bit_dec.read_fragmented(read_items) - field.set_val(pkt, lst) - - -# ---- CHOICE ------------------------------------------------------------- - -def ber_choice_encode_to(enc, field, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - if value is None: - value = getattr(pkt, field.name) - enc.write(ber_choice_bytes(field, pkt, value)) - - -def ber_choice_decode_from(dec, field, pkt): - # type: (Any, Any, Any) -> None - val, remain = ber_choice_decode(field, pkt, dec.remaining()) - field.set_val(pkt, val) - dec.set_remainder(remain) - - -def oer_choice_encode_to(enc, field, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - if value is None: - value = getattr(pkt, field.name) - enc.write(oer_choice_bytes(field, pkt, value)) - - -def oer_choice_decode_from(dec, field, pkt): - # type: (Any, Any, Any) -> None - val, remain = oer_choice_decode(field, pkt, dec.remaining()) - field.set_val(pkt, val) - dec.set_remainder(remain) - - -def ber_choice_decode(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - from scapy.asn1.ber import BER_id_dec - from scapy.asn1fields import ASN1F_field - - if len(s) == 0: - raise ASN1_Error("ASN1F_CHOICE: got empty string") - s = field._apply_tagging_dec(s, pkt) - tag, _ = BER_id_dec(s) - if tag in field.choices: - choice = field.choices[tag] - elif field.flexible_tag: - choice = ASN1F_field - else: - raise ASN1_Error( - "ASN1F_CHOICE: unexpected field in '%s' " - "(tag %s not in possible tags %s)" % ( - field.name, tag, list(field.choices.keys()) - ) - ) - if hasattr(choice, "ASN1_root"): - return field.extract_packet(choice, s, _underlayer=pkt, _parent=pkt) - if isinstance(choice, type): - return choice(field.name, b"").m2i(pkt, s) - return choice.m2i(pkt, s) - - -def ber_choice_bytes(field, pkt, x): - # type: (Any, Any, Any) -> bytes - if x is None: - s = b"" - else: - if isinstance(x, ASN1_Object): - s = x.enc(pkt.ASN1_codec) - else: - s = bytes(x) - if type(x) in field.pktchoices: - imp, exp = field.pktchoices[type(x)] - s = field._tagging_enc(pkt, s, implicit_tag=imp, explicit_tag=exp) - _imp, exp = field._tagging_tags(pkt) - return field._tagging_enc(pkt, s, explicit_tag=exp) - - -def oer_choice_bytes(field, pkt, x): - # type: (Any, Any, Any) -> bytes - from scapy.asn1.oer import OER_tag_enc, OER_tag_parts - - if x is None: - s = b"" - else: - if isinstance(x, ASN1_Object): - s = x.enc(pkt.ASN1_codec) - else: - s = bytes(x) - index = field.alternative_index(x) - if index is not None: - tag_class, tag_number = OER_tag_parts(field.choice_order[index]) - s = OER_tag_enc(tag_number, tag_class) + s - return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) - - -def oer_choice_decode(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - from scapy.asn1fields import ASN1F_field - from scapy.asn1.oer import OER_tag_dec, OER_tag_parts - - s = field._apply_tagging_dec(s, pkt) - tag_class, tag_number, payload = OER_tag_dec(s) - choice = None - for key, alternative in field.choices.items(): - if OER_tag_parts(key) == (tag_class, tag_number): - choice = alternative - break - if choice is None: - if not field.flexible_tag: - raise ASN1_Error( - "ASN1F_CHOICE: unexpected field in '%s' " - "(tag %s not in possible tags %s)" % ( - field.name, tag_class | tag_number, - list(field.choices.keys()) - ) - ) - choice = ASN1F_field - if hasattr(choice, "ASN1_root"): - return field.extract_packet(choice, payload, _underlayer=pkt, _parent=pkt) - if isinstance(choice, type): - return choice(field.name, b"").m2i(pkt, payload) - cls = (choice.next_cls_cb(pkt) or choice.cls) if choice.next_cls_cb else choice.cls - return field.extract_packet(cls, payload, _underlayer=pkt, _parent=pkt) - - -def uper_choice_encode_to(enc, field, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - from scapy.asn1.uper import UPER_choice_index_enc - - bit_enc = enc.bit_encoder - if value is None: - value = getattr(pkt, field.name) - if value is None: - return - index = field.alternative_index(value) - if index is None: - raise ASN1_Error( - "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % - field.name - ) - if field.constraints.extensible: - bit_enc.append_bit(0) - order = field.canonical_order - tag = field.choice_order[index] - canon_idx = field.canonical_index[tag] - if len(order) > 1: - UPER_choice_index_enc(bit_enc, canon_idx, len(order)) - choice = order[canon_idx] - if isinstance(choice, type) and hasattr(choice, "ASN1_root"): - value.ASN1_root.encode_to(value, enc) - elif hasattr(choice, "cls"): - uper_packet_encode_to(enc, choice, pkt, value) - elif isinstance(choice, type): - choice(field.name, b"").encode_into(bit_enc, pkt, value) - else: - choice.encode_into(bit_enc, pkt, value) - - -def uper_choice_decode_from(dec, field, pkt): - # type: (Any, Any, Any) -> None - from scapy.asn1.uper import UPER_Decoding_Error, UPER_choice_index_dec - - bit_dec = dec.bit_decoder - if field.constraints.extensible: - if bit_dec.read_bit(): - raise UPER_Decoding_Error( - "ASN1F_CHOICE: extension additions are not supported" - ) - order = field.canonical_order - if len(order) > 1: - index = UPER_choice_index_dec(bit_dec, len(order)) - else: - index = 0 - if index >= len(order): - raise ASN1_Error( - "ASN1F_CHOICE: unexpected index %s in '%s'" % - (index, field.name) - ) - choice = order[index] - if isinstance(choice, type) and hasattr(choice, "ASN1_root"): - p = choice() - p.add_underlayer(pkt) - p.add_parent(pkt) - p.ASN1_root.decode_from(p, dec) - field.set_val(pkt, p) - return - if hasattr(choice, "cls"): - field.set_val(pkt, uper_packet_decode_from_decoder(choice, pkt, dec)) - return - if isinstance(choice, type): - field.set_val( - pkt, choice(field.name, b"").m2i_from_decoder(pkt, bit_dec), - ) - return - field.set_val(pkt, choice.m2i_from_decoder(pkt, bit_dec)) - - -# ---- PACKET (nested ASN1_Packet) ------------------------------------------ - -def ber_packet_encode_to(enc, field, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - if value is None: - value = getattr(pkt, field.name) - enc.write(ber_oer_packet_bytes(field, pkt, value)) - - -def ber_packet_decode_from(dec, field, pkt): - # type: (Any, Any, Any) -> None - val, remain = ber_oer_packet_decode(field, pkt, dec.remaining()) - field.set_val(pkt, val) - dec.set_remainder(remain) - - -def ber_oer_packet_decode(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] - cls = (field.next_cls_cb(pkt) or field.cls) if field.next_cls_cb else field.cls - from scapy.asn1packet import ASN1_Packet as _ASN1_Packet - if not issubclass(cls, _ASN1_Packet): - return field.extract_packet(cls, s, _underlayer=pkt, _parent=pkt) - s = field._apply_tagging_dec( - s, pkt, - hidden_tag=cls.ASN1_root.ASN1_tag, - _fname=field.name, - ) - if not s: - return None, s - return field.extract_packet(cls, s, _underlayer=pkt, _parent=pkt) - - -def ber_oer_packet_bytes(field, pkt, x): - # type: (Any, Any, Any) -> bytes - if x is None: - s = b"" - elif isinstance(x, bytes): - s = x - elif isinstance(x, ASN1_Object): - s = bytes(x.val) if x.val else b"" - else: - s = bytes(x) - from scapy.asn1packet import ASN1_Packet as _ASN1_Packet - if not isinstance(x, _ASN1_Packet): - return s - imp, exp = field._tagging_tags(pkt) - return field._tagging_enc(pkt, s, implicit_tag=imp, explicit_tag=exp) - - -def uper_packet_encode_to(enc, field, pkt, value=None): - # type: (Any, Any, Any, Any) -> None - if value is None: - value = getattr(pkt, field.name) - if value is None: - return - if isinstance(value, ASN1_Object): - value = value.val - value.ASN1_root.encode_to(value, enc) - - -def uper_packet_decode_from(dec, field, pkt): - # type: (Any, Any, Any) -> None - field.set_val(pkt, uper_packet_decode_from_decoder(field, pkt, dec)) - - -def uper_packet_decode_from_decoder(field, pkt, dec): - # type: (Any, Any, Any) -> Any - cls = (field.next_cls_cb(pkt) or field.cls) if field.next_cls_cb else field.cls - p = cls() - p.add_underlayer(pkt) - p.add_parent(pkt) - p.ASN1_root.decode_from(p, dec) - return p diff --git a/scapy/asn1/compound_ber.py b/scapy/asn1/compound_ber.py new file mode 100644 index 00000000000..f6306bfa1a1 --- /dev/null +++ b/scapy/asn1/compound_ber.py @@ -0,0 +1,191 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +"""BER compound-type encode/decode hooks (SEQUENCE, CHOICE, SEQUENCE OF, PACKET). + +Context-first signatures so these functions can be bound on +``BER_Encoder`` / ``BER_Decoder``. OER reuses the PACKET hooks via +inheritance. +""" + +from typing import Any, Tuple + +from scapy.asn1.asn1 import ( + ASN1_Class_UNIVERSAL, + ASN1_Codecs, + ASN1_Error, + ASN1_Object, +) + + +# ---- SEQUENCE ------------------------------------------------------------- + +def ber_sequence_encode_to(enc, field, pkt): + # type: (Any, Any, Any) -> None + # Encode children into a nested context, then wrap as one SEQUENCE TLV. + child_enc = type(enc)(codec=enc.codec) + for obj in field.seq: + obj.encode_to(pkt, child_enc) + enc.write(field.i2m(pkt, child_enc.finish())) + + +def ber_sequence_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None + s = dec.remaining() + s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) + from scapy.asn1.ber import BER_Decoding_Error + codec = field.ASN1_tag.get_codec(ASN1_Codecs.BER) + _i, s, remain = codec.check_type_check_len(s) + s = field._dissect_sequence_children(pkt, s) + if len(s) > 0: + raise BER_Decoding_Error( + "unexpected remainder in %s" % pkt.name, + remaining=s, + ) + dec.set_remainder(remain) + + +# ---- SEQUENCE OF ---------------------------------------------------------- + +def ber_sequence_of_encode_to(enc, field, pkt): + # type: (Any, Any, Any) -> None + val = getattr(pkt, field.name) + if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: + s = val # type: Any + elif val is None: + s = b"" + elif field.holds_packets: + s = b"".join(bytes(i) for i in val) + else: + s = b"".join(field.fld.i2m(pkt, i) for i in val) + enc.write(field.i2m(pkt, s)) + + +def ber_sequence_of_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None + from scapy.asn1.ber import BER_Decoding_Error + s = dec.remaining() + s = field._apply_tagging_dec(s, pkt) + codec = field.ASN1_tag.get_codec(ASN1_Codecs.BER) + _i, s, remain = codec.check_type_check_len(s) + lst = [] + while s: + c, s = field._extract_packet(s, pkt) + if c: + lst.append(c) + if len(s) > 0: + raise BER_Decoding_Error( + "unexpected remainder in %s" % pkt.name, + remaining=s, + ) + field.set_val(pkt, lst) + dec.set_remainder(remain) + + +# ---- CHOICE ------------------------------------------------------------- + +def ber_choice_encode_to(enc, field, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + if value is None: + value = getattr(pkt, field.name) + enc.write(ber_choice_bytes(field, pkt, value)) + + +def ber_choice_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None + val, remain = ber_choice_decode(field, pkt, dec.remaining()) + field.set_val(pkt, val) + dec.set_remainder(remain) + + +def ber_choice_decode(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + from scapy.asn1.ber import BER_id_dec + from scapy.asn1fields import ASN1F_field + + if len(s) == 0: + raise ASN1_Error("ASN1F_CHOICE: got empty string") + s = field._apply_tagging_dec(s, pkt) + tag, _ = BER_id_dec(s) + if tag in field.choices: + choice = field.choices[tag] + elif field.flexible_tag: + choice = ASN1F_field + else: + raise ASN1_Error( + "ASN1F_CHOICE: unexpected field in '%s' " + "(tag %s not in possible tags %s)" % ( + field.name, tag, list(field.choices.keys()) + ) + ) + if hasattr(choice, "ASN1_root"): + return field.extract_packet(choice, s, _underlayer=pkt, _parent=pkt) + if isinstance(choice, type): + return choice(field.name, b"").m2i(pkt, s) + return choice.m2i(pkt, s) + + +def ber_choice_bytes(field, pkt, x): + # type: (Any, Any, Any) -> bytes + if x is None: + s = b"" + else: + if isinstance(x, ASN1_Object): + s = x.enc(pkt.ASN1_codec) + else: + s = bytes(x) + if type(x) in field.pktchoices: + imp, exp = field.pktchoices[type(x)] + s = field._tagging_enc(pkt, s, implicit_tag=imp, explicit_tag=exp) + _imp, exp = field._tagging_tags(pkt) + return field._tagging_enc(pkt, s, explicit_tag=exp) + + +# ---- PACKET (nested ASN1_Packet; also used by OER) ------------------------ + +def ber_packet_encode_to(enc, field, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + if value is None: + value = getattr(pkt, field.name) + enc.write(nested_packet_bytes(field, pkt, value)) + + +def ber_packet_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None + val, remain = nested_packet_decode(field, pkt, dec.remaining()) + field.set_val(pkt, val) + dec.set_remainder(remain) + + +def nested_packet_decode(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + cls = (field.next_cls_cb(pkt) or field.cls) if field.next_cls_cb else field.cls + from scapy.asn1packet import ASN1_Packet as _ASN1_Packet + if not issubclass(cls, _ASN1_Packet): + return field.extract_packet(cls, s, _underlayer=pkt, _parent=pkt) + s = field._apply_tagging_dec( + s, pkt, + hidden_tag=cls.ASN1_root.ASN1_tag, + _fname=field.name, + ) + if not s: + return None, s + return field.extract_packet(cls, s, _underlayer=pkt, _parent=pkt) + + +def nested_packet_bytes(field, pkt, x): + # type: (Any, Any, Any) -> bytes + if x is None: + s = b"" + elif isinstance(x, bytes): + s = x + elif isinstance(x, ASN1_Object): + s = bytes(x.val) if x.val else b"" + else: + s = bytes(x) + from scapy.asn1packet import ASN1_Packet as _ASN1_Packet + if not isinstance(x, _ASN1_Packet): + return s + imp, exp = field._tagging_tags(pkt) + return field._tagging_enc(pkt, s, implicit_tag=imp, explicit_tag=exp) diff --git a/scapy/asn1/compound_oer.py b/scapy/asn1/compound_oer.py new file mode 100644 index 00000000000..5a1570e5631 --- /dev/null +++ b/scapy/asn1/compound_oer.py @@ -0,0 +1,184 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +"""OER compound-type encode/decode hooks (SEQUENCE, CHOICE, SEQUENCE OF). + +Context-first signatures so these functions can be bound on +``OER_Encoder`` / ``OER_Decoder``. PACKET encoding is inherited from BER. +""" + +from typing import Any, List, Tuple + +from scapy.asn1.asn1 import ( + ASN1_Class_UNIVERSAL, + ASN1_Error, + ASN1_Object, +) +from scapy.asn1.compound import ( + sequence_decode_children, + sequence_encode_children, +) + + +def read_oer_presence_bits(s, field): + # type: (bytes, Any) -> Tuple[List[bool], bytes] + from scapy.asn1.oer import OER_Decoding_Error, _OER_check_len + + number_of_optionals = len(field.optionals) + number_of_bits = ( + (1 if field.constraints.extensible else 0) + number_of_optionals + ) + if number_of_bits == 0: + return [], s + number_of_bytes = (number_of_bits + 7) // 8 + _OER_check_len("ASN1F_SEQUENCE", s, number_of_bytes) + value = int.from_bytes(s[:number_of_bytes], "big") + bits = [ + bool((value >> (8 * number_of_bytes - 1 - i)) & 1) + for i in range(number_of_bits) + ] + if field.constraints.extensible: + if bits[0]: + raise OER_Decoding_Error( + "ASN1F_SEQUENCE: extension additions are not supported", + remaining=s, + ) + bits = bits[1:] + return bits, s[number_of_bytes:] + + +def write_oer_presence_bits(bits): + # type: (List[int]) -> bytes + if not bits: + return b"" + number_of_bytes = (len(bits) + 7) // 8 + value = 0 + for bit in bits: + value = (value << 1) | bit + value <<= 8 * number_of_bytes - len(bits) + return value.to_bytes(number_of_bytes, "big") + + +# ---- SEQUENCE ------------------------------------------------------------- + +def oer_sequence_encode_to(enc, field, pkt): + # type: (Any, Any, Any) -> None + bits = [0] if field.constraints.extensible else [] + bits += [1 if opt.is_present(pkt) else 0 for opt in field.optionals] + enc.write(write_oer_presence_bits(bits)) + sequence_encode_children( + field, pkt, + lambda obj: obj.encode_to(pkt, enc), + ) + + +def oer_sequence_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None + s = dec.remaining() + s = field._apply_tagging_dec(s, pkt, _fname=pkt.name) + presence, s = read_oer_presence_bits(s, field) + child_dec = type(dec)(s) + sequence_decode_children( + field, pkt, presence, + lambda obj: obj.decode_from(pkt, child_dec), + ) + dec.set_remainder(child_dec.remaining()) + + +# ---- SEQUENCE OF ---------------------------------------------------------- + +def oer_sequence_of_encode_to(enc, field, pkt): + # type: (Any, Any, Any) -> None + from scapy.asn1.oer import OER_unsigned_integer_enc + + val = getattr(pkt, field.name) + if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: + enc.write(field.i2m(pkt, val)) + return + items = [ + bytes(item) if field.holds_packets else field.fld.i2m(pkt, item) + for item in val or [] + ] + enc.write(field.i2m( + pkt, OER_unsigned_integer_enc(len(items)) + b"".join(items), + )) + + +def oer_sequence_of_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None + from scapy.asn1.oer import OER_unsigned_integer_dec + + s = field._apply_tagging_dec(dec.remaining(), pkt) + count, s = OER_unsigned_integer_dec(s) + lst = [] + for _ in range(count): + c, s = field._extract_packet(s, pkt) + if c: + lst.append(c) + field.set_val(pkt, lst) + dec.set_remainder(s) + + +# ---- CHOICE ------------------------------------------------------------- + +def oer_choice_encode_to(enc, field, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + if value is None: + value = getattr(pkt, field.name) + enc.write(oer_choice_bytes(field, pkt, value)) + + +def oer_choice_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None + val, remain = oer_choice_decode(field, pkt, dec.remaining()) + field.set_val(pkt, val) + dec.set_remainder(remain) + + +def oer_choice_bytes(field, pkt, x): + # type: (Any, Any, Any) -> bytes + from scapy.asn1.oer import OER_tag_enc, OER_tag_parts + + if x is None: + s = b"" + else: + if isinstance(x, ASN1_Object): + s = x.enc(pkt.ASN1_codec) + else: + s = bytes(x) + index = field.alternative_index(x) + if index is not None: + tag_class, tag_number = OER_tag_parts(field.choice_order[index]) + s = OER_tag_enc(tag_number, tag_class) + s + return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) + + +def oer_choice_decode(field, pkt, s): + # type: (Any, Any, bytes) -> Tuple[Any, bytes] + from scapy.asn1fields import ASN1F_field + from scapy.asn1.oer import OER_tag_dec, OER_tag_parts + + s = field._apply_tagging_dec(s, pkt) + tag_class, tag_number, payload = OER_tag_dec(s) + choice = None + for key, alternative in field.choices.items(): + if OER_tag_parts(key) == (tag_class, tag_number): + choice = alternative + break + if choice is None: + if not field.flexible_tag: + raise ASN1_Error( + "ASN1F_CHOICE: unexpected field in '%s' " + "(tag %s not in possible tags %s)" % ( + field.name, tag_class | tag_number, + list(field.choices.keys()) + ) + ) + choice = ASN1F_field + if hasattr(choice, "ASN1_root"): + return field.extract_packet(choice, payload, _underlayer=pkt, _parent=pkt) + if isinstance(choice, type): + return choice(field.name, b"").m2i(pkt, payload) + cls = (choice.next_cls_cb(pkt) or choice.cls) if choice.next_cls_cb else choice.cls + return field.extract_packet(cls, payload, _underlayer=pkt, _parent=pkt) diff --git a/scapy/asn1/compound_uper.py b/scapy/asn1/compound_uper.py new file mode 100644 index 00000000000..23840b56dc3 --- /dev/null +++ b/scapy/asn1/compound_uper.py @@ -0,0 +1,226 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +"""UPER compound-type encode/decode hooks (SEQUENCE, CHOICE, SEQUENCE OF, PACKET). + +Context-first signatures so these functions can be bound on +``UPER_EncoderContext`` / ``UPER_DecoderContext``. Imports of +``scapy.asn1.uper`` stay lazy so BER/OER paths do not pull UPER in. +""" + +from typing import Any, List + +from scapy.asn1.asn1 import ASN1_Error, ASN1_Object +from scapy.asn1.compound import ( + sequence_decode_children, + sequence_encode_children, +) + + +def read_uper_presence_bits(dec, field): + # type: (Any, Any) -> List[bool] + from scapy.asn1.uper import UPER_Decoding_Error + + if field.constraints.extensible: + if dec.read_bit(): + raise UPER_Decoding_Error( + "ASN1F_SEQUENCE: extension additions are not supported" + ) + return [dec.read_bit() for _ in field.optionals] + + +# ---- SEQUENCE ------------------------------------------------------------- + +def uper_sequence_encode_to(enc, field, pkt): + # type: (Any, Any, Any) -> None + bit_enc = enc.bit_encoder + if field.constraints.extensible: + bit_enc.append_bit(0) + for opt in field.optionals: + bit_enc.append_bit(1 if opt.is_present(pkt) else 0) + sequence_encode_children( + field, pkt, + lambda obj: obj.encode_to(pkt, enc), + ) + + +def uper_sequence_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None + presence = read_uper_presence_bits(dec.bit_decoder, field) + sequence_decode_children( + field, pkt, presence, + lambda obj: obj.decode_from(pkt, dec), + ) + + +# ---- SEQUENCE OF ---------------------------------------------------------- + +def uper_sequence_of_encode_to(enc, field, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + from scapy.asn1.uper import UPER_constrained_int_enc + + bit_enc = enc.bit_encoder + if value is None: + value = getattr(pkt, field.name) + if value is None: + value = [] + count = len(value) + + def append_items(offset, size): + # type: (int, int) -> None + for i in range(offset, offset + size): + item = value[i] + if field.holds_packets: + item.ASN1_root.encode_to(item, enc) + else: + field.fld.encode_into(bit_enc, pkt, item) + + uper_min, uper_max = field.constraints.minimum, field.constraints.maximum + if field.constraints.extensible: + if ( + uper_min is not None and uper_max is not None and + uper_min <= count <= uper_max + ): + bit_enc.append_bit(0) + else: + bit_enc.append_bit(1) + bit_enc.append_fragmented(count, append_items) + return + if uper_min is not None and uper_max is not None: + UPER_constrained_int_enc(bit_enc, count, uper_min, uper_max) + append_items(0, count) + else: + bit_enc.append_fragmented(count, append_items) + + +def uper_sequence_of_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None + from scapy.asn1.uper import UPER_constrained_int_dec + + bit_dec = dec.bit_decoder + lst = [] + + def read_items(count): + # type: (int) -> None + for _ in range(count): + if field.holds_packets: + p = field.cls() + p.add_underlayer(pkt) + p.add_parent(pkt) + p.ASN1_root.decode_from(p, dec) + lst.append(p) + else: + lst.append(field.fld.m2i_from_decoder(pkt, bit_dec)) + + if field.constraints.extensible and bit_dec.read_bit(): + bit_dec.read_fragmented(read_items) + else: + uper_min, uper_max = field.constraints.minimum, field.constraints.maximum + if uper_min is not None and uper_max is not None: + read_items(UPER_constrained_int_dec(bit_dec, uper_min, uper_max)) + else: + bit_dec.read_fragmented(read_items) + field.set_val(pkt, lst) + + +# ---- CHOICE ------------------------------------------------------------- + +def uper_choice_encode_to(enc, field, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + from scapy.asn1.uper import UPER_choice_index_enc + + bit_enc = enc.bit_encoder + if value is None: + value = getattr(pkt, field.name) + if value is None: + return + index = field.alternative_index(value) + if index is None: + raise ASN1_Error( + "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % + field.name + ) + if field.constraints.extensible: + bit_enc.append_bit(0) + order = field.canonical_order + tag = field.choice_order[index] + canon_idx = field.canonical_index[tag] + if len(order) > 1: + UPER_choice_index_enc(bit_enc, canon_idx, len(order)) + choice = order[canon_idx] + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + value.ASN1_root.encode_to(value, enc) + elif hasattr(choice, "cls"): + uper_packet_encode_to(enc, choice, pkt, value) + elif isinstance(choice, type): + choice(field.name, b"").encode_into(bit_enc, pkt, value) + else: + choice.encode_into(bit_enc, pkt, value) + + +def uper_choice_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None + from scapy.asn1.uper import UPER_Decoding_Error, UPER_choice_index_dec + + bit_dec = dec.bit_decoder + if field.constraints.extensible: + if bit_dec.read_bit(): + raise UPER_Decoding_Error( + "ASN1F_CHOICE: extension additions are not supported" + ) + order = field.canonical_order + if len(order) > 1: + index = UPER_choice_index_dec(bit_dec, len(order)) + else: + index = 0 + if index >= len(order): + raise ASN1_Error( + "ASN1F_CHOICE: unexpected index %s in '%s'" % + (index, field.name) + ) + choice = order[index] + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + p = choice() + p.add_underlayer(pkt) + p.add_parent(pkt) + p.ASN1_root.decode_from(p, dec) + field.set_val(pkt, p) + return + if hasattr(choice, "cls"): + field.set_val(pkt, uper_packet_decode_from_decoder(choice, pkt, dec)) + return + if isinstance(choice, type): + field.set_val( + pkt, choice(field.name, b"").m2i_from_decoder(pkt, bit_dec), + ) + return + field.set_val(pkt, choice.m2i_from_decoder(pkt, bit_dec)) + + +# ---- PACKET --------------------------------------------------------------- + +def uper_packet_encode_to(enc, field, pkt, value=None): + # type: (Any, Any, Any, Any) -> None + if value is None: + value = getattr(pkt, field.name) + if value is None: + return + if isinstance(value, ASN1_Object): + value = value.val + value.ASN1_root.encode_to(value, enc) + + +def uper_packet_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None + field.set_val(pkt, uper_packet_decode_from_decoder(field, pkt, dec)) + + +def uper_packet_decode_from_decoder(field, pkt, dec): + # type: (Any, Any, Any) -> Any + cls = (field.next_cls_cb(pkt) or field.cls) if field.next_cls_cb else field.cls + p = cls() + p.add_underlayer(pkt) + p.add_parent(pkt) + p.ASN1_root.decode_from(p, dec) + return p diff --git a/scapy/asn1/context.py b/scapy/asn1/context.py index e608f99874c..0e550effdb3 100644 --- a/scapy/asn1/context.py +++ b/scapy/asn1/context.py @@ -7,14 +7,18 @@ from typing import Any from scapy.asn1.asn1 import ASN1_Codecs -from scapy.asn1.compound import ( +from scapy.asn1.compound_ber import ( ber_sequence_encode_to, ber_sequence_decode_from, ber_sequence_of_encode_to, ber_sequence_of_decode_from, ber_choice_encode_to, ber_choice_decode_from, ber_packet_encode_to, ber_packet_decode_from, +) +from scapy.asn1.compound_oer import ( oer_sequence_encode_to, oer_sequence_decode_from, oer_sequence_of_encode_to, oer_sequence_of_decode_from, oer_choice_encode_to, oer_choice_decode_from, +) +from scapy.asn1.compound_uper import ( uper_sequence_encode_to, uper_sequence_decode_from, uper_sequence_of_encode_to, uper_sequence_of_decode_from, uper_choice_encode_to, uper_choice_decode_from, From 703d4ab3bf4ea595dd02289ea7b6e300358f126e Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 21:40:54 +0200 Subject: [PATCH 38/46] Speed up UPER bit reads and simplify OER length helpers. Drop dead decoder offset/chunk counters, use int.from_bytes/to_bytes for OER length determinants, and make UPER read_bit avoid redundant arithmetic. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/context.py | 4 +--- scapy/asn1/oer.py | 18 ++++++------------ scapy/asn1/uper.py | 19 +++++++------------ 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/scapy/asn1/context.py b/scapy/asn1/context.py index 0e550effdb3..a709b84692e 100644 --- a/scapy/asn1/context.py +++ b/scapy/asn1/context.py @@ -78,16 +78,14 @@ def __init__(self, data, codec=None): # type: (bytes, Any) -> None self.codec = codec or self.codec self._data = data - self._offset = 0 def remaining(self): # type: () -> bytes - return self._data[self._offset:] + return self._data def set_remainder(self, remainder): # type: (bytes) -> None self._data = remainder - self._offset = 0 class OER_Encoder(BER_Encoder): diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index ab8f83d985f..a3c3a0e3778 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -92,16 +92,13 @@ def OER_len_enc(ll): # type: (int) -> bytes if ll < 128: return chb(ll) - encoded = [] - value = ll - while value > 0: - encoded.insert(0, value & 0xff) - value >>= 8 - if len(encoded) > 127: + number_of_bytes = (ll.bit_length() + 7) // 8 + if number_of_bytes > 127: raise OER_Exception( - "OER_len_enc: Length too long (%i) to be encoded" % len(encoded) + "OER_len_enc: Length too long (%i) to be encoded" % + number_of_bytes ) - return chb(0x80 | len(encoded)) + bytes(encoded) + return chb(0x80 | number_of_bytes) + ll.to_bytes(number_of_bytes, "big") def OER_len_dec(s): @@ -113,10 +110,7 @@ def OER_len_dec(s): return tmp_len, s[1:] tmp_len &= 0x7f _OER_check_len("OER_len_dec", s, tmp_len, offset=1) - ll = 0 - for c in s[1:tmp_len + 1]: - ll <<= 8 - ll |= c + ll = int.from_bytes(s[1:tmp_len + 1], "big") return ll, s[tmp_len + 1:] diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 4e2a47d6234..eb418794c58 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -94,7 +94,6 @@ def __init__(self): # type: () -> None self.number_of_bits = 0 self.value = 0 - self.chunks_number_of_bits = 0 self.chunks = [] # type: List[List[int]] def append_bit(self, bit): @@ -117,7 +116,6 @@ def append_non_negative_binary_integer(self, value, number_of_bits): return if self.number_of_bits > 4096: self.chunks.append([self.value, self.number_of_bits]) - self.chunks_number_of_bits += self.number_of_bits self.number_of_bits = 0 self.value = 0 self.number_of_bits += number_of_bits @@ -195,26 +193,22 @@ def __init__(self, encoded): else: self._bits = 0 - def _read_offset(self): - # type: () -> int - return self.total_number_of_bits - self.number_of_bits - def _read_bits_int(self, number_of_bits): # type: (int) -> int if number_of_bits == 0: return 0 - consumed = self._read_offset() - shift = self.total_number_of_bits - consumed - number_of_bits + # Remaining bits sit in the low end of ``_bits``; shift equals how + # many unread bits will still be left after this read. + shift = self.number_of_bits - number_of_bits mask = (1 << number_of_bits) - 1 return (self._bits >> shift) & mask def read_bit(self): # type: () -> int - if self.number_of_bits == 0: + if not self.number_of_bits: raise UPER_Decoding_Error("UPER_Decoder: out of data") - bit = self._read_bits_int(1) self.number_of_bits -= 1 - return bit + return (self._bits >> self.number_of_bits) & 1 def read_bits(self, number_of_bits): # type: (int) -> bytes @@ -238,7 +232,8 @@ def remaining_bytes(self): # A standalone UPER encoding is padded to an octet boundary, so the # bits left over inside the current octet are padding; only whole # octets after it are actual remaining input / Scapy payload. - pad = -self._read_offset() % 8 + consumed = self.total_number_of_bits - self.number_of_bits + pad = -consumed % 8 if pad: if pad > self.number_of_bits: raise UPER_Decoding_Error("UPER_Decoder: truncated padding") From be80c17329d89ee526b22d639ab6b11e93c0048d Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 21:49:17 +0200 Subject: [PATCH 39/46] Inline residual ASN.1 compound adapter helpers. Fold CHOICE/PACKET second-layer helpers into the bound codec hooks, drop sequence_encode_children and UPER set_remainder, and keep only the shared OPTIONAL decode walk. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/compound.py | 10 ---- scapy/asn1/compound_ber.py | 105 +++++++++++++++++------------------- scapy/asn1/compound_oer.py | 80 ++++++++++++++------------- scapy/asn1/compound_uper.py | 39 ++++++-------- scapy/asn1/context.py | 9 ---- 5 files changed, 105 insertions(+), 138 deletions(-) diff --git a/scapy/asn1/compound.py b/scapy/asn1/compound.py index f177c4912f5..ab7c56e71c1 100644 --- a/scapy/asn1/compound.py +++ b/scapy/asn1/compound.py @@ -29,13 +29,3 @@ def sequence_decode_children(field, pkt, presence, dissect): dissect(obj) except ASN1F_badsequence: break - - -def sequence_encode_children(field, pkt, encode): - # type: (Any, Any, Callable[[Any], None]) -> None - from scapy.asn1fields import ASN1F_optional - - for obj in field.seq: - if isinstance(obj, ASN1F_optional) and not obj.is_present(pkt): - continue - encode(obj) diff --git a/scapy/asn1/compound_ber.py b/scapy/asn1/compound_ber.py index f6306bfa1a1..8c37822a7c4 100644 --- a/scapy/asn1/compound_ber.py +++ b/scapy/asn1/compound_ber.py @@ -9,7 +9,7 @@ inheritance. """ -from typing import Any, Tuple +from typing import Any from scapy.asn1.asn1 import ( ASN1_Class_UNIVERSAL, @@ -89,21 +89,26 @@ def ber_choice_encode_to(enc, field, pkt, value=None): # type: (Any, Any, Any, Any) -> None if value is None: value = getattr(pkt, field.name) - enc.write(ber_choice_bytes(field, pkt, value)) + if value is None: + s = b"" + else: + if isinstance(value, ASN1_Object): + s = value.enc(pkt.ASN1_codec) + else: + s = bytes(value) + if type(value) in field.pktchoices: + imp, exp = field.pktchoices[type(value)] + s = field._tagging_enc(pkt, s, implicit_tag=imp, explicit_tag=exp) + _imp, exp = field._tagging_tags(pkt) + enc.write(field._tagging_enc(pkt, s, explicit_tag=exp)) def ber_choice_decode_from(dec, field, pkt): # type: (Any, Any, Any) -> None - val, remain = ber_choice_decode(field, pkt, dec.remaining()) - field.set_val(pkt, val) - dec.set_remainder(remain) - - -def ber_choice_decode(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] from scapy.asn1.ber import BER_id_dec from scapy.asn1fields import ASN1F_field + s = dec.remaining() if len(s) == 0: raise ASN1_Error("ASN1F_CHOICE: got empty string") s = field._apply_tagging_dec(s, pkt) @@ -120,26 +125,15 @@ def ber_choice_decode(field, pkt, s): ) ) if hasattr(choice, "ASN1_root"): - return field.extract_packet(choice, s, _underlayer=pkt, _parent=pkt) - if isinstance(choice, type): - return choice(field.name, b"").m2i(pkt, s) - return choice.m2i(pkt, s) - - -def ber_choice_bytes(field, pkt, x): - # type: (Any, Any, Any) -> bytes - if x is None: - s = b"" + val, remain = field.extract_packet( + choice, s, _underlayer=pkt, _parent=pkt, + ) + elif isinstance(choice, type): + val, remain = choice(field.name, b"").m2i(pkt, s) else: - if isinstance(x, ASN1_Object): - s = x.enc(pkt.ASN1_codec) - else: - s = bytes(x) - if type(x) in field.pktchoices: - imp, exp = field.pktchoices[type(x)] - s = field._tagging_enc(pkt, s, implicit_tag=imp, explicit_tag=exp) - _imp, exp = field._tagging_tags(pkt) - return field._tagging_enc(pkt, s, explicit_tag=exp) + val, remain = choice.m2i(pkt, s) + field.set_val(pkt, val) + dec.set_remainder(remain) # ---- PACKET (nested ASN1_Packet; also used by OER) ------------------------ @@ -148,44 +142,43 @@ def ber_packet_encode_to(enc, field, pkt, value=None): # type: (Any, Any, Any, Any) -> None if value is None: value = getattr(pkt, field.name) - enc.write(nested_packet_bytes(field, pkt, value)) + if value is None: + s = b"" + elif isinstance(value, bytes): + s = value + elif isinstance(value, ASN1_Object): + s = bytes(value.val) if value.val else b"" + else: + s = bytes(value) + from scapy.asn1packet import ASN1_Packet as _ASN1_Packet + if not isinstance(value, _ASN1_Packet): + enc.write(s) + return + imp, exp = field._tagging_tags(pkt) + enc.write(field._tagging_enc(pkt, s, implicit_tag=imp, explicit_tag=exp)) def ber_packet_decode_from(dec, field, pkt): # type: (Any, Any, Any) -> None - val, remain = nested_packet_decode(field, pkt, dec.remaining()) - field.set_val(pkt, val) - dec.set_remainder(remain) - - -def nested_packet_decode(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] cls = (field.next_cls_cb(pkt) or field.cls) if field.next_cls_cb else field.cls from scapy.asn1packet import ASN1_Packet as _ASN1_Packet + s = dec.remaining() if not issubclass(cls, _ASN1_Packet): - return field.extract_packet(cls, s, _underlayer=pkt, _parent=pkt) + val, remain = field.extract_packet( + cls, s, _underlayer=pkt, _parent=pkt, + ) + field.set_val(pkt, val) + dec.set_remainder(remain) + return s = field._apply_tagging_dec( s, pkt, hidden_tag=cls.ASN1_root.ASN1_tag, _fname=field.name, ) if not s: - return None, s - return field.extract_packet(cls, s, _underlayer=pkt, _parent=pkt) - - -def nested_packet_bytes(field, pkt, x): - # type: (Any, Any, Any) -> bytes - if x is None: - s = b"" - elif isinstance(x, bytes): - s = x - elif isinstance(x, ASN1_Object): - s = bytes(x.val) if x.val else b"" - else: - s = bytes(x) - from scapy.asn1packet import ASN1_Packet as _ASN1_Packet - if not isinstance(x, _ASN1_Packet): - return s - imp, exp = field._tagging_tags(pkt) - return field._tagging_enc(pkt, s, implicit_tag=imp, explicit_tag=exp) + field.set_val(pkt, None) + dec.set_remainder(s) + return + val, remain = field.extract_packet(cls, s, _underlayer=pkt, _parent=pkt) + field.set_val(pkt, val) + dec.set_remainder(remain) diff --git a/scapy/asn1/compound_oer.py b/scapy/asn1/compound_oer.py index 5a1570e5631..2a8c68aecb3 100644 --- a/scapy/asn1/compound_oer.py +++ b/scapy/asn1/compound_oer.py @@ -15,10 +15,7 @@ ASN1_Error, ASN1_Object, ) -from scapy.asn1.compound import ( - sequence_decode_children, - sequence_encode_children, -) +from scapy.asn1.compound import sequence_decode_children def read_oer_presence_bits(s, field): @@ -64,13 +61,15 @@ def write_oer_presence_bits(bits): def oer_sequence_encode_to(enc, field, pkt): # type: (Any, Any, Any) -> None + from scapy.asn1fields import ASN1F_optional + bits = [0] if field.constraints.extensible else [] bits += [1 if opt.is_present(pkt) else 0 for opt in field.optionals] enc.write(write_oer_presence_bits(bits)) - sequence_encode_children( - field, pkt, - lambda obj: obj.encode_to(pkt, enc), - ) + for obj in field.seq: + if isinstance(obj, ASN1F_optional) and not obj.is_present(pkt): + continue + obj.encode_to(pkt, enc) def oer_sequence_decode_from(dec, field, pkt): @@ -96,12 +95,13 @@ def oer_sequence_of_encode_to(enc, field, pkt): if isinstance(val, ASN1_Object) and val.tag == ASN1_Class_UNIVERSAL.RAW: enc.write(field.i2m(pkt, val)) return - items = [ - bytes(item) if field.holds_packets else field.fld.i2m(pkt, item) - for item in val or [] - ] + items = val or [] enc.write(field.i2m( - pkt, OER_unsigned_integer_enc(len(items)) + b"".join(items), + pkt, + OER_unsigned_integer_enc(len(items)) + b"".join( + bytes(item) if field.holds_packets else field.fld.i2m(pkt, item) + for item in items + ), )) @@ -124,42 +124,30 @@ def oer_sequence_of_decode_from(dec, field, pkt): def oer_choice_encode_to(enc, field, pkt, value=None): # type: (Any, Any, Any, Any) -> None - if value is None: - value = getattr(pkt, field.name) - enc.write(oer_choice_bytes(field, pkt, value)) - - -def oer_choice_decode_from(dec, field, pkt): - # type: (Any, Any, Any) -> None - val, remain = oer_choice_decode(field, pkt, dec.remaining()) - field.set_val(pkt, val) - dec.set_remainder(remain) - - -def oer_choice_bytes(field, pkt, x): - # type: (Any, Any, Any) -> bytes from scapy.asn1.oer import OER_tag_enc, OER_tag_parts - if x is None: + if value is None: + value = getattr(pkt, field.name) + if value is None: s = b"" else: - if isinstance(x, ASN1_Object): - s = x.enc(pkt.ASN1_codec) + if isinstance(value, ASN1_Object): + s = value.enc(pkt.ASN1_codec) else: - s = bytes(x) - index = field.alternative_index(x) + s = bytes(value) + index = field.alternative_index(value) if index is not None: tag_class, tag_number = OER_tag_parts(field.choice_order[index]) s = OER_tag_enc(tag_number, tag_class) + s - return field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag) + enc.write(field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag)) -def oer_choice_decode(field, pkt, s): - # type: (Any, Any, bytes) -> Tuple[Any, bytes] +def oer_choice_decode_from(dec, field, pkt): + # type: (Any, Any, Any) -> None from scapy.asn1fields import ASN1F_field from scapy.asn1.oer import OER_tag_dec, OER_tag_parts - s = field._apply_tagging_dec(s, pkt) + s = field._apply_tagging_dec(dec.remaining(), pkt) tag_class, tag_number, payload = OER_tag_dec(s) choice = None for key, alternative in field.choices.items(): @@ -177,8 +165,18 @@ def oer_choice_decode(field, pkt, s): ) choice = ASN1F_field if hasattr(choice, "ASN1_root"): - return field.extract_packet(choice, payload, _underlayer=pkt, _parent=pkt) - if isinstance(choice, type): - return choice(field.name, b"").m2i(pkt, payload) - cls = (choice.next_cls_cb(pkt) or choice.cls) if choice.next_cls_cb else choice.cls - return field.extract_packet(cls, payload, _underlayer=pkt, _parent=pkt) + val, remain = field.extract_packet( + choice, payload, _underlayer=pkt, _parent=pkt, + ) + elif isinstance(choice, type): + val, remain = choice(field.name, b"").m2i(pkt, payload) + else: + cls = ( + (choice.next_cls_cb(pkt) or choice.cls) + if choice.next_cls_cb else choice.cls + ) + val, remain = field.extract_packet( + cls, payload, _underlayer=pkt, _parent=pkt, + ) + field.set_val(pkt, val) + dec.set_remainder(remain) diff --git a/scapy/asn1/compound_uper.py b/scapy/asn1/compound_uper.py index 23840b56dc3..2d2939191e6 100644 --- a/scapy/asn1/compound_uper.py +++ b/scapy/asn1/compound_uper.py @@ -9,45 +9,40 @@ ``scapy.asn1.uper`` stay lazy so BER/OER paths do not pull UPER in. """ -from typing import Any, List +from typing import Any from scapy.asn1.asn1 import ASN1_Error, ASN1_Object -from scapy.asn1.compound import ( - sequence_decode_children, - sequence_encode_children, -) - - -def read_uper_presence_bits(dec, field): - # type: (Any, Any) -> List[bool] - from scapy.asn1.uper import UPER_Decoding_Error - - if field.constraints.extensible: - if dec.read_bit(): - raise UPER_Decoding_Error( - "ASN1F_SEQUENCE: extension additions are not supported" - ) - return [dec.read_bit() for _ in field.optionals] +from scapy.asn1.compound import sequence_decode_children # ---- SEQUENCE ------------------------------------------------------------- def uper_sequence_encode_to(enc, field, pkt): # type: (Any, Any, Any) -> None + from scapy.asn1fields import ASN1F_optional + bit_enc = enc.bit_encoder if field.constraints.extensible: bit_enc.append_bit(0) for opt in field.optionals: bit_enc.append_bit(1 if opt.is_present(pkt) else 0) - sequence_encode_children( - field, pkt, - lambda obj: obj.encode_to(pkt, enc), - ) + for obj in field.seq: + if isinstance(obj, ASN1F_optional) and not obj.is_present(pkt): + continue + obj.encode_to(pkt, enc) def uper_sequence_decode_from(dec, field, pkt): # type: (Any, Any, Any) -> None - presence = read_uper_presence_bits(dec.bit_decoder, field) + from scapy.asn1.uper import UPER_Decoding_Error + + bit_dec = dec.bit_decoder + if field.constraints.extensible: + if bit_dec.read_bit(): + raise UPER_Decoding_Error( + "ASN1F_SEQUENCE: extension additions are not supported" + ) + presence = [bit_dec.read_bit() for _ in field.optionals] sequence_decode_children( field, pkt, presence, lambda obj: obj.decode_from(pkt, dec), diff --git a/scapy/asn1/context.py b/scapy/asn1/context.py index a709b84692e..09adea7e1fb 100644 --- a/scapy/asn1/context.py +++ b/scapy/asn1/context.py @@ -41,10 +41,6 @@ def remaining(self): # type: () -> bytes raise NotImplementedError - def set_remainder(self, remainder): - # type: (bytes) -> None - raise NotImplementedError - class BER_Encoder(ASN1Encoder): codec = ASN1_Codecs.BER @@ -136,11 +132,6 @@ def remaining(self): # type: () -> bytes return self.bit_decoder.remaining_bytes() - def set_remainder(self, remainder): - # type: (bytes) -> None - from scapy.asn1.uper import UPER_Decoder - self.bit_decoder = UPER_Decoder(remainder) - def new_encoder(codec): # type: (Any) -> ASN1Encoder From 9eff1c6ef0b467ebb2f29a9fdb5534162a0ad50a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 21:59:39 +0200 Subject: [PATCH 40/46] Make UPER decoding scale and trim ASN.1 decode allocations. Store UPER input as a byte buffer with a bit cursor instead of one giant integer, drop per-field kwargs copies, look up CHOICE by tag, and join OER SEQUENCE OF payloads once. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/compound_ber.py | 6 --- scapy/asn1/compound_oer.py | 19 ++++---- scapy/asn1/compound_uper.py | 5 +-- scapy/asn1/oer.py | 9 ++-- scapy/asn1/uper.py | 89 ++++++++++++++++++++++++++----------- scapy/asn1fields.py | 42 ++++++----------- test/scapy/layers/ber.uts | 8 ++-- test/scapy/layers/uper.uts | 2 +- 8 files changed, 96 insertions(+), 84 deletions(-) diff --git a/scapy/asn1/compound_ber.py b/scapy/asn1/compound_ber.py index 8c37822a7c4..3793bb8262d 100644 --- a/scapy/asn1/compound_ber.py +++ b/scapy/asn1/compound_ber.py @@ -64,7 +64,6 @@ def ber_sequence_of_encode_to(enc, field, pkt): def ber_sequence_of_decode_from(dec, field, pkt): # type: (Any, Any, Any) -> None - from scapy.asn1.ber import BER_Decoding_Error s = dec.remaining() s = field._apply_tagging_dec(s, pkt) codec = field.ASN1_tag.get_codec(ASN1_Codecs.BER) @@ -74,11 +73,6 @@ def ber_sequence_of_decode_from(dec, field, pkt): c, s = field._extract_packet(s, pkt) if c: lst.append(c) - if len(s) > 0: - raise BER_Decoding_Error( - "unexpected remainder in %s" % pkt.name, - remaining=s, - ) field.set_val(pkt, lst) dec.set_remainder(remain) diff --git a/scapy/asn1/compound_oer.py b/scapy/asn1/compound_oer.py index 2a8c68aecb3..958ea6afb59 100644 --- a/scapy/asn1/compound_oer.py +++ b/scapy/asn1/compound_oer.py @@ -96,13 +96,12 @@ def oer_sequence_of_encode_to(enc, field, pkt): enc.write(field.i2m(pkt, val)) return items = val or [] - enc.write(field.i2m( - pkt, - OER_unsigned_integer_enc(len(items)) + b"".join( - bytes(item) if field.holds_packets else field.fld.i2m(pkt, item) - for item in items - ), - )) + parts = [OER_unsigned_integer_enc(len(items))] + parts.extend( + bytes(item) if field.holds_packets else field.fld.i2m(pkt, item) + for item in items + ) + enc.write(field.i2m(pkt, b"".join(parts))) def oer_sequence_of_decode_from(dec, field, pkt): @@ -135,9 +134,9 @@ def oer_choice_encode_to(enc, field, pkt, value=None): s = value.enc(pkt.ASN1_codec) else: s = bytes(value) - index = field.alternative_index(value) - if index is not None: - tag_class, tag_number = OER_tag_parts(field.choice_order[index]) + tag = field.alternative_tag(value) + if tag is not None: + tag_class, tag_number = OER_tag_parts(tag) s = OER_tag_enc(tag_number, tag_class) + s enc.write(field._tagging_enc(pkt, s, explicit_tag=field.explicit_tag)) diff --git a/scapy/asn1/compound_uper.py b/scapy/asn1/compound_uper.py index 2d2939191e6..c4a43849d41 100644 --- a/scapy/asn1/compound_uper.py +++ b/scapy/asn1/compound_uper.py @@ -130,8 +130,8 @@ def uper_choice_encode_to(enc, field, pkt, value=None): value = getattr(pkt, field.name) if value is None: return - index = field.alternative_index(value) - if index is None: + tag = field.alternative_tag(value) + if tag is None: raise ASN1_Error( "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % field.name @@ -139,7 +139,6 @@ def uper_choice_encode_to(enc, field, pkt, value=None): if field.constraints.extensible: bit_enc.append_bit(0) order = field.canonical_order - tag = field.choice_order[index] canon_idx = field.canonical_index[tag] if len(order) > 1: UPER_choice_index_enc(bit_enc, canon_idx, len(order)) diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index a3c3a0e3778..548343ab657 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -245,18 +245,17 @@ def dec(cls, **_kwargs # type: Any ): # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] - call_kw = dict(_kwargs) if field is not None: - call_kw["field"] = field + _kwargs["field"] = field if pkt is not None: - call_kw["pkt"] = pkt + _kwargs["pkt"] = pkt if not safe: return cls.do_dec( - s, context=context, safe=safe, **call_kw, + s, context=context, safe=safe, **_kwargs, ) try: return cls.do_dec( - s, context=context, safe=safe, **call_kw, + s, context=context, safe=safe, **_kwargs, ) except OER_Decoding_Error as e: return ASN1_DECODING_ERROR(s, exc=e), b"" diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index eb418794c58..061f9ff8ab0 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -20,7 +20,7 @@ ``ASN1F_CHOICE`` alternatives are indexed in X.691 10.2 canonical tag order (via ``ASN1F_CHOICE.canonical_order``). Declaration order is kept for -``alternative_index`` / BER tag lookup. +BER tag lookup (``choices``) and ``alternative_tag``. """ from scapy.compat import bytes_encode @@ -186,29 +186,52 @@ def as_bytes(self): class UPER_Decoder(object): def __init__(self, encoded): # type: (bytes) -> None + # Byte buffer + bit cursor: avoid shifting a whole-input Python int + # on every small field read (super-linear on large encodings). + self._data = encoded + self._pos = 0 self.total_number_of_bits = 8 * len(encoded) - self.number_of_bits = self.total_number_of_bits - if encoded: - self._bits = int.from_bytes(encoded, "big") - else: - self._bits = 0 - def _read_bits_int(self, number_of_bits): + @property + def number_of_bits(self): + # type: () -> int + return self.total_number_of_bits - self._pos + + def _peek_bits_int(self, number_of_bits): # type: (int) -> int if number_of_bits == 0: return 0 - # Remaining bits sit in the low end of ``_bits``; shift equals how - # many unread bits will still be left after this read. - shift = self.number_of_bits - number_of_bits - mask = (1 << number_of_bits) - 1 - return (self._bits >> shift) & mask + if number_of_bits > self.number_of_bits: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + byte_index = self._pos // 8 + bit_offset = self._pos % 8 + value = 0 + bits_left = number_of_bits + while bits_left: + avail = 8 - bit_offset + take = bits_left if bits_left < avail else avail + shift = avail - take + chunk = (self._data[byte_index] >> shift) & ((1 << take) - 1) + value = (value << take) | chunk + bits_left -= take + byte_index += 1 + bit_offset = 0 + return value + + def _read_bits_int(self, number_of_bits): + # type: (int) -> int + value = self._peek_bits_int(number_of_bits) + self._pos += number_of_bits + return value def read_bit(self): # type: () -> int if not self.number_of_bits: raise UPER_Decoding_Error("UPER_Decoder: out of data") - self.number_of_bits -= 1 - return (self._bits >> self.number_of_bits) & 1 + byte_index = self._pos // 8 + bit_offset = self._pos % 8 + self._pos += 1 + return (self._data[byte_index] >> (7 - bit_offset)) & 1 def read_bits(self, number_of_bits): # type: (int) -> bytes @@ -216,38 +239,52 @@ def read_bits(self, number_of_bits): raise UPER_Decoding_Error("UPER_Decoder: out of data") if number_of_bits == 0: return b"" + if number_of_bits % 8 == 0 and self._pos % 8 == 0: + return self.read_bytes(number_of_bits // 8) value = self._read_bits_int(number_of_bits) - self.number_of_bits -= number_of_bits return _uper_bits_to_bytes(value, number_of_bits) def remaining(self): # type: () -> bytes - if self.number_of_bits == 0: + n = self.number_of_bits + if n == 0: return b"" - value = self._read_bits_int(self.number_of_bits) - return _uper_bits_to_bytes(value, self.number_of_bits) + return _uper_bits_to_bytes(self._peek_bits_int(n), n) def remaining_bytes(self): # type: () -> bytes # A standalone UPER encoding is padded to an octet boundary, so the # bits left over inside the current octet are padding; only whole # octets after it are actual remaining input / Scapy payload. - consumed = self.total_number_of_bits - self.number_of_bits - pad = -consumed % 8 + pad = -self._pos % 8 if pad: if pad > self.number_of_bits: raise UPER_Decoding_Error("UPER_Decoder: truncated padding") - if self._read_bits_int(pad) != 0: + if self._peek_bits_int(pad) != 0: raise UPER_Decoding_Error( "UPER_Decoder: non-zero padding bits", remaining=self.remaining(), ) - self.number_of_bits -= pad - return self.remaining() + self._pos += pad + n = self.number_of_bits + if n == 0: + return b"" + if n % 8: + raise UPER_Decoding_Error("UPER_Decoder: truncated padding") + start = self._pos // 8 + return self._data[start:] def read_bytes(self, number_of_bytes): # type: (int) -> bytes - return self.read_bits(8 * number_of_bytes) + number_of_bits = 8 * number_of_bytes + if number_of_bits > self.number_of_bits: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + if self._pos % 8 == 0: + start = self._pos // 8 + end = start + number_of_bytes + self._pos += number_of_bits + return self._data[start:end] + return self.read_bits(number_of_bits) def read_non_negative_binary_integer(self, number_of_bits): # type: (int) -> int @@ -255,9 +292,7 @@ def read_non_negative_binary_integer(self, number_of_bits): raise UPER_Decoding_Error("UPER_Decoder: out of data") if number_of_bits == 0: return 0 - value = self._read_bits_int(number_of_bits) - self.number_of_bits -= number_of_bits - return value + return self._read_bits_int(number_of_bits) def _read_length_determinant(self): # type: () -> Tuple[int, bool] diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 72904af591c..91f10058ea6 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -235,20 +235,16 @@ def m2i(self, pkt, s): """ s = self._apply_tagging_dec(s, pkt, _fname=self.name) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - kwargs = dict( - context=self.context, - field=self, - pkt=pkt, - size_len=self.size_len, - ) - if self.flexible_tag: - return cast( - Tuple[_A, bytes], - codec.safedec(s, **kwargs), - ) + decode = codec.safedec if self.flexible_tag else codec.dec return cast( Tuple[_A, bytes], - codec.dec(s, **kwargs), + decode( + s, + context=self.context, + field=self, + pkt=pkt, + size_len=self.size_len, + ), ) def i2m(self, pkt, x): @@ -998,33 +994,23 @@ def __init__(self, name, default, *args, **kwargs): tag: i for i, (tag, _alt) in enumerate(canon_items) } # type: Dict[int, int] - @property - def choice_order(self): - # type: () -> List[int] - return list(self.choices.keys()) - - def alternative_index(self, x): + def alternative_tag(self, x): # type: (Any) -> Optional[int] - """Position in choice_order of the alternative that carries x.""" - for index, choice in enumerate(self.choices.values()): + """Return the CHOICE alternative tag that carries x, or None.""" + for tag, choice in self.choices.items(): if isinstance(choice, type): if hasattr(choice, "ASN1_root"): # ASN1_Packet subclass if isinstance(x, choice): - return index + return tag elif isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: # ASN1F_field subclass - return index + return tag elif isinstance(x, choice.cls): # ASN1F_PACKET instance, holding a tagged packet - return index + return tag return None - @property - def choice_list(self): - # type: () -> List[_CHOICE_T] - return list(self.choices.values()) - def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] if len(s) == 0: diff --git a/test/scapy/layers/ber.uts b/test/scapy/layers/ber.uts index 3c13033609d..8ea788cde6b 100644 --- a/test/scapy/layers/ber.uts +++ b/test/scapy/layers/ber.uts @@ -539,16 +539,16 @@ assert ConstrainedBer(raw(ConstrainedBer(n=5))).n.val == 5 True -= CHOICE order properties += CHOICE tag map properties choice = ASN1F_CHOICE( "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, ) -assert choice.choice_order == [2, 4] +assert list(choice.choices.keys()) == [2, 4] -assert choice.choice_list[0] is ASN1F_INTEGER +assert list(choice.choices.values())[0] is ASN1F_INTEGER -assert choice.choice_list[1] is ASN1F_STRING +assert list(choice.choices.values())[1] is ASN1F_STRING True diff --git a/test/scapy/layers/uper.uts b/test/scapy/layers/uper.uts index d634b086dfc..d75f27a1933 100644 --- a/test/scapy/layers/uper.uts +++ b/test/scapy/layers/uper.uts @@ -2658,7 +2658,7 @@ class _PacketChoice(ASN1_Packet): assert _PacketChoice.ASN1_root.randval() is not None -packet_field = _PacketChoice.ASN1_root.choice_list[0] +packet_field = list(_PacketChoice.ASN1_root.choices.values())[0] assert packet_field.randval() is not None From ae2370a7da88be2d18d6b16d8ef24156e2f7467b Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 22:11:24 +0200 Subject: [PATCH 41/46] Fix unaligned UPER read_bytes scaling and trim decode checks. Large OCTET STRING reads at a non-byte offset use one bulk int.from_bytes/shift instead of growing an integer per source byte; delegate whole-octet read_bits to that path and drop duplicate bounds checks on the small-field integer reader. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/uper.py | 37 +++++++++++++++++++++++-------------- scapy/asn1fields.py | 3 +-- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 061f9ff8ab0..b0e8bd1c47f 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -235,14 +235,14 @@ def read_bit(self): def read_bits(self, number_of_bits): # type: (int) -> bytes - if number_of_bits > self.number_of_bits: - raise UPER_Decoding_Error("UPER_Decoder: out of data") - if number_of_bits == 0: - return b"" - if number_of_bits % 8 == 0 and self._pos % 8 == 0: + # Whole-octet requests (aligned or not) use read_bytes(); only + # non-multiple-of-8 widths go through the small-field integer path. + if number_of_bits % 8 == 0: return self.read_bytes(number_of_bits // 8) - value = self._read_bits_int(number_of_bits) - return _uper_bits_to_bytes(value, number_of_bits) + return _uper_bits_to_bytes( + self._read_bits_int(number_of_bits), + number_of_bits, + ) def remaining(self): # type: () -> bytes @@ -276,22 +276,31 @@ def remaining_bytes(self): def read_bytes(self, number_of_bytes): # type: (int) -> bytes + # Do not route large unaligned octet strings through _peek_bits_int: + # growing a Python int one source byte at a time is super-linear. number_of_bits = 8 * number_of_bytes if number_of_bits > self.number_of_bits: raise UPER_Decoding_Error("UPER_Decoder: out of data") - if self._pos % 8 == 0: - start = self._pos // 8 + if number_of_bytes == 0: + return b"" + start = self._pos // 8 + offset = self._pos % 8 + if offset == 0: end = start + number_of_bytes self._pos += number_of_bits return self._data[start:end] - return self.read_bits(number_of_bits) + # One bulk integer conversion/shift for the n+1 overlapping source + # bytes, rather than growing an int per source byte. + window = int.from_bytes( + self._data[start:start + number_of_bytes + 1], + "big", + ) + value = (window >> (8 - offset)) & ((1 << number_of_bits) - 1) + self._pos += number_of_bits + return value.to_bytes(number_of_bytes, "big") def read_non_negative_binary_integer(self, number_of_bits): # type: (int) -> int - if number_of_bits > self.number_of_bits: - raise UPER_Decoding_Error("UPER_Decoder: out of data") - if number_of_bits == 0: - return 0 return self._read_bits_int(number_of_bits) def _read_length_determinant(self): diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 91f10058ea6..1497aec8b79 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -984,9 +984,8 @@ def __init__(self, name, default, *args, **kwargs): else: raise ASN1_Error("ASN1F_CHOICE: no tag found for one field") # X.691 10.2: PER indexes alternatives in canonical tag order. - decl_items = list(self.choices.items()) canon_items = sorted( - decl_items, + self.choices.items(), key=lambda item: asn1_tag_parts(item[0])[:2], ) self.canonical_order = [alt for _tag, alt in canon_items] From 4c623bb48e8d55895994cf65b754b8a9285258f0 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 2 Sep 2026 22:17:19 +0200 Subject: [PATCH 42/46] Clarify SEQUENCE OF element contract and compound encode APIs. Reject SEQUENCE/CHOICE/SEQUENCE OF as SEQUENCE OF field elements with an explicit error, keep ASN1F_PACKET (used by Kerberos) via UPER context hooks, and drop misleading compound encode_into/dissect_from_decoder wrappers so only primitives use the raw-bit API. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/compound_uper.py | 6 ++++ scapy/asn1fields.py | 56 +++++++++++++++++++---------------- test/scapy/layers/uper.uts | 58 +++++++++++++++++++++++++++++-------- 3 files changed, 83 insertions(+), 37 deletions(-) diff --git a/scapy/asn1/compound_uper.py b/scapy/asn1/compound_uper.py index c4a43849d41..1927b6f4e8a 100644 --- a/scapy/asn1/compound_uper.py +++ b/scapy/asn1/compound_uper.py @@ -54,6 +54,7 @@ def uper_sequence_decode_from(dec, field, pkt): def uper_sequence_of_encode_to(enc, field, pkt, value=None): # type: (Any, Any, Any, Any) -> None from scapy.asn1.uper import UPER_constrained_int_enc + from scapy.asn1fields import ASN1F_PACKET bit_enc = enc.bit_encoder if value is None: @@ -68,6 +69,8 @@ def append_items(offset, size): item = value[i] if field.holds_packets: item.ASN1_root.encode_to(item, enc) + elif isinstance(field.fld, ASN1F_PACKET): + enc.encode_packet(field.fld, pkt, item) else: field.fld.encode_into(bit_enc, pkt, item) @@ -92,6 +95,7 @@ def append_items(offset, size): def uper_sequence_of_decode_from(dec, field, pkt): # type: (Any, Any, Any) -> None from scapy.asn1.uper import UPER_constrained_int_dec + from scapy.asn1fields import ASN1F_PACKET bit_dec = dec.bit_decoder lst = [] @@ -105,6 +109,8 @@ def read_items(count): p.add_parent(pkt) p.ASN1_root.decode_from(p, dec) lst.append(p) + elif isinstance(field.fld, ASN1F_PACKET): + lst.append(uper_packet_decode_from_decoder(field.fld, pkt, dec)) else: lst.append(field.fld.m2i_from_decoder(pkt, bit_dec)) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 1497aec8b79..2482056eae9 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -664,14 +664,6 @@ def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None enc.encode_sequence(self, pkt) - def encode_into(self, enc, pkt, value=None): - # type: (Any, ASN1_Packet, Any) -> None - self.encode_to(pkt, enc) - - def dissect_from_decoder(self, pkt, dec): - # type: (ASN1_Packet, Any) -> None - self.decode_from(pkt, dec) - def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None dec.decode_sequence(self, pkt) @@ -689,10 +681,32 @@ class ASN1F_SET(ASN1F_SEQUENCE): ] +def _is_compound_asn1_field(fld): + # type: (ASN1F_field[Any, Any]) -> bool + """True for nested SEQUENCE/SET/CHOICE/SEQUENCE OF field instances. + + ASN1F_PACKET is allowed as a SEQUENCE OF element (Kerberos and others); + UPER handles it via the context packet hooks rather than encode_into. + """ + # Resolved at call time so ASN1F_SEQUENCE_OF can run before CHOICE is + # defined in this module. + return isinstance(fld, ( + ASN1F_SEQUENCE, + ASN1F_CHOICE, + ASN1F_SEQUENCE_OF, + )) + + class ASN1F_SEQUENCE_OF(ASN1F_field[List[_SEQ_T], List[ASN1_Object[Any]]]): """ - Two types are allowed as cls: ASN1_Packet, ASN1F_field + Two types are allowed as cls: + - ASN1_Packet (or callable returning one) for structured / compound items + - a *primitive* ASN1F_field (class or instance) for scalar items + + Compound ASN1F_field elements (SEQUENCE, SET, CHOICE, SEQUENCE OF / + SET OF) are rejected: nest an ASN1_Packet instead. ASN1F_PACKET + elements are allowed (tagged nested packets). """ ASN1_tag = ASN1_Class_UNIVERSAL.SEQUENCE islist = 1 @@ -713,6 +727,14 @@ def __init__(self, self.fld = cls(name, b"") else: self.fld = cls + # UPER SEQUENCE OF uses the raw-bit primitive API on fld + # (encode_into / m2i_from_decoder). Compound fields only + # implement the context API (encode_to / decode_from). + if _is_compound_asn1_field(self.fld): + raise ValueError( + "ASN1F_SEQUENCE_OF: compound ASN1F_field elements are " + "not supported; use an ASN1_Packet for structured items" + ) self._extract_packet = lambda s, pkt: self.fld.m2i(pkt, s) self.holds_packets = 0 elif hasattr(cls, "ASN1_root") or callable(cls): @@ -855,14 +877,6 @@ def build(self, pkt): return b"" return self._field.build(pkt) - def dissect_from_decoder(self, pkt, dec): - # type: (ASN1_Packet, Any) -> None - self._field.dissect_from_decoder(pkt, dec) - - def encode_into(self, enc, pkt, value=None): - # type: (Any, ASN1_Packet, Any) -> None - self._field.encode_into(enc, pkt, value) - def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None if self.is_present(pkt): @@ -1022,10 +1036,6 @@ def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None enc.encode_choice(self, pkt) - def encode_into(self, enc, pkt, value=None): - # type: (Any, ASN1_Packet, Any) -> None - enc.encode_choice(self, pkt, value) - def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None dec.decode_choice(self, pkt) @@ -1081,10 +1091,6 @@ def encode_to(self, pkt, enc): # type: (ASN1_Packet, Any) -> None enc.encode_packet(self, pkt) - def encode_into(self, enc, pkt, value=None): - # type: (Any, ASN1_Packet, Any) -> None - enc.encode_packet(self, pkt, value) - def decode_from(self, pkt, dec): # type: (ASN1_Packet, Any) -> None dec.decode_packet(self, pkt) diff --git a/test/scapy/layers/uper.uts b/test/scapy/layers/uper.uts index d75f27a1933..72c5bb60192 100644 --- a/test/scapy/layers/uper.uts +++ b/test/scapy/layers/uper.uts @@ -2203,7 +2203,7 @@ dec = UPER_DecoderContext(b"\x80") _raises( UPER_Decoding_Error, - lambda: _ExtSeq.ASN1_root.dissect_from_decoder(_ExtSeq(), dec), + lambda: _ExtSeq.ASN1_root.decode_from(_ExtSeq(), dec), ) class _ExtChoice(ASN1_Packet): @@ -2217,11 +2217,11 @@ choice = _ExtChoice(c=ASN1_INTEGER(4)) assert raw(choice) -dec = UPER_Decoder(b"\x80") +dec = UPER_DecoderContext(b"\x80") _raises( UPER_Decoding_Error, - lambda: _ExtChoice.ASN1_root.m2i_from_decoder(_ExtChoice(), dec), + lambda: _ExtChoice.ASN1_root.decode_from(_ExtChoice(), dec), ) class _InnerItem(ASN1_Packet): @@ -2299,6 +2299,40 @@ assert _EmptySeqOf.ASN1_root.i2repr( _raises(ValueError, lambda: ASN1F_SEQUENCE_OF("bad", [], object())) +_raises( + ValueError, + lambda: ASN1F_SEQUENCE_OF( + "items", + [], + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, minimum=0, maximum=15), + ), + ), +) + +_raises( + ValueError, + lambda: ASN1F_SEQUENCE_OF( + "items", + [], + ASN1F_SEQUENCE_OF("inner", [], ASN1F_INTEGER), + ), +) + +# ASN1F_PACKET elements remain valid (same as ASN1_Packet nesting). +class _PktItem(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, minimum=0, maximum=7) + +class _SeqOfPacketField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], ASN1F_PACKET("item", None, _PktItem), + ) + +pkt = _SeqOfPacketField(items=[_PktItem(n=1), _PktItem(n=2)]) +assert [x.n.val for x in _SeqOfPacketField(raw(pkt)).items] == [1, 2] + True = asn1fields choice advanced @@ -2370,8 +2404,8 @@ _raises( _raises( ASN1_Error, - lambda: _PerChoice.ASN1_root.encode_into( - UPER_EncoderContext(), _PerChoice(), 42, + lambda: UPER_EncoderContext().encode_choice( + _PerChoice.ASN1_root, _PerChoice(), 42, ), ) @@ -2732,11 +2766,11 @@ class _ExtChoice(ASN1_Packet): extensible=True, ) -dec = UPER_Decoder(b"\x80") +dec = UPER_DecoderContext(b"\x80") _raises( UPER_Decoding_Error, - lambda: _ExtChoice.ASN1_root.m2i_from_decoder(_ExtChoice(), dec), + lambda: _ExtChoice.ASN1_root.decode_from(_ExtChoice(), dec), ) class _SingleChoice(ASN1_Packet): @@ -2866,18 +2900,18 @@ assert _val(decoded.inner.x) == 7 True -= uper field encode_into nesting += uper field encode_to nesting built = UPERWrappedPacket(id=1, inner=UPERInnerPacket(x=7)) enc = UPER_EncoderContext() -UPERWrappedPacket.ASN1_root.encode_into(enc, built) +UPERWrappedPacket.ASN1_root.encode_to(built, enc) assert enc.finish() == raw(built) empty = UPERWrappedPacket() -UPERWrappedPacket.ASN1_root.dissect_from_decoder( +UPERWrappedPacket.ASN1_root.decode_from( empty, UPER_DecoderContext(raw(built)), ) @@ -3618,14 +3652,14 @@ _raises(UPER_Encoding_Error, lambda: UPERcodec_UTC_TIME.enc(b"250101000000Z")) _raises(UPER_Encoding_Error, lambda: UPERcodec_GENERALIZED_TIME.enc( b"20250101000000Z")) -= CHOICE encode_into does not mutate packet fields += CHOICE encode_choice does not mutate packet fields import copy from scapy.asn1.context import UPER_EncoderContext pkt = UPERChoiceField(c=ASN1_INTEGER(1)) before = copy.copy(pkt.fields) enc = UPER_EncoderContext() -pkt.ASN1_root.encode_into(enc, pkt, value=ASN1_INTEGER(9)) +enc.encode_choice(pkt.ASN1_root, pkt, value=ASN1_INTEGER(9)) assert pkt.fields == before assert enc.finish() == bytes.fromhex("008480") True From 6affdf7c22e89d77c511397798e2e903313cd96d Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 3 Sep 2026 09:20:10 +0200 Subject: [PATCH 43/46] Make UPER encoding byte-oriented and soften SEQUENCE OF limits. Replace chunked giant-int finalization with a bytearray plus pending-bit accumulator, speed up Decoder.remaining(), and reject compound SEQUENCE OF elements only on the UPER path so BER/OER keep master construction behavior. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/compound_uper.py | 38 +++++++++++-- scapy/asn1/uper.py | 105 +++++++++++++++++++++++++++--------- scapy/asn1fields.py | 34 ++---------- test/scapy/layers/uper.uts | 31 +++++++---- 4 files changed, 139 insertions(+), 69 deletions(-) diff --git a/scapy/asn1/compound_uper.py b/scapy/asn1/compound_uper.py index 1927b6f4e8a..d7aede1c94c 100644 --- a/scapy/asn1/compound_uper.py +++ b/scapy/asn1/compound_uper.py @@ -53,8 +53,23 @@ def uper_sequence_decode_from(dec, field, pkt): def uper_sequence_of_encode_to(enc, field, pkt, value=None): # type: (Any, Any, Any, Any) -> None - from scapy.asn1.uper import UPER_constrained_int_enc - from scapy.asn1fields import ASN1F_PACKET + from scapy.asn1.uper import UPER_Encoding_Error, UPER_constrained_int_enc + from scapy.asn1fields import ( + ASN1F_CHOICE, + ASN1F_PACKET, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ) + + if ( + not field.holds_packets and + not isinstance(field.fld, ASN1F_PACKET) and + isinstance(field.fld, (ASN1F_SEQUENCE, ASN1F_CHOICE, ASN1F_SEQUENCE_OF)) + ): + raise UPER_Encoding_Error( + "ASN1F_SEQUENCE_OF: compound ASN1F_field elements are not " + "supported in UPER; use an ASN1_Packet for structured items" + ) bit_enc = enc.bit_encoder if value is None: @@ -94,8 +109,23 @@ def append_items(offset, size): def uper_sequence_of_decode_from(dec, field, pkt): # type: (Any, Any, Any) -> None - from scapy.asn1.uper import UPER_constrained_int_dec - from scapy.asn1fields import ASN1F_PACKET + from scapy.asn1.uper import UPER_Decoding_Error, UPER_constrained_int_dec + from scapy.asn1fields import ( + ASN1F_CHOICE, + ASN1F_PACKET, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ) + + if ( + not field.holds_packets and + not isinstance(field.fld, ASN1F_PACKET) and + isinstance(field.fld, (ASN1F_SEQUENCE, ASN1F_CHOICE, ASN1F_SEQUENCE_OF)) + ): + raise UPER_Decoding_Error( + "ASN1F_SEQUENCE_OF: compound ASN1F_field elements are not " + "supported in UPER; use an ASN1_Packet for structured items" + ) bit_dec = dec.bit_decoder lst = [] diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index b0e8bd1c47f..c37f8596d2c 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -90,22 +90,35 @@ def _uper_bits_to_bytes(value, number_of_bits): class UPER_Encoder(object): + """Byte-oriented UPER bit writer. + + Completed octets live in a ``bytearray``; at most seven pending bits are + kept in ``_acc``. Large octet strings stay as bytes instead of being + folded into a growing multi-precision integer. + """ + def __init__(self): # type: () -> None - self.number_of_bits = 0 - self.value = 0 - self.chunks = [] # type: List[List[int]] + self._buf = bytearray() # type: bytearray + self._acc = 0 # type: int + self._nbits = 0 # type: int def append_bit(self, bit): # type: (int) -> None - self.number_of_bits += 1 - self.value <<= 1 - self.value |= 1 if bit else 0 + self._acc = (self._acc << 1) | (1 if bit else 0) + self._nbits += 1 + if self._nbits == 8: + self._buf.append(self._acc) + self._acc = 0 + self._nbits = 0 def append_bits(self, data, number_of_bits): # type: (bytes, int) -> None if number_of_bits == 0: return + if number_of_bits == 8 * len(data): + self.append_bytes(data) + return value = int.from_bytes(data, "big") value >>= (8 * len(data) - number_of_bits) self.append_non_negative_binary_integer(value, number_of_bits) @@ -114,17 +127,56 @@ def append_non_negative_binary_integer(self, value, number_of_bits): # type: (int, int) -> None if number_of_bits == 0: return - if self.number_of_bits > 4096: - self.chunks.append([self.value, self.number_of_bits]) - self.number_of_bits = 0 - self.value = 0 - self.number_of_bits += number_of_bits - self.value <<= number_of_bits - self.value |= value & ((1 << number_of_bits) - 1) + value &= (1 << number_of_bits) - 1 + # Fill the pending octet first so the middle can be raw bytes. + if self._nbits: + space = 8 - self._nbits + if number_of_bits <= space: + self._acc = (self._acc << number_of_bits) | value + self._nbits += number_of_bits + if self._nbits == 8: + self._buf.append(self._acc) + self._acc = 0 + self._nbits = 0 + return + self._acc = ( + (self._acc << space) | (value >> (number_of_bits - space)) + ) + self._buf.append(self._acc) + number_of_bits -= space + value &= (1 << number_of_bits) - 1 + self._acc = 0 + self._nbits = 0 + full_bytes = number_of_bits // 8 + rem = number_of_bits % 8 + if full_bytes: + mid = value >> rem if rem else value + self._buf.extend(mid.to_bytes(full_bytes, "big")) + if rem: + value &= (1 << rem) - 1 + number_of_bits = rem + if number_of_bits: + self._acc = value + self._nbits = number_of_bits def append_bytes(self, data): # type: (bytes) -> None - self.append_bits(data, 8 * len(data)) + if not data: + return + if self._nbits == 0: + self._buf.extend(data) + return + # Unaligned: each source octet yields one completed output octet and + # leaves the same number of pending bits. + offset = self._nbits + acc = self._acc + buf = self._buf + mask = (1 << offset) - 1 + for byte in data: + combined = (acc << 8) | byte + buf.append(combined >> offset) + acc = combined & mask + self._acc = acc def append_length_determinant(self, length): # type: (int) -> None @@ -171,16 +223,10 @@ def append_unconstrained_whole_number(self, value): def as_bytes(self): # type: () -> bytes - value = 0 - number_of_bits = 0 - for chunk_value, chunk_number_of_bits in self.chunks: - value <<= chunk_number_of_bits - value |= chunk_value - number_of_bits += chunk_number_of_bits - value <<= self.number_of_bits - value |= self.value - number_of_bits += self.number_of_bits - return _uper_bits_to_bytes(value, number_of_bits) + if self._nbits == 0: + return bytes(self._buf) + # X.691 11.1: pad with zero bits up to an octet boundary. + return bytes(self._buf) + bytes([self._acc << (8 - self._nbits)]) class UPER_Decoder(object): @@ -249,7 +295,16 @@ def remaining(self): n = self.number_of_bits if n == 0: return b"" - return _uper_bits_to_bytes(self._peek_bits_int(n), n) + if self._pos % 8 == 0: + return self._data[self._pos // 8:] + # One bulk conversion for the remaining window (error-path helper). + start = self._pos // 8 + offset = self._pos % 8 + nbytes = (offset + n + 7) // 8 + window = int.from_bytes(self._data[start:start + nbytes], "big") + shift = nbytes * 8 - offset - n + value = (window >> shift) & ((1 << n) - 1) + return _uper_bits_to_bytes(value, n) def remaining_bytes(self): # type: () -> bytes diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 2482056eae9..9320f33ec8f 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -681,32 +681,14 @@ class ASN1F_SET(ASN1F_SEQUENCE): ] -def _is_compound_asn1_field(fld): - # type: (ASN1F_field[Any, Any]) -> bool - """True for nested SEQUENCE/SET/CHOICE/SEQUENCE OF field instances. - - ASN1F_PACKET is allowed as a SEQUENCE OF element (Kerberos and others); - UPER handles it via the context packet hooks rather than encode_into. - """ - # Resolved at call time so ASN1F_SEQUENCE_OF can run before CHOICE is - # defined in this module. - return isinstance(fld, ( - ASN1F_SEQUENCE, - ASN1F_CHOICE, - ASN1F_SEQUENCE_OF, - )) - - class ASN1F_SEQUENCE_OF(ASN1F_field[List[_SEQ_T], List[ASN1_Object[Any]]]): """ - Two types are allowed as cls: - - ASN1_Packet (or callable returning one) for structured / compound items - - a *primitive* ASN1F_field (class or instance) for scalar items + Two types are allowed as cls: ASN1_Packet, ASN1F_field - Compound ASN1F_field elements (SEQUENCE, SET, CHOICE, SEQUENCE OF / - SET OF) are rejected: nest an ASN1_Packet instead. ASN1F_PACKET - elements are allowed (tagged nested packets). + Structured items are normally ASN1_Packet (or ASN1F_PACKET). Compound + ASN1F_field elements (SEQUENCE / CHOICE / SEQUENCE OF) remain constructible + for BER/OER; UPER rejects them at encode/decode time. """ ASN1_tag = ASN1_Class_UNIVERSAL.SEQUENCE islist = 1 @@ -727,14 +709,6 @@ def __init__(self, self.fld = cls(name, b"") else: self.fld = cls - # UPER SEQUENCE OF uses the raw-bit primitive API on fld - # (encode_into / m2i_from_decoder). Compound fields only - # implement the context API (encode_to / decode_from). - if _is_compound_asn1_field(self.fld): - raise ValueError( - "ASN1F_SEQUENCE_OF: compound ASN1F_field elements are " - "not supported; use an ASN1_Packet for structured items" - ) self._extract_packet = lambda s, pkt: self.fld.m2i(pkt, s) self.holds_packets = 0 elif hasattr(cls, "ASN1_root") or callable(cls): diff --git a/test/scapy/layers/uper.uts b/test/scapy/layers/uper.uts index 72c5bb60192..fd8976a42ef 100644 --- a/test/scapy/layers/uper.uts +++ b/test/scapy/layers/uper.uts @@ -2299,26 +2299,37 @@ assert _EmptySeqOf.ASN1_root.i2repr( _raises(ValueError, lambda: ASN1F_SEQUENCE_OF("bad", [], object())) -_raises( - ValueError, - lambda: ASN1F_SEQUENCE_OF( +# Compound ASN1F_field elements stay constructible (BER/OER); UPER rejects +# them at encode/decode time rather than at construction. +class _UperSeqOfSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( "items", [], ASN1F_SEQUENCE( ASN1F_INTEGER("x", 0, minimum=0, maximum=15), ), - ), -) + ) +_raises(UPER_Encoding_Error, lambda: raw(_UperSeqOfSeq(items=[]))) _raises( - ValueError, - lambda: ASN1F_SEQUENCE_OF( - "items", - [], - ASN1F_SEQUENCE_OF("inner", [], ASN1F_INTEGER), + UPER_Decoding_Error, + lambda: _UperSeqOfSeq.ASN1_root.decode_from( + _UperSeqOfSeq(), UPER_DecoderContext(b"\x00"), ), ) +class _BerSeqOfChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], + ASN1F_CHOICE("c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING), + ) + +assert [x.val for x in _BerSeqOfChoice(raw( + _BerSeqOfChoice(items=[ASN1_INTEGER(5)]) +)).items] == [5] + # ASN1F_PACKET elements remain valid (same as ASN1_Packet nesting). class _PktItem(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER From f4c384502e5c88200058b2fdd49cb6446edef86a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 3 Sep 2026 10:26:42 +0200 Subject: [PATCH 44/46] Speed up UPER bit primitives with bulk window operations. Replace the per-byte _peek_bits_int loop, keep append_bits on the byte path except for a trailing partial octet, bulk-shift unaligned append_bytes, and trim redundant SEQUENCE OF PACKET checks. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/compound_uper.py | 2 - scapy/asn1/uper.py | 88 +++++++++++++++---------------------- 2 files changed, 36 insertions(+), 54 deletions(-) diff --git a/scapy/asn1/compound_uper.py b/scapy/asn1/compound_uper.py index d7aede1c94c..b9b9d5e1be4 100644 --- a/scapy/asn1/compound_uper.py +++ b/scapy/asn1/compound_uper.py @@ -63,7 +63,6 @@ def uper_sequence_of_encode_to(enc, field, pkt, value=None): if ( not field.holds_packets and - not isinstance(field.fld, ASN1F_PACKET) and isinstance(field.fld, (ASN1F_SEQUENCE, ASN1F_CHOICE, ASN1F_SEQUENCE_OF)) ): raise UPER_Encoding_Error( @@ -119,7 +118,6 @@ def uper_sequence_of_decode_from(dec, field, pkt): if ( not field.holds_packets and - not isinstance(field.fld, ASN1F_PACKET) and isinstance(field.fld, (ASN1F_SEQUENCE, ASN1F_CHOICE, ASN1F_SEQUENCE_OF)) ): raise UPER_Decoding_Error( diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index c37f8596d2c..c62aae6129d 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -116,12 +116,16 @@ def append_bits(self, data, number_of_bits): # type: (bytes, int) -> None if number_of_bits == 0: return - if number_of_bits == 8 * len(data): - self.append_bytes(data) - return - value = int.from_bytes(data, "big") - value >>= (8 * len(data) - number_of_bits) - self.append_non_negative_binary_integer(value, number_of_bits) + # Keep whole octets on the byte path; only the final 1–7 bits become + # an integer (avoids int.from_bytes of a multi-megabit BIT STRING). + full_bytes, remaining_bits = divmod(number_of_bits, 8) + if full_bytes: + self.append_bytes(data[:full_bytes]) + if remaining_bits: + self.append_non_negative_binary_integer( + data[full_bytes] >> (8 - remaining_bits), + remaining_bits, + ) def append_non_negative_binary_integer(self, value, number_of_bits): # type: (int, int) -> None @@ -166,17 +170,18 @@ def append_bytes(self, data): if self._nbits == 0: self._buf.extend(data) return - # Unaligned: each source octet yields one completed output octet and - # leaves the same number of pending bits. + # One bulk shift for the whole block rather than a Python loop per + # source octet (large OCTET STRINGs after a presence/extension bit). offset = self._nbits - acc = self._acc - buf = self._buf - mask = (1 << offset) - 1 - for byte in data: - combined = (acc << 8) | byte - buf.append(combined >> offset) - acc = combined & mask - self._acc = acc + size = len(data) + value = int.from_bytes(data, "big") + self._buf.extend( + ( + (self._acc << (8 * size - offset)) | + (value >> offset) + ).to_bytes(size, "big") + ) + self._acc = value & ((1 << offset) - 1) def append_length_determinant(self, length): # type: (int) -> None @@ -226,7 +231,10 @@ def as_bytes(self): if self._nbits == 0: return bytes(self._buf) # X.691 11.1: pad with zero bits up to an octet boundary. - return bytes(self._buf) + bytes([self._acc << (8 - self._nbits)]) + return b"".join(( + self._buf, + bytes([self._acc << (8 - self._nbits)]), + )) class UPER_Decoder(object): @@ -249,20 +257,12 @@ def _peek_bits_int(self, number_of_bits): return 0 if number_of_bits > self.number_of_bits: raise UPER_Decoding_Error("UPER_Decoder: out of data") - byte_index = self._pos // 8 - bit_offset = self._pos % 8 - value = 0 - bits_left = number_of_bits - while bits_left: - avail = 8 - bit_offset - take = bits_left if bits_left < avail else avail - shift = avail - take - chunk = (self._data[byte_index] >> shift) & ((1 << take) - 1) - value = (value << take) | chunk - bits_left -= take - byte_index += 1 - bit_offset = 0 - return value + start = self._pos // 8 + offset = self._pos % 8 + nbytes = (offset + number_of_bits + 7) // 8 + window = int.from_bytes(self._data[start:start + nbytes], "big") + shift = nbytes * 8 - offset - number_of_bits + return (window >> shift) & ((1 << number_of_bits) - 1) def _read_bits_int(self, number_of_bits): # type: (int) -> int @@ -297,14 +297,7 @@ def remaining(self): return b"" if self._pos % 8 == 0: return self._data[self._pos // 8:] - # One bulk conversion for the remaining window (error-path helper). - start = self._pos // 8 - offset = self._pos % 8 - nbytes = (offset + n + 7) // 8 - window = int.from_bytes(self._data[start:start + nbytes], "big") - shift = nbytes * 8 - offset - n - value = (window >> shift) & ((1 << n) - 1) - return _uper_bits_to_bytes(value, n) + return _uper_bits_to_bytes(self._peek_bits_int(n), n) def remaining_bytes(self): # type: () -> bytes @@ -331,28 +324,19 @@ def remaining_bytes(self): def read_bytes(self, number_of_bytes): # type: (int) -> bytes - # Do not route large unaligned octet strings through _peek_bits_int: - # growing a Python int one source byte at a time is super-linear. number_of_bits = 8 * number_of_bytes if number_of_bits > self.number_of_bits: raise UPER_Decoding_Error("UPER_Decoder: out of data") if number_of_bytes == 0: return b"" - start = self._pos // 8 - offset = self._pos % 8 - if offset == 0: + if self._pos % 8 == 0: + start = self._pos // 8 end = start + number_of_bytes self._pos += number_of_bits return self._data[start:end] - # One bulk integer conversion/shift for the n+1 overlapping source - # bytes, rather than growing an int per source byte. - window = int.from_bytes( - self._data[start:start + number_of_bytes + 1], - "big", + return self._read_bits_int(number_of_bits).to_bytes( + number_of_bytes, "big", ) - value = (window >> (8 - offset)) & ((1 << number_of_bits) - 1) - self._pos += number_of_bits - return value.to_bytes(number_of_bytes, "big") def read_non_negative_binary_integer(self, number_of_bits): # type: (int) -> int From 2ed4cfad57a69aeee83ca1a2310678948352739a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 3 Sep 2026 12:20:43 +0200 Subject: [PATCH 45/46] Tighten OER/UPER hot-path leftovers and drop CI from this PR. Cache ENUMERATED PER value order, avoid fragment bytes copies with memoryview, replace OER_Exception with OER_Encoding_Error, collapse redundant ASN1_Error handlers, and restore check_commits.sh so the CI fix stays out of the ASN.1 PR. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- .config/ci/check_commits.sh | 9 +-------- scapy/asn1/oer.py | 8 +------- scapy/asn1/uper.py | 21 +++++++++++++++------ scapy/asn1fields.py | 5 ++++- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/.config/ci/check_commits.sh b/.config/ci/check_commits.sh index 8ca9bc16bc8..f5efc667c48 100755 --- a/.config/ci/check_commits.sh +++ b/.config/ci/check_commits.sh @@ -6,14 +6,7 @@ # We copy Wireshark's contributing guide, thanks to them for the idea ! # This script is inspired by https://gitlab.com/wireshark/wireshark/-/blob/master/.gitlab-ci.yml -# On pull_request, actions/checkout creates a merge of the PR into the base -# branch (HEAD^1=base tip, HEAD^2=PR tip). Restrict the check to PR commits -# so base-branch history is not false-failed for missing trailers. -if git rev-parse -q --verify HEAD^2 >/dev/null 2>&1; then - commits=$(git rev-list --no-merges HEAD^1..HEAD^2) -else - commits=$(git rev-list --no-merges --after="2026-01-00T00:00:00" --max-count=$((PR_FETCH_DEPTH - 1)) HEAD) -fi +commits=$(git rev-list --no-merges --after="2026-01-00T00:00:00" --max-count=$((PR_FETCH_DEPTH - 1)) HEAD) if [ -z "$commits" ]; then echo "No commit to check in PR. OK." exit 0 diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index 548343ab657..840f2e5d7bb 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -57,10 +57,6 @@ ################## -class OER_Exception(Exception): - pass - - class OER_Encoding_Error(ASN1_Encoding_Error): pass @@ -94,7 +90,7 @@ def OER_len_enc(ll): return chb(ll) number_of_bytes = (ll.bit_length() + 7) // 8 if number_of_bytes > 127: - raise OER_Exception( + raise OER_Encoding_Error( "OER_len_enc: Length too long (%i) to be encoded" % number_of_bytes ) @@ -257,8 +253,6 @@ def dec(cls, return cls.do_dec( s, context=context, safe=safe, **_kwargs, ) - except OER_Decoding_Error as e: - return ASN1_DECODING_ERROR(s, exc=e), b"" except ASN1_Error as e: return ASN1_DECODING_ERROR(s, exc=e), b"" diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index c62aae6129d..ce63f981751 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -462,6 +462,7 @@ def UPER_octet_string_enc(enc, data, minimum=None, maximum=None, extensible=False): # type: (UPER_Encoder, bytes, Optional[int], Optional[int], bool) -> None length = len(data) + data_view = memoryview(data) if extensible and minimum is not None and maximum is not None: if minimum <= length <= maximum: enc.append_bit(0) @@ -469,7 +470,9 @@ def UPER_octet_string_enc(enc, data, minimum=None, maximum=None, enc.append_bit(1) enc.append_fragmented( length, - lambda offset, size: enc.append_bytes(data[offset:offset + size]), + lambda offset, size: enc.append_bytes( + data_view[offset:offset + size] + ), ) return if minimum is not None and maximum is not None: @@ -485,7 +488,9 @@ def UPER_octet_string_enc(enc, data, minimum=None, maximum=None, else: enc.append_fragmented( length, - lambda offset, size: enc.append_bytes(data[offset:offset + size]), + lambda offset, size: enc.append_bytes( + data_view[offset:offset + size] + ), ) @@ -584,7 +589,7 @@ def dec(cls, s, context=None, safe=False, **kwargs): return cls.do_dec(s, context, safe, **kwargs) try: return cls.do_dec(s, context, safe, **kwargs) - except (UPER_Decoding_Error, ASN1_Error) as e: + except ASN1_Error as e: return ASN1_DECODING_ERROR(s, exc=e), b"" @classmethod @@ -718,6 +723,7 @@ def encode_into(cls, minimum, maximum, extensible = resolve_uper_size_bounds( field, size_len, minimum, maximum, extensible, ) + s_view = memoryview(s) if extensible and minimum is not None and maximum is not None: if minimum <= nbits <= maximum: enc.append_bit(0) @@ -726,7 +732,7 @@ def encode_into(cls, enc.append_fragmented( nbits, lambda offset, size: enc.append_bits( - s[offset // 8:(offset + size + 7) // 8], size + s_view[offset // 8:(offset + size + 7) // 8], size ), ) return @@ -745,7 +751,7 @@ def encode_into(cls, # Fragments hold whole multiples of 16K bits, so every chunk # but the last starts and ends on an octet boundary. lambda offset, size: enc.append_bits( - s[offset // 8:(offset + size + 7) // 8], size + s_view[offset // 8:(offset + size + 7) // 8], size ), ) @@ -866,9 +872,12 @@ def encode_into(cls, enc, _oid, **_kwargs): oid = bytes_encode(_oid) lst = oid_dotted_to_subidentifiers(oid) body = b"".join(BER_num_enc(k) for k in lst) + body_view = memoryview(body) enc.append_fragmented( len(body), - lambda offset, size: enc.append_bytes(body[offset:offset + size]), + lambda offset, size: enc.append_bytes( + body_view[offset:offset + size] + ), ) @classmethod diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 9320f33ec8f..afe02f64b49 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -432,10 +432,13 @@ def __init__(self, for k in keys: i2s[k] = enum[k] s2i[enum[k]] = k + # Canonical PER index order (X.691); built once — i2s is not mutated + # after construction. + self._uper_enum_values = sorted(i2s) # type: List[int] def uper_enum_values(self): # type: () -> List[int] - return sorted(self.i2s) + return self._uper_enum_values def normalize_encode_value(self, pkt, value): # type: (ASN1_Packet, Any) -> Any From 37fc5bf22146f2e8ed0cb701046d22a83beda073 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 3 Sep 2026 19:42:52 +0200 Subject: [PATCH 46/46] Fix string-keyed ENUMERATED and make memoryview fragmentation-only. Drop the broken _uper_enum_values cache (sorted the swapped local map), create memoryviews only for >=16K fragmented payloads, slim the UPER enum helper, and avoid copying textual BIT STRING padding. AI-Assisted: yes (Cursor Agent) Co-authored-by: Cursor --- scapy/asn1/constraints.py | 14 ++++---- scapy/asn1/uper.py | 73 +++++++++++++++++++------------------- scapy/asn1fields.py | 7 ++-- test/scapy/layers/uper.uts | 14 ++++++++ 4 files changed, 59 insertions(+), 49 deletions(-) diff --git a/scapy/asn1/constraints.py b/scapy/asn1/constraints.py index 0b72ee4169b..8614e20a6bd 100644 --- a/scapy/asn1/constraints.py +++ b/scapy/asn1/constraints.py @@ -99,14 +99,12 @@ def resolve_oer_size_bounds(field=None, size_len=None): return None, None -def uper_enum_values(field=None, pkt=None, uper_enum_values=None): - # type: (Any, Any, Optional[List[int]]) -> Optional[List[int]] - if uper_enum_values is not None: - return uper_enum_values - if field is not None and pkt is not None and hasattr(field, "uper_enum_values"): - from scapy.asn1.asn1 import ASN1_Codecs - if getattr(pkt, "ASN1_codec", None) is ASN1_Codecs.PER: - return field.uper_enum_values() +def uper_enum_values(field=None, values=None): + # type: (Any, Optional[List[int]]) -> Optional[List[int]] + if values is not None: + return values + if field is not None and hasattr(field, "uper_enum_values"): + return field.uper_enum_values() return None diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index ce63f981751..9e0da5bd871 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -462,18 +462,19 @@ def UPER_octet_string_enc(enc, data, minimum=None, maximum=None, extensible=False): # type: (UPER_Encoder, bytes, Optional[int], Optional[int], bool) -> None length = len(data) - data_view = memoryview(data) + + def append_slice(offset, size): + # type: (int, int) -> None + enc.append_bytes(data[offset:offset + size]) + if extensible and minimum is not None and maximum is not None: if minimum <= length <= maximum: enc.append_bit(0) else: enc.append_bit(1) - enc.append_fragmented( - length, - lambda offset, size: enc.append_bytes( - data_view[offset:offset + size] - ), - ) + if length >= UPER_FRAGMENT_SIZE: + data = memoryview(data) + enc.append_fragmented(length, append_slice) return if minimum is not None and maximum is not None: _uper_check_size( @@ -486,12 +487,9 @@ def UPER_octet_string_enc(enc, data, minimum=None, maximum=None, ) enc.append_bytes(data) else: - enc.append_fragmented( - length, - lambda offset, size: enc.append_bytes( - data_view[offset:offset + size] - ), - ) + if length >= UPER_FRAGMENT_SIZE: + data = memoryview(data) + enc.append_fragmented(length, append_slice) def UPER_octet_string_dec(dec, minimum=None, maximum=None, extensible=False): @@ -713,9 +711,11 @@ def encode_into(cls, s = bytes_encode(data) elif isinstance(_s, str) and _s and all(c in "01" for c in _s): nbits = len(_s) - padded = _s + "0" * ((8 - nbits % 8) % 8) - s = int(padded or "0", 2).to_bytes( - max(1, len(padded) // 8), "big" + padding = -nbits % 8 + value = int(_s or "0", 2) << padding + s = value.to_bytes( + max(1, (nbits + padding) // 8), + "big", ) else: s = bytes_encode(_s) @@ -723,18 +723,21 @@ def encode_into(cls, minimum, maximum, extensible = resolve_uper_size_bounds( field, size_len, minimum, maximum, extensible, ) - s_view = memoryview(s) + + def append_bits_slice(offset, size): + # type: (int, int) -> None + enc.append_bits( + s[offset // 8:(offset + size + 7) // 8], size + ) + if extensible and minimum is not None and maximum is not None: if minimum <= nbits <= maximum: enc.append_bit(0) else: enc.append_bit(1) - enc.append_fragmented( - nbits, - lambda offset, size: enc.append_bits( - s_view[offset // 8:(offset + size + 7) // 8], size - ), - ) + if nbits >= UPER_FRAGMENT_SIZE: + s = memoryview(s) + enc.append_fragmented(nbits, append_bits_slice) return if minimum is not None and maximum is not None: _uper_check_size(cls.__name__, "bits", nbits, minimum, maximum) @@ -746,14 +749,11 @@ def encode_into(cls, else: # X.691 16.11: the determinant counts bits, not octets, and no # padding is inserted before whatever follows the bit string. - enc.append_fragmented( - nbits, - # Fragments hold whole multiples of 16K bits, so every chunk - # but the last starts and ends on an octet boundary. - lambda offset, size: enc.append_bits( - s_view[offset // 8:(offset + size + 7) // 8], size - ), - ) + if nbits >= UPER_FRAGMENT_SIZE: + s = memoryview(s) + # Fragments hold whole multiples of 16K bits, so every chunk + # but the last starts and ends on an octet boundary. + enc.append_fragmented(nbits, append_bits_slice) @classmethod def dec_from_decoder(cls, @@ -872,12 +872,11 @@ def encode_into(cls, enc, _oid, **_kwargs): oid = bytes_encode(_oid) lst = oid_dotted_to_subidentifiers(oid) body = b"".join(BER_num_enc(k) for k in lst) - body_view = memoryview(body) + if len(body) >= UPER_FRAGMENT_SIZE: + body = memoryview(body) enc.append_fragmented( len(body), - lambda offset, size: enc.append_bytes( - body_view[offset:offset + size] - ), + lambda offset, size: enc.append_bytes(body[offset:offset + size]), ) @classmethod @@ -945,7 +944,7 @@ def encode_into(cls, minimum = field.constraints.minimum maximum = field.constraints.maximum uper_enum_values = _uper_enum_values( - field, pkt, uper_enum_values, + field, values=uper_enum_values, ) if extensible is None: extensible = ( @@ -990,7 +989,7 @@ def dec_from_decoder(cls, minimum = field.constraints.minimum maximum = field.constraints.maximum uper_enum_values = _uper_enum_values( - field, pkt, uper_enum_values, + field, values=uper_enum_values, ) if extensible is None: extensible = ( diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index afe02f64b49..6d9a7ee4ff4 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -432,13 +432,12 @@ def __init__(self, for k in keys: i2s[k] = enum[k] s2i[enum[k]] = k - # Canonical PER index order (X.691); built once — i2s is not mutated - # after construction. - self._uper_enum_values = sorted(i2s) # type: List[int] def uper_enum_values(self): # type: () -> List[int] - return self._uper_enum_values + # Sort on each call: i2s remains a normal mutable Scapy mapping, and + # ENUMERATED sets are normally tiny. + return sorted(self.i2s) def normalize_encode_value(self, pkt, value): # type: (ASN1_Packet, Any) -> Any diff --git a/test/scapy/layers/uper.uts b/test/scapy/layers/uper.uts index fd8976a42ef..6139e03a0aa 100644 --- a/test/scapy/layers/uper.uts +++ b/test/scapy/layers/uper.uts @@ -3717,3 +3717,17 @@ class UPERNamedEnum(ASN1_Packet): assert raw(UPERNamedEnum(e="one")) == raw(UPERNamedEnum(e=1)) assert _roundtrip(UPERNamedEnum, UPERNamedEnum(e="one")).e.val == 1 True + += UPER ENUMERATED string-keyed definition maps names to integer indexes +class UPERStrKeyEnum(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_ENUMERATED( + "state", + 1, + {"off": 0, "on": 1}, + ) + +assert UPERStrKeyEnum.ASN1_root.uper_enum_values() == [0, 1] +assert raw(UPERStrKeyEnum(state="on")) == raw(UPERStrKeyEnum(state=1)) +assert _roundtrip(UPERStrKeyEnum, UPERStrKeyEnum(state="on")).state.val == 1 +True