From d5594daddee34f5d0821d90e84733b9de8291352 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 1 Jul 2026 14:41:31 +0200 Subject: [PATCH 1/6] 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 1c67e8b06ad..a719168f532 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 134df920b801f5e74122571db4a5cd26c4aebfeb Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 1 Jul 2026 20:49:30 +0200 Subject: [PATCH 2/6] 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 a719168f532..e00590b38c1 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 f40991737b7fedd4df64b06c9c77233ead02978f Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 1 Jul 2026 21:39:59 +0200 Subject: [PATCH 3/6] 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 76256cd4876f72276012c5b80b137710ccf04fc8 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 1 Jul 2026 21:50:29 +0200 Subject: [PATCH 4/6] 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 7e653d0d81f66906c4e4b5a5f4659301dce7153e Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 2 Jul 2026 08:12:07 +0200 Subject: [PATCH 5/6] 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 d5eb126311b6ca8b4fa4144da609a5ca970b78c3 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 2 Jul 2026 20:49:20 +0200 Subject: [PATCH 6/6] 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):