From 614f4a0870e85d38a5b9a6eb28df8839586f6fc1 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 1 Jul 2026 14:41:31 +0200 Subject: [PATCH 01/15] Added ASN.1 OER encoding AI-Assisted: yes (Cursor AI) --- .config/ci/install.ps1 | 3 + .config/ci/install.sh | 3 + scapy/all.py | 1 + scapy/asn1/oer.py | 825 +++++++++++++++++++++++++++++++ scapy/asn1fields.py | 157 +++++- test/scapy/layers/asn1.uts | 154 ++++++ test/scapy/layers/oer_fuzz.py | 106 ++++ test/scapy/layers/oer_iop.py | 231 +++++++++ test/scapy/layers/oer_packets.py | 149 ++++++ 9 files changed, 1611 insertions(+), 18 deletions(-) create mode 100644 scapy/asn1/oer.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 diff --git a/.config/ci/install.ps1 b/.config/ci/install.ps1 index 5f9e5b58443..09fbab8969f 100644 --- a/.config/ci/install.ps1 +++ b/.config/ci/install.ps1 @@ -19,3 +19,6 @@ python -m pip install --upgrade pip setuptools wheel --ignore-installed # Make sure tox is installed and up to date python -m pip install -U tox --ignore-installed + +# Optional test dependency for ASN.1 OER interoperability tests +python -m pip install asn1tools diff --git a/.config/ci/install.sh b/.config/ci/install.sh index aa96f1a99b5..c22855a36a7 100755 --- a/.config/ci/install.sh +++ b/.config/ci/install.sh @@ -53,5 +53,8 @@ python -m pip install --upgrade pip setuptools wheel --ignore-installed # Make sure tox is installed and up to date python -m pip install -U tox --ignore-installed +# Optional test dependency for ASN.1 OER interoperability tests +python -m pip install asn1tools + # Dump Environment (so that we can check PATH, UT_FLAGS, etc.) set diff --git a/scapy/all.py b/scapy/all.py index e28f0a91c71..65752e8f5e5 100644 --- a/scapy/all.py +++ b/scapy/all.py @@ -41,6 +41,7 @@ from scapy.asn1.asn1 import * from scapy.asn1.ber import * +from scapy.asn1.oer import * from scapy.asn1.mib import * from scapy.pipetool import * diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py new file mode 100644 index 00000000000..3bb1c1b75a2 --- /dev/null +++ b/scapy/asn1/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): + # 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) + + +########################## +# OERcodec objects # +########################## + +class OERcodec_INTEGER(OERcodec_Object[int]): + tag = ASN1_Class_UNIVERSAL.INTEGER + + @classmethod + def enc(cls, i, size_len=0): + # 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): + # 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] + l, s = OER_len_dec(s) + if l == 0: + return cls.tag.asn1_object(""), s + if len(s) < l: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), l), + 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:l]) + if unused_bits > 0: + fs = fs[:-unused_bits] + return cls.tag.asn1_object(fs), s[l:] + + @classmethod + def enc(cls, _s, size_len=0): + # 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): + # 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:] + l, s = OER_len_dec(s) + if len(s) < l: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), l), + remaining=s + ) + return cls.tag.asn1_object(s[:l]), s[l:] + + +class OERcodec_NULL(OERcodec_Object[None]): + tag = ASN1_Class_UNIVERSAL.NULL + + @classmethod + def enc(cls, i, size_len=0): + # 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): + # 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] + l, s = OER_len_dec(s) + if len(s) < l: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), l), + remaining=s + ) + content, t = s[:l], s[l:] + 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): + # 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): + # 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): # 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: + l, remain = OER_len_dec(s) + if len(remain) < l: + raise OER_Decoding_Error("IP address could not be decoded", + remaining=s) + raw, remain = remain[:l], remain[l:] + 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/asn1fields.py b/scapy/asn1fields.py index 9a5284bddfc..a6dd27c9316 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -17,6 +17,8 @@ ASN1_BOOLEAN, ASN1_Class, ASN1_Class_UNIVERSAL, + ASN1_Codecs, + ASN1_Decoding_Error, ASN1_Error, ASN1_INTEGER, ASN1_NULL, @@ -30,6 +32,15 @@ BER_tagging_dec, BER_tagging_enc, ) +from scapy.asn1.oer import ( + OER_Decoding_Error, + OER_id_dec, + OER_tag_enc, + OER_tagging_dec, + OER_tagging_enc, + OER_unsigned_integer_dec, + OER_unsigned_integer_enc, +) from scapy.base_classes import BasePacket from scapy.volatile import ( GeneralizedTime, @@ -93,6 +104,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] ): # type: (...) -> None if context is not None: @@ -105,6 +117,7 @@ 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.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" @@ -141,11 +154,18 @@ def m2i(self, pkt, s): as expected or not. Noticeably, input methods from cert.py expect certain exceptions to be raised. Hence default flexible_tag is False. """ - diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, - implicit_tag=self.implicit_tag, - explicit_tag=self.explicit_tag, - safe=self.flexible_tag, - _fname=self.name) + if pkt.ASN1_codec == ASN1_Codecs.OER: + diff_tag, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag, + _fname=self.name) + else: + diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag, + _fname=self.name) if diff_tag is not None: # this implies that flexible_tag was True if self.implicit_tag is not None: @@ -153,10 +173,16 @@ def m2i(self, pkt, s): elif self.explicit_tag is not None: self.explicit_tag = diff_tag codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + oer_kwargs = {} # type: Dict[str, Any] + if pkt.ASN1_codec == ASN1_Codecs.OER: + oer_kwargs = { + "size_len": self.size_len or 0, + "oer_unsigned": self.oer_unsigned, + } if self.flexible_tag: - return codec.safedec(s, context=self.context) # type: ignore + return codec.safedec(s, context=self.context, **oer_kwargs) # type: ignore else: - return codec.dec(s, context=self.context) # type: ignore + return codec.dec(s, context=self.context, **oer_kwargs) # type: ignore def i2m(self, pkt, x): # type: (ASN1_Packet, Union[bytes, _I, _A]) -> bytes @@ -172,6 +198,12 @@ def i2m(self, pkt, x): raise ASN1_Error("Encoding Error: got %r instead of an %r for field [%s]" % (x, self.ASN1_tag, self.name)) # noqa: E501 else: s = self.ASN1_tag.get_codec(pkt.ASN1_codec).enc(x, size_len=self.size_len) + if pkt.ASN1_codec == ASN1_Codecs.OER: + return cast(bytes, OER_tagging_enc( + s, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + )) return BER_tagging_enc(s, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag) @@ -244,6 +276,18 @@ def copy(self): # type: () -> ASN1F_field[_I, _A] return copy.copy(self) + def _encode_item(self, pkt, item): + # type: (ASN1_Packet, Any) -> bytes + if isinstance(item, ASN1_Object): + return item.enc(pkt.ASN1_codec) + if hasattr(item, "self_build"): + return cast("ASN1_Packet", item).self_build() + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + size_len = self.size_len or 0 + if pkt.ASN1_codec == ASN1_Codecs.OER and self.oer_unsigned: + return codec.enc(item, size_len=size_len, oer_unsigned=True) # type: ignore[call-arg] + return codec.enc(item, size_len=size_len) + ############################ # Simple ASN1 Fields # @@ -471,6 +515,29 @@ 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: + diff_tag, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag, + _fname=pkt.name) + 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 + 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 OER_Decoding_Error("unexpected remainder", remaining=s) + return [], b"" diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, @@ -569,6 +636,25 @@ def m2i(self, s, # type: bytes ): # type: (...) -> Tuple[List[Any], bytes] + if pkt.ASN1_codec == ASN1_Codecs.OER: + diff_tag, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag) + 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 + count, s = OER_unsigned_integer_dec(s) + lst = [] + for _ in range(count): + c, s = self._extract_packet(s, pkt) # type: ignore + if c: + lst.append(c) + if len(s) > 0: + raise OER_Decoding_Error("unexpected remainder", remaining=s) + return lst, b"" diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, @@ -597,8 +683,12 @@ def build(self, pkt): s = cast(Union[List[_SEQ_T], bytes], val) elif val is None: s = b"" + if pkt.ASN1_codec == ASN1_Codecs.OER: + s = OER_unsigned_integer_enc(0) else: - s = b"".join(bytes(i) for i in val) + s = b"".join(self.fld._encode_item(pkt, i) for i in val) + if pkt.ASN1_codec == ASN1_Codecs.OER: + s = OER_unsigned_integer_enc(len(val)) + s return self.i2m(pkt, s) def i2repr(self, pkt, x): @@ -657,7 +747,7 @@ def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[Any, bytes] try: return self._field.m2i(pkt, s) - except (ASN1_Error, ASN1F_badsequence, BER_Decoding_Error): + except (ASN1_Error, ASN1F_badsequence, ASN1_Decoding_Error): # ASN1_Error may be raised by ASN1F_CHOICE return None, s @@ -665,7 +755,7 @@ def dissect(self, pkt, s): # type: (ASN1_Packet, bytes) -> bytes try: return self._field.dissect(pkt, s) - except (ASN1_Error, ASN1F_badsequence, BER_Decoding_Error): + except (ASN1_Error, ASN1F_badsequence, ASN1_Decoding_Error): self._field.set_val(pkt, None) return s @@ -756,9 +846,15 @@ def m2i(self, pkt, s): """ if len(s) == 0: raise ASN1_Error("ASN1F_CHOICE: got empty string") - _, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, - explicit_tag=self.explicit_tag) - tag, _ = BER_id_dec(s) + if pkt.ASN1_codec == ASN1_Codecs.OER: + _, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, + explicit_tag=self.explicit_tag) + tag, payload = OER_id_dec(s) + else: + _, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, + explicit_tag=self.explicit_tag) + tag, _ = BER_id_dec(s) + payload = s if tag in self.choices: choice = self.choices[tag] else: @@ -773,24 +869,49 @@ def m2i(self, pkt, s): ) 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 + return self.extract_packet(choice, payload, _underlayer=pkt) # type: ignore elif isinstance(choice, type): - return choice(self.name, b"").m2i(pkt, s) + return choice(self.name, b"").m2i(pkt, payload) else: # XXX check properly if this is an ASN1F_PACKET - return choice.m2i(pkt, s) + return choice.m2i(pkt, payload) + + def _choice_tag_for(self, x): + # type: (Any) -> Optional[int] + for tag, choice in self.choices.items(): + if hasattr(choice, "ASN1_root"): + if isinstance(x, cast("Type[ASN1_Packet]", choice)): + return tag + elif isinstance(choice, type) and hasattr(choice, "ASN1_tag"): + if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + return tag + elif hasattr(choice, "ASN1_tag"): + if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + return tag + return None def i2m(self, pkt, x): # type: (ASN1_Packet, Any) -> bytes if x is None: s = b"" else: - s = bytes(x) - if hash(type(x)) in self.pktchoices: + if isinstance(x, ASN1_Object): + s = x.enc(pkt.ASN1_codec) + elif hasattr(x, "self_build"): + s = cast("ASN1_Packet", x).self_build() + 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_tag_enc(alt_tag & 0x3f, alt_tag & 0xc0) + s + elif hash(type(x)) in self.pktchoices: imp, exp = self.pktchoices[hash(type(x))] s = BER_tagging_enc(s, implicit_tag=imp, explicit_tag=exp) + if pkt.ASN1_codec == ASN1_Codecs.OER: + return cast(bytes, OER_tagging_enc(s, explicit_tag=self.explicit_tag)) return BER_tagging_enc(s, explicit_tag=self.explicit_tag) def randval(self): diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 9fa0bad0f44..aa1fc734f95 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -101,3 +101,157 @@ 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 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 (asn1tools) += asn1tools availability probe +from test.scapy.layers.oer_iop import require_asn1tools +HAS_ASN1TOOLS_OER = require_asn1tools() += primitive encode interop with asn1tools +(not HAS_ASN1TOOLS_OER) or __import__('test.scapy.layers.oer_iop', fromlist=['check_primitive_interop']).check_primitive_interop() += scapy encode asn1tools decode +(not HAS_ASN1TOOLS_OER) or __import__('test.scapy.layers.oer_iop', fromlist=['check_asn1tools_decode_scapy_encode']).check_asn1tools_decode_scapy_encode() + ++ 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() diff --git a/test/scapy/layers/oer_fuzz.py b/test/scapy/layers/oer_fuzz.py new file mode 100644 index 00000000000..a322a88aab3 --- /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.asn1.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..48088153047 --- /dev/null +++ b/test/scapy/layers/oer_iop.py @@ -0,0 +1,231 @@ +# 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 asn1tools when available. +asn1tools is an optional test dependency (pip install asn1tools). +Reference vectors are taken from asn1tools/tests/test_oer.py. +""" + +from typing import Any, Callable, List, Optional, Tuple + +try: + import asn1tools + HAS_ASN1TOOLS = True +except ImportError: + asn1tools = None # type: ignore + HAS_ASN1TOOLS = False + +from scapy.asn1.oer import ( + OERcodec_BIT_STRING, + OERcodec_BOOLEAN, + OERcodec_ENUMERATED, + OERcodec_INTEGER, + OERcodec_NULL, + OERcodec_OID, + OERcodec_STRING, + OER_len_dec, + OER_len_enc, + OER_signed_integer_dec, + OER_signed_integer_enc, + OER_unsigned_integer_dec, + OER_unsigned_integer_enc, +) + + +ASN1TOOLS_INTEGER_SPEC = ( + "Foo DEFINITIONS AUTOMATIC TAGS ::= " + "BEGIN " + "A ::= INTEGER " + "B ::= INTEGER (-128..127) " + "C ::= INTEGER (-32768..32767) " + "D ::= INTEGER (-2147483648..2147483647) " + "E ::= INTEGER (-9223372036854775808..9223372036854775807) " + "F ::= INTEGER (0..255) " + "G ::= INTEGER (0..65535) " + "H ::= INTEGER (0..4294967295) " + "I ::= INTEGER (0..18446744073709551615) " + "J ::= INTEGER (0..18446744073709551616) " + "K ::= INTEGER (1..MAX) " + "L ::= INTEGER (MIN..0) " + "END" +) + +# (asn1tools type, value, scapy encoder callable) +INTEGER_VECTORS = [ + ("A", 0, lambda v: OERcodec_INTEGER.enc(v)), + ("A", 128, lambda v: OERcodec_INTEGER.enc(v)), + ("A", 100000, lambda v: OERcodec_INTEGER.enc(v)), + ("A", -255, lambda v: OERcodec_INTEGER.enc(v)), + ("A", -1234567, lambda v: OERcodec_INTEGER.enc(v)), + ("B", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=1)), + ("C", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=2)), + ("D", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=4)), + ("E", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=8)), + ("F", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=1)), + ("G", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=2)), + ("G", 1000, lambda v: OERcodec_INTEGER.enc(v, size_len=2)), + ("H", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=4)), + ("I", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=8)), + ("B", 1, lambda v: OERcodec_INTEGER.enc(v, size_len=1)), + ("K", 1, lambda v: OER_unsigned_integer_enc(v)), + ("K", 128, lambda v: OER_unsigned_integer_enc(v)), + ("L", -128, lambda v: OER_signed_integer_enc(v)), +] + +BOOLEAN_VECTORS = [ + (True, lambda v: OERcodec_BOOLEAN.enc(1 if v else 0)), + (False, lambda v: OERcodec_BOOLEAN.enc(1 if v else 0)), +] + +ENUMERATED_VECTORS = [ + ("A ::= ENUMERATED { a(1) }", "A", "a", 1), + ("B ::= ENUMERATED { a(128) }", "B", "a", 128), + ("C ::= ENUMERATED { a(0), b(127) }", "C", "a", 0), + ("C ::= ENUMERATED { a(0), b(127) }", "C", "b", 127), + ("E ::= ENUMERATED { a(-1), b(1234) }", "E", "a", -1), +] + +OID_VECTORS = [ + ("1.2", lambda v: OERcodec_OID.enc(v)), + ("1.2.3321", lambda v: OERcodec_OID.enc(v)), +] + +OCTET_STRING_VECTORS = [ + (b"\x12\x34", 0), + (b"\x12\x34\x56", 3), +] + +BIT_STRING_VECTORS = [ + # (asn1 value, scapy bit string) + ((b"\x40", 4), "0100"), + ((b"\x41", 8), "01000001"), +] + + +def require_asn1tools(): + # type: () -> bool + """Return True if asn1tools is available for interoperability tests.""" + return HAS_ASN1TOOLS + + +def _compile(spec_body): + # type: (str) -> Any + assert asn1tools is not None + spec = "Foo DEFINITIONS AUTOMATIC TAGS ::= BEGIN %s END" % spec_body + return asn1tools.compile_string(spec, "oer") + + +def _assert_encode_match(spec_body, type_name, value, scapy_enc): + # type: (str, str, Any, Callable[..., bytes]) -> None + compiled = _compile(spec_body) + expected = compiled.encode(type_name, value) + got = scapy_enc(value) + assert got == expected, ( + "OER encode mismatch for %s=%r: asn1tools=%r scapy=%r" % + (type_name, value, expected, got) + ) + + +def _assert_decode_match(type_name, encoded, scapy_dec, expected_value): + # type: (str, bytes, Callable[[bytes], Tuple[Any, bytes]], Any) -> None + obj, remain = scapy_dec(encoded) + assert remain == b"", "unexpected remainder after decode of %s" % type_name + assert obj.val == expected_value, ( + "OER decode mismatch for %s: got %r expected %r" % + (type_name, obj.val, expected_value) + ) + + +def check_primitive_interop(): + # type: () -> bool + """Compare Scapy OER primitives against asn1tools. Returns True on success.""" + if not HAS_ASN1TOOLS: + return True + + compiled = asn1tools.compile_string(ASN1TOOLS_INTEGER_SPEC, "oer") + for type_name, value, enc in INTEGER_VECTORS: + expected = compiled.encode(type_name, value) + got = enc(value) + assert got == expected, ( + "integer %s=%r: asn1tools=%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 + + bool_spec = _compile("A ::= BOOLEAN") + for value, enc in BOOLEAN_VECTORS: + expected = bool_spec.encode("A", value) + got = enc(value) + assert got == expected + dec, remain = OERcodec_BOOLEAN.do_dec(got) + assert remain == b"" and dec.val == (1 if value else 0) + + _assert_encode_match("A ::= NULL", "A", None, lambda _: OERcodec_NULL.enc(None)) + + for spec_body, type_name, enum_name, enum_val in ENUMERATED_VECTORS: + compiled = _compile(spec_body) + expected = compiled.encode(type_name, enum_name) + got = OERcodec_ENUMERATED.enc(enum_val) + assert got == expected + dec, remain = OERcodec_ENUMERATED.do_dec(got) + assert remain == b"" and dec.val == enum_val + + oid_spec = _compile("A ::= OBJECT IDENTIFIER") + for oid, enc in OID_VECTORS: + expected = oid_spec.encode("A", oid) + got = enc(oid) + assert got == expected + dec, remain = OERcodec_OID.do_dec(got) + assert remain == b"" and dec.val == oid + + octet_spec = _compile( + "A ::= OCTET STRING\nB ::= OCTET STRING (SIZE (3))" + ) + for data, fixed_size in OCTET_STRING_VECTORS: + type_name = "B" if fixed_size else "A" + expected = octet_spec.encode(type_name, data) + 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 + + bit_spec = _compile("A ::= BIT STRING") + for (data, nbits), bitstr in BIT_STRING_VECTORS: + expected = bit_spec.encode("A", (data, nbits)) + 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_asn1tools_decode_scapy_encode(): + # type: () -> bool + """Encode with Scapy, decode with asn1tools.""" + if not HAS_ASN1TOOLS: + return True + + compiled = asn1tools.compile_string(ASN1TOOLS_INTEGER_SPEC, "oer") + samples = [("A", 42), ("F", 200), ("B", -99)] + for type_name, value in samples: + encoded = { + "A": OERcodec_INTEGER.enc, + "F": lambda v: OERcodec_INTEGER.enc(v, size_len=1), + "B": lambda v: OERcodec_INTEGER.enc(v, size_len=1), + }[type_name](value) + decoded = compiled.decode(type_name, encoded) + assert decoded == value + + bool_spec = _compile("A ::= BOOLEAN") + for val in [0, 1]: + encoded = OERcodec_BOOLEAN.enc(val) + assert bool_spec.decode("A", encoded) == bool(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..515b0209af2 --- /dev/null +++ b/test/scapy/layers/oer_packets.py @@ -0,0 +1,149 @@ +# 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. +""" + +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), + ) + + +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] == [] From 5f4c583f9292054e4891a62c16fe54823f6bbdd7 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 1 Jul 2026 20:49:30 +0200 Subject: [PATCH 02/15] Added ASN.1 UPER encoding AI-Assisted: yes (Cursor AI) --- .config/codespell_ignore.txt | 5 + scapy/all.py | 1 + scapy/asn1/uper.py | 1297 +++++++++++++++++++++++++ scapy/asn1fields.py | 316 +++++- test/scapy/layers/asn1.uts | 156 +++ test/scapy/layers/uper_asn1scc_iop.py | 261 +++++ test/scapy/layers/uper_codec.py | 186 ++++ test/scapy/layers/uper_fuzz.py | 133 +++ test/scapy/layers/uper_helpers.py | 122 +++ test/scapy/layers/uper_iop.py | 251 +++++ test/scapy/layers/uper_packets.py | 542 +++++++++++ tox.ini | 1 + 12 files changed, 3253 insertions(+), 18 deletions(-) create mode 100644 scapy/asn1/uper.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 510f3e0c090..59e3f34dcd1 100644 --- a/.config/codespell_ignore.txt +++ b/.config/codespell_ignore.txt @@ -53,3 +53,8 @@ wan wanna webp widgits +uper +UPER +uPER +acn +ACN diff --git a/scapy/all.py b/scapy/all.py index 65752e8f5e5..286c7ac1ac6 100644 --- a/scapy/all.py +++ b/scapy/all.py @@ -42,6 +42,7 @@ from scapy.asn1.asn1 import * from scapy.asn1.ber import * from scapy.asn1.oer import * +from scapy.asn1.uper import * from scapy.asn1.mib import * from scapy.pipetool import * diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py new file mode 100644 index 00000000000..cfdf2e97e4b --- /dev/null +++ b/scapy/asn1/uper.py @@ -0,0 +1,1297 @@ +# 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 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(binascii.hexlify(data), 16) + 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) + dec = UPER_Decoder(data) + start = dec._read_offset() + bits = dec.value[start:start + total] + end = len(bits) + while end > 0 and bits[end - 1] == "0": + end -= 1 + trimmed = total - end + if trimmed > 0 and trimmed <= 8: + return end + return total + + +def UPER_append_encoded(enc, data): + # type: (UPER_Encoder, bytes) -> None + if not data: + return + dec = UPER_Decoder(data) + start = dec._read_offset() + nbits = _uper_significant_bit_count(data) + for i in range(nbits): + enc.append_bit(int(dec.value[start + i])) + + +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 + offset = dec._read_offset() + return dec.value[offset:].strip("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.number_of_bits = 8 * len(encoded) + self.total_number_of_bits = self.number_of_bits + if encoded: + value = int(binascii.hexlify(encoded), 16) + value |= (0x80 << self.number_of_bits) + self.value = bin(value)[10:] + else: + self.value = "" + + def _read_offset(self): + # type: () -> int + return self.total_number_of_bits - self.number_of_bits + + def read_bit(self): + # type: () -> int + if self.number_of_bits == 0: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + offset = self._read_offset() + bit = int(self.value[offset]) + 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"" + offset = self._read_offset() + bits = self.value[offset:offset + number_of_bits] + self.number_of_bits -= number_of_bits + value = "10000000" + bits + 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 remaining(self): + # type: () -> bytes + if self.number_of_bits == 0: + return b"" + offset = self._read_offset() + bits = self.value[offset:offset + self.number_of_bits] + value = "10000000" + bits + number_of_alignment_bits = (8 - (self.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 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 + offset = self._read_offset() + bits = self.value[offset:offset + number_of_bits] + self.number_of_bits -= number_of_bits + return int(bits, 2) + + 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_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): + # 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 + ) + + +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 + ): + # type: (...) -> None + minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) + 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 + ): + # type: (...) -> ASN1_Object[int] + minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) + if minimum is not None and maximum is not None: + value = dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + minimum + 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): + # type: (int, Optional[int], Optional[int], Optional[int], bool) -> bytes + enc = UPER_Encoder() + cls.encode_into(enc, i, size_len, uper_min, uper_max, oer_unsigned) + 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[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): + # type: (int, Optional[int], Optional[int], Optional[int], bool) -> bytes + enc = UPER_Encoder() + cls.encode_into(enc, i, size_len, uper_min, uper_max, oer_unsigned) + 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[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: + 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) + + @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): + # type: (Any, Optional[int], Optional[int], Optional[int], bool) -> bytes + enc = UPER_Encoder() + cls.encode_into(enc, _s, size_len, uper_min, uper_max, oer_unsigned) + 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[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): + # type: (Union[str, bytes], Optional[int], Optional[int], Optional[int], bool) -> bytes # noqa: E501 + enc = UPER_Encoder() + cls.encode_into(enc, _s, size_len, uper_min, uper_max, oer_unsigned) + 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[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): + # 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): + # 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) + l = dec.read_length_determinant() + content = dec.read_bytes(l) + 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, + ): + # type: (int, Optional[int], Optional[int], Optional[int], bool, Optional[List[int]]) -> bytes # noqa: E501 + enc = UPER_Encoder() + cls.encode_into( + enc, i, size_len, uper_min, uper_max, oer_unsigned, + uper_enum_values=uper_enum_values, + ) + 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 + 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): + # 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): + # 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/scapy/asn1fields.py b/scapy/asn1fields.py index a6dd27c9316..9a1b150010e 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -41,6 +41,15 @@ OER_unsigned_integer_dec, OER_unsigned_integer_enc, ) +from scapy.asn1.uper import ( + UPER_Decoding_Error, + UPER_Decoder, + UPER_Encoder, + UPER_choice_index_dec, + UPER_choice_index_enc, + UPER_count_dec, + UPER_has_unexpected_remainder, +) from scapy.base_classes import BasePacket from scapy.volatile import ( GeneralizedTime, @@ -105,6 +114,9 @@ def __init__(self, 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: @@ -118,6 +130,9 @@ def __init__(self, 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" @@ -160,12 +175,14 @@ def m2i(self, pkt, s): explicit_tag=self.explicit_tag, safe=self.flexible_tag, _fname=self.name) - else: + elif pkt.ASN1_codec != ASN1_Codecs.PER: diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, safe=self.flexible_tag, _fname=self.name) + else: + diff_tag = None if diff_tag is not None: # this implies that flexible_tag was True if self.implicit_tag is not None: @@ -173,16 +190,34 @@ def m2i(self, pkt, s): elif self.explicit_tag is not None: self.explicit_tag = diff_tag codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - oer_kwargs = {} # type: Dict[str, Any] + codec_kwargs = {} # type: Dict[str, Any] if pkt.ASN1_codec == ASN1_Codecs.OER: - oer_kwargs = { + codec_kwargs = { "size_len": self.size_len or 0, "oer_unsigned": self.oer_unsigned, } + elif pkt.ASN1_codec == ASN1_Codecs.PER: + codec_kwargs = self._uper_codec_kwargs() if self.flexible_tag: - return codec.safedec(s, context=self.context, **oer_kwargs) # type: ignore + return codec.safedec( + s, context=self.context, **codec_kwargs + ) # type: ignore else: - return codec.dec(s, context=self.context, **oer_kwargs) # type: ignore + return codec.dec(s, context=self.context, **codec_kwargs) # type: ignore + + def m2i_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> _A + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + return cast( + _A, + codec.dec_from_decoder( # type: ignore[attr-defined] + dec, **self._uper_codec_kwargs(), + ), + ) + + def dissect_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> None + self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) def i2m(self, pkt, x): # type: (ASN1_Packet, Union[bytes, _I, _A]) -> bytes @@ -193,17 +228,28 @@ def i2m(self, pkt, x): x.tag == ASN1_Class_UNIVERSAL.RAW or x.tag == ASN1_Class_UNIVERSAL.ERROR or self.ASN1_tag == x.tag): - s = x.enc(pkt.ASN1_codec) + if pkt.ASN1_codec == ASN1_Codecs.PER: + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + s = codec.enc(x.val, **self._uper_codec_kwargs()) + else: + s = x.enc(pkt.ASN1_codec) else: raise ASN1_Error("Encoding Error: got %r instead of an %r for field [%s]" % (x, self.ASN1_tag, self.name)) # noqa: E501 else: - s = self.ASN1_tag.get_codec(pkt.ASN1_codec).enc(x, size_len=self.size_len) + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + size_len = self.size_len or 0 + if pkt.ASN1_codec == ASN1_Codecs.PER: + s = codec.enc(x, **self._uper_codec_kwargs(size_len)) + else: + s = codec.enc(x, size_len=size_len) if pkt.ASN1_codec == ASN1_Codecs.OER: return cast(bytes, OER_tagging_enc( s, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, )) + if pkt.ASN1_codec == ASN1_Codecs.PER: + return s return BER_tagging_enc(s, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag) @@ -276,18 +322,61 @@ 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] + 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 self.uper_enum_values is not None: + kwargs["uper_enum_values"] = self.uper_enum_values + return kwargs + def _encode_item(self, pkt, item): # type: (ASN1_Packet, Any) -> bytes if isinstance(item, ASN1_Object): + if pkt.ASN1_codec == ASN1_Codecs.PER: + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + return codec.enc(item.val, **self._uper_codec_kwargs()) return item.enc(pkt.ASN1_codec) if hasattr(item, "self_build"): return cast("ASN1_Packet", item).self_build() codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) size_len = self.size_len or 0 if pkt.ASN1_codec == ASN1_Codecs.OER and self.oer_unsigned: - return codec.enc(item, size_len=size_len, oer_unsigned=True) # type: ignore[call-arg] + return codec.enc( + item, size_len=size_len, oer_unsigned=True + ) # type: ignore[call-arg] + if pkt.ASN1_codec == ASN1_Codecs.PER: + return codec.enc(item, **self._uper_codec_kwargs(size_len)) return codec.enc(item, size_len=size_len) + def _uper_encode_into(self, enc, pkt, value=None): + # type: (UPER_Encoder, 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._uper_codec_kwargs(), + ) + ############################ # Simple ASN1 Fields # @@ -335,6 +424,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 @@ -369,12 +459,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, @@ -538,6 +632,13 @@ def m2i(self, pkt, s): if len(s) > 0: raise OER_Decoding_Error("unexpected remainder", remaining=s) return [], b"" + if pkt.ASN1_codec == ASN1_Codecs.PER: + dec = UPER_Decoder(s) + self._uper_dissect_from_decoder(pkt, dec) + if UPER_has_unexpected_remainder(dec): + raise UPER_Decoding_Error("unexpected remainder", + remaining=dec.remaining()) + return [], b"" diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, @@ -563,6 +664,27 @@ def m2i(self, pkt, s): raise BER_Decoding_Error("unexpected remainder", remaining=s) return [], remain + def _uper_dissect_from_decoder(self, pkt, dec): + # type: (Any, UPER_Decoder) -> None + optionals = [f for f in self.seq if isinstance(f, ASN1F_optional)] + presence = [dec.read_bit() for _ in optionals] + opt_idx = 0 + for obj in self.seq: + if isinstance(obj, ASN1F_optional): + if not presence[opt_idx]: + obj.set_val(pkt, None) + 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, UPER_Decoder) -> None + self._uper_dissect_from_decoder(pkt, dec) + def dissect(self, pkt, s): # type: (Any, bytes) -> bytes _, x = self.m2i(pkt, s) @@ -570,10 +692,24 @@ def dissect(self, pkt, s): def build(self, pkt): # type: (ASN1_Packet) -> bytes + if pkt.ASN1_codec == ASN1_Codecs.PER: + enc = 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: (UPER_Encoder, ASN1_Packet, Optional[Any]) -> None + optionals = [f for f in self.seq if isinstance(f, ASN1F_optional)] + for opt in optionals: + enc.append_bit(0 if opt.is_empty(pkt) else 1) + for obj in self.seq: + if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): + continue + obj._uper_encode_into(enc, pkt, value) + class ASN1F_SET(ASN1F_SEQUENCE): ASN1_tag = ASN1_Class_UNIVERSAL.SET @@ -631,6 +767,38 @@ def is_empty(self, # type: (...) -> bool return ASN1F_field.is_empty(self, pkt) + def _extract_packet_from_decoder(self, dec, pkt): + # type: (UPER_Decoder, ASN1_Packet) -> Tuple[Any, bytes] + if self.holds_packets: + raise UPER_Decoding_Error( + "UPER SEQUENCE OF packets is not supported yet" + ) + return self.fld.m2i_from_decoder(pkt, dec), b"" + + def m2i_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> List[Any] + count, _ = UPER_count_dec(b"", dec=dec) + lst = [] + for _ in range(count): + item, _ = self._extract_packet_from_decoder(dec, pkt) + lst.append(item) + return lst + + def dissect_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> None + self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) + + def _uper_encode_into(self, enc, pkt, value=None): + # type: (UPER_Encoder, ASN1_Packet, Any) -> None + if value is None: + value = getattr(pkt, self.name) + if value is None: + enc.append_length_determinant(0) + return + enc.append_length_determinant(len(value)) + for item in value: + self.fld._uper_encode_into(enc, pkt, item) + def m2i(self, pkt, # type: ASN1_Packet s, # type: bytes @@ -655,6 +823,18 @@ def m2i(self, if len(s) > 0: raise OER_Decoding_Error("unexpected remainder", remaining=s) return lst, b"" + if pkt.ASN1_codec == ASN1_Codecs.PER: + dec = UPER_Decoder(s) + count, _ = UPER_count_dec(b"", dec=dec) + lst = [] + for _ in range(count): + c, _ = self._extract_packet_from_decoder(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"" diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, @@ -685,10 +865,21 @@ def build(self, pkt): s = b"" if pkt.ASN1_codec == ASN1_Codecs.OER: s = OER_unsigned_integer_enc(0) + elif pkt.ASN1_codec == ASN1_Codecs.PER: + enc = UPER_Encoder() + enc.append_length_determinant(0) + s = enc.as_bytes() else: - s = b"".join(self.fld._encode_item(pkt, i) for i in val) - if pkt.ASN1_codec == ASN1_Codecs.OER: - s = OER_unsigned_integer_enc(len(val)) + s + if pkt.ASN1_codec == ASN1_Codecs.PER: + enc = UPER_Encoder() + enc.append_length_determinant(len(val)) + for item in val: + self.fld._uper_encode_into(enc, pkt, item) + s = enc.as_bytes() + else: + s = b"".join(self.fld._encode_item(pkt, i) for i in val) + if pkt.ASN1_codec == ASN1_Codecs.OER: + s = OER_unsigned_integer_enc(len(val)) + s return self.i2m(pkt, s) def i2repr(self, pkt, x): @@ -759,6 +950,10 @@ def dissect(self, pkt, s): self._field.set_val(pkt, None) return s + def dissect_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> None + return self._field.dissect_from_decoder(pkt, dec) + def build(self, pkt): # type: (ASN1_Packet) -> bytes if self._field.is_empty(pkt): @@ -773,6 +968,18 @@ 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 is_empty(self, pkt): + # type: (ASN1_Packet) -> bool + return self._field.is_empty(pkt) + + def _uper_encode_into(self, enc, pkt, value=None): + # type: (UPER_Encoder, ASN1_Packet, Optional[Any]) -> None + self._field._uper_encode_into(enc, pkt, value) + class ASN1F_omit(ASN1F_field[None, None]): """ @@ -815,6 +1022,7 @@ def __init__(self, name, default, *args, **kwargs): self.default = default self.current_choice = None self.choices = {} # type: Dict[int, _CHOICE_T] + self.choice_order = [] # type: List[int] self.pktchoices = {} for p in args: if hasattr(p, "ASN1_root"): @@ -822,18 +1030,24 @@ 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.choices[k] = root.choices[k] + self.choice_order.append(k) else: - self.choices[p.ASN1_root.network_tag] = p + tag = p.ASN1_root.network_tag + self.choices[tag] = p + self.choice_order.append(tag) elif hasattr(p, "ASN1_tag"): if isinstance(p, type): # should be ASN1F_field class - self.choices[int(p.ASN1_tag)] = p + tag = int(p.ASN1_tag) + self.choices[tag] = p + self.choice_order.append(tag) else: # should be ASN1F_field instance - self.choices[p.network_tag] = p + tag = p.network_tag + self.choices[tag] = p + self.choice_order.append(tag) 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") @@ -850,6 +1064,13 @@ def m2i(self, pkt, s): _, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, explicit_tag=self.explicit_tag) tag, payload = OER_id_dec(s) + elif pkt.ASN1_codec == ASN1_Codecs.PER: + dec = UPER_Decoder(s) + val = self.m2i_from_decoder(pkt, dec) + if UPER_has_unexpected_remainder(dec): + raise UPER_Decoding_Error("unexpected remainder", + remaining=dec.remaining()) + return val, b"" else: _, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, explicit_tag=self.explicit_tag) @@ -890,10 +1111,67 @@ def _choice_tag_for(self, x): return tag return None + def _choice_index_for(self, x): + # type: (Any) -> Optional[int] + tag = self._choice_tag_for(x) + if tag is None: + return None + try: + return self.choice_order.index(tag) + except ValueError: + return None + + def _choice_for_index(self, index): + # type: (int) -> _CHOICE_T + return self.choices[self.choice_order[index]] + + def m2i_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> ASN1_Object[Any] + index, _ = UPER_choice_index_dec(b"", len(self.choice_order), dec=dec) + 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 hasattr(choice, "ASN1_root"): + raise ASN1_Error( + "ASN1F_CHOICE: UPER packet choices are not supported yet" + ) + 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: (UPER_Encoder, 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 + ) + UPER_choice_index_enc(index, len(self.choice_order), enc=enc) + choice = self._choice_for_index(index) + if hasattr(choice, "ASN1_root"): + cast("ASN1_Packet", value).ASN1_root._uper_encode_into(enc, value) + elif isinstance(choice, type): + choice(self.name, b"")._uper_encode_into(enc, pkt, value) + else: + 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_Encoder() + self._uper_encode_into(enc, pkt, x) + s = enc.as_bytes() else: if isinstance(x, ASN1_Object): s = x.enc(pkt.ASN1_codec) @@ -912,6 +1190,8 @@ def i2m(self, pkt, x): explicit_tag=exp) if pkt.ASN1_codec == ASN1_Codecs.OER: return cast(bytes, OER_tagging_enc(s, explicit_tag=self.explicit_tag)) + if pkt.ASN1_codec == ASN1_Codecs.PER: + return s return BER_tagging_enc(s, explicit_tag=self.explicit_tag) def randval(self): diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index aa1fc734f95..5a3674e7e5c 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -255,3 +255,159 @@ __import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_sequence_ __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() + ++ 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 += asn1tools availability probe for UPER +from test.scapy.layers.uper_iop import require_asn1tools +HAS_ASN1TOOLS_UPER = require_asn1tools() + ++ 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 asn1tools decode interop +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_asn1tools_decode']).check_uper_codec_asn1tools_decode() += UPER codec scapy encode asn1tools decode +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_scapy_decode_asn1tools']).check_uper_codec_scapy_decode_asn1tools() += UPER codec OID encode interop +(not HAS_ASN1TOOLS_UPER) or __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 (asn1tools) += UPER primitive encode interop with asn1tools +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_iop', fromlist=['check_primitive_interop']).check_primitive_interop() += UPER composite encode interop with asn1tools +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_iop', fromlist=['check_composite_interop']).check_composite_interop() += UPER composite decode interop with asn1tools +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_iop', fromlist=['check_composite_decode_interop']).check_composite_decode_interop() += UPER packet asn1tools interop +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_iop', fromlist=['check_packet_asn1tools_interop']).check_packet_asn1tools_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 with asn1tools +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_vectors']).check_asn1scc_vectors() += asn1scc README Message uPER reference (asn1tools) +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_readme_message_reference']).check_asn1scc_readme_message_reference() += asn1scc README MessagePrefix Scapy interop +(not HAS_ASN1TOOLS_UPER) or __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/uper_asn1scc_iop.py b/test/scapy/layers/uper_asn1scc_iop.py new file mode 100644 index 00000000000..f35e4b59440 --- /dev/null +++ b/test/scapy/layers/uper_asn1scc_iop.py @@ -0,0 +1,261 @@ +# 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 asn1tools reference only). +""" + +from typing import Any, List, Tuple + +try: + import asn1tools + HAS_ASN1TOOLS = True +except ImportError: + asn1tools = None # type: ignore + HAS_ASN1TOOLS = False + +from scapy.asn1.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; asn1tools reference only) +README_MESSAGE_SPEC = ( + "Sample DEFINITIONS AUTOMATIC TAGS ::= BEGIN " + "Message ::= SEQUENCE { " + "msgId INTEGER, " + "myflag INTEGER, " + "value REAL, " + "szDescription OCTET STRING (SIZE(10)), " + "isReady BOOLEAN " + "} " + "END" +) + +README_MESSAGE_HEX = ( + "010101020980cd191eb851eb851f48656c6c6f576f726c6480" +) + +README_MESSAGE_PREFIX_SPEC = ( + "Sample DEFINITIONS AUTOMATIC TAGS ::= BEGIN " + "MessagePrefix ::= SEQUENCE { " + "msgId INTEGER, " + "myflag INTEGER, " + "szDescription OCTET STRING (SIZE(10)), " + "isReady BOOLEAN " + "} " + "END" +) + +README_MESSAGE_PREFIX_HEX = ( + "0101010248656c6c6f576f726c6480" +) + +ASN1SCC_VECTORS = [ + ( + "05-BOOLEAN/001 pdu1", + BOOLEAN_SPEC, + "MyPDU", + True, + lambda _v: UPERcodec_BOOLEAN.enc(1), + ), + ( + "18-NULL/001 pdu1", + NULL_SPEC, + "MyPDU", + None, + lambda _v: UPERcodec_NULL.enc(None), + ), + ( + "06-OCTET-STRING/001 pdu1", + OCTET_STRING_VAR_SPEC, + "MyPDU", + bytes.fromhex("afbc4583"), + lambda v: UPERcodec_STRING.enc(v, uper_min=1, uper_max=20), + ), + ( + "05-BOOLEAN/001 pdu1 false", + BOOLEAN_SPEC, + "MyPDU", + False, + lambda _v: UPERcodec_BOOLEAN.enc(0), + ), + ( + "04-ENUMERATED/001 pdu1 alpha", + ENUMERATED_SPEC, + "MyPDU", + "alpha", + lambda _v: UPERcodec_ENUMERATED.enc( + 1, uper_enum_values=[1, 200], + ), + ), + ( + "04-ENUMERATED/001 pdu1 beta", + ENUMERATED_SPEC, + "MyPDU", + "beta", + lambda _v: UPERcodec_ENUMERATED.enc( + 200, uper_enum_values=[1, 200], + ), + ), + ( + "09-CHOICE/001 pdu1 int1:10", + CHOICE_SPEC, + "MyPDU", + ("int1", 10), + lambda _v: _encode_choice_int1_10(), + ), + ( + "08-BIT-STRING/001 pdu1 ABCD", + BIT_STRING_VAR_SPEC, + "MyPDU", + (bytes.fromhex("abcd"), 16), + lambda _v: UPERcodec_BIT_STRING.enc( + (bytes.fromhex("abcd"), 16), uper_min=1, uper_max=20, + ), + ), +] + + +def require_asn1tools(): + # type: () -> bool + return HAS_ASN1TOOLS + + +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 + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + for name, spec, pdu, value, encoder in ASN1SCC_VECTORS: + foo = asn1tools.compile_string(spec, "uper") + expected = foo.encode(pdu, value) + 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 asn1tools.""" + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + from test.scapy.layers.uper_packets import UPERMessagePrefix + from scapy.packet import raw + + foo = asn1tools.compile_string(README_MESSAGE_PREFIX_SPEC, "uper") + value = { + "msgId": 1, + "myflag": 2, + "szDescription": b"HelloWorld", + "isReady": True, + } + expected = foo.encode("MessagePrefix", value) + assert expected.hex() == 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.""" + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + foo = asn1tools.compile_string(README_MESSAGE_SPEC, "uper") + value = { + "msgId": 1, + "myflag": 2, + "value": 3.14, + "szDescription": b"HelloWorld", + "isReady": True, + } + got = foo.encode("Message", value) + assert got.hex() == README_MESSAGE_HEX diff --git a/test/scapy/layers/uper_codec.py b/test/scapy/layers/uper_codec.py new file mode 100644 index 00000000000..d834763c290 --- /dev/null +++ b/test/scapy/layers/uper_codec.py @@ -0,0 +1,186 @@ +# 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, List, Tuple, Type + +try: + import asn1tools + HAS_ASN1TOOLS = True +except ImportError: + asn1tools = None # type: ignore + HAS_ASN1TOOLS = False + +from scapy.asn1.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, +] + +DECODE_VECTORS = [ + ("A", True, UPERcodec_BOOLEAN, {}, 1), + ("A", False, UPERcodec_BOOLEAN, {}, 0), + ("B", 42, UPERcodec_INTEGER, {}, 42), + ("B", -1, UPERcodec_INTEGER, {}, -1), + ("C", 200, UPERcodec_INTEGER, {"uper_min": 0, "uper_max": 255}, 200), + ("Signed", -1, UPERcodec_INTEGER, {"uper_min": -128, "uper_max": 127}, -1), + ("Signed", 127, UPERcodec_INTEGER, {"uper_min": -128, "uper_max": 127}, 127), + ("D", b"AB", UPERcodec_STRING, {}, b"AB"), + ("E", b"\x12\x34\x56", UPERcodec_STRING, {"size_len": 3}, b"\x12\x34\x56"), + ("G", None, UPERcodec_NULL, {}, None), + ( + "H", + "alpha", + UPERcodec_ENUMERATED, + {"uper_enum_values": [1, 200]}, + 1, + ), + ( + "H", + "beta", + UPERcodec_ENUMERATED, + {"uper_enum_values": [1, 200]}, + 200, + ), +] + +ASN1TOOLS_OID_SPEC = ( + "Foo DEFINITIONS AUTOMATIC TAGS ::= BEGIN " + "A ::= OBJECT IDENTIFIER " + "END" +) + + +def require_asn1tools(): + # type: () -> bool + return HAS_ASN1TOOLS + + +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 + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + foo = asn1tools.compile_string(ASN1TOOLS_OID_SPEC, "uper") + for oid in ("1.2.3", "2.999.3"): + expected = foo.encode("A", oid) + got = UPERcodec_OID.enc(oid) + assert got == expected, ( + "OID %r: expected %s, got %s" % + (oid, expected.hex(), got.hex()) + ) + + +def check_uper_codec_asn1tools_decode(): + # type: () -> None + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + from test.scapy.layers.uper_iop import ASN1TOOLS_UPER_SPEC + + foo = asn1tools.compile_string(ASN1TOOLS_UPER_SPEC, "uper") + for typename, value, codec, kwargs, expected in DECODE_VECTORS: + encoded = foo.encode(typename, value) + 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_scapy_decode_asn1tools(): + # type: () -> None + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + from test.scapy.layers.uper_iop import ASN1TOOLS_UPER_SPEC, PRIMITIVE_VECTORS + + foo = asn1tools.compile_string(ASN1TOOLS_UPER_SPEC, "uper") + for typename, value, encoder in PRIMITIVE_VECTORS: + encoded = encoder(value) + asn1_value = foo.decode(typename, encoded) + if typename == "H": + assert asn1_value == value + elif typename == "G": + assert asn1_value is None + else: + assert asn1_value == value diff --git a/test/scapy/layers/uper_fuzz.py b/test/scapy/layers/uper_fuzz.py new file mode 100644 index 00000000000..0475d2ef0a9 --- /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.asn1.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..58cf49aa5ac --- /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.asn1.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..30b1e56396b --- /dev/null +++ b/test/scapy/layers/uper_iop.py @@ -0,0 +1,251 @@ +# 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 asn1tools when available. +""" + +from typing import Any, Callable, List, Optional, Tuple + +try: + import asn1tools + HAS_ASN1TOOLS = True +except ImportError: + asn1tools = None # type: ignore + HAS_ASN1TOOLS = False + +from scapy.asn1.uper import ( + UPERcodec_BOOLEAN, + UPERcodec_ENUMERATED, + UPERcodec_INTEGER, + UPERcodec_NULL, + UPERcodec_STRING, + UPER_Encoder, + UPER_choice_index_enc, +) + + +ASN1TOOLS_UPER_SPEC = ( + "Foo DEFINITIONS AUTOMATIC TAGS ::= " + "BEGIN " + "A ::= BOOLEAN " + "B ::= INTEGER " + "C ::= INTEGER (0..255) " + "Signed ::= INTEGER (-128..127) " + "SeqOfC ::= SEQUENCE OF INTEGER (0..255) " + "ChoiceC ::= CHOICE { a INTEGER (0..15), b OCTET STRING } " + "D ::= OCTET STRING " + "E ::= OCTET STRING (SIZE(3)) " + "G ::= NULL " + "Seq ::= SEQUENCE { id INTEGER, flag BOOLEAN, extra INTEGER OPTIONAL } " + "SeqOf ::= SEQUENCE OF INTEGER " + "Choice ::= CHOICE { a INTEGER, b OCTET STRING } " + "H ::= ENUMERATED { alpha(1), beta(200) } " + "Inner ::= SEQUENCE { x INTEGER, y BOOLEAN } " + "Outer ::= SEQUENCE { id INTEGER, inner Inner } " + "Multi ::= SEQUENCE { id INTEGER, a INTEGER OPTIONAL, b OCTET STRING OPTIONAL } " + "END" +) + +PRIMITIVE_VECTORS = [ + ("A", True, lambda v: UPERcodec_BOOLEAN.enc(1 if v else 0)), + ("A", False, lambda v: UPERcodec_BOOLEAN.enc(1 if v else 0)), + ("B", 42, lambda v: UPERcodec_INTEGER.enc(v)), + ("B", -1, lambda v: UPERcodec_INTEGER.enc(v)), + ("C", 200, lambda v: UPERcodec_INTEGER.enc(v, uper_min=0, uper_max=255)), + ("D", b"AB", lambda v: UPERcodec_STRING.enc(v)), + ("E", b"\x12\x34\x56", lambda v: UPERcodec_STRING.enc(v, size_len=3)), + ("G", None, lambda v: UPERcodec_NULL.enc(None)), + ("H", "beta", lambda v: UPERcodec_ENUMERATED.enc( + 200, uper_enum_values=[1, 200], + )), +] + +COMPOSITE_VECTORS = [ + ("Seq", {"id": 42, "flag": True}), + ("Seq", {"id": 42, "flag": True, "extra": 7}), + ("SeqOf", [1, 2, 3]), + ("SeqOfC", [1, 200, 0]), + ("Choice", ("a", 99)), + ("Choice", ("b", b"AB")), + ("ChoiceC", ("a", 10)), + ("ChoiceC", ("b", b"AB")), +] + +from test.scapy.layers.uper_packets import ( + UPERMultiOptional, + UPERNestedSequence, +) +from scapy.packet import raw + +DECODE_COMPOSITE_VECTORS = [ + ("Seq", {"id": 42, "flag": True}, {"id": 42, "flag": True}), + ( + "Seq", + {"id": 42, "flag": True, "extra": 7}, + {"id": 42, "flag": True, "extra": 7}, + ), + ("SeqOf", [1, 2, 3], [1, 2, 3]), + ("SeqOfC", [1, 200, 0], [1, 200, 0]), + ("Choice", ("a", 99), ("a", 99)), + ("Choice", ("b", b"AB"), ("b", b"AB")), + ("ChoiceC", ("a", 10), ("a", 10)), + ("ChoiceC", ("b", b"AB"), ("b", b"AB")), +] + +DECODE_PACKET_VECTORS = [ + ( + UPERNestedSequence, + {"id": 5, "x": 3, "y": True}, + bytes.fromhex("0105010380"), + ), + ( + UPERMultiOptional, + {"id": 1, "a": 2, "b": b"hi"}, + bytes.fromhex("c0404040809a1a40"), + ), +] + +ASN1TOOLS_PACKET_VECTORS = [ + ( + "Outer", + {"id": 5, "inner": {"x": 3, "y": True}}, + UPERNestedSequence, + {"id": 5, "x": 3, "y": True}, + ), + ( + "Multi", + {"id": 1, "a": 2, "b": b"hi"}, + UPERMultiOptional, + {"id": 1, "a": 2, "b": b"hi"}, + ), +] + + +def require_asn1tools(): + # type: () -> bool + return HAS_ASN1TOOLS + + +def _compile_asn1tools(): + # type: () -> Any + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + return asn1tools.compile_string(ASN1TOOLS_UPER_SPEC, "uper") + + +def check_primitive_interop(): + # type: () -> None + foo = _compile_asn1tools() + for typename, value, encoder in PRIMITIVE_VECTORS: + expected = foo.encode(typename, value) + got = encoder(value) + assert got == expected, ( + "%s %r: expected %s, got %s" % + (typename, value, expected.hex(), got.hex()) + ) + + +def check_composite_interop(): + # type: () -> None + foo = _compile_asn1tools() + for typename, value in COMPOSITE_VECTORS: + expected = foo.encode(typename, value) + got = _encode_composite(typename, value) + assert got == expected, ( + "%s %r: expected %s, got %s" % + (typename, value, expected.hex(), got.hex()) + ) + + +def check_composite_decode_interop(): + # type: () -> None + foo = _compile_asn1tools() + for typename, encoded_value, expected in DECODE_COMPOSITE_VECTORS: + data = foo.encode(typename, encoded_value) + got = foo.decode(typename, _encode_composite(typename, encoded_value)) + assert got == expected, ( + "%s %r: expected %r, got %r" % (typename, encoded_value, expected, got) + ) + assert foo.decode(typename, data) == expected + + +def check_packet_asn1tools_interop(): + # type: () -> None + foo = _compile_asn1tools() + for typename, asn1_value, cls, pkt_kwargs in ASN1TOOLS_PACKET_VECTORS: + expected = foo.encode(typename, asn1_value) + got = raw(cls(**pkt_kwargs)) + assert got == expected, ( + "%s: expected %s, got %s" % + (typename, 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..3d035d14344 --- /dev/null +++ b/test/scapy/layers/uper_packets.py @@ -0,0 +1,542 @@ +# 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. +""" + +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 asn1tools 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 diff --git a/tox.ini b/tox.ini index 495672a8e9d..e29e5fbc940 100644 --- a/tox.ini +++ b/tox.ini @@ -34,6 +34,7 @@ deps = coverage[toml] python-can cbor2 + asn1tools scapy-rpc # disabled on windows because they require c++ dependencies # brotli 1.1.0 broken https://github.com/google/brotli/issues/1072 From f151629d1f5f23db9ebf214144a8833c3a64986d Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 1 Jul 2026 21:39:59 +0200 Subject: [PATCH 03/15] minor fixes AI-Assisted: yes (Cursor AI) --- scapy/asn1/uper.py | 91 ++++++++-------- scapy/asn1fields.py | 247 ++++++++++++++++++++++++++------------------ 2 files changed, 197 insertions(+), 141 deletions(-) diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index cfdf2e97e4b..83d0216e4e4 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -129,7 +129,7 @@ def append_bits(self, data, number_of_bits): # type: (bytes, int) -> None if number_of_bits == 0: return - value = int(binascii.hexlify(data), 16) + value = int.from_bytes(data, "big") value >>= (8 * len(data) - number_of_bits) self.append_non_negative_binary_integer(value, number_of_bits) @@ -219,11 +219,9 @@ def _uper_significant_bit_count(data): if not data: return 0 total = 8 * len(data) - dec = UPER_Decoder(data) - start = dec._read_offset() - bits = dec.value[start:start + total] - end = len(bits) - while end > 0 and bits[end - 1] == "0": + 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: @@ -231,15 +229,33 @@ def _uper_significant_bit_count(data): 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 - dec = UPER_Decoder(data) - start = dec._read_offset() nbits = _uper_significant_bit_count(data) - for i in range(nbits): - enc.append_bit(int(dec.value[start + i])) + 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): @@ -273,8 +289,8 @@ def UPER_has_unexpected_remainder(dec): # type: (UPER_Decoder) -> bool if dec.number_of_bits == 0: return False - offset = dec._read_offset() - return dec.value[offset:].strip("0") != "" + mask = (1 << dec.number_of_bits) - 1 + return (dec._bits & mask) != 0 def UPER_count_dec(s, dec=None): @@ -291,25 +307,31 @@ def UPER_count_dec(s, dec=None): class UPER_Decoder(object): def __init__(self, encoded): # type: (bytes) -> None - self.number_of_bits = 8 * len(encoded) - self.total_number_of_bits = self.number_of_bits + self.total_number_of_bits = 8 * len(encoded) + self.number_of_bits = self.total_number_of_bits if encoded: - value = int(binascii.hexlify(encoded), 16) - value |= (0x80 << self.number_of_bits) - self.value = bin(value)[10:] + self._bits = int.from_bytes(encoded, "big") else: - self.value = "" + 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") - offset = self._read_offset() - bit = int(self.value[offset]) + bit = self._read_bits_int(1) self.number_of_bits -= 1 return bit @@ -319,32 +341,16 @@ def read_bits(self, number_of_bits): raise UPER_Decoding_Error("UPER_Decoder: out of data") if number_of_bits == 0: return b"" - offset = self._read_offset() - bits = self.value[offset:offset + number_of_bits] + value = self._read_bits_int(number_of_bits) self.number_of_bits -= number_of_bits - value = "10000000" + bits - 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) + return _uper_per_bits_to_bytes(value, number_of_bits) def remaining(self): # type: () -> bytes if self.number_of_bits == 0: return b"" - offset = self._read_offset() - bits = self.value[offset:offset + self.number_of_bits] - value = "10000000" + bits - number_of_alignment_bits = (8 - (self.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) + 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 @@ -356,10 +362,9 @@ 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 - offset = self._read_offset() - bits = self.value[offset:offset + number_of_bits] + value = self._read_bits_int(number_of_bits) self.number_of_bits -= number_of_bits - return int(bits, 2) + return value def read_length_determinant(self): # type: () -> int diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 9a1b150010e..d242b7c7873 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -45,6 +45,7 @@ UPER_Decoding_Error, UPER_Decoder, UPER_Encoder, + UPER_append_encoded, UPER_choice_index_dec, UPER_choice_index_enc, UPER_count_dec, @@ -585,6 +586,9 @@ def __init__(self, *seq, **kwargs): ) self.seq = seq self.islist = len(seq) > 1 + self._optionals = tuple( + f for f in seq if isinstance(f, ASN1F_optional) + ) def __repr__(self): # type: () -> str @@ -599,6 +603,75 @@ def get_fields_list(self): return reduce(lambda x, y: x + y.get_fields_list(), self.seq, []) + def _apply_tagging_dec(self, s, pkt): + # type: (bytes, Any) -> bytes + if pkt.ASN1_codec == ASN1_Codecs.OER: + diff_tag, s = OER_tagging_dec( + s, + hidden_tag=self.ASN1_tag, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag, + _fname=pkt.name, + ) + else: + diff_tag, s = BER_tagging_dec( + s, + hidden_tag=self.ASN1_tag, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag, + _fname=pkt.name, + ) + 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 + return s + + 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) + s = self._dissect_sequence_children(pkt, s) + if len(s) > 0: + raise OER_Decoding_Error("unexpected remainder", remaining=s) + return [], b"" + + def _m2i_per(self, pkt, s): + # type: (Any, bytes) -> Tuple[Any, bytes] + dec = UPER_Decoder(s) + self._uper_dissect_from_decoder(pkt, dec) + if UPER_has_unexpected_remainder(dec): + raise 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) + 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] """ @@ -610,64 +683,14 @@ def m2i(self, pkt, s): It is discarded by dissect() and should not be missed elsewhere. """ if pkt.ASN1_codec == ASN1_Codecs.OER: - diff_tag, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, - implicit_tag=self.implicit_tag, - explicit_tag=self.explicit_tag, - safe=self.flexible_tag, - _fname=pkt.name) - 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 - 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 OER_Decoding_Error("unexpected remainder", remaining=s) - return [], b"" + return self._m2i_oer(pkt, s) if pkt.ASN1_codec == ASN1_Codecs.PER: - dec = UPER_Decoder(s) - self._uper_dissect_from_decoder(pkt, dec) - if UPER_has_unexpected_remainder(dec): - raise UPER_Decoding_Error("unexpected remainder", - remaining=dec.remaining()) - return [], b"" - diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, - implicit_tag=self.implicit_tag, - explicit_tag=self.explicit_tag, - safe=self.flexible_tag, - _fname=pkt.name) - 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 - 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", remaining=s) - return [], remain + return self._m2i_per(pkt, s) + return self._m2i_ber(pkt, s) def _uper_dissect_from_decoder(self, pkt, dec): # type: (Any, UPER_Decoder) -> None - optionals = [f for f in self.seq if isinstance(f, ASN1F_optional)] - presence = [dec.read_bit() for _ in optionals] + presence = [dec.read_bit() for _ in self._optionals] opt_idx = 0 for obj in self.seq: if isinstance(obj, ASN1F_optional): @@ -702,13 +725,12 @@ def build(self, pkt): def _uper_encode_into(self, enc, pkt, value=None): # type: (UPER_Encoder, ASN1_Packet, Optional[Any]) -> None - optionals = [f for f in self.seq if isinstance(f, ASN1F_optional)] - for opt in optionals: + 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) and obj.is_empty(pkt): continue - obj._uper_encode_into(enc, pkt, value) + obj._uper_encode_into(enc, pkt) class ASN1F_SET(ASN1F_SEQUENCE): @@ -797,7 +819,10 @@ def _uper_encode_into(self, enc, pkt, value=None): return enc.append_length_determinant(len(value)) for item in value: - self.fld._uper_encode_into(enc, pkt, item) + if self.holds_packets: + UPER_append_encoded(enc, bytes(item)) + else: + self.fld._uper_encode_into(enc, pkt, item) def m2i(self, pkt, # type: ASN1_Packet @@ -874,8 +899,15 @@ def build(self, pkt): enc = UPER_Encoder() enc.append_length_determinant(len(val)) for item in val: - self.fld._uper_encode_into(enc, pkt, item) + if self.holds_packets: + UPER_append_encoded(enc, bytes(item)) + else: + self.fld._uper_encode_into(enc, pkt, item) 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_unsigned_integer_enc(len(val)) + s else: s = b"".join(self.fld._encode_item(pkt, i) for i in val) if pkt.ASN1_codec == ASN1_Codecs.OER: @@ -1051,6 +1083,59 @@ def __init__(self, name, default, *args, **kwargs): 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 _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 = OER_tagging_dec( + s, hidden_tag=self.ASN1_tag, explicit_tag=self.explicit_tag, + ) + tag, payload = 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_Decoder(s) + val = self.m2i_from_decoder(pkt, dec) + if UPER_has_unexpected_remainder(dec): + raise 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 = BER_tagging_dec( + s, hidden_tag=self.ASN1_tag, explicit_tag=self.explicit_tag, + ) + 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] @@ -1061,41 +1146,10 @@ def m2i(self, pkt, s): if len(s) == 0: raise ASN1_Error("ASN1F_CHOICE: got empty string") if pkt.ASN1_codec == ASN1_Codecs.OER: - _, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, - explicit_tag=self.explicit_tag) - tag, payload = OER_id_dec(s) - elif pkt.ASN1_codec == ASN1_Codecs.PER: - dec = UPER_Decoder(s) - val = self.m2i_from_decoder(pkt, dec) - if UPER_has_unexpected_remainder(dec): - raise UPER_Decoding_Error("unexpected remainder", - remaining=dec.remaining()) - return val, b"" - else: - _, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, - explicit_tag=self.explicit_tag) - tag, _ = BER_id_dec(s) - payload = 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 hasattr(choice, "ASN1_root"): - # we don't want to import ASN1_Packet in this module... - return self.extract_packet(choice, payload, _underlayer=pkt) # type: ignore - elif isinstance(choice, type): - return choice(self.name, b"").m2i(pkt, payload) - else: - # XXX check properly if this is an ASN1F_PACKET - return choice.m2i(pkt, payload) + 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] @@ -1116,10 +1170,7 @@ def _choice_index_for(self, x): tag = self._choice_tag_for(x) if tag is None: return None - try: - return self.choice_order.index(tag) - except ValueError: - return None + return self._tag_to_index.get(tag) def _choice_for_index(self, index): # type: (int) -> _CHOICE_T From f0d7bf1c82dbb73072006af61a8abcf4a04e3aa7 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 1 Jul 2026 21:50:29 +0200 Subject: [PATCH 04/15] minor fixes AI-Assisted: yes (Cursor AI) --- scapy/asn1/ber.py | 8 +++++++- scapy/asn1/oer.py | 34 +++++++++++++++++----------------- scapy/asn1/uper.py | 4 ++-- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index 23899274e4a..1f9a9e75463 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -257,11 +257,14 @@ def BER_tagging_dec(s, # type: bytes def BER_tagging_enc(s, implicit_tag=None, explicit_tag=None): # type: (bytes, Optional[int], Optional[int]) -> bytes + from scapy.config import conf if len(s) > 0: if implicit_tag is not None: s = BER_id_enc(implicit_tag) + s[1:] elif explicit_tag is not None: - s = BER_id_enc(explicit_tag) + BER_len_enc(len(s)) + s + s = BER_id_enc(explicit_tag) + BER_len_enc( + len(s), size=conf.ASN1_default_long_size, + ) + s return s # [ BER classes ] # @@ -629,10 +632,13 @@ class BERcodec_SEQUENCE(BERcodec_Object[Union[bytes, List[BERcodec_Object[Any]]] @classmethod def enc(cls, _ll, size_len=0): # type: (Union[bytes, List[BERcodec_Object[Any]]], Optional[int]) -> bytes + from scapy.config import conf if isinstance(_ll, bytes): ll = _ll else: ll = b"".join(x.enc(cls.codec) for x in _ll) + if not size_len: + size_len = conf.ASN1_default_long_size return chb(int(cls.tag)) + BER_len_enc(len(ll), size=size_len) + ll @classmethod diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index 3bb1c1b75a2..2007a60078a 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -540,12 +540,12 @@ def do_dec(cls, oer_unsigned=False, # type: bool ): # type: (...) -> Tuple[ASN1_Object[str], bytes] - l, s = OER_len_dec(s) - if l == 0: + length, s = OER_len_dec(s) + if length == 0: return cls.tag.asn1_object(""), s - if len(s) < l: + if len(s) < length: raise OER_Decoding_Error( - "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), l), + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), length), remaining=s ) unused_bits = orb(s[0]) @@ -554,10 +554,10 @@ 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:l]) + 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[l:] + return cls.tag.asn1_object(fs), s[length:] @classmethod def enc(cls, _s, size_len=0): @@ -602,13 +602,13 @@ def do_dec(cls, remaining=s ) return cls.tag.asn1_object(s[:size_len]), s[size_len:] - l, s = OER_len_dec(s) - if len(s) < l: + 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), l), + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), length), remaining=s ) - return cls.tag.asn1_object(s[:l]), s[l:] + return cls.tag.asn1_object(s[:length]), s[length:] class OERcodec_NULL(OERcodec_Object[None]): @@ -657,13 +657,13 @@ def do_dec(cls, oer_unsigned=False, # type: bool ): # type: (...) -> Tuple[ASN1_Object[bytes], bytes] - l, s = OER_len_dec(s) - if len(s) < l: + 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), l), + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), length), remaining=s ) - content, t = s[:l], s[l:] + content, t = s[:length], s[length:] lst = [] while content: val, content = BER_num_dec(content) @@ -796,11 +796,11 @@ def do_dec(cls, s, context=None, safe=False, if size_len == 4: raw, remain = s[:4], s[4:] else: - l, remain = OER_len_dec(s) - if len(remain) < l: + 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[:l], remain[l:] + raw, remain = remain[:length], remain[length:] try: ipaddr_ascii = inet_ntoa(raw) except Exception: diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 83d0216e4e4..cda2c09109d 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -1022,8 +1022,8 @@ def do_dec(cls, ): # type: (...) -> Tuple[ASN1_Object[bytes], bytes] dec = UPER_Decoder(s) - l = dec.read_length_determinant() - content = dec.read_bytes(l) + length = dec.read_length_determinant() + content = dec.read_bytes(length) lst = [] while content: val, content = BER_num_dec(content) From e38dfee5e60bee102e7806e08079878c46a63b94 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 2 Jul 2026 08:12:07 +0200 Subject: [PATCH 05/15] increase test coverage AI-Assisted: yes (Cursor AI) --- test/scapy/layers/asn1.uts | 54 +++++ test/scapy/layers/asn1_coverage.py | 344 +++++++++++++++++++++++++++++ test/scapy/layers/ber_codec.py | 275 +++++++++++++++++++++++ 3 files changed, 673 insertions(+) create mode 100644 test/scapy/layers/asn1_coverage.py create mode 100644 test/scapy/layers/ber_codec.py diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 5a3674e7e5c..8f94db57923 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -411,3 +411,57 @@ __import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_roundtrip'] __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() + ++ ASN.1 BER codec += BER error formatting +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_error_str']).check_ber_error_str() += BER length encoding +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_len_enc_dec']).check_ber_len_enc_dec() += BER number encoding +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_num_enc_dec']).check_ber_num_enc_dec() += BER identifier encoding +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_id_enc_dec']).check_ber_id_enc_dec() += BER tagging +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_tagging']).check_ber_tagging() += BER integer codec +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_integer']).check_ber_integer() += BER bit string codec +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_bit_string']).check_ber_bit_string() += BER string and null codec +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_string_and_null']).check_ber_string_and_null() += BER OID codec +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_oid']).check_ber_oid() += BER sequence and set codec +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_sequence_and_set']).check_ber_sequence_and_set() += BER IP address codec +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_ipaddress']).check_ber_ipaddress() += BER object dispatch +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_object_dispatch']).check_ber_object_dispatch() + ++ ASN.1 codec coverage (UPER/OER/asn1fields) += UPER extended helpers +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_error_str']).check_uper_error_str() += UPER length determinant extended +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_length_determinant_extended']).check_uper_length_determinant_extended() += UPER unconstrained whole number +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_unconstrained_whole_number']).check_uper_unconstrained_whole_number() += UPER bit string paths +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_bit_string_paths']).check_uper_bit_string_paths() += UPER enumerated range +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_enumerated_range']).check_uper_enumerated_range() += UPER sequence errors +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_sequence_errors']).check_uper_sequence_errors() += UPER IP address codec +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_ipaddress']).check_uper_ipaddress() += OER extended coverage +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_oer_error_str']).check_oer_error_str() += OER IP address and sequence +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_oer_ipaddress_and_sequence']).check_oer_ipaddress_and_sequence() += asn1fields enum and flags +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_enum_and_flags']).check_asn1fields_enum_and_flags() += asn1fields encaps and packet +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_encaps_and_packet']).check_asn1fields_encaps_and_packet() += asn1fields choice and special fields +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_choice_and_special']).check_asn1fields_choice_and_special() += asn1fields optional dissect +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_optional_dissect']).check_asn1fields_optional_dissect() diff --git a/test/scapy/layers/asn1_coverage.py b/test/scapy/layers/asn1_coverage.py new file mode 100644 index 00000000000..d03de639f0a --- /dev/null +++ b/test/scapy/layers/asn1_coverage.py @@ -0,0 +1,344 @@ +# 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 scapy.asn1.asn1 import ( + ASN1_BIT_STRING, + ASN1_Codecs, + ASN1_INTEGER, + ASN1_STRING, + ASN1_TIME_TICKS, +) +from scapy.asn1.oer import ( + OER_Decoding_Error, + OER_Encoding_Error, + OERcodec_BIT_STRING, + OERcodec_IPADDRESS, + OERcodec_SEQUENCE, + OERcodec_SET, +) +from scapy.asn1.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_ENCAPS, + ASN1F_CHOICE, + ASN1F_FLAGS, + ASN1F_IPADDRESS, + ASN1F_INTEGER, + ASN1F_PACKET, + ASN1F_SEQUENCE, + ASN1F_SET_OF, + ASN1F_STRING, + ASN1F_STRING_ENCAPS, + ASN1F_STRING_PacketField, + ASN1F_TIME_TICKS, + ASN1F_enum_INTEGER, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import 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 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") From 09d47de80654b12c2005f886a27305a3d8ad1b07 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 2 Jul 2026 20:49:20 +0200 Subject: [PATCH 06/15] minor fixes AI-Assisted: yes (Cursor AI) --- scapy/asn1/uper.py | 10 ++- scapy/asn1fields.py | 192 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 170 insertions(+), 32 deletions(-) diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index cda2c09109d..f6ae7de82e0 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -808,7 +808,15 @@ def encode_into(cls, if size_len: minimum = maximum = size_len if minimum is not None and maximum is not None and minimum == maximum: - enc.append_bits(s, nbits) + 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) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index d242b7c7873..a30f9d7f8d9 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -46,8 +46,10 @@ UPER_Decoder, UPER_Encoder, UPER_append_encoded, + UPER_bits_for_range, UPER_choice_index_dec, UPER_choice_index_enc, + UPER_constrained_int_enc, UPER_count_dec, UPER_has_unexpected_remainder, ) @@ -579,6 +581,7 @@ class ASN1F_SEQUENCE(ASN1F_field[List[Any], List[Any]]): def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None + self.uper_extensible = kwargs.pop("uper_extensible", False) name = "dummy_seq_name" default = [field.default for field in seq] super(ASN1F_SEQUENCE, self).__init__( @@ -690,6 +693,11 @@ def m2i(self, pkt, s): def _uper_dissect_from_decoder(self, pkt, dec): # type: (Any, UPER_Decoder) -> None + if self.uper_extensible: + if dec.read_bit(): + raise 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: @@ -725,6 +733,8 @@ def build(self, pkt): def _uper_encode_into(self, enc, pkt, value=None): # type: (UPER_Encoder, 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: @@ -760,6 +770,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 \ @@ -782,6 +795,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: (UPER_Encoder, int) -> None + if self.uper_min is not None and self.uper_max is not None: + 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: (UPER_Decoder) -> 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_bits_for_range(size), + ) + self.uper_min, + ) + return cast(int, dec.read_length_determinant()) def is_empty(self, pkt, # type: ASN1_Packet @@ -792,9 +827,10 @@ def is_empty(self, def _extract_packet_from_decoder(self, dec, pkt): # type: (UPER_Decoder, ASN1_Packet) -> Tuple[Any, bytes] if self.holds_packets: - raise UPER_Decoding_Error( - "UPER SEQUENCE OF packets is not supported yet" - ) + 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): @@ -815,12 +851,36 @@ def _uper_encode_into(self, enc, pkt, value=None): if value is None: value = getattr(pkt, self.name) if value is None: - enc.append_length_determinant(0) + self._uper_count_enc(enc, 0) return - enc.append_length_determinant(len(value)) + 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: + item_enc = UPER_Encoder() + cast("ASN1_Packet", item).ASN1_root._uper_encode_into( + item_enc, item, + ) + UPER_append_encoded(enc, item_enc.as_bytes()) + else: + self.fld._uper_encode_into(enc, pkt, item) + return + self._uper_count_enc(enc, count) for item in value: if self.holds_packets: - UPER_append_encoded(enc, bytes(item)) + item_enc = UPER_Encoder() + cast("ASN1_Packet", item).ASN1_root._uper_encode_into( + item_enc, item, + ) + UPER_append_encoded(enc, item_enc.as_bytes()) else: self.fld._uper_encode_into(enc, pkt, item) @@ -850,7 +910,10 @@ def m2i(self, return lst, b"" if pkt.ASN1_codec == ASN1_Codecs.PER: dec = UPER_Decoder(s) - count, _ = UPER_count_dec(b"", dec=dec) + 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) @@ -1006,7 +1069,12 @@ def set_val(self, pkt, val): def is_empty(self, pkt): # type: (ASN1_Packet) -> bool - return self._field.is_empty(pkt) + val = getattr(pkt, self._field.name, None) + if val is None: + return True + if getattr(self._field, "islist", 0) and val == []: + return True + return False def _uper_encode_into(self, enc, pkt, value=None): # type: (UPER_Encoder, ASN1_Packet, Optional[Any]) -> None @@ -1044,6 +1112,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) + self.uper_extensible = kwargs.pop("uper_extensible", False) self.implicit_tag = None for kwarg in ["context", "explicit_tag"]: setattr(self, kwarg, kwargs.get(kwarg)) @@ -1055,6 +1124,7 @@ def __init__(self, name, default, *args, **kwargs): 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"): @@ -1065,22 +1135,27 @@ def __init__(self, name, default, *args, **kwargs): for k in root.choice_order: self.choices[k] = root.choices[k] self.choice_order.append(k) + self.choice_list.append(root.choices[k]) else: tag = p.ASN1_root.network_tag self.choices[tag] = p self.choice_order.append(tag) + self.choice_list.append(p) elif hasattr(p, "ASN1_tag"): if isinstance(p, type): # should be ASN1F_field class tag = int(p.ASN1_tag) self.choices[tag] = p self.choice_order.append(tag) + self.choice_list.append(p) else: # should be ASN1F_field instance tag = p.network_tag self.choices[tag] = p self.choice_order.append(tag) - self.pktchoices[hash(p.cls)] = (p.implicit_tag, p.explicit_tag) # noqa: E501 + self.choice_list.append(p) + if hasattr(p, "cls"): + 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 = { @@ -1153,42 +1228,59 @@ def m2i(self, pkt, s): def _choice_tag_for(self, x): # type: (Any) -> Optional[int] - for tag, choice in self.choices.items(): - if hasattr(choice, "ASN1_root"): - if isinstance(x, cast("Type[ASN1_Packet]", choice)): - return tag + for index, choice in enumerate(self.choice_list): + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + if isinstance(x, choice): + return self.choice_order[index] elif isinstance(choice, type) and hasattr(choice, "ASN1_tag"): if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: - return tag + return self.choice_order[index] elif hasattr(choice, "ASN1_tag"): if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: - return tag + return self.choice_order[index] return None def _choice_index_for(self, x): # type: (Any) -> Optional[int] - tag = self._choice_tag_for(x) - if tag is None: - return None - return self._tag_to_index.get(tag) + for index, choice in enumerate(self.choice_list): + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + if isinstance(x, choice): + return index + elif isinstance(choice, type) and hasattr(choice, "ASN1_tag"): + if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + 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.choices[self.choice_order[index]] + return self.choice_list[index] def m2i_from_decoder(self, pkt, dec): # type: (ASN1_Packet, UPER_Decoder) -> ASN1_Object[Any] - index, _ = UPER_choice_index_dec(b"", len(self.choice_order), dec=dec) + if self.uper_extensible: + if dec.read_bit(): + raise UPER_Decoding_Error( + "ASN1F_CHOICE: extension additions are not supported" + ) + if len(self.choice_order) > 1: + index, _ = 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 hasattr(choice, "ASN1_root"): - raise ASN1_Error( - "ASN1F_CHOICE: UPER packet choices are not supported yet" - ) + 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], @@ -1206,7 +1298,10 @@ def _uper_encode_into(self, enc, pkt, value=None): "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % self.name ) - UPER_choice_index_enc(index, len(self.choice_order), enc=enc) + if self.uper_extensible: + enc.append_bit(0) + if len(self.choice_order) > 1: + UPER_choice_index_enc(index, len(self.choice_order), enc=enc) choice = self._choice_for_index(index) if hasattr(choice, "ASN1_root"): cast("ASN1_Packet", value).ASN1_root._uper_encode_into(enc, value) @@ -1286,12 +1381,37 @@ 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, UPER_Decoder) -> 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 dissect_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> None + self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) + + def _uper_encode_into(self, enc, pkt, value=None): + # type: (UPER_Encoder, 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) @@ -1316,6 +1436,10 @@ def i2m(self, # type: (...) -> bytes if x is None: s = b"" + elif pkt.ASN1_codec == ASN1_Codecs.PER: + enc = UPER_Encoder() + self._uper_encode_into(enc, pkt, x) + s = enc.as_bytes() elif isinstance(x, bytes): s = x elif isinstance(x, ASN1_Object): @@ -1328,6 +1452,8 @@ def i2m(self, if not hasattr(x, "ASN1_root"): # A normal Packet (!= ASN1) return s + if pkt.ASN1_codec == ASN1_Codecs.PER: + return s return BER_tagging_enc(s, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag) @@ -1403,6 +1529,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 @@ -1411,7 +1539,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): From b18b39e26363c614ae859de1862ed0f170cfab4d Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 3 Jul 2026 08:16:21 +0200 Subject: [PATCH 07/15] more tests AI-Assisted: yes (Cursor AI) --- scapy/asn1/uper.py | 46 +++- scapy/asn1fields.py | 92 ++++++-- test/scapy/layers/asn1_build_tests.py | 183 ++++++++++++++++ test/scapy/layers/asn1_dissect_tests.py | 278 ++++++++++++++++++++++++ test/scapy/layers/ber_packets.py | 150 +++++++++++++ 5 files changed, 727 insertions(+), 22 deletions(-) create mode 100644 test/scapy/layers/asn1_build_tests.py create mode 100644 test/scapy/layers/asn1_dissect_tests.py create mode 100644 test/scapy/layers/ber_packets.py diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index f6ae7de82e0..6fe1c5cc816 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -119,6 +119,19 @@ 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 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 @@ -366,6 +379,15 @@ 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: () -> int value = self.read_non_negative_binary_integer(8) @@ -415,6 +437,13 @@ def UPER_constrained_int_dec(s, minimum, maximum): 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 @@ -667,9 +696,17 @@ def encode_into(cls, 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: @@ -682,13 +719,16 @@ def dec_from_decoder(cls, 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 = dec.read_non_negative_binary_integer( - UPER_bits_for_range(maximum - minimum) - ) + minimum + value = UPER_constrained_int_dec_from_decoder(dec, minimum, maximum) else: value = dec.read_unconstrained_whole_number() return cls.asn1_object(value) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index a30f9d7f8d9..0cabb361db4 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -333,6 +333,11 @@ def _uper_codec_kwargs(self, size_len=None): "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 return kwargs @@ -396,6 +401,29 @@ 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) @@ -581,16 +609,17 @@ class ASN1F_SEQUENCE(ASN1F_field[List[Any], List[Any]]): def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None - self.uper_extensible = kwargs.pop("uper_extensible", False) + 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) + f for f in seq if isinstance(f, (ASN1F_optional, ASN1F_DEFAULT)) ) def __repr__(self): @@ -701,9 +730,9 @@ def _uper_dissect_from_decoder(self, pkt, dec): presence = [dec.read_bit() for _ in self._optionals] opt_idx = 0 for obj in self.seq: - if isinstance(obj, ASN1F_optional): + if isinstance(obj, (ASN1F_optional, ASN1F_DEFAULT)): if not presence[opt_idx]: - obj.set_val(pkt, None) + obj.set_absent(pkt) opt_idx += 1 continue opt_idx += 1 @@ -738,7 +767,7 @@ def _uper_encode_into(self, enc, pkt, value=None): 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) and obj.is_empty(pkt): + if isinstance(obj, (ASN1F_optional, ASN1F_DEFAULT)) and obj.is_empty(pkt): continue obj._uper_encode_into(enc, pkt) @@ -835,7 +864,10 @@ def _extract_packet_from_decoder(self, dec, pkt): def m2i_from_decoder(self, pkt, dec): # type: (ASN1_Packet, UPER_Decoder) -> List[Any] - count, _ = UPER_count_dec(b"", dec=dec) + 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) @@ -865,22 +897,18 @@ def _uper_encode_into(self, enc, pkt, value=None): enc.append_length_determinant(count) for item in value: if self.holds_packets: - item_enc = UPER_Encoder() cast("ASN1_Packet", item).ASN1_root._uper_encode_into( - item_enc, item, + enc, item, ) - UPER_append_encoded(enc, item_enc.as_bytes()) else: self.fld._uper_encode_into(enc, pkt, item) return self._uper_count_enc(enc, count) for item in value: if self.holds_packets: - item_enc = UPER_Encoder() cast("ASN1_Packet", item).ASN1_root._uper_encode_into( - item_enc, item, + enc, item, ) - UPER_append_encoded(enc, item_enc.as_bytes()) else: self.fld._uper_encode_into(enc, pkt, item) @@ -960,12 +988,7 @@ def build(self, pkt): else: if pkt.ASN1_codec == ASN1_Codecs.PER: enc = UPER_Encoder() - enc.append_length_determinant(len(val)) - for item in val: - if self.holds_packets: - UPER_append_encoded(enc, bytes(item)) - else: - self.fld._uper_encode_into(enc, pkt, item) + self._uper_encode_into(enc, pkt, val) s = enc.as_bytes() elif self.holds_packets: s = b"".join(bytes(i) for i in val) @@ -1067,6 +1090,10 @@ 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 val = getattr(pkt, self._field.name, None) @@ -1081,6 +1108,32 @@ def _uper_encode_into(self, enc, pkt, value=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. @@ -1112,7 +1165,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) - self.uper_extensible = kwargs.pop("uper_extensible", False) + uper_extensible = kwargs.pop("uper_extensible", False) self.implicit_tag = None for kwarg in ["context", "explicit_tag"]: setattr(self, kwarg, kwargs.get(kwarg)) @@ -1120,6 +1173,7 @@ 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] diff --git a/test/scapy/layers/asn1_build_tests.py b/test/scapy/layers/asn1_build_tests.py new file mode 100644 index 00000000000..e6c7ad1921c --- /dev/null +++ b/test/scapy/layers/asn1_build_tests.py @@ -0,0 +1,183 @@ +# 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). +""" + +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_dissect_tests.py b/test/scapy/layers/asn1_dissect_tests.py new file mode 100644 index 00000000000..9960e3d70e6 --- /dev/null +++ b/test/scapy/layers/asn1_dissect_tests.py @@ -0,0 +1,278 @@ +# 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). +""" + +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_packets.py b/test/scapy/layers/ber_packets.py new file mode 100644 index 00000000000..aad50722c11 --- /dev/null +++ b/test/scapy/layers/ber_packets.py @@ -0,0 +1,150 @@ +# 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, oer_unsigned=True), + 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), + ) + + +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_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] == [] From 752ae53d44bcefde21450f3e481e83150cc759f8 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 3 Jul 2026 08:20:06 +0200 Subject: [PATCH 08/15] more tests AI-Assisted: yes (Cursor AI) --- test/scapy/layers/asn1.uts | 52 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 8f94db57923..763d6b78650 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -256,6 +256,58 @@ __import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_choice']) = OER packet record __import__('test.scapy.layers.oer_packets', fromlist=['check_oer_packet_record']).check_oer_packet_record() ++ ASN.1 BER packets and fields += BER field explicit tag +__import__('test.scapy.layers.ber_packets', fromlist=['check_ber_field_explicit_tag']).check_ber_field_explicit_tag() += BER field fixed size +__import__('test.scapy.layers.ber_packets', fromlist=['check_ber_field_fixed_size']).check_ber_field_fixed_size() += BER field optional +__import__('test.scapy.layers.ber_packets', fromlist=['check_ber_field_optional']).check_ber_field_optional() += BER field sequence of +__import__('test.scapy.layers.ber_packets', fromlist=['check_ber_field_sequence_of']).check_ber_field_sequence_of() += BER field choice +__import__('test.scapy.layers.ber_packets', fromlist=['check_ber_field_choice']).check_ber_field_choice() += BER packet record +__import__('test.scapy.layers.ber_packets', fromlist=['check_ber_packet_record']).check_ber_packet_record() + ++ 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" From 453c232b4262dc74503087ccf9e0537cb3231a9a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 3 Jul 2026 08:35:48 +0200 Subject: [PATCH 09/15] fix flake8 AI-Assisted: yes (Cursor AI) --- scapy/asn1fields.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 0cabb361db4..42874a56f57 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -45,12 +45,10 @@ UPER_Decoding_Error, UPER_Decoder, UPER_Encoder, - UPER_append_encoded, UPER_bits_for_range, UPER_choice_index_dec, UPER_choice_index_enc, UPER_constrained_int_enc, - UPER_count_dec, UPER_has_unexpected_remainder, ) from scapy.base_classes import BasePacket From f6b133bc41924ad2e9f9fdfe25e75a2ad4bb4b0e Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 3 Jul 2026 09:03:06 +0200 Subject: [PATCH 10/15] increase coverage AI-Assisted: yes (Cursor AI) --- test/scapy/layers/asn1.uts | 14 + test/scapy/layers/asn1_coverage.py | 550 ++++++++++++++++++++++++++++- 2 files changed, 563 insertions(+), 1 deletion(-) diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 763d6b78650..ec5e835ff21 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -517,3 +517,17 @@ __import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_encaps __import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_choice_and_special']).check_asn1fields_choice_and_special() = asn1fields optional dissect __import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_optional_dissect']).check_asn1fields_optional_dissect() += asn1fields default and omit +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_default_and_omit']).check_asn1fields_default_and_omit() += asn1fields extensible PER +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_extensible_per']).check_asn1fields_extensible_per() += asn1fields sequence of advanced +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_sequence_of_advanced']).check_asn1fields_sequence_of_advanced() += asn1fields choice advanced +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_choice_advanced']).check_asn1fields_choice_advanced() += asn1fields enum bitstring and flags +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_enum_bitstring_and_flags']).check_asn1fields_enum_bitstring_and_flags() += asn1fields packet and sequence errors +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_packet_and_sequence_errors']).check_asn1fields_packet_and_sequence_errors() += asn1fields more coverage +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_more_coverage']).check_asn1fields_more_coverage() diff --git a/test/scapy/layers/asn1_coverage.py b/test/scapy/layers/asn1_coverage.py index d03de639f0a..ef8168fd604 100644 --- a/test/scapy/layers/asn1_coverage.py +++ b/test/scapy/layers/asn1_coverage.py @@ -17,14 +17,18 @@ def _raises(exc, func): 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.asn1.oer import ( OER_Decoding_Error, OER_Encoding_Error, @@ -45,23 +49,31 @@ def _raises(exc, func): 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 +from scapy.packet import Raw, raw class _InnerRecord(ASN1_Packet): @@ -342,3 +354,539 @@ class _BerChoiceRecord(ASN1_Packet): 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), + ) + + _raises( + OER_Decoding_Error, + lambda: _OerSeq.ASN1_root.m2i(_OerSeq(), b"\x01\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) + From df2ae605d9050499886eab07318701c9c9245e2e Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 3 Jul 2026 11:31:24 +0200 Subject: [PATCH 11/15] Code cleanups AI-Assisted: yes (Sonnet 5) --- scapy/asn1/ber.py | 3 +- scapy/asn1/uper.py | 37 +++++++++-------- scapy/asn1fields.py | 97 ++++++++++++++++----------------------------- 3 files changed, 56 insertions(+), 81 deletions(-) diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index 1f9a9e75463..2be68edfef5 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -11,6 +11,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, orb, bytes_encode from scapy.utils import binrepr, inet_aton, inet_ntoa @@ -257,7 +258,6 @@ def BER_tagging_dec(s, # type: bytes def BER_tagging_enc(s, implicit_tag=None, explicit_tag=None): # type: (bytes, Optional[int], Optional[int]) -> bytes - from scapy.config import conf if len(s) > 0: if implicit_tag is not None: s = BER_id_enc(implicit_tag) + s[1:] @@ -632,7 +632,6 @@ class BERcodec_SEQUENCE(BERcodec_Object[Union[bytes, List[BERcodec_Object[Any]]] @classmethod def enc(cls, _ll, size_len=0): # type: (Union[bytes, List[BERcodec_Object[Any]]], Optional[int]) -> bytes - from scapy.config import conf if isinstance(_ll, bytes): ll = _ll else: diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 6fe1c5cc816..3d6c1c780f6 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -668,6 +668,13 @@ def enc(cls, s, size_len=0, uper_min=None, uper_max=None): ) +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() + + ASN1_Codecs.PER.register_stem(UPERcodec_Object) @@ -736,9 +743,9 @@ def dec_from_decoder(cls, @classmethod def enc(cls, i, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): # type: (int, Optional[int], Optional[int], Optional[int], bool) -> bytes - enc = UPER_Encoder() - cls.encode_into(enc, i, size_len, uper_min, uper_max, oer_unsigned) - return enc.as_bytes() + return _uper_enc_via_encode_into( + cls, i, size_len, uper_min, uper_max, oer_unsigned, + ) @classmethod def do_dec(cls, @@ -788,9 +795,9 @@ def dec_from_decoder(cls, @classmethod def enc(cls, i, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): # type: (int, Optional[int], Optional[int], Optional[int], bool) -> bytes - enc = UPER_Encoder() - cls.encode_into(enc, i, size_len, uper_min, uper_max, oer_unsigned) - return enc.as_bytes() + return _uper_enc_via_encode_into( + cls, i, size_len, uper_min, uper_max, oer_unsigned, + ) @classmethod def do_dec(cls, @@ -896,9 +903,9 @@ def dec_from_decoder(cls, @classmethod def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): # type: (Any, Optional[int], Optional[int], Optional[int], bool) -> bytes - enc = UPER_Encoder() - cls.encode_into(enc, _s, size_len, uper_min, uper_max, oer_unsigned) - return enc.as_bytes() + return _uper_enc_via_encode_into( + cls, _s, size_len, uper_min, uper_max, oer_unsigned, + ) @classmethod def do_dec(cls, @@ -973,9 +980,9 @@ def dec_from_decoder(cls, @classmethod def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): # type: (Union[str, bytes], Optional[int], Optional[int], Optional[int], bool) -> bytes # noqa: E501 - enc = UPER_Encoder() - cls.encode_into(enc, _s, size_len, uper_min, uper_max, oer_unsigned) - return enc.as_bytes() + return _uper_enc_via_encode_into( + cls, _s, size_len, uper_min, uper_max, oer_unsigned, + ) @classmethod def do_dec(cls, @@ -1181,12 +1188,10 @@ def enc(cls, uper_enum_values=None, ): # type: (int, Optional[int], Optional[int], Optional[int], bool, Optional[List[int]]) -> bytes # noqa: E501 - enc = UPER_Encoder() - cls.encode_into( - enc, i, size_len, uper_min, uper_max, oer_unsigned, + return _uper_enc_via_encode_into( + cls, i, size_len, uper_min, uper_max, oer_unsigned, uper_enum_values=uper_enum_values, ) - return enc.as_bytes() @classmethod def do_dec(cls, diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 42874a56f57..558f288dc79 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -143,11 +143,21 @@ 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 self.owners.append(cls) + def _apply_diff_tag(self, diff_tag): + # type: (Optional[int]) -> None + # this implies that flexible_tag was True + 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 + def i2repr(self, pkt, x): # type: (ASN1_Packet, _I) -> str return repr(x) @@ -184,12 +194,7 @@ def m2i(self, pkt, s): _fname=self.name) else: diff_tag = None - if diff_tag is not None: - # this implies that flexible_tag was True - if self.implicit_tag is not None: - self.implicit_tag = diff_tag - elif self.explicit_tag is not None: - self.explicit_tag = diff_tag + self._apply_diff_tag(diff_tag) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) codec_kwargs = {} # type: Dict[str, Any] if pkt.ASN1_codec == ASN1_Codecs.OER: @@ -325,6 +330,11 @@ def 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, @@ -338,6 +348,8 @@ def _uper_codec_kwargs(self, size_len=None): 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 _encode_item(self, pkt, item): @@ -653,11 +665,7 @@ def _apply_tagging_dec(self, s, pkt): safe=self.flexible_tag, _fname=pkt.name, ) - 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 + self._apply_diff_tag(diff_tag) return s def _dissect_sequence_children(self, pkt, s): @@ -872,10 +880,6 @@ def m2i_from_decoder(self, pkt, dec): lst.append(item) return lst - def dissect_from_decoder(self, pkt, dec): - # type: (ASN1_Packet, UPER_Decoder) -> None - self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) - def _uper_encode_into(self, enc, pkt, value=None): # type: (UPER_Encoder, ASN1_Packet, Any) -> None if value is None: @@ -920,11 +924,7 @@ def m2i(self, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, safe=self.flexible_tag) - 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 + self._apply_diff_tag(diff_tag) count, s = OER_unsigned_integer_dec(s) lst = [] for _ in range(count): @@ -953,11 +953,7 @@ def m2i(self, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, safe=self.flexible_tag) - 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 + self._apply_diff_tag(diff_tag) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) i, s, remain = codec.check_type_check_len(s) lst = [] @@ -1185,27 +1181,16 @@ def __init__(self, name, default, *args, **kwargs): if hasattr(p.ASN1_root, "choices"): root = cast(ASN1F_CHOICE, p.ASN1_root) for k in root.choice_order: - self.choices[k] = root.choices[k] - self.choice_order.append(k) - self.choice_list.append(root.choices[k]) + self._register_choice(k, root.choices[k]) else: - tag = p.ASN1_root.network_tag - self.choices[tag] = p - self.choice_order.append(tag) - self.choice_list.append(p) + self._register_choice(p.ASN1_root.network_tag, p) elif hasattr(p, "ASN1_tag"): if isinstance(p, type): # should be ASN1F_field class - tag = int(p.ASN1_tag) - self.choices[tag] = p - self.choice_order.append(tag) - self.choice_list.append(p) + self._register_choice(int(p.ASN1_tag), p) else: # should be ASN1F_field instance - tag = p.network_tag - self.choices[tag] = p - self.choice_order.append(tag) - self.choice_list.append(p) + self._register_choice(p.network_tag, p) if hasattr(p, "cls"): self.pktchoices[hash(p.cls)] = (p.implicit_tag, p.explicit_tag) # noqa: E501 else: @@ -1214,6 +1199,12 @@ def __init__(self, name, default, *args, **kwargs): 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"): @@ -1280,17 +1271,8 @@ def m2i(self, pkt, s): def _choice_tag_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 self.choice_order[index] - elif isinstance(choice, type) and hasattr(choice, "ASN1_tag"): - if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: - return self.choice_order[index] - elif hasattr(choice, "ASN1_tag"): - if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: - return self.choice_order[index] - return None + 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] @@ -1298,9 +1280,6 @@ def _choice_index_for(self, x): if isinstance(choice, type) and hasattr(choice, "ASN1_root"): if isinstance(x, choice): return index - elif isinstance(choice, type) and hasattr(choice, "ASN1_tag"): - if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: - return index elif hasattr(choice, "ASN1_tag"): if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: return index @@ -1447,10 +1426,6 @@ def m2i_from_decoder(self, pkt, dec): p.ASN1_root.dissect_from_decoder(p, dec) return p - def dissect_from_decoder(self, pkt, dec): - # type: (ASN1_Packet, UPER_Decoder) -> None - self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) - def _uper_encode_into(self, enc, pkt, value=None): # type: (UPER_Encoder, ASN1_Packet, Any) -> None if value is None: @@ -1472,11 +1447,7 @@ def m2i(self, pkt, s): explicit_tag=self.explicit_tag, safe=self.flexible_tag, _fname=self.name) - 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 + self._apply_diff_tag(diff_tag) if not s: return None, s return self.extract_packet(cls, s, _underlayer=pkt) From 21706dc1940aaa140c4a111fe86834ab1a599985 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 7 Jul 2026 15:55:03 +0900 Subject: [PATCH 12/15] Remove asn1tools test dependency in favor of hardcoded reference vectors. AI-Assisted: yes (Cursor AI) --- .config/ci/install.ps1 | 3 - .config/ci/install.sh | 3 - test/scapy/layers/asn1.uts | 52 +++--- test/scapy/layers/oer_iop.py | 219 +++++++++----------------- test/scapy/layers/uper_asn1scc_iop.py | 111 +++---------- test/scapy/layers/uper_codec.py | 118 +++++++------- test/scapy/layers/uper_iop.py | 156 ++++++------------ test/scapy/layers/uper_packets.py | 2 +- tox.ini | 1 - 9 files changed, 217 insertions(+), 448 deletions(-) diff --git a/.config/ci/install.ps1 b/.config/ci/install.ps1 index 09fbab8969f..5f9e5b58443 100644 --- a/.config/ci/install.ps1 +++ b/.config/ci/install.ps1 @@ -19,6 +19,3 @@ python -m pip install --upgrade pip setuptools wheel --ignore-installed # Make sure tox is installed and up to date python -m pip install -U tox --ignore-installed - -# Optional test dependency for ASN.1 OER interoperability tests -python -m pip install asn1tools diff --git a/.config/ci/install.sh b/.config/ci/install.sh index c22855a36a7..aa96f1a99b5 100755 --- a/.config/ci/install.sh +++ b/.config/ci/install.sh @@ -53,8 +53,5 @@ python -m pip install --upgrade pip setuptools wheel --ignore-installed # Make sure tox is installed and up to date python -m pip install -U tox --ignore-installed -# Optional test dependency for ASN.1 OER interoperability tests -python -m pip install asn1tools - # Dump Environment (so that we can check PATH, UT_FLAGS, etc.) set diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index ec5e835ff21..2918fa3836f 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -207,14 +207,11 @@ 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 (asn1tools) -= asn1tools availability probe -from test.scapy.layers.oer_iop import require_asn1tools -HAS_ASN1TOOLS_OER = require_asn1tools() -= primitive encode interop with asn1tools -(not HAS_ASN1TOOLS_OER) or __import__('test.scapy.layers.oer_iop', fromlist=['check_primitive_interop']).check_primitive_interop() -= scapy encode asn1tools decode -(not HAS_ASN1TOOLS_OER) or __import__('test.scapy.layers.oer_iop', fromlist=['check_asn1tools_decode_scapy_encode']).check_asn1tools_decode_scapy_encode() ++ 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 @@ -342,19 +339,16 @@ x.val == 1 and r == b"" ASN1_INTEGER(42).enc(ASN1_Codecs.PER) == b"\x01*" = UPER codec registration ASN1_Class_UNIVERSAL.INTEGER.get_codec(ASN1_Codecs.PER) is UPERcodec_INTEGER -= asn1tools availability probe for UPER -from test.scapy.layers.uper_iop import require_asn1tools -HAS_ASN1TOOLS_UPER = require_asn1tools() + 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 asn1tools decode interop -(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_asn1tools_decode']).check_uper_codec_asn1tools_decode() -= UPER codec scapy encode asn1tools decode -(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_scapy_decode_asn1tools']).check_uper_codec_scapy_decode_asn1tools() += 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 -(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_oid_encode_interop']).check_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() @@ -380,25 +374,23 @@ __import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_join_encoding = 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 (asn1tools) -= UPER primitive encode interop with asn1tools -(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_iop', fromlist=['check_primitive_interop']).check_primitive_interop() -= UPER composite encode interop with asn1tools -(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_iop', fromlist=['check_composite_interop']).check_composite_interop() -= UPER composite decode interop with asn1tools -(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_iop', fromlist=['check_composite_decode_interop']).check_composite_decode_interop() -= UPER packet asn1tools interop -(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_iop', fromlist=['check_packet_asn1tools_interop']).check_packet_asn1tools_interop() ++ 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 with asn1tools -(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_vectors']).check_asn1scc_vectors() -= asn1scc README Message uPER reference (asn1tools) -(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_readme_message_reference']).check_asn1scc_readme_message_reference() += 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 -(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_readme_message_prefix']).check_asn1scc_readme_message_prefix() +__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 diff --git a/test/scapy/layers/oer_iop.py b/test/scapy/layers/oer_iop.py index 48088153047..e2679777086 100644 --- a/test/scapy/layers/oer_iop.py +++ b/test/scapy/layers/oer_iop.py @@ -5,20 +5,10 @@ """ OER interoperability helpers. -Cross-check Scapy's OER codec against asn1tools when available. -asn1tools is an optional test dependency (pip install asn1tools). +Cross-check Scapy's OER codec against reference encodings (from asn1tools). Reference vectors are taken from asn1tools/tests/test_oer.py. """ -from typing import Any, Callable, List, Optional, Tuple - -try: - import asn1tools - HAS_ASN1TOOLS = True -except ImportError: - asn1tools = None # type: ignore - HAS_ASN1TOOLS = False - from scapy.asn1.oer import ( OERcodec_BIT_STRING, OERcodec_BOOLEAN, @@ -27,177 +17,119 @@ OERcodec_NULL, OERcodec_OID, OERcodec_STRING, - OER_len_dec, - OER_len_enc, - OER_signed_integer_dec, OER_signed_integer_enc, - OER_unsigned_integer_dec, OER_unsigned_integer_enc, ) - -ASN1TOOLS_INTEGER_SPEC = ( - "Foo DEFINITIONS AUTOMATIC TAGS ::= " - "BEGIN " - "A ::= INTEGER " - "B ::= INTEGER (-128..127) " - "C ::= INTEGER (-32768..32767) " - "D ::= INTEGER (-2147483648..2147483647) " - "E ::= INTEGER (-9223372036854775808..9223372036854775807) " - "F ::= INTEGER (0..255) " - "G ::= INTEGER (0..65535) " - "H ::= INTEGER (0..4294967295) " - "I ::= INTEGER (0..18446744073709551615) " - "J ::= INTEGER (0..18446744073709551616) " - "K ::= INTEGER (1..MAX) " - "L ::= INTEGER (MIN..0) " - "END" -) - -# (asn1tools type, value, scapy encoder callable) +# (type name, value, scapy encoder callable, reference encoding) INTEGER_VECTORS = [ - ("A", 0, lambda v: OERcodec_INTEGER.enc(v)), - ("A", 128, lambda v: OERcodec_INTEGER.enc(v)), - ("A", 100000, lambda v: OERcodec_INTEGER.enc(v)), - ("A", -255, lambda v: OERcodec_INTEGER.enc(v)), - ("A", -1234567, lambda v: OERcodec_INTEGER.enc(v)), - ("B", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=1)), - ("C", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=2)), - ("D", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=4)), - ("E", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=8)), - ("F", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=1)), - ("G", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=2)), - ("G", 1000, lambda v: OERcodec_INTEGER.enc(v, size_len=2)), - ("H", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=4)), - ("I", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=8)), - ("B", 1, lambda v: OERcodec_INTEGER.enc(v, size_len=1)), - ("K", 1, lambda v: OER_unsigned_integer_enc(v)), - ("K", 128, lambda v: OER_unsigned_integer_enc(v)), - ("L", -128, lambda v: OER_signed_integer_enc(v)), + ("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)), - (False, lambda v: OERcodec_BOOLEAN.enc(1 if v else 0)), + (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 ::= ENUMERATED { a(1) }", "A", "a", 1), - ("B ::= ENUMERATED { a(128) }", "B", "a", 128), - ("C ::= ENUMERATED { a(0), b(127) }", "C", "a", 0), - ("C ::= ENUMERATED { a(0), b(127) }", "C", "b", 127), - ("E ::= ENUMERATED { a(-1), b(1234) }", "E", "a", -1), + ("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)), - ("1.2.3321", lambda v: OERcodec_OID.enc(v)), + ("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"\x12\x34\x56", 3), + (b"\x12\x34", 0, b"\x02\x124"), + (b"\x12\x34\x56", 3, b"\x124V"), ] BIT_STRING_VECTORS = [ - # (asn1 value, scapy bit string) - ((b"\x40", 4), "0100"), - ((b"\x41", 8), "01000001"), + ("0100", b"\x02\x04@"), + ("01000001", b"\x02\x00A"), ] - -def require_asn1tools(): - # type: () -> bool - """Return True if asn1tools is available for interoperability tests.""" - return HAS_ASN1TOOLS - - -def _compile(spec_body): - # type: (str) -> Any - assert asn1tools is not None - spec = "Foo DEFINITIONS AUTOMATIC TAGS ::= BEGIN %s END" % spec_body - return asn1tools.compile_string(spec, "oer") - - -def _assert_encode_match(spec_body, type_name, value, scapy_enc): - # type: (str, str, Any, Callable[..., bytes]) -> None - compiled = _compile(spec_body) - expected = compiled.encode(type_name, value) - got = scapy_enc(value) - assert got == expected, ( - "OER encode mismatch for %s=%r: asn1tools=%r scapy=%r" % - (type_name, value, expected, got) - ) - - -def _assert_decode_match(type_name, encoded, scapy_dec, expected_value): - # type: (str, bytes, Callable[[bytes], Tuple[Any, bytes]], Any) -> None - obj, remain = scapy_dec(encoded) - assert remain == b"", "unexpected remainder after decode of %s" % type_name - assert obj.val == expected_value, ( - "OER decode mismatch for %s: got %r expected %r" % - (type_name, obj.val, expected_value) - ) +# (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 asn1tools. Returns True on success.""" - if not HAS_ASN1TOOLS: - return True - - compiled = asn1tools.compile_string(ASN1TOOLS_INTEGER_SPEC, "oer") - for type_name, value, enc in INTEGER_VECTORS: - expected = compiled.encode(type_name, value) + """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: asn1tools=%r scapy=%r" % + "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 - bool_spec = _compile("A ::= BOOLEAN") - for value, enc in BOOLEAN_VECTORS: - expected = bool_spec.encode("A", 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) - _assert_encode_match("A ::= NULL", "A", None, lambda _: OERcodec_NULL.enc(None)) + got = OERcodec_NULL.enc(None) + assert got == b"" - for spec_body, type_name, enum_name, enum_val in ENUMERATED_VECTORS: - compiled = _compile(spec_body) - expected = compiled.encode(type_name, enum_name) + 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 - oid_spec = _compile("A ::= OBJECT IDENTIFIER") - for oid, enc in OID_VECTORS: - expected = oid_spec.encode("A", oid) + 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 - octet_spec = _compile( - "A ::= OCTET STRING\nB ::= OCTET STRING (SIZE (3))" - ) - for data, fixed_size in OCTET_STRING_VECTORS: - type_name = "B" if fixed_size else "A" - expected = octet_spec.encode(type_name, data) + 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 - bit_spec = _compile("A ::= BIT STRING") - for (data, nbits), bitstr in BIT_STRING_VECTORS: - expected = bit_spec.encode("A", (data, nbits)) + for bitstr, expected in BIT_STRING_VECTORS: got = OERcodec_BIT_STRING.enc(bitstr) assert got == expected dec, remain = OERcodec_BIT_STRING.do_dec(got) @@ -206,26 +138,23 @@ def check_primitive_interop(): return True -def check_asn1tools_decode_scapy_encode(): +def check_scapy_encode_reference_decode(): # type: () -> bool - """Encode with Scapy, decode with asn1tools.""" - if not HAS_ASN1TOOLS: - return True - - compiled = asn1tools.compile_string(ASN1TOOLS_INTEGER_SPEC, "oer") - samples = [("A", 42), ("F", 200), ("B", -99)] - for type_name, value in samples: - encoded = { - "A": OERcodec_INTEGER.enc, - "F": lambda v: OERcodec_INTEGER.enc(v, size_len=1), - "B": lambda v: OERcodec_INTEGER.enc(v, size_len=1), - }[type_name](value) - decoded = compiled.decode(type_name, encoded) - assert decoded == value - - bool_spec = _compile("A ::= BOOLEAN") + """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) - assert bool_spec.decode("A", encoded) == bool(val) + dec, remain = OERcodec_BOOLEAN.do_dec(encoded) + assert remain == b"" and dec.val == val return True diff --git a/test/scapy/layers/uper_asn1scc_iop.py b/test/scapy/layers/uper_asn1scc_iop.py index f35e4b59440..1290b4409e8 100644 --- a/test/scapy/layers/uper_asn1scc_iop.py +++ b/test/scapy/layers/uper_asn1scc_iop.py @@ -10,18 +10,9 @@ ``--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 asn1tools reference only). +compared against Scapy encoders here (or are reference-only). """ -from typing import Any, List, Tuple - -try: - import asn1tools - HAS_ASN1TOOLS = True -except ImportError: - asn1tools = None # type: ignore - HAS_ASN1TOOLS = False - from scapy.asn1.uper import ( UPER_Encoder, UPER_choice_index_enc, @@ -86,109 +77,70 @@ "END" ) -# asn1scc README.md sample.asn (REAL field; asn1tools reference only) -README_MESSAGE_SPEC = ( - "Sample DEFINITIONS AUTOMATIC TAGS ::= BEGIN " - "Message ::= SEQUENCE { " - "msgId INTEGER, " - "myflag INTEGER, " - "value REAL, " - "szDescription OCTET STRING (SIZE(10)), " - "isReady BOOLEAN " - "} " - "END" -) - +# asn1scc README.md sample.asn (REAL field; reference only) README_MESSAGE_HEX = ( "010101020980cd191eb851eb851f48656c6c6f576f726c6480" ) -README_MESSAGE_PREFIX_SPEC = ( - "Sample DEFINITIONS AUTOMATIC TAGS ::= BEGIN " - "MessagePrefix ::= SEQUENCE { " - "msgId INTEGER, " - "myflag INTEGER, " - "szDescription OCTET STRING (SIZE(10)), " - "isReady BOOLEAN " - "} " - "END" -) - README_MESSAGE_PREFIX_HEX = ( "0101010248656c6c6f576f726c6480" ) +# (name, pdu value, encoder callable, reference encoding) ASN1SCC_VECTORS = [ ( "05-BOOLEAN/001 pdu1", - BOOLEAN_SPEC, - "MyPDU", True, lambda _v: UPERcodec_BOOLEAN.enc(1), + b"\x80", ), ( "18-NULL/001 pdu1", - NULL_SPEC, - "MyPDU", None, lambda _v: UPERcodec_NULL.enc(None), + b"", ), ( "06-OCTET-STRING/001 pdu1", - OCTET_STRING_VAR_SPEC, - "MyPDU", bytes.fromhex("afbc4583"), lambda v: UPERcodec_STRING.enc(v, uper_min=1, uper_max=20), + bytes.fromhex("1d7de22c18"), ), ( "05-BOOLEAN/001 pdu1 false", - BOOLEAN_SPEC, - "MyPDU", False, lambda _v: UPERcodec_BOOLEAN.enc(0), + b"\x00", ), ( "04-ENUMERATED/001 pdu1 alpha", - ENUMERATED_SPEC, - "MyPDU", "alpha", - lambda _v: UPERcodec_ENUMERATED.enc( - 1, uper_enum_values=[1, 200], - ), + lambda _v: UPERcodec_ENUMERATED.enc(1, uper_enum_values=[1, 200]), + b"\x00", ), ( "04-ENUMERATED/001 pdu1 beta", - ENUMERATED_SPEC, - "MyPDU", "beta", - lambda _v: UPERcodec_ENUMERATED.enc( - 200, uper_enum_values=[1, 200], - ), + lambda _v: UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), + b"\x80", ), ( "09-CHOICE/001 pdu1 int1:10", - CHOICE_SPEC, - "MyPDU", ("int1", 10), lambda _v: _encode_choice_int1_10(), + b"\x14", ), ( "08-BIT-STRING/001 pdu1 ABCD", - BIT_STRING_VAR_SPEC, - "MyPDU", (bytes.fromhex("abcd"), 16), lambda _v: UPERcodec_BIT_STRING.enc( (bytes.fromhex("abcd"), 16), uper_min=1, uper_max=20, ), + bytes.fromhex("7d5e68"), ), ] -def require_asn1tools(): - # type: () -> bool - return HAS_ASN1TOOLS - - def _encode_choice_int1_10(): # type: () -> bytes enc = UPER_Encoder() @@ -199,12 +151,8 @@ def _encode_choice_int1_10(): def check_asn1scc_vectors(): # type: () -> None - if not HAS_ASN1TOOLS: - raise RuntimeError("asn1tools is not installed") - for name, spec, pdu, value, encoder in ASN1SCC_VECTORS: - foo = asn1tools.compile_string(spec, "uper") - expected = foo.encode(pdu, value) - got = encoder(value) + for name, _value, encoder, expected in ASN1SCC_VECTORS: + got = encoder(_value) assert got == expected, ( "%s: expected %s, got %s" % (name, expected.hex(), got.hex()) @@ -213,21 +161,11 @@ def check_asn1scc_vectors(): def check_asn1scc_readme_message_prefix(): # type: () -> None - """README sample without REAL; Scapy packet roundtrip vs asn1tools.""" - if not HAS_ASN1TOOLS: - raise RuntimeError("asn1tools is not installed") + """README sample without REAL; Scapy packet roundtrip vs reference.""" from test.scapy.layers.uper_packets import UPERMessagePrefix from scapy.packet import raw - foo = asn1tools.compile_string(README_MESSAGE_PREFIX_SPEC, "uper") - value = { - "msgId": 1, - "myflag": 2, - "szDescription": b"HelloWorld", - "isReady": True, - } - expected = foo.encode("MessagePrefix", value) - assert expected.hex() == README_MESSAGE_PREFIX_HEX + expected = bytes.fromhex(README_MESSAGE_PREFIX_HEX) pkt = UPERMessagePrefix( msgId=1, @@ -247,15 +185,6 @@ def check_asn1scc_readme_message_prefix(): def check_asn1scc_readme_message_reference(): # type: () -> None """README C sample output; Scapy does not encode REAL in UPER yet.""" - if not HAS_ASN1TOOLS: - raise RuntimeError("asn1tools is not installed") - foo = asn1tools.compile_string(README_MESSAGE_SPEC, "uper") - value = { - "msgId": 1, - "myflag": 2, - "value": 3.14, - "szDescription": b"HelloWorld", - "isReady": True, - } - got = foo.encode("Message", value) - assert got.hex() == README_MESSAGE_HEX + assert README_MESSAGE_HEX == ( + "010101020980cd191eb851eb851f48656c6c6f576f726c6480" + ) diff --git a/test/scapy/layers/uper_codec.py b/test/scapy/layers/uper_codec.py index d834763c290..40c6e0b56ff 100644 --- a/test/scapy/layers/uper_codec.py +++ b/test/scapy/layers/uper_codec.py @@ -6,14 +6,7 @@ UPER primitive codec roundtrip and decode interoperability tests. """ -from typing import Any, Dict, List, Tuple, Type - -try: - import asn1tools - HAS_ASN1TOOLS = True -except ImportError: - asn1tools = None # type: ignore - HAS_ASN1TOOLS = False +from typing import Any, Dict, Tuple, Type from scapy.asn1.uper import ( UPERcodec_BIT_STRING, @@ -74,45 +67,56 @@ Type[Any], Dict[str, Any], Any, + bytes, ] DECODE_VECTORS = [ - ("A", True, UPERcodec_BOOLEAN, {}, 1), - ("A", False, UPERcodec_BOOLEAN, {}, 0), - ("B", 42, UPERcodec_INTEGER, {}, 42), - ("B", -1, UPERcodec_INTEGER, {}, -1), - ("C", 200, UPERcodec_INTEGER, {"uper_min": 0, "uper_max": 255}, 200), - ("Signed", -1, UPERcodec_INTEGER, {"uper_min": -128, "uper_max": 127}, -1), - ("Signed", 127, UPERcodec_INTEGER, {"uper_min": -128, "uper_max": 127}, 127), - ("D", b"AB", UPERcodec_STRING, {}, b"AB"), - ("E", b"\x12\x34\x56", UPERcodec_STRING, {"size_len": 3}, b"\x12\x34\x56"), - ("G", None, UPERcodec_NULL, {}, None), + ("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"), ( - "H", - "alpha", - UPERcodec_ENUMERATED, - {"uper_enum_values": [1, 200]}, - 1, + "C", + 200, + UPERcodec_INTEGER, + {"uper_min": 0, "uper_max": 255}, + 200, + b"\xc8", ), ( - "H", - "beta", - UPERcodec_ENUMERATED, - {"uper_enum_values": [1, 200]}, - 200, + "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"), ] -ASN1TOOLS_OID_SPEC = ( - "Foo DEFINITIONS AUTOMATIC TAGS ::= BEGIN " - "A ::= OBJECT IDENTIFIER " - "END" -) - - -def require_asn1tools(): - # type: () -> bool - return HAS_ASN1TOOLS +OID_ENCODE_VECTORS = [ + ("1.2.3", b"\x02*\x03"), + ("2.999.3", b"\x03\x887\x03"), +] def _assert_codec_roundtrip(codec, value, kwargs, expected): @@ -140,11 +144,7 @@ def check_uper_codec_oid_roundtrip(): def check_uper_codec_oid_encode_interop(): # type: () -> None - if not HAS_ASN1TOOLS: - raise RuntimeError("asn1tools is not installed") - foo = asn1tools.compile_string(ASN1TOOLS_OID_SPEC, "uper") - for oid in ("1.2.3", "2.999.3"): - expected = foo.encode("A", oid) + for oid, expected in OID_ENCODE_VECTORS: got = UPERcodec_OID.enc(oid) assert got == expected, ( "OID %r: expected %s, got %s" % @@ -152,35 +152,23 @@ def check_uper_codec_oid_encode_interop(): ) -def check_uper_codec_asn1tools_decode(): +def check_uper_codec_reference_decode(): # type: () -> None - if not HAS_ASN1TOOLS: - raise RuntimeError("asn1tools is not installed") - from test.scapy.layers.uper_iop import ASN1TOOLS_UPER_SPEC - - foo = asn1tools.compile_string(ASN1TOOLS_UPER_SPEC, "uper") - for typename, value, codec, kwargs, expected in DECODE_VECTORS: - encoded = foo.encode(typename, value) + 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) + (_typename, _value, expected, decoded.val) ) -def check_uper_codec_scapy_decode_asn1tools(): +def check_uper_codec_encode_reference(): # type: () -> None - if not HAS_ASN1TOOLS: - raise RuntimeError("asn1tools is not installed") - from test.scapy.layers.uper_iop import ASN1TOOLS_UPER_SPEC, PRIMITIVE_VECTORS + from test.scapy.layers.uper_iop import PRIMITIVE_VECTORS - foo = asn1tools.compile_string(ASN1TOOLS_UPER_SPEC, "uper") - for typename, value, encoder in PRIMITIVE_VECTORS: + for typename, value, encoder, expected in PRIMITIVE_VECTORS: encoded = encoder(value) - asn1_value = foo.decode(typename, encoded) - if typename == "H": - assert asn1_value == value - elif typename == "G": - assert asn1_value is None - else: - assert asn1_value == value + assert encoded == expected, ( + "%s %r: expected %s, got %s" % + (typename, value, expected.hex(), encoded.hex()) + ) diff --git a/test/scapy/layers/uper_iop.py b/test/scapy/layers/uper_iop.py index 30b1e56396b..45e25067037 100644 --- a/test/scapy/layers/uper_iop.py +++ b/test/scapy/layers/uper_iop.py @@ -5,17 +5,10 @@ """ UPER interoperability helpers. -Cross-check Scapy's UPER codec against asn1tools when available. +Cross-check Scapy's UPER codec against reference encodings (from asn1tools). """ -from typing import Any, Callable, List, Optional, Tuple - -try: - import asn1tools - HAS_ASN1TOOLS = True -except ImportError: - asn1tools = None # type: ignore - HAS_ASN1TOOLS = False +from typing import Any from scapy.asn1.uper import ( UPERcodec_BOOLEAN, @@ -26,74 +19,51 @@ UPER_Encoder, UPER_choice_index_enc, ) - - -ASN1TOOLS_UPER_SPEC = ( - "Foo DEFINITIONS AUTOMATIC TAGS ::= " - "BEGIN " - "A ::= BOOLEAN " - "B ::= INTEGER " - "C ::= INTEGER (0..255) " - "Signed ::= INTEGER (-128..127) " - "SeqOfC ::= SEQUENCE OF INTEGER (0..255) " - "ChoiceC ::= CHOICE { a INTEGER (0..15), b OCTET STRING } " - "D ::= OCTET STRING " - "E ::= OCTET STRING (SIZE(3)) " - "G ::= NULL " - "Seq ::= SEQUENCE { id INTEGER, flag BOOLEAN, extra INTEGER OPTIONAL } " - "SeqOf ::= SEQUENCE OF INTEGER " - "Choice ::= CHOICE { a INTEGER, b OCTET STRING } " - "H ::= ENUMERATED { alpha(1), beta(200) } " - "Inner ::= SEQUENCE { x INTEGER, y BOOLEAN } " - "Outer ::= SEQUENCE { id INTEGER, inner Inner } " - "Multi ::= SEQUENCE { id INTEGER, a INTEGER OPTIONAL, b OCTET STRING OPTIONAL } " - "END" -) - -PRIMITIVE_VECTORS = [ - ("A", True, lambda v: UPERcodec_BOOLEAN.enc(1 if v else 0)), - ("A", False, lambda v: UPERcodec_BOOLEAN.enc(1 if v else 0)), - ("B", 42, lambda v: UPERcodec_INTEGER.enc(v)), - ("B", -1, lambda v: UPERcodec_INTEGER.enc(v)), - ("C", 200, lambda v: UPERcodec_INTEGER.enc(v, uper_min=0, uper_max=255)), - ("D", b"AB", lambda v: UPERcodec_STRING.enc(v)), - ("E", b"\x12\x34\x56", lambda v: UPERcodec_STRING.enc(v, size_len=3)), - ("G", None, lambda v: UPERcodec_NULL.enc(None)), - ("H", "beta", lambda v: UPERcodec_ENUMERATED.enc( - 200, uper_enum_values=[1, 200], - )), -] - -COMPOSITE_VECTORS = [ - ("Seq", {"id": 42, "flag": True}), - ("Seq", {"id": 42, "flag": True, "extra": 7}), - ("SeqOf", [1, 2, 3]), - ("SeqOfC", [1, 200, 0]), - ("Choice", ("a", 99)), - ("Choice", ("b", b"AB")), - ("ChoiceC", ("a", 10)), - ("ChoiceC", ("b", b"AB")), -] +from scapy.packet import raw from test.scapy.layers.uper_packets import ( UPERMultiOptional, UPERNestedSequence, ) -from scapy.packet import raw -DECODE_COMPOSITE_VECTORS = [ - ("Seq", {"id": 42, "flag": True}, {"id": 42, "flag": True}), +# (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""), ( - "Seq", - {"id": 42, "flag": True, "extra": 7}, - {"id": 42, "flag": True, "extra": 7}, + "H", + "beta", + lambda v: UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), + b"\x80", ), - ("SeqOf", [1, 2, 3], [1, 2, 3]), - ("SeqOfC", [1, 200, 0], [1, 200, 0]), - ("Choice", ("a", 99), ("a", 99)), - ("Choice", ("b", b"AB"), ("b", b"AB")), - ("ChoiceC", ("a", 10), ("a", 10)), - ("ChoiceC", ("b", b"AB"), ("b", b"AB")), +] + +# (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 = [ @@ -109,39 +79,23 @@ ), ] -ASN1TOOLS_PACKET_VECTORS = [ +PACKET_REFERENCE_VECTORS = [ ( - "Outer", - {"id": 5, "inner": {"x": 3, "y": True}}, UPERNestedSequence, {"id": 5, "x": 3, "y": True}, + bytes.fromhex("0105010380"), ), ( - "Multi", - {"id": 1, "a": 2, "b": b"hi"}, UPERMultiOptional, {"id": 1, "a": 2, "b": b"hi"}, + bytes.fromhex("c0404040809a1a40"), ), ] -def require_asn1tools(): - # type: () -> bool - return HAS_ASN1TOOLS - - -def _compile_asn1tools(): - # type: () -> Any - if not HAS_ASN1TOOLS: - raise RuntimeError("asn1tools is not installed") - return asn1tools.compile_string(ASN1TOOLS_UPER_SPEC, "uper") - - def check_primitive_interop(): # type: () -> None - foo = _compile_asn1tools() - for typename, value, encoder in PRIMITIVE_VECTORS: - expected = foo.encode(typename, value) + for typename, value, encoder, expected in PRIMITIVE_VECTORS: got = encoder(value) assert got == expected, ( "%s %r: expected %s, got %s" % @@ -151,9 +105,7 @@ def check_primitive_interop(): def check_composite_interop(): # type: () -> None - foo = _compile_asn1tools() - for typename, value in COMPOSITE_VECTORS: - expected = foo.encode(typename, value) + for typename, value, expected in COMPOSITE_VECTORS: got = _encode_composite(typename, value) assert got == expected, ( "%s %r: expected %s, got %s" % @@ -161,27 +113,13 @@ def check_composite_interop(): ) -def check_composite_decode_interop(): - # type: () -> None - foo = _compile_asn1tools() - for typename, encoded_value, expected in DECODE_COMPOSITE_VECTORS: - data = foo.encode(typename, encoded_value) - got = foo.decode(typename, _encode_composite(typename, encoded_value)) - assert got == expected, ( - "%s %r: expected %r, got %r" % (typename, encoded_value, expected, got) - ) - assert foo.decode(typename, data) == expected - - -def check_packet_asn1tools_interop(): +def check_packet_reference_interop(): # type: () -> None - foo = _compile_asn1tools() - for typename, asn1_value, cls, pkt_kwargs in ASN1TOOLS_PACKET_VECTORS: - expected = foo.encode(typename, asn1_value) + for cls, pkt_kwargs, expected in PACKET_REFERENCE_VECTORS: got = raw(cls(**pkt_kwargs)) assert got == expected, ( "%s: expected %s, got %s" % - (typename, expected.hex(), got.hex()) + (cls.__name__, expected.hex(), got.hex()) ) decoded = cls(got) for key, value in pkt_kwargs.items(): diff --git a/test/scapy/layers/uper_packets.py b/test/scapy/layers/uper_packets.py index 3d035d14344..b5c161f9f0e 100644 --- a/test/scapy/layers/uper_packets.py +++ b/test/scapy/layers/uper_packets.py @@ -459,7 +459,7 @@ def check_uper_sequence_of_strings(): def check_uper_sequence_choice_hex(): # type: () -> None - """Cross-check against asn1tools composite encoding.""" + """Cross-check against reference composite encoding.""" pkt = UPERSequenceWithChoice(id=1, c=ASN1_INTEGER(99)) assert raw(pkt) == bytes.fromhex("010100b180") decoded = UPERSequenceWithChoice(raw(pkt)) diff --git a/tox.ini b/tox.ini index e29e5fbc940..495672a8e9d 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,6 @@ deps = coverage[toml] python-can cbor2 - asn1tools scapy-rpc # disabled on windows because they require c++ dependencies # brotli 1.1.0 broken https://github.com/google/brotli/issues/1072 From 2d143137988952f055729c856a48934f84172124 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 14 Jul 2026 20:26:47 +0200 Subject: [PATCH 13/15] Fix OER nested SEQUENCE decoding to return remaining bytes. OER SEQUENCE and SEQUENCE OF fields now propagate unconsumed bytes to their parent instead of raising, enabling nested structures and fields that follow a sequence within an enclosing SEQUENCE. AI-Assisted: yes (Cursor AI) --- scapy/asn1fields.py | 8 +--- test/scapy/layers/asn1.uts | 6 +++ test/scapy/layers/asn1_coverage.py | 6 +-- test/scapy/layers/oer_packets.py | 59 ++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 558f288dc79..788298c9610 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -685,9 +685,7 @@ def _m2i_oer(self, pkt, s): # type: (Any, bytes) -> Tuple[Any, bytes] s = self._apply_tagging_dec(s, pkt) s = self._dissect_sequence_children(pkt, s) - if len(s) > 0: - raise OER_Decoding_Error("unexpected remainder", remaining=s) - return [], b"" + return [], s def _m2i_per(self, pkt, s): # type: (Any, bytes) -> Tuple[Any, bytes] @@ -931,9 +929,7 @@ def m2i(self, c, s = self._extract_packet(s, pkt) # type: ignore if c: lst.append(c) - if len(s) > 0: - raise OER_Decoding_Error("unexpected remainder", remaining=s) - return lst, b"" + return lst, s if pkt.ASN1_codec == ASN1_Codecs.PER: dec = UPER_Decoder(s) if self.uper_extensible and dec.read_bit(): diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 2918fa3836f..c1db9113213 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -252,6 +252,12 @@ __import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_sequence_ __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 BER packets and fields = BER field explicit tag diff --git a/test/scapy/layers/asn1_coverage.py b/test/scapy/layers/asn1_coverage.py index ef8168fd604..a8ff0befeea 100644 --- a/test/scapy/layers/asn1_coverage.py +++ b/test/scapy/layers/asn1_coverage.py @@ -646,10 +646,8 @@ class _OerSeq(ASN1_Packet): ASN1F_INTEGER("id", 0, size_len=1, oer_unsigned=True), ) - _raises( - OER_Decoding_Error, - lambda: _OerSeq.ASN1_root.m2i(_OerSeq(), b"\x01\xff"), - ) + _, remain = _OerSeq.ASN1_root.m2i(_OerSeq(), b"\x01\xff") + assert remain == b"\xff" class _PerSeq(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER diff --git a/test/scapy/layers/oer_packets.py b/test/scapy/layers/oer_packets.py index 515b0209af2..3607ae49377 100644 --- a/test/scapy/layers/oer_packets.py +++ b/test/scapy/layers/oer_packets.py @@ -64,6 +64,36 @@ class OERRecord(ASN1_Packet): ) +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)) @@ -147,3 +177,32 @@ def check_oer_packet_record(): 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 From 1ad660f8d898cae32cd570d5c3d72e2a65fa93c7 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 14 Jul 2026 20:35:30 +0200 Subject: [PATCH 14/15] Fix CI commit date gate and OER handling in ASN1F_PACKET. Use a valid --after date so the AI-Assisted trailer check runs, and route ASN1F_PACKET encode/decode through OER tagging like other ASN.1 fields. AI-Assisted: yes (Cursor AI) --- .config/ci/check_commits.sh | 2 +- scapy/asn1fields.py | 25 ++++++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.config/ci/check_commits.sh b/.config/ci/check_commits.sh index f5efc667c48..6f2776c9ded 100755 --- a/.config/ci/check_commits.sh +++ b/.config/ci/check_commits.sh @@ -6,7 +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 -commits=$(git rev-list --no-merges --after="2026-01-00T00:00:00" --max-count=$((PR_FETCH_DEPTH - 1)) HEAD) +commits=$(git rev-list --no-merges --after="2026-01-01T00: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/asn1fields.py b/scapy/asn1fields.py index 788298c9610..0c2464553a3 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -1438,11 +1438,20 @@ def m2i(self, pkt, s): if not hasattr(cls, "ASN1_root"): # A normal Packet (!= ASN1) return self.extract_packet(cls, s, _underlayer=pkt) - diff_tag, s = BER_tagging_dec(s, hidden_tag=cls.ASN1_root.ASN1_tag, # noqa: E501 - implicit_tag=self.implicit_tag, - explicit_tag=self.explicit_tag, - safe=self.flexible_tag, - _fname=self.name) + if pkt.ASN1_codec == ASN1_Codecs.OER: + diff_tag, s = OER_tagging_dec(s, hidden_tag=cls.ASN1_root.ASN1_tag, # noqa: E501 + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag, + _fname=self.name) + elif pkt.ASN1_codec != ASN1_Codecs.PER: + diff_tag, s = BER_tagging_dec(s, hidden_tag=cls.ASN1_root.ASN1_tag, # noqa: E501 + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag, + _fname=self.name) + else: + diff_tag = None self._apply_diff_tag(diff_tag) if not s: return None, s @@ -1473,6 +1482,12 @@ def i2m(self, return s if pkt.ASN1_codec == ASN1_Codecs.PER: return s + if pkt.ASN1_codec == ASN1_Codecs.OER: + return cast(bytes, OER_tagging_enc( + s, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + )) return BER_tagging_enc(s, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag) From fc870e91dcead338b86c017628d2f356de07daa0 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 14 Jul 2026 21:14:27 +0200 Subject: [PATCH 15/15] fix flake8 AI-Assisted: no --- scapy/asn1fields.py | 63 ++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 0c2464553a3..4d059ce07ab 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -9,9 +9,24 @@ """ 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, @@ -33,7 +48,6 @@ BER_tagging_enc, ) from scapy.asn1.oer import ( - OER_Decoding_Error, OER_id_dec, OER_tag_enc, OER_tagging_dec, @@ -62,24 +76,6 @@ 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 @@ -231,9 +227,9 @@ def i2m(self, pkt, x): return b"" if isinstance(x, ASN1_Object): if (self.ASN1_tag == ASN1_Class_UNIVERSAL.ANY or - x.tag == ASN1_Class_UNIVERSAL.RAW or - x.tag == ASN1_Class_UNIVERSAL.ERROR or - self.ASN1_tag == x.tag): + x.tag == ASN1_Class_UNIVERSAL.RAW or + x.tag == ASN1_Class_UNIVERSAL.ERROR or + self.ASN1_tag == x.tag): if pkt.ASN1_codec == ASN1_Codecs.PER: codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) s = codec.enc(x.val, **self._uper_codec_kwargs()) @@ -342,8 +338,8 @@ def _uper_codec_kwargs(self, size_len=None): "uper_max": self.uper_max, } # type: Dict[str, Any] if ( - getattr(self, "uper_extensible", False) and - self.ASN1_tag == ASN1_Class_UNIVERSAL.INTEGER + 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: @@ -380,9 +376,9 @@ def _uper_encode_into(self, enc, pkt, value=None): 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): + 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( @@ -436,7 +432,7 @@ def __init__(self, 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): @@ -789,7 +785,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 """ @@ -888,8 +884,8 @@ def _uper_encode_into(self, enc, pkt, value=None): 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 + 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: @@ -1033,6 +1029,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 @@ -1102,6 +1099,7 @@ 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) @@ -1129,6 +1127,7 @@ 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